Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/lib/cloud/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ export class CloudSyncService {
// Encrypt file client-side
const encrypted = await encryptFile(fileData.buffer, password, user.salt);

// Check remaining quota before attempting upload
const quota = await this.getStorageQuota();
const remainingBytes = quota.quota - quota.used;

if (encrypted.encryptedData.byteLength > remainingBytes) {
const requiredMb = (encrypted.encryptedData.byteLength / (1024 * 1024)).toFixed(1);
const remainingMb = (remainingBytes / (1024 * 1024)).toFixed(1);
throw new Error(
`Not enough storage left to upload this file. Requires ${requiredMb} MB, but only ${remainingMb} MB is available.`
);
}

// Request upload URL
const requestResponse = await this.authSession.authenticatedFetch(
'/api/files/upload/request',
Expand Down
100 changes: 100 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1752,6 +1752,106 @@ body {
color: #ff9800;
}

.storage-badge {
align-items: center;
background: #fef3c7;
border: 2px solid #111;
border-radius: 999px;
box-shadow: 4px 4px 0 #111;
color: #111;
display: inline-flex;
font-size: 0.85rem;
font-weight: 800;
gap: 6px;
letter-spacing: -0.02em;
margin-bottom: 10px;
padding: 6px 10px;
text-transform: uppercase;
}

.quota-card {
background: #fff;
border: 2px solid #111;
border-radius: 12px;
box-shadow: 6px 6px 0 #111;
padding: 14px 16px;
}

.quota-card__header {
align-items: center;
display: flex;
justify-content: space-between;
gap: 12px;
}

.quota-label {
color: #111;
font-size: 0.9rem;
font-weight: 700;
letter-spacing: -0.01em;
text-transform: uppercase;
}

.quota-values {
color: #374151;
font-size: 0.85rem;
margin-top: 4px;
}

.quota-chip {
background: #d1fae5;
border: 2px solid #111;
border-radius: 999px;
box-shadow: 4px 4px 0 #111;
color: #065f46;
font-size: 0.9rem;
font-weight: 800;
padding: 6px 10px;
}

.quota-progress.brutalist {
background: #fff;
border: 2px solid #111;
border-radius: 10px;
box-shadow: 4px 4px 0 #111;
height: 12px;
margin: 12px 0;
overflow: hidden;
}

.quota-progress__fill {
background: linear-gradient(90deg, #111827 0%, #f59e0b 40%, #ef4444 100%);
height: 100%;
}

.quota-meta {
align-items: center;
color: #111;
display: flex;
font-size: 0.9rem;
justify-content: space-between;
}

.quota-card.loading .quota-progress__fill,
.quota-card.loading .skeleton-text,
.quota-card.loading .skeleton-pill {
animation: pulse 1.4s ease-in-out infinite;
}

.skeleton-text {
background: #e5e7eb;
border-radius: 4px;
display: inline-block;
height: 12px;
}

.skeleton-pill {
background: #e5e7eb;
border-radius: 999px;
display: inline-block;
height: 18px;
}

/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
Expand Down
45 changes: 41 additions & 4 deletions src/ui/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { Settings } from './components/Settings';
import { MLDownloadPrompt } from './components/MLDownloadPrompt';
import { TextViewer } from './components/TextViewer';
import { SanitizeOptionsModal } from './components/SanitizeOptions';
import { AuthSession } from '../lib/auth/session.js';
import { CloudSyncService, type StorageQuota } from '../lib/cloud/sync.js';
import { AuthModal } from './components/auth/AuthModal.js';
import { UserMenu } from './components/auth/UserMenu.js';
import { Dashboard } from './components/Dashboard.js';
import { themeManager } from '../lib/theme/ThemeManager';

// Lazy-loaded modules (code splitting)
Expand Down Expand Up @@ -70,6 +75,7 @@ export class App {
private authSession: AuthSession;
private cloudSync: CloudSyncService | null = null;
private userMenu: UserMenu | null = null;
private storageQuota: StorageQuota | null = null;

private files: FileItem[] = [];
private currentFileIndex: number = -1;
Expand Down Expand Up @@ -119,7 +125,7 @@ export class App {
() => this.handleNewFile(),
() => this.openSettings(),
() => this.handleShowAuth(),
() => this.handleShowDashboard(),
() => void this.handleShowDashboard(),
() => this.handleBatchExport()
);

Expand Down Expand Up @@ -1452,10 +1458,25 @@ export class App {
this.userMenu = new UserMenu(
user,
() => void this.handleLogout(),
() => this.handleShowDashboard()
() => void this.handleShowDashboard(),
this.storageQuota
);

this.toolbar.showUserMenu(this.userMenu.getElement());
void this.fetchAndApplyQuota();
}

private async fetchAndApplyQuota(): Promise<StorageQuota> {
if (!this.cloudSync) {
throw new Error('Cloud sync not initialized');
}

const quota = await this.cloudSync.getStorageQuota();
this.storageQuota = quota;
const remaining = Math.max(quota.quota - quota.used, 0);
this.toolbar.updateStorageBadge(remaining, quota.quota);
this.userMenu?.updateQuota(quota);
return quota;
}

/**
Expand Down Expand Up @@ -1497,7 +1518,9 @@ export class App {
await this.authSession.logout();
this.cloudSync = null;
this.userMenu = null;
this.storageQuota = null;
this.toolbar.showLoginButton();
this.toolbar.updateStorageBadge(null);
this.toast.info('Signed out');
} catch (error) {
console.error('Logout error:', error);
Expand All @@ -1508,12 +1531,20 @@ export class App {
/**
* Show cloud file dashboard
*/
private handleShowDashboard(): void {
private async handleShowDashboard(): Promise<void> {
if (!this.cloudSync) {
this.toast.error('Cloud sync not initialized');
return;
}

let quota: StorageQuota | null = null;
try {
quota = await this.fetchAndApplyQuota();
} catch (error) {
console.error('Quota refresh error:', error);
this.toast.error('Unable to refresh storage info');
}

const dashboard = new Dashboard(
() => dashboard.hide(),
async (fileId) => {
Expand Down Expand Up @@ -1548,7 +1579,13 @@ export class App {
return [];
}
},
this.authSession
async () => {
if (!this.cloudSync) {
throw new Error('Cloud sync not initialized');
}
return this.fetchAndApplyQuota();
},
quota
);

dashboard.show();
Expand Down
Loading
Loading