This commit is contained in:
metacryst
2026-04-28 20:05:00 -05:00
commit 0d6c7683ff
123 changed files with 20922 additions and 0 deletions

View File

@@ -0,0 +1,396 @@
import server from "/@server/server.js"
import calendarUtil from "../../calendarUtil.js"
import "../../../components/Avatar.js"
class DesktopEventDetails extends Shadow {
attachmentsOpen = false
constructor(calendars, event, onUpdated = null, onDeleted = null, onEdit = null) {
super()
this.calendars = calendars
this.event = event
this.attachmentsOpen = (event?.attachments?.length > 0)
this.onUpdated = onUpdated
this.onDeleted = onDeleted
this.onEdit = onEdit
}
render() {
if (!this.event) return
const eventCals = this.calendars.filter(c => this.event.calendars?.includes(c.id))
const isOwner = this.event.creator_id === global.profile?.id
VStack(() => {
this.renderHeader(isOwner)
this.renderBody(eventCals)
HStack(() => {
const members = global.currentNetwork.data?.members || []
const creator = members.find(m => m.id === this.event.creator_id)
if (creator) {
Avatar(creator, 1.6)
VStack(() => {
p(`Created ${calendarUtil.timeAgo(this.event.created)} by ${creator.first_name}`)
.margin(0).fontSize(0.7, em).color("var(--headertext)").opacity(0.5)
if (this.event.updated_at && this.event.updated_at !== this.event.created) {
p(`Last updated ${calendarUtil.timeAgo(this.event.updated_at)}`)
.margin(0).fontSize(0.7, em).color("var(--headertext)").opacity(0.4)
}
})
.gap(0.15, em)
}
})
.paddingHorizontal(1, em)
.paddingVertical(0.65, em)
.boxSizing("border-box")
.alignItems("center")
.gap(0.5, em)
.flexShrink(0)
})
.height(100, pct)
.boxSizing("border-box")
}
// ── Header ────────────────────────────────────────────────────────────────
renderHeader(isOwner) {
HStack(() => {
VStack(() => {
h2(this.event.title || "Untitled")
.margin(0)
.fontSize(1.45, em)
.fontWeight("700")
.color("var(--headertext)")
.lineHeight("1.2")
})
.flex(1)
.paddingHorizontal(1.4, em)
.paddingTop(2.5, em)
.paddingBottom(0.5, em)
.justifyContent("center")
if (isOwner) {
button("Delete")
.paddingVertical(0.34, em)
.paddingHorizontal(0.85, em)
.border("1px solid var(--quillred)")
.borderRadius(0.45, em)
.background("transparent")
.color("var(--quillred)")
.cursor("pointer")
.fontSize(0.8, em)
.fontWeight("600")
.marginRight(0.5, em)
.marginTop("auto")
.marginBottom(1, em)
.flexShrink(0)
.onClick((done) => { if (done) this.handleDelete() })
.onHover(function(hovering) {
this.style.background = hovering ? "var(--quillred)" : "transparent";
this.style.color = hovering ? "white" : "var(--quillred)";
})
button("Edit")
.paddingVertical(0.34, em)
.paddingHorizontal(0.85, em)
.border("1px solid var(--divider)")
.borderRadius(0.45, em)
.background("transparent")
.color("var(--headertext)")
.cursor("pointer")
.fontSize(0.8, em)
.marginRight(1.4, em)
.marginTop("auto")
.marginBottom(1, em)
.flexShrink(0)
.onClick((done) => {
if (!done) return
// Attach template dates for override events so scope='all' anchors correctly
let eventForEdit = this.event
if (this.event.recurrence_parent_id && !this.event._templateStart) {
const template = global.currentNetwork.data.events.find(e => e.id === this.event.recurrence_parent_id)
if (template) {
eventForEdit = {
...this.event,
_templateStart: new Date(template.time_start),
_templateEnd: new Date(template.time_end)
}
}
}
this.onEdit(eventForEdit)
})
.onHover(function(hovering) {
this.style.background = hovering ? "var(--divider)" : "transparent";
})
}
})
.width(100, pct)
.alignItems("stretch")
.background("var(--darkaccent)")
.borderBottom("1px solid var(--divider)")
.boxSizing("border-box")
.flexShrink(0)
}
// ── Body ─────────────────────────────────────────────────────────────────
renderBody(eventCals) {
VStack(() => {
VStack(() => {
this.prop("WHEN", () => {
p(calendarUtil.formatEventTime(this.event))
.margin(0)
.fontSize(0.88, em)
.color("var(--headertext)")
})
if (this.event.recurrence) {
this.prop("REPEATS", () => {
p(this._recurrenceLabel())
.margin(0)
.fontSize(0.88, em)
.color("var(--headertext)")
})
}
this.prop("CALENDARS", () => {
HStack(() => {
eventCals.forEach(cal => {
p(cal.name)
.margin(0)
.fontSize(0.78, em)
.fontWeight("600")
.color("white")
.paddingHorizontal(0.65, em)
.paddingVertical(0.28, em)
.background(cal.color)
.borderRadius(0.45, em)
})
})
.flexWrap("wrap")
.gap(0.45, em)
})
if (this.event.location) {
this.prop("LOCATION", () => {
p(this.event.location)
.margin(0)
.fontSize(0.88, em)
.color("var(--headertext)")
.lineHeight("1.5")
})
}
if (this.event.description) {
this.prop("DESCRIPTION", () => {
p(this.event.description)
.margin(0)
.fontSize(0.88, em)
.color("var(--headertext)")
.lineHeight("1.65")
.whiteSpace("pre-wrap")
})
}
})
if (this.event.attachments?.length > 0) {
this.renderAttachments()
}
})
.flex(1)
.overflowY("scroll")
.width(100, pct)
.boxSizing("border-box")
}
prop(label, contentFn) {
VStack(() => {
p(label)
.margin(0)
.marginBottom(1, em)
.fontSize(0.67, em)
.fontWeight("600")
.letterSpacing("0.06em")
.color("var(--headertext)")
.opacity(0.38)
VStack(() => { contentFn() })
.width(100, pct)
})
.paddingHorizontal(1.5, em)
.paddingTop(0.75, em)
.paddingBottom(0.4, em)
.boxSizing("border-box")
.width(100, pct)
}
_recurrenceLabel() {
const r = this.event.recurrence
if (!r) return ""
if (r.frequency === 'daily') return "Daily"
if (r.frequency === 'weekly' && r.interval === 2) return "Every 2 weeks"
if (r.frequency === 'weekly') return "Weekly"
if (r.frequency === 'monthly') return "Monthly"
if (r.frequency === 'yearly') return "Yearly"
return ""
}
// ── Attachments ───────────────────────────────────────────────────────────
renderAttachments() {
VStack(() => {
HStack(() => {
p("Attachments")
.margin(0)
.fontSize(0.82, em)
.fontWeight("600")
.color("var(--headertext)")
.opacity(0.4)
p("▼")
.attr({ id: "desktop-attachments-chevron" })
.margin(0)
.fontSize(0.65, em)
.color("var(--headertext)")
.opacity(0.4)
.display("inline-block")
.transition("transform 0.22s ease")
.transform(this.attachmentsOpen ? "rotate(180deg)" : "rotate(0deg)")
.userSelect("none")
})
.gap(0.5, em)
.alignItems("center")
.cursor("pointer")
.onClick((done) => { if (!done) return; this.toggleAttachments() })
VStack(() => {
const images = this.event.attachments.filter(f => f.type?.startsWith("image/"))
const files = this.event.attachments.filter(f => !f.type?.startsWith("image/"))
if (images.length > 0) {
HStack(() => {
images.forEach(file => {
const url = `${config.SERVER}/db/images/events/${file.name}`
VStack(() => {
img(url, "100%", "100%")
.objectFit("cover")
.display("block")
})
.width("6.5em")
.height("6.5em")
.flexShrink(0)
.border("1px solid var(--divider)")
.borderRadius(6, px)
.overflow("hidden")
.cursor("pointer")
.onClick((done) => { if (!done) return; $("filepreview-").open(file, url) })
})
})
.flexWrap("wrap")
.gap(0.5, em)
.boxSizing("border-box")
.width(100, pct)
}
if (files.length > 0) {
VStack(() => {
files.forEach(file => this.renderFile(file))
})
.gap(0.5, em)
.width("max-content")
.boxSizing("border-box")
}
})
.attr({ id: "desktop-attachments-content" })
.width(100, pct)
.display(this.attachmentsOpen ? "" : "none")
.gap(1, em)
})
.width(100, pct)
.boxSizing("border-box")
.paddingHorizontal(1.5, em)
.paddingVertical(0.85, em)
.gap(1, em)
}
renderFile(file) {
const url = `${config.SERVER}/db/images/events/${file.name}`
HStack(() => {
p("📎")
.margin(0)
.fontSize(0.9, em)
p(file.original_name ?? file.name)
.margin(0)
.color("var(--headertext)")
.fontSize(0.85, em)
.overflow("hidden")
.whiteSpace("nowrap")
.textOverflow("ellipsis")
})
.gap(0.5, em)
.alignItems("center")
.padding(0.55, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(0.45, em)
.boxSizing("border-box")
.cursor("pointer")
.onClick((done) => { if (!done) return; $("filepreview-").open(file, url) })
}
toggleAttachments() {
this.attachmentsOpen = !this.attachmentsOpen
const content = this.$("#desktop-attachments-content")
const chevron = this.$("#desktop-attachments-chevron")
if (content) content.style.display = this.attachmentsOpen ? "" : "none"
if (chevron) chevron.style.transform = this.attachmentsOpen ? "rotate(180deg)" : "rotate(0deg)"
}
handleDelete() {
const event = this.event
const isRecurring = !!(event?._isOccurrence || event?.recurrence_parent_id || event?.recurrence_id)
if (isRecurring) {
$('actionsheetpopup-').show(
"Delete Recurring Event",
[
{ label: "Delete just this event", onTap: () => this.performDelete('single') },
{ label: "Delete this and future events", onTap: () => this.performDelete('future') },
{ label: "Delete all events in series", onTap: () => this.performDelete('all') },
],
() => {}
)
return
}
this.performDelete(null)
}
async performDelete(scope) {
const event = this.event
const isOverride = !!event.recurrence_parent_id
const templateId = isOverride ? event.recurrence_parent_id : event.id
const occurrenceDate = isOverride
? (event.recurrence_exception_date instanceof Date
? event.recurrence_exception_date.toISOString()
: event.recurrence_exception_date) ?? null
: event._occurrenceDate?.toISOString() ?? null
const serverEventId = (scope === 'single' && isOverride) ? event.id : templateId
try {
const result = await server.deleteEvent(serverEventId, global.currentNetwork.id, scope, occurrenceDate)
if (result.status === 200) {
$("modal-").forceClose()
const deleteResult = { scope: scope ?? 'all', templateId, occurrenceDate, overrideId: isOverride ? event.id : null }
if (this.onDeleted) this.onDeleted(deleteResult)
} else {
$("modal-")?.showError(result.error ?? "Failed to delete event.")
}
} catch (err) {
console.error("Failed to delete event:", err)
$("modal-")?.showError("Failed to delete event.")
}
}
}
register(DesktopEventDetails)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,154 @@
class FilePreview extends Window {
_visible = false
_file = null
_url = null
open(file, url) {
this._file = file
this._url = url
this._visible = true
this.rerender()
}
close() {
this._visible = false
this.rerender()
}
render() {
this.style.position = "fixed"
this.style.inset = "0"
this.style.zIndex = "300"
this.style.pointerEvents = this._visible ? "all" : "none"
if (!this._visible) return
const x = this.getX()
const y = this.getY()
const w = this.getWidth()
const h = this.getHeight()
const type = this._file?.type ?? ""
const isImage = type.startsWith("image/")
const isPDF = type === "application/pdf"
const isOffice = [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/msword',
'application/vnd.ms-excel',
'application/vnd.ms-powerpoint',
'text/plain',
].includes(type)
const displayName = this._file?.original_name ?? this._file?.name ?? "File"
VStack(() => {
const panel = VStack(() => {
// Header
HStack(() => {
HStack(() => {
div().attr({ class: "tl tl-close" }).onClick((done) => { if (done) this.close() })
div().attr({ class: "tl tl-min" })
div().attr({ class: "tl tl-max" })
})
.attr({ class: "traffic-lights" })
.flexShrink(0)
p(displayName)
.margin(0)
.fontSize(0.88, em)
.fontWeight("600")
.color("var(--headertext)")
.overflow("hidden")
.whiteSpace("nowrap")
.textOverflow("ellipsis")
.flex(1)
.textAlign("center")
div().width("52px").flexShrink(0)
})
.paddingHorizontal(1, em)
.paddingVertical(0.75, em)
.alignItems("center")
.gap(1, em)
.background("var(--darkaccent)")
.borderBottom("1px solid var(--divider)")
.width(100, pct)
.boxSizing("border-box")
.flexShrink(0)
// Content
if (isImage) {
img(this._url, "auto", "auto")
.display("block")
.maxWidth(100, pct)
.maxHeight(h - 48, px)
.margin("0 auto")
} else if (isPDF) {
const wrap = div()
.flex(1)
.width(100, pct)
.overflow("hidden")
wrap.innerHTML = `<iframe src="${this._url}" style="width:100%;height:100%;border:none;display:block;"></iframe>`
} else {
const icon = {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': "📄",
'application/msword': "📄",
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': "📊",
'application/vnd.ms-excel': "📊",
'application/vnd.openxmlformats-officedocument.presentationml.presentation': "📋",
'application/vnd.ms-powerpoint': "📋",
'text/plain': "📄",
}[type] ?? "📎"
VStack(() => {
p(icon).fontSize(3, em).margin(0)
p(displayName)
.margin(0)
.fontSize(0.9, em)
.color("var(--headertext)")
.textAlign("center")
a(this._url, "Download")
.attr({ download: displayName })
.fontSize(0.85, em)
.color("var(--quillred)")
.fontWeight("600")
.fontFamily("Arial")
.textDecoration("none")
.cursor("pointer")
.marginTop(0.25, em)
.onHover(function(hovering) {
this.style.textDecoration = hovering ? "underline" : "none"
})
})
.gap(0.5, em)
.alignItems("center")
.padding(2, em)
}
})
.background("var(--window)")
.backdropFilter("blur(18px)")
.border("0.5px solid var(--window-border)")
.borderRadius(12, px)
.overflow("hidden")
.width(100, pct)
.boxSizing("border-box")
if (isPDF) panel.height(h, px)
else panel.maxHeight(h, px)
panel.onClick((done, e) => { e.stopPropagation() })
})
.position("fixed")
.x(x, px)
.y(y, px)
.width(w, px)
.height(h, px)
.justifyContent(isPDF ? "flex-start" : "center")
.alignItems("center")
.onClick((done) => { if (done) this.close() })
.onEvent("resize", () => this.rerender())
}
}
register(FilePreview)