- ✅ Enhanced accessibility features (focus states, reduced motion) - ✅ Added connection status component styles - ✅ Improved responsive design - ✅ Added utility classes for common patterns - ✅ Added semantic HTML5 elements (`<header>`, `<main>`, `<nav>`) - ✅ Comprehensive ARIA labels and roles for accessibility - ✅ Keyboard navigation support (Alt+1-4 for sections, Esc for menus) - ✅ Better event handling and state management - ✅ Theme change subscriber with meta theme-color sync - ✅ Online/offline connection monitoring - ✅ Enhanced console logging with app info - ✅ `THEMES.md` (400+ lines) - Complete theme system guide - ✅ `README.md` (433+ lines) - Main application documentation - ✅ `COMPONENTS.md` (773+ lines) - UI component library reference - ✅ `QUICKSTART.md` (359+ lines) - Quick start guide for developers - ✅ `REBUILD_NOTES.md` - This summary document **Theme files define base colors:** ```css :root { --primary: 217 91% 60%; /* HSL: blue */ --background: 0 0% 100%; /* HSL: white */ } ``` **App.css bridges to working variables:** ```css :root { --accent-color: hsl(var(--primary)); --primary-bg: hsl(var(--background)); --accent-light: hsla(var(--primary) / 0.1); } ``` **Components use working variables:** ```css .button { background: var(--accent-color); color: hsl(var(--primary-foreground)); } ``` - ✅ Keyboard shortcuts (Alt+1-4, Esc) - ✅ System dark mode detection - ✅ Theme change event subscription - ✅ Automatic document title updates - ✅ Meta theme-color synchronization - ✅ Enhanced console logging - ✅ Better error handling - ✅ Improved accessibility - ✅ Theme switching via dropdown - ✅ Theme persistence to localStorage - ✅ Apps menu with section switching - ✅ Dynamic section loading (Chat, Drive, Tasks, Mail) - ✅ WebSocket chat functionality - ✅ Alpine.js integration for other modules - ✅ Responsive design - ✅ Loading states - [x] Theme switching works across all 19 themes - [x] All sections load correctly - [x] Keyboard shortcuts functional - [x] Responsive on mobile/tablet/desktop - [x] Accessibility features working - [x] No console errors - [x] Theme persistence works - [x] Dark mode detection works ``` documentation/ ├── README.md # Main docs - start here ├── QUICKSTART.md # 5-minute guide ├── THEMES.md # Theme system details ├── COMPONENTS.md # UI component library └── REBUILD_NOTES.md # This summary ``` 1. **HSL Bridge System**: Allows theme files to use shadcn-style HSL variables while the app automatically derives working CSS properties 2. **No Breaking Changes**: All existing functionality preserved and enhanced 3. **Developer-Friendly**: Comprehensive documentation for customization 4. **Accessibility First**: ARIA labels, keyboard navigation, focus management 5. **Performance Optimized**: Instant theme switching, minimal reflows - **Rebuild**: ✅ Complete - **Testing**: ✅ Passed - **Documentation**: ✅ Complete - **Production Ready**: ✅ Yes The rebuild successfully integrates the theme system throughout the UI while maintaining all functionality and adding comprehensive documentation for future development.
520 lines
14 KiB
JavaScript
520 lines
14 KiB
JavaScript
window.driveApp = function driveApp() {
|
|
return {
|
|
currentView: "all",
|
|
viewMode: "tree",
|
|
sortBy: "name",
|
|
searchQuery: "",
|
|
selectedItem: null,
|
|
currentPath: "/",
|
|
currentBucket: null,
|
|
showUploadDialog: false,
|
|
|
|
showEditor: false,
|
|
editorContent: "",
|
|
editorFilePath: "",
|
|
editorFileName: "",
|
|
editorLoading: false,
|
|
editorSaving: false,
|
|
|
|
quickAccess: [
|
|
{ id: "all", label: "All Files", icon: "📁", count: null },
|
|
{ id: "recent", label: "Recent", icon: "🕐", count: null },
|
|
{ id: "starred", label: "Starred", icon: "⭐", count: 3 },
|
|
{ id: "shared", label: "Shared", icon: "👥", count: 5 },
|
|
{ id: "trash", label: "Trash", icon: "🗑️", count: 0 },
|
|
],
|
|
|
|
storageUsed: "12.3 GB",
|
|
storageTotal: "50 GB",
|
|
storagePercent: 25,
|
|
|
|
fileTree: [],
|
|
loading: false,
|
|
error: null,
|
|
|
|
get allItems() {
|
|
const flatten = (items) => {
|
|
let result = [];
|
|
items.forEach((item) => {
|
|
result.push(item);
|
|
if (item.children && item.expanded) {
|
|
result = result.concat(flatten(item.children));
|
|
}
|
|
});
|
|
return result;
|
|
};
|
|
return flatten(this.fileTree);
|
|
},
|
|
|
|
get filteredItems() {
|
|
let items = this.allItems;
|
|
|
|
if (this.searchQuery.trim()) {
|
|
const query = this.searchQuery.toLowerCase();
|
|
items = items.filter((item) => item.name.toLowerCase().includes(query));
|
|
}
|
|
|
|
items = [...items].sort((a, b) => {
|
|
if (a.type === "folder" && b.type !== "folder") return -1;
|
|
if (a.type !== "folder" && b.type === "folder") return 1;
|
|
|
|
switch (this.sortBy) {
|
|
case "name":
|
|
return a.name.localeCompare(b.name);
|
|
case "modified":
|
|
return new Date(b.modified) - new Date(a.modified);
|
|
case "size":
|
|
return (
|
|
this.sizeToBytes(b.size || "0") - this.sizeToBytes(a.size || "0")
|
|
);
|
|
case "type":
|
|
return (a.type || "").localeCompare(b.type || "");
|
|
default:
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
return items;
|
|
},
|
|
|
|
get breadcrumbs() {
|
|
const crumbs = [{ name: "Home", path: "/" }];
|
|
|
|
if (this.currentBucket) {
|
|
crumbs.push({
|
|
name: this.currentBucket,
|
|
path: `/${this.currentBucket}`,
|
|
});
|
|
|
|
if (this.currentPath && this.currentPath !== "/") {
|
|
const parts = this.currentPath.split("/").filter(Boolean);
|
|
let currentPath = `/${this.currentBucket}`;
|
|
parts.forEach((part) => {
|
|
currentPath += `/${part}`;
|
|
crumbs.push({ name: part, path: currentPath });
|
|
});
|
|
}
|
|
}
|
|
|
|
return crumbs;
|
|
},
|
|
|
|
async loadFiles(bucket = null, path = null) {
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (bucket) params.append("bucket", bucket);
|
|
if (path) params.append("path", path);
|
|
|
|
const response = await fetch(`/files/list?${params.toString()}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const files = await response.json();
|
|
this.fileTree = this.convertToTree(files, bucket, path);
|
|
this.currentBucket = bucket;
|
|
this.currentPath = path || "/";
|
|
} catch (err) {
|
|
console.error("Error loading files:", err);
|
|
this.error = err.toString();
|
|
this.fileTree = this.getMockData();
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
convertToTree(files, bucket, basePath) {
|
|
return files.map((file) => {
|
|
const depth = basePath ? basePath.split("/").filter(Boolean).length : 0;
|
|
|
|
return {
|
|
id: file.path,
|
|
name: file.name,
|
|
type: file.is_dir ? "folder" : this.getFileTypeFromName(file.name),
|
|
path: file.path,
|
|
bucket: bucket,
|
|
depth: depth,
|
|
expanded: false,
|
|
modified: new Date().toISOString().split("T")[0],
|
|
created: new Date().toISOString().split("T")[0],
|
|
size: file.is_dir ? null : "0 KB",
|
|
children: file.is_dir ? [] : undefined,
|
|
isDir: file.is_dir,
|
|
icon: file.icon,
|
|
};
|
|
});
|
|
},
|
|
|
|
getFileTypeFromName(filename) {
|
|
const ext = filename.split(".").pop().toLowerCase();
|
|
const typeMap = {
|
|
pdf: "pdf",
|
|
doc: "document",
|
|
docx: "document",
|
|
txt: "text",
|
|
md: "text",
|
|
bas: "code",
|
|
ast: "code",
|
|
xls: "spreadsheet",
|
|
xlsx: "spreadsheet",
|
|
csv: "spreadsheet",
|
|
ppt: "presentation",
|
|
pptx: "presentation",
|
|
jpg: "image",
|
|
jpeg: "image",
|
|
png: "image",
|
|
gif: "image",
|
|
svg: "image",
|
|
mp4: "video",
|
|
avi: "video",
|
|
mov: "video",
|
|
mp3: "audio",
|
|
wav: "audio",
|
|
zip: "archive",
|
|
rar: "archive",
|
|
tar: "archive",
|
|
gz: "archive",
|
|
js: "code",
|
|
ts: "code",
|
|
py: "code",
|
|
java: "code",
|
|
cpp: "code",
|
|
rs: "code",
|
|
go: "code",
|
|
html: "code",
|
|
css: "code",
|
|
json: "code",
|
|
xml: "code",
|
|
gbkb: "knowledge",
|
|
exe: "executable",
|
|
};
|
|
return typeMap[ext] || "file";
|
|
},
|
|
|
|
getMockData() {
|
|
return [
|
|
{
|
|
id: 1,
|
|
name: "Documents",
|
|
type: "folder",
|
|
path: "/Documents",
|
|
depth: 0,
|
|
expanded: true,
|
|
modified: "2024-01-15",
|
|
created: "2024-01-01",
|
|
isDir: true,
|
|
icon: "📁",
|
|
children: [
|
|
{
|
|
id: 2,
|
|
name: "notes.txt",
|
|
type: "text",
|
|
path: "/Documents/notes.txt",
|
|
depth: 1,
|
|
size: "4 KB",
|
|
modified: "2024-01-14",
|
|
created: "2024-01-13",
|
|
icon: "📃",
|
|
},
|
|
],
|
|
},
|
|
];
|
|
},
|
|
|
|
getFileIcon(item) {
|
|
if (item.icon) return item.icon;
|
|
|
|
const iconMap = {
|
|
folder: "📁",
|
|
pdf: "📄",
|
|
document: "📝",
|
|
text: "📃",
|
|
spreadsheet: "📊",
|
|
presentation: "📽️",
|
|
image: "🖼️",
|
|
video: "🎬",
|
|
audio: "🎵",
|
|
archive: "📦",
|
|
code: "💻",
|
|
knowledge: "📚",
|
|
executable: "⚙️",
|
|
};
|
|
return iconMap[item.type] || "📄";
|
|
},
|
|
|
|
async toggleFolder(item) {
|
|
if (item.type === "folder") {
|
|
item.expanded = !item.expanded;
|
|
|
|
if (item.expanded && item.children.length === 0) {
|
|
try {
|
|
const params = new URLSearchParams();
|
|
params.append("bucket", item.bucket || item.name);
|
|
if (item.path !== item.name) {
|
|
params.append("path", item.path);
|
|
}
|
|
|
|
const response = await fetch(`/files/list?${params.toString()}`);
|
|
if (response.ok) {
|
|
const files = await response.json();
|
|
item.children = this.convertToTree(
|
|
files,
|
|
item.bucket || item.name,
|
|
item.path,
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.error("Error loading folder contents:", err);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
openFolder(item) {
|
|
if (item.type === "folder") {
|
|
this.loadFiles(item.bucket || item.name, item.path);
|
|
}
|
|
},
|
|
|
|
selectItem(item) {
|
|
this.selectedItem = item;
|
|
},
|
|
|
|
navigateToPath(path) {
|
|
if (path === "/") {
|
|
this.loadFiles(null, null);
|
|
} else {
|
|
const parts = path.split("/").filter(Boolean);
|
|
const bucket = parts[0];
|
|
const filePath = parts.slice(1).join("/");
|
|
this.loadFiles(bucket, filePath || "/");
|
|
}
|
|
},
|
|
|
|
isEditableFile(item) {
|
|
if (item.type === "folder") return false;
|
|
const editableTypes = ["text", "code"];
|
|
const editableExtensions = [
|
|
"txt",
|
|
"md",
|
|
"js",
|
|
"ts",
|
|
"json",
|
|
"html",
|
|
"css",
|
|
"xml",
|
|
"csv",
|
|
"log",
|
|
"yml",
|
|
"yaml",
|
|
"ini",
|
|
"conf",
|
|
"sh",
|
|
"bat",
|
|
"bas",
|
|
"ast",
|
|
"gbkb",
|
|
];
|
|
|
|
if (editableTypes.includes(item.type)) return true;
|
|
|
|
const ext = item.name.split(".").pop().toLowerCase();
|
|
return editableExtensions.includes(ext);
|
|
},
|
|
|
|
async editFile(item) {
|
|
if (!this.isEditableFile(item)) {
|
|
alert(`Cannot edit ${item.type} files. Only text files can be edited.`);
|
|
return;
|
|
}
|
|
|
|
this.editorLoading = true;
|
|
this.showEditor = true;
|
|
this.editorFileName = item.name;
|
|
this.editorFilePath = item.path;
|
|
|
|
try {
|
|
const response = await fetch("/files/read", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
bucket: item.bucket || this.currentBucket,
|
|
path: item.path,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || "Failed to read file");
|
|
}
|
|
|
|
const data = await response.json();
|
|
this.editorContent = data.content;
|
|
} catch (err) {
|
|
console.error("Error reading file:", err);
|
|
alert(`Error opening file: ${err.message}`);
|
|
this.showEditor = false;
|
|
} finally {
|
|
this.editorLoading = false;
|
|
}
|
|
},
|
|
|
|
async saveFile() {
|
|
if (!this.editorFilePath) return;
|
|
|
|
this.editorSaving = true;
|
|
|
|
try {
|
|
const response = await fetch("/files/write", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
bucket: this.currentBucket,
|
|
path: this.editorFilePath,
|
|
content: this.editorContent,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || "Failed to save file");
|
|
}
|
|
|
|
alert("File saved successfully!");
|
|
} catch (err) {
|
|
console.error("Error saving file:", err);
|
|
alert(`Error saving file: ${err.message}`);
|
|
} finally {
|
|
this.editorSaving = false;
|
|
}
|
|
},
|
|
|
|
closeEditor() {
|
|
if (
|
|
this.editorContent &&
|
|
confirm("Close editor? Unsaved changes will be lost.")
|
|
) {
|
|
this.showEditor = false;
|
|
this.editorContent = "";
|
|
this.editorFilePath = "";
|
|
this.editorFileName = "";
|
|
} else if (!this.editorContent) {
|
|
this.showEditor = false;
|
|
}
|
|
},
|
|
|
|
async downloadItem(item) {
|
|
window.open(
|
|
`/files/download?bucket=${item.bucket}&path=${item.path}`,
|
|
"_blank",
|
|
);
|
|
},
|
|
|
|
shareItem(item) {
|
|
const shareUrl = `${window.location.origin}/files/share?bucket=${item.bucket}&path=${item.path}`;
|
|
prompt("Share link:", shareUrl);
|
|
},
|
|
|
|
async deleteItem(item) {
|
|
if (!confirm(`Are you sure you want to delete "${item.name}"?`)) return;
|
|
|
|
try {
|
|
const response = await fetch("/files/delete", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
bucket: item.bucket || this.currentBucket,
|
|
path: item.path,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || "Failed to delete");
|
|
}
|
|
|
|
alert("Deleted successfully!");
|
|
this.loadFiles(this.currentBucket, this.currentPath);
|
|
this.selectedItem = null;
|
|
} catch (err) {
|
|
console.error("Error deleting:", err);
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
},
|
|
|
|
async createFolder() {
|
|
const name = prompt("Enter folder name:");
|
|
if (!name) return;
|
|
|
|
try {
|
|
const response = await fetch("/files/create-folder", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
bucket: this.currentBucket,
|
|
path: this.currentPath === "/" ? "" : this.currentPath,
|
|
name: name,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || "Failed to create folder");
|
|
}
|
|
|
|
alert("Folder created!");
|
|
this.loadFiles(this.currentBucket, this.currentPath);
|
|
} catch (err) {
|
|
console.error("Error creating folder:", err);
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
},
|
|
|
|
sizeToBytes(sizeStr) {
|
|
if (!sizeStr || sizeStr === "—") return 0;
|
|
|
|
const units = {
|
|
B: 1,
|
|
KB: 1024,
|
|
MB: 1024 * 1024,
|
|
GB: 1024 * 1024 * 1024,
|
|
TB: 1024 * 1024 * 1024 * 1024,
|
|
};
|
|
|
|
const match = sizeStr.match(/^([\d.]+)\s*([A-Z]+)$/i);
|
|
if (!match) return 0;
|
|
|
|
const value = parseFloat(match[1]);
|
|
const unit = match[2].toUpperCase();
|
|
|
|
return value * (units[unit] || 1);
|
|
},
|
|
|
|
renderChildren(item) {
|
|
return "";
|
|
},
|
|
|
|
init() {
|
|
console.log("✓ Drive component initialized");
|
|
this.loadFiles(null, null);
|
|
|
|
const section = document.querySelector("#section-drive");
|
|
if (section) {
|
|
section.addEventListener("section-shown", () => {
|
|
console.log("Drive section shown");
|
|
this.loadFiles(this.currentBucket, this.currentPath);
|
|
});
|
|
|
|
section.addEventListener("section-hidden", () => {
|
|
console.log("Drive section hidden");
|
|
});
|
|
}
|
|
},
|
|
};
|
|
};
|
|
|
|
console.log("✓ Drive app function registered");
|