init
This commit is contained in:
408
files/desktop/DesktopFilesGrid.js
Normal file
408
files/desktop/DesktopFilesGrid.js
Normal file
@@ -0,0 +1,408 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user