Files
apps/files/desktop/DesktopFilesGrid.js
metacryst 0d6c7683ff init
2026-04-28 20:05:00 -05:00

409 lines
14 KiB
JavaScript

class DesktopFilesGrid extends Shadow {
constructor(files, groups, viewMode, onFileClick) {
super()
this.files = files
this.groups = groups
this.viewMode = viewMode
this.onFileClick = onFileClick
}
render() {
if (this.files.length === 0) {
VStack(() => {
p("🗂")
.margin(0)
.fontSize(2.5, em)
.opacity(0.18)
p("No files here")
.margin(0)
.marginTop(0.65, em)
.fontSize(0.9, em)
.color("var(--headertext)")
.opacity(0.32)
})
.flex(1)
.justifyContent("center")
.alignItems("center")
.height(100, pct)
return;
}
if (this.viewMode === "list") {
this.renderListView()
} else {
this.renderGridView()
}
}
renderListView() {
VStack(() => {
// Header row
HStack(() => {
p("Name") .styles(this.colHeader).flex(1)
p("Shared with").styles(this.colHeader).width(160, px).flexShrink(0)
p("Modified") .styles(this.colHeader).width(130, px).flexShrink(0)
p("Size") .styles(this.colHeader).width(80, px).flexShrink(0)
p("Group") .styles(this.colHeader).width(120, px).flexShrink(0)
})
.paddingHorizontal(1.25, em)
.paddingVertical(0.45, em)
.borderBottom("1px solid var(--divider)")
.flexShrink(0)
VStack(() => {
this.files.forEach((file, i) => this.renderListRow(file, i))
})
.overflowY("auto")
.flex(1)
})
.width(100, pct)
.height(100, pct)
.overflow("hidden")
}
renderListRow(file, index) {
const self = this
HStack(() => {
// Icon + name
HStack(() => {
p(this.fileIcon(file))
.margin(0)
.fontSize(1.15, em)
.lineHeight("1")
.flexShrink(0)
VStack(() => {
p(file.name)
.margin(0)
.fontSize(0.88, em)
.fontWeight("500")
.color("var(--headertext)")
.overflow("hidden")
.whiteSpace("nowrap")
.textOverflow("ellipsis")
})
.flex(1)
.minWidth(0)
})
.gap(0.6, em)
.flex(1)
.minWidth(0)
.alignItems("center")
// Active editors badge
HStack(() => {
if (file.activeEditors && file.activeEditors.length > 0) {
this.renderActiveEditors(file.activeEditors, "list")
} else if (file.sharedWith && file.sharedWith.length > 0) {
this.renderSharedAvatars(file.sharedWith)
} else {
p("—").margin(0).fontSize(0.8, em).color("var(--headertext)").opacity(0.2)
}
})
.width(160, px)
.flexShrink(0)
.alignItems("center")
.gap(0.3, em)
// Modified
p(this.relativeDate(file.modifiedAt))
.margin(0)
.fontSize(0.78, em)
.color("var(--headertext)")
.opacity(0.45)
.width(130, px)
.flexShrink(0)
.overflow("hidden")
.whiteSpace("nowrap")
// Size
p(file.size || "—")
.margin(0)
.fontSize(0.78, em)
.color("var(--headertext)")
.opacity(0.45)
.width(80, px)
.flexShrink(0)
// Group tag
HStack(() => {
const group = this.groups.find(g => g.id === file.groupId);
if (group) {
VStack(() => {})
.width(0.5, em).height(0.5, em)
.borderRadius(50, pct)
.background(group.color)
.flexShrink(0)
p(group.name)
.margin(0)
.fontSize(0.72, em)
.color("var(--headertext)")
.opacity(0.55)
.overflow("hidden")
.whiteSpace("nowrap")
.textOverflow("ellipsis")
}
})
.gap(0.38, em)
.width(120, px)
.flexShrink(0)
.alignItems("center")
.minWidth(0)
})
.paddingHorizontal(1.25, em)
.paddingVertical(0.62, em)
.borderBottom("1px solid var(--divider)")
.cursor("pointer")
.alignItems("center")
.width(100, pct)
.boxSizing("border-box")
.onClick(function(done){ if(done){ self.onFileClick(file) } })
}
renderGridView() {
VStack(() => {
ZStack(() => {
this.files.forEach(file => this.renderGridCard(file))
})
.display("grid")
.attr({ style: "display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1em;" })
})
.padding(1.5, em)
.overflowY("auto")
.height(100, pct)
.width(100, pct)
.boxSizing("border-box")
}
renderGridCard(file) {
VStack(() => {
// Thumbnail area
ZStack(() => {
// File type large icon
VStack(() => {
p(this.fileIcon(file))
.margin(0)
.fontSize(2.4, em)
.lineHeight("1")
})
.width(100, pct)
.height(100, pct)
.justifyContent("center")
.alignItems("center")
.background(this.fileTint(file))
// Active editor badge (top-right)
if (file.activeEditors && file.activeEditors.length > 0) {
VStack(() => {
this.renderActiveEditors(file.activeEditors, "grid")
})
.position("absolute")
.top(0.45, em)
.right(0.45, em)
}
// Group color stripe (bottom of thumbnail)
VStack(() => {
const group = this.groups.find(g => g.id === file.groupId);
if (group) {
VStack(() => {})
.width(100, pct)
.height(3, px)
.background(group.color)
}
})
.position("absolute")
.bottom(0).left(0).right(0)
})
.position("relative")
.width(100, pct)
.height(6.5, em)
.borderRadius("0.5em 0.5em 0 0")
.overflow("hidden")
.flexShrink(0)
// Info area
VStack(() => {
p(file.name)
.margin(0)
.fontSize(0.8, em)
.fontWeight("600")
.color("var(--headertext)")
.overflow("hidden")
.whiteSpace("nowrap")
.textOverflow("ellipsis")
.width(100, pct)
p(this.relativeDate(file.modifiedAt))
.margin(0)
.marginTop(0.18, em)
.fontSize(0.68, em)
.color("var(--headertext)")
.opacity(0.38)
})
.padding(0.65, em)
.background("var(--darkaccent)")
.borderRadius("0 0 0.5em 0.5em")
.width(100, pct)
.boxSizing("border-box")
})
.border("1px solid var(--divider)")
.borderRadius(0.5, em)
.overflow("hidden")
.cursor("pointer")
.onClick((done) => {if(!done) return; this.onFileClick(file)})
}
renderActiveEditors(editors, context) {
const isGrid = context === "grid";
HStack(() => {
// Pulsing green dot
ZStack(() => {
VStack(() => {})
.width(isGrid ? 0.55 : 0.48, em)
.height(isGrid ? 0.55 : 0.48, em)
.borderRadius(50, pct)
.background("#22c55e")
.opacity(0.4)
.attr({ style: "animation: pulse-ring 1.5s ease-out infinite;" })
VStack(() => {})
.width(isGrid ? 0.4 : 0.35, em)
.height(isGrid ? 0.4 : 0.35, em)
.borderRadius(50, pct)
.background("#22c55e")
.position("absolute")
})
.position("relative")
.width(isGrid ? 0.55 : 0.48, em)
.height(isGrid ? 0.55 : 0.48, em)
.flexShrink(0)
// Editor avatar stack
HStack(() => {
editors.slice(0, 3).forEach((editor, i) => {
VStack(() => {
p(editor[0].toUpperCase())
.margin(0)
.fontSize(isGrid ? 0.5 : 0.45, em)
.fontWeight("700")
.color("white")
.lineHeight("1")
})
.width(isGrid ? 1.4 : 1.25, em)
.height(isGrid ? 1.4 : 1.25, em)
.borderRadius(50, pct)
.background(this.avatarColor(editor))
.justifyContent("center")
.alignItems("center")
.boxSizing("border-box")
.flexShrink(0)
.marginLeft(i > 0 ? -0.4 : 0, em)
})
})
.alignItems("center")
if (!isGrid) {
p(editors.length === 1
? `${editors[0]} is editing`
: `${editors.length} editing`)
.margin(0)
.fontSize(0.72, em)
.color("#22c55e")
.fontWeight("500")
.whiteSpace("nowrap")
}
})
.gap(isGrid ? 0.25 : 0.45, em)
.alignItems("center")
.paddingHorizontal(isGrid ? 0.4 : 0)
.paddingVertical(isGrid ? 0.22 : 0)
.background(isGrid ? "rgba(0,0,0,0.55)" : "transparent")
.borderRadius(isGrid ? 100 : 0, px)
}
renderSharedAvatars(people) {
HStack(() => {
people.slice(0, 4).forEach((name, i) => {
VStack(() => {
p(name[0].toUpperCase())
.margin(0)
.fontSize(0.45, em)
.fontWeight("700")
.color("white")
.lineHeight("1")
})
.width(1.25, em)
.height(1.25, em)
.borderRadius(50, pct)
.background(this.avatarColor(name))
.justifyContent("center")
.alignItems("center")
.boxSizing("border-box")
.flexShrink(0)
.marginLeft(i > 0 ? -0.38 : 0, em)
})
})
.alignItems("center")
}
colHeader(el) {
return el
.margin(0)
.fontSize(0.72, em)
.fontWeight("600")
.letterSpacing("0.03em")
.color("var(--headertext)")
.opacity(0.4)
}
fileIcon(file) {
const ext = file.name.split(".").pop().toLowerCase();
const map = {
pdf: "📄", doc: "📝", docx: "📝", txt: "📃", md: "📃",
xls: "📊", xlsx: "📊", csv: "📊",
ppt: "📋", pptx: "📋",
jpg: "🖼", jpeg: "🖼", png: "🖼", gif: "🖼", svg: "🖼", webp: "🖼",
mp4: "🎬", mov: "🎬", avi: "🎬",
mp3: "🎵", wav: "🎵",
zip: "🗜", rar: "🗜",
js: "⚙️", ts: "⚙️", py: "⚙️", json: "⚙️",
folder: "📁",
};
if (file.type === "folder") return "📁";
return map[ext] || "📄";
}
fileTint(file) {
const ext = file.name.split(".").pop().toLowerCase();
const tints = {
pdf: "rgba(239,68,68,0.08)", doc: "rgba(59,130,246,0.08)", docx: "rgba(59,130,246,0.08)",
xls: "rgba(16,185,129,0.08)", xlsx: "rgba(16,185,129,0.08)", csv: "rgba(16,185,129,0.08)",
jpg: "rgba(245,158,11,0.08)", jpeg: "rgba(245,158,11,0.08)", png: "rgba(245,158,11,0.08)",
mp4: "rgba(139,92,246,0.08)", mov: "rgba(139,92,246,0.08)",
zip: "rgba(107,114,128,0.08)",
};
if (file.type === "folder") return "rgba(59,130,246,0.06)";
return tints[ext] || "var(--darkaccent)";
}
relativeDate(date) {
const d = new Date(date);
const days = Math.floor((Date.now() - d) / 86400000);
if (days === 0) return "Today";
if (days === 1) return "Yesterday";
if (days < 7) return `${days} days ago`;
return d.toLocaleDateString([], { month: "short", day: "numeric", year: "numeric" });
}
avatarColor(name) {
const colors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"];
let hash = 0;
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
return colors[Math.abs(hash) % colors.length];
}
}
register(DesktopFilesGrid)