feat: implement trash auto-purge, settings layout update, larger folder icons, and legacy version cleanup
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m18s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m18s
This commit is contained in:
parent
c3b1fe8de1
commit
71ba029fbc
8 changed files with 256 additions and 103 deletions
|
|
@ -139,11 +139,9 @@ func (db *DB) migrate() error {
|
||||||
INSERT INTO files_fts(rowid, name, path) VALUES (new.id, new.name, new.path);
|
INSERT INTO files_fts(rowid, name, path) VALUES (new.id, new.name, new.path);
|
||||||
END`,
|
END`,
|
||||||
|
|
||||||
// Default settings
|
|
||||||
`INSERT OR IGNORE INTO settings (key, value) VALUES ('theme', 'dark')`,
|
`INSERT OR IGNORE INTO settings (key, value) VALUES ('theme', 'dark')`,
|
||||||
`INSERT OR IGNORE INTO settings (key, value) VALUES ('thumbnail_images', 'true')`,
|
`INSERT OR IGNORE INTO settings (key, value) VALUES ('thumbnail_images', 'true')`,
|
||||||
`INSERT OR IGNORE INTO settings (key, value) VALUES ('thumbnail_videos', 'true')`,
|
`INSERT OR IGNORE INTO settings (key, value) VALUES ('thumbnail_videos', 'true')`,
|
||||||
`INSERT OR IGNORE INTO settings (key, value) VALUES ('max_versions', '5')`,
|
|
||||||
`INSERT OR IGNORE INTO settings (key, value) VALUES ('trash_auto_purge_days', '30')`,
|
`INSERT OR IGNORE INTO settings (key, value) VALUES ('trash_auto_purge_days', '30')`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ func (h *SettingsHandler) UpdateSettings(c *fiber.Ctx) error {
|
||||||
"theme": true,
|
"theme": true,
|
||||||
"thumbnail_images": true,
|
"thumbnail_images": true,
|
||||||
"thumbnail_videos": true,
|
"thumbnail_videos": true,
|
||||||
"max_versions": true,
|
|
||||||
"trash_auto_purge_days": true,
|
"trash_auto_purge_days": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ func main() {
|
||||||
workers.InitTaskManager(cfg)
|
workers.InitTaskManager(cfg)
|
||||||
workers.InitThumbnailManager(cfg, db)
|
workers.InitThumbnailManager(cfg, db)
|
||||||
workers.StartBackupWorker(cfg, db)
|
workers.StartBackupWorker(cfg, db)
|
||||||
|
workers.StartTrashWorker(cfg, db)
|
||||||
|
|
||||||
// Create Fiber app
|
// Create Fiber app
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
|
|
|
||||||
85
backend/workers/trash.go
Normal file
85
backend/workers/trash.go
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package workers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.elijahkuntz.com/Elijah/drive/config"
|
||||||
|
"git.elijahkuntz.com/Elijah/drive/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StartTrashWorker runs a periodic task to permanently delete old items from the trash
|
||||||
|
func StartTrashWorker(cfg *config.Config, db *database.DB) {
|
||||||
|
ticker := time.NewTicker(24 * time.Hour)
|
||||||
|
go func() {
|
||||||
|
for range ticker.C {
|
||||||
|
runTrashPurge(cfg, db)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Also run one purge 5 minutes after startup
|
||||||
|
time.AfterFunc(5*time.Minute, func() {
|
||||||
|
runTrashPurge(cfg, db)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTrashPurge(cfg *config.Config, db *database.DB) {
|
||||||
|
purgeDaysStr, err := db.GetSetting("trash_auto_purge_days")
|
||||||
|
if err != nil {
|
||||||
|
purgeDaysStr = "30" // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
purgeDays, err := strconv.Atoi(purgeDaysStr)
|
||||||
|
if err != nil {
|
||||||
|
purgeDays = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
if purgeDays <= 0 {
|
||||||
|
return // 0 means disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("🗑️ Running trash auto-purge (items older than %d days)...", purgeDays)
|
||||||
|
|
||||||
|
rows, err := db.Query(
|
||||||
|
"SELECT id, path FROM files WHERE is_trashed = 1 AND trashed_at <= date('now', '-' || ? || ' days')",
|
||||||
|
purgeDays,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ Failed to query old trash items: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var deletedCount int
|
||||||
|
for rows.Next() {
|
||||||
|
var id int
|
||||||
|
var path string
|
||||||
|
if err := rows.Scan(&id, &path); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
trashPath := filepath.Join(cfg.TrashDir, filepath.FromSlash(path))
|
||||||
|
|
||||||
|
// Remove physical file/directory
|
||||||
|
if err := os.RemoveAll(trashPath); err != nil {
|
||||||
|
log.Printf("❌ Failed to delete physical trash file %s: %v", trashPath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from DB
|
||||||
|
if _, err := db.Exec("DELETE FROM files WHERE id = ?", id); err != nil {
|
||||||
|
log.Printf("❌ Failed to delete DB record for %s: %v", path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if deletedCount > 0 {
|
||||||
|
log.Printf("✅ Trash auto-purge completed: deleted %d old items.", deletedCount)
|
||||||
|
db.AddAuditLog("trash_auto_purge", strconv.Itoa(deletedCount)+" items permanently deleted", "system")
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/next-env.d.ts
vendored
Normal file
6
frontend/next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
/// <reference path="./.next/types/routes.d.ts" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
Folder, File, Search, Pin, Trash2, HardDrive, Settings,
|
Folder, File, Search, Pin, Trash2, HardDrive, Settings,
|
||||||
|
|
@ -25,6 +25,7 @@ interface FileItem {
|
||||||
is_pinned: boolean;
|
is_pinned: boolean;
|
||||||
is_trashed: boolean;
|
is_trashed: boolean;
|
||||||
mod_time: string;
|
mod_time: string;
|
||||||
|
trashed_at?: string;
|
||||||
checksum?: string;
|
checksum?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,12 +97,14 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
|
|
||||||
const [gridSize, setGridSize] = useState('200');
|
const [gridSize, setGridSize] = useState('200');
|
||||||
const [theme, setTheme] = useState('dark');
|
const [theme, setTheme] = useState('dark');
|
||||||
|
const [trashAutoPurgeDays, setTrashAutoPurgeDays] = useState(30);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.getSettings().then((data) => {
|
api.getSettings().then((data) => {
|
||||||
const s = data?.settings || data;
|
const s = data?.settings || data;
|
||||||
if (s) {
|
if (s) {
|
||||||
if (s.grid_size) setGridSize(s.grid_size);
|
if (s.grid_size) setGridSize(s.grid_size);
|
||||||
|
if (s.trash_auto_purge_days) setTrashAutoPurgeDays(parseInt(s.trash_auto_purge_days, 10));
|
||||||
const fetchedTheme = s.theme || 'dark';
|
const fetchedTheme = s.theme || 'dark';
|
||||||
setTheme(fetchedTheme);
|
setTheme(fetchedTheme);
|
||||||
if (fetchedTheme === 'light') {
|
if (fetchedTheme === 'light') {
|
||||||
|
|
@ -228,6 +231,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
setFiles(data.items?.map((item: any) => ({
|
setFiles(data.items?.map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
is_trashed: true,
|
is_trashed: true,
|
||||||
|
trashed_at: item.trashed_at,
|
||||||
})) || []);
|
})) || []);
|
||||||
} catch {
|
} catch {
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
|
|
@ -470,7 +474,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
if (file.is_dir) {
|
if (file.is_dir) {
|
||||||
if (large) {
|
if (large) {
|
||||||
return (
|
return (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-accent)" className="w-20 h-20 opacity-90 drop-shadow-sm">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-accent)" className="w-28 h-28 opacity-90 drop-shadow-sm">
|
||||||
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
|
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
@ -554,6 +558,60 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
return sortAsc ? cmp : -cmp;
|
return sortAsc ? cmp : -cmp;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const groupedFiles = useMemo(() => {
|
||||||
|
const baseFiles = displayFiles.filter(f => activeSection !== 'files' || !f.is_pinned);
|
||||||
|
if (activeSection !== 'trash' || trashAutoPurgeDays <= 0) {
|
||||||
|
const defaultName = activeSection === 'files' && displayFiles.some(f => f.is_pinned) ? 'All Files' : '';
|
||||||
|
return [{ name: defaultName, files: baseFiles }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
||||||
|
|
||||||
|
const groupsMap: { [key: string]: FileItem[] } = {};
|
||||||
|
const groupSortValue: { [key: string]: number } = {};
|
||||||
|
|
||||||
|
baseFiles.forEach(f => {
|
||||||
|
if (!f.trashed_at) {
|
||||||
|
if (!groupsMap['Unknown']) groupsMap['Unknown'] = [];
|
||||||
|
groupsMap['Unknown'].push(f);
|
||||||
|
groupSortValue['Unknown'] = 999;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tStr = f.trashed_at;
|
||||||
|
if (tStr.includes(' ')) tStr = tStr.replace(' ', 'T');
|
||||||
|
const trashedDate = new Date(tStr);
|
||||||
|
if (isNaN(trashedDate.getTime())) {
|
||||||
|
if (!groupsMap['Unknown']) groupsMap['Unknown'] = [];
|
||||||
|
groupsMap['Unknown'].push(f);
|
||||||
|
groupSortValue['Unknown'] = 999;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trashedDay = new Date(trashedDate.getFullYear(), trashedDate.getMonth(), trashedDate.getDate()).getTime();
|
||||||
|
|
||||||
|
if (trashedDay === today) {
|
||||||
|
if (!groupsMap['Deleted Today']) groupsMap['Deleted Today'] = [];
|
||||||
|
groupsMap['Deleted Today'].push(f);
|
||||||
|
groupSortValue['Deleted Today'] = -1;
|
||||||
|
} else {
|
||||||
|
const expirationDate = trashedDate.getTime() + (trashAutoPurgeDays * 24 * 60 * 60 * 1000);
|
||||||
|
const daysRemaining = Math.ceil((expirationDate - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
let groupName = `Deletes in ${daysRemaining} days`;
|
||||||
|
if (daysRemaining === 1) groupName = `Deletes in 1 day`;
|
||||||
|
else if (daysRemaining <= 0) groupName = `Pending Deletion`;
|
||||||
|
|
||||||
|
if (!groupsMap[groupName]) groupsMap[groupName] = [];
|
||||||
|
groupsMap[groupName].push(f);
|
||||||
|
groupSortValue[groupName] = daysRemaining;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedGroupNames = Object.keys(groupsMap).sort((a, b) => groupSortValue[a] - groupSortValue[b]);
|
||||||
|
return sortedGroupNames.map(name => ({ name, files: groupsMap[name] }));
|
||||||
|
}, [displayFiles, activeSection, trashAutoPurgeDays]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
<div className="flex h-screen" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
||||||
{/* === SIDEBAR === */}
|
{/* === SIDEBAR === */}
|
||||||
|
|
@ -602,12 +660,14 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="pt-4">
|
</nav>
|
||||||
<div className="h-px mx-2 mb-4" style={{ backgroundColor: 'var(--color-border-subtle)' }} />
|
|
||||||
|
{/* Settings Footer */}
|
||||||
|
<div className="px-3 py-4 border-t" style={{ borderColor: 'var(--color-border-subtle)' }}>
|
||||||
<button
|
<button
|
||||||
id="nav-settings"
|
id="nav-settings"
|
||||||
onClick={() => handleSectionChange('settings')}
|
onClick={() => handleSectionChange('settings')}
|
||||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150"
|
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150 hover:opacity-80"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: activeSection === 'settings' ? 'var(--color-accent-subtle)' : 'transparent',
|
backgroundColor: activeSection === 'settings' ? 'var(--color-accent-subtle)' : 'transparent',
|
||||||
color: activeSection === 'settings' ? 'var(--color-accent)' : 'var(--color-text-secondary)',
|
color: activeSection === 'settings' ? 'var(--color-accent)' : 'var(--color-text-secondary)',
|
||||||
|
|
@ -617,20 +677,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
Settings
|
Settings
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Logout */}
|
|
||||||
<div className="px-3 py-4 border-t" style={{ borderColor: 'var(--color-border-subtle)' }}>
|
|
||||||
<button
|
|
||||||
id="logout-button"
|
|
||||||
onClick={onLogout}
|
|
||||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150 hover:opacity-80"
|
|
||||||
style={{ color: 'var(--color-danger)' }}
|
|
||||||
>
|
|
||||||
<LogOut className="w-4.5 h-4.5" />
|
|
||||||
Sign Out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* === MAIN CONTENT === */}
|
{/* === MAIN CONTENT === */}
|
||||||
|
|
@ -674,6 +720,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Bar */}
|
{/* Search Bar */}
|
||||||
|
{activeSection !== 'settings' && (
|
||||||
<div className="relative w-72">
|
<div className="relative w-72">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
|
||||||
<input
|
<input
|
||||||
|
|
@ -698,6 +745,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -805,7 +853,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
|
|
||||||
{/* Dynamic Content Area */}
|
{/* Dynamic Content Area */}
|
||||||
{activeSection === 'settings' ? (
|
{activeSection === 'settings' ? (
|
||||||
<SettingsPage />
|
<SettingsPage onLogout={onLogout} />
|
||||||
) : activeSection === 'storage' ? (
|
) : activeSection === 'storage' ? (
|
||||||
<StorageDashboard />
|
<StorageDashboard />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -974,11 +1022,13 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{activeSection === 'files' && displayFiles.some(f => f.is_pinned) && (
|
{groupedFiles.map((group, groupIdx) => (
|
||||||
<h3 className="text-xs font-semibold uppercase tracking-wider mb-3 px-1" style={{ color: 'var(--color-text-tertiary)' }}>All Files</h3>
|
<div key={group.name} className={groupIdx > 0 || (activeSection === 'files' && displayFiles.some(f => f.is_pinned)) ? 'mt-8' : ''}>
|
||||||
|
{group.name && (
|
||||||
|
<h3 className="text-xs font-semibold uppercase tracking-wider mb-3 px-1" style={{ color: 'var(--color-text-tertiary)' }}>{group.name}</h3>
|
||||||
)}
|
)}
|
||||||
<div className="grid gap-3" style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${gridSize}px, 1fr))` }}>
|
<div className="grid gap-3" style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${gridSize}px, 1fr))` }}>
|
||||||
{displayFiles.filter(f => activeSection !== 'files' || !f.is_pinned).map((file, idx) => (
|
{group.files.map((file, idx) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={file.path}
|
key={file.path}
|
||||||
draggable
|
draggable
|
||||||
|
|
@ -1050,6 +1100,8 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* List View */
|
/* List View */
|
||||||
|
|
@ -1118,14 +1170,19 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
{groupedFiles.map((group, groupIdx) => (
|
||||||
|
<div key={group.name} className={groupIdx > 0 || (activeSection === 'files' && displayFiles.some(f => f.is_pinned)) ? 'mt-8' : ''}>
|
||||||
|
{group.name && (
|
||||||
|
<h3 className="text-xs font-semibold uppercase tracking-wider mb-2 px-4" style={{ color: 'var(--color-text-tertiary)' }}>{group.name}</h3>
|
||||||
|
)}
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="grid grid-cols-[1fr_100px_140px_40px] gap-4 px-4 py-2 text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--color-text-tertiary)' }}>
|
<div className="grid grid-cols-[1fr_100px_140px_40px] gap-4 px-4 py-2 text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||||
<span>{activeSection === 'files' && displayFiles.some(f => f.is_pinned) ? 'All Files' : 'Name'}</span>
|
<span>Name</span>
|
||||||
<span>Size</span>
|
<span>Size</span>
|
||||||
<span>Modified</span>
|
<span>Modified</span>
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
{displayFiles.filter(f => activeSection !== 'files' || !f.is_pinned).map((file, idx) => (
|
{group.files.map((file, idx) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={file.path}
|
key={file.path}
|
||||||
draggable
|
draggable
|
||||||
|
|
@ -1182,6 +1239,8 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Settings, Shield, Image as ImageIcon, History, Trash2, Save, Loader2 } from 'lucide-react';
|
import { Settings, Shield, Image as ImageIcon, History, Trash2, Save, Loader2, LogOut } from 'lucide-react';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
|
|
||||||
interface SettingsData {
|
interface SettingsData {
|
||||||
|
|
@ -10,11 +10,14 @@ interface SettingsData {
|
||||||
grid_size: string;
|
grid_size: string;
|
||||||
thumbnail_images: string;
|
thumbnail_images: string;
|
||||||
thumbnail_videos: string;
|
thumbnail_videos: string;
|
||||||
max_versions: string;
|
|
||||||
trash_auto_purge_days: string;
|
trash_auto_purge_days: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsPage() {
|
interface SettingsPageProps {
|
||||||
|
onLogout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SettingsPage({ onLogout }: SettingsPageProps) {
|
||||||
const [settings, setSettings] = useState<SettingsData | null>(null);
|
const [settings, setSettings] = useState<SettingsData | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
@ -54,7 +57,6 @@ export default function SettingsPage() {
|
||||||
grid_size: s.grid_size || '200',
|
grid_size: s.grid_size || '200',
|
||||||
thumbnail_images: s.thumbnail_images || 'true',
|
thumbnail_images: s.thumbnail_images || 'true',
|
||||||
thumbnail_videos: s.thumbnail_videos || 'true',
|
thumbnail_videos: s.thumbnail_videos || 'true',
|
||||||
max_versions: s.max_versions || '1',
|
|
||||||
trash_auto_purge_days: s.trash_auto_purge_days || '14',
|
trash_auto_purge_days: s.trash_auto_purge_days || '14',
|
||||||
};
|
};
|
||||||
setSettings(loaded);
|
setSettings(loaded);
|
||||||
|
|
@ -227,30 +229,7 @@ export default function SettingsPage() {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'versioning',
|
|
||||||
title: 'File Versioning',
|
|
||||||
icon: <History className="w-5 h-5" />,
|
|
||||||
content: (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-primary)' }}>Maximum Versions to Keep</label>
|
|
||||||
<p className="text-xs mb-3" style={{ color: 'var(--color-text-secondary)' }}>
|
|
||||||
Older versions will be automatically deleted when a file is overwritten more times than this limit. Set to 0 to disable versioning.
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
value={settings.max_versions}
|
|
||||||
onChange={(e) => setSettings({ ...settings, max_versions: e.target.value })}
|
|
||||||
className="w-full max-w-xs px-3 py-2 rounded-lg text-sm outline-none"
|
|
||||||
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'trash',
|
id: 'trash',
|
||||||
title: 'Trash Auto-Purge',
|
title: 'Trash Auto-Purge',
|
||||||
|
|
@ -322,7 +301,16 @@ export default function SettingsPage() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="mt-8 flex justify-end">
|
<div className="mt-8 flex justify-between items-center">
|
||||||
|
<button
|
||||||
|
onClick={onLogout}
|
||||||
|
className="flex items-center gap-2 px-6 py-2.5 rounded-xl text-sm font-medium transition-all hover:opacity-80"
|
||||||
|
style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', color: 'var(--color-danger)' }}
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
Sign Out
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
@ -13,11 +17,24 @@
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [{ "name": "next" }],
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"next-env.d.ts",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Reference in a new issue