dpv007 commited on
Commit
d3dfeb4
·
1 Parent(s): 3fdd49b
keystone-app/app/(tabs)/index.tsx CHANGED
@@ -160,6 +160,13 @@ const GalleryRow = React.memo(({
160
  return prevSelected.every((s, i) => s === nextSelected[i]);
161
  });
162
 
 
 
 
 
 
 
 
163
  // ─── Main component ──────────────────────────────────────────────
164
  export default function PhotosScreen() {
165
  const insets = useSafeAreaInsets();
@@ -435,6 +442,8 @@ export default function PhotosScreen() {
435
  }
436
  }, [groupAssetsByDate]);
437
 
 
 
438
  const syncAssets = async () => {
439
  if (isSyncing || itemsToBackup === 0) return;
440
 
@@ -899,6 +908,12 @@ export default function PhotosScreen() {
899
  {allAssets[viewerIndex].width} x {allAssets[viewerIndex].height}
900
  </Text>
901
  </View>
 
 
 
 
 
 
902
  {allAssets[viewerIndex].isCloudOnly ? (
903
  <View style={styles.infoRow}>
904
  <MaterialIcons name="cloud" size={20} color="#1a73e8" />
 
160
  return prevSelected.every((s, i) => s === nextSelected[i]);
161
  });
162
 
163
+ // Helper function to format file size
164
+ const formatSize = (bytes?: number) => {
165
+ if (!bytes) return 'Unknown size';
166
+ const mb = bytes / (1024 * 1024);
167
+ return mb >= 1 ? `${mb.toFixed(1)} MB` : `${(bytes / 1024).toFixed(0)} KB`;
168
+ };
169
+
170
  // ─── Main component ──────────────────────────────────────────────
171
  export default function PhotosScreen() {
172
  const insets = useSafeAreaInsets();
 
442
  }
443
  }, [groupAssetsByDate]);
444
 
445
+
446
+
447
  const syncAssets = async () => {
448
  if (isSyncing || itemsToBackup === 0) return;
449
 
 
908
  {allAssets[viewerIndex].width} x {allAssets[viewerIndex].height}
909
  </Text>
910
  </View>
911
+ <View style={styles.infoRow}>
912
+ <MaterialIcons name="sd-storage" size={20} color="#5f6368" />
913
+ <Text style={styles.infoText}>
914
+ {formatSize(allAssets[viewerIndex].size)}
915
+ </Text>
916
+ </View>
917
  {allAssets[viewerIndex].isCloudOnly ? (
918
  <View style={styles.infoRow}>
919
  <MaterialIcons name="cloud" size={20} color="#1a73e8" />
keystone-app/app/(tabs)/search.tsx CHANGED
@@ -54,6 +54,12 @@ export default function SearchScreen() {
54
  const [searched, setSearched] = useState(false);
55
  const [viewerAsset, setViewerAsset] = useState<MediaLibrary.Asset | null>(null);
56
 
 
 
 
 
 
 
57
  const runSearch = useCallback(async (q: string, type: FilterType, range: QuickRange | null) => {
58
  setIsSearching(true);
59
  setSearched(true);
@@ -292,7 +298,7 @@ export default function SearchScreen() {
292
  {viewerAsset && (
293
  <View style={{ position: 'absolute', bottom: 40, left: 0, right: 0, alignItems: 'center' }}>
294
  <Text style={{ color: '#fff', fontSize: 13, backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16 }}>
295
- {viewerAsset.filename} · {format(new Date(viewerAsset.creationTime), 'MMM d, yyyy')}
296
  </Text>
297
  </View>
298
  )}
 
54
  const [searched, setSearched] = useState(false);
55
  const [viewerAsset, setViewerAsset] = useState<MediaLibrary.Asset | null>(null);
56
 
57
+ const formatSize = (bytes?: number) => {
58
+ if (!bytes) return '';
59
+ const mb = bytes / (1024 * 1024);
60
+ return ` · ${mb >= 1 ? `${mb.toFixed(1)} MB` : `${(bytes / 1024).toFixed(0)} KB`}`;
61
+ };
62
+
63
  const runSearch = useCallback(async (q: string, type: FilterType, range: QuickRange | null) => {
64
  setIsSearching(true);
65
  setSearched(true);
 
298
  {viewerAsset && (
299
  <View style={{ position: 'absolute', bottom: 40, left: 0, right: 0, alignItems: 'center' }}>
300
  <Text style={{ color: '#fff', fontSize: 13, backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16 }}>
301
+ {viewerAsset.filename} · {format(new Date(viewerAsset.creationTime), 'MMM d, yyyy')}{formatSize((viewerAsset as any).size)}
302
  </Text>
303
  </View>
304
  )}
keystone-app/app/(tabs)/settings.tsx CHANGED
@@ -9,6 +9,14 @@ import { useAuth } from '../../components/AuthContext';
9
 
10
  export const SYNC_ON_CELLULAR_KEY = '@setting_sync_cellular';
11
 
 
 
 
 
 
 
 
 
12
  export default function SettingsScreen() {
13
  const { logout, username } = useAuth();
14
  const [syncOnCellular, setSyncOnCellular] = useState(false);
@@ -164,10 +172,10 @@ export default function SettingsScreen() {
164
  <View style={{ padding: 16 }}>
165
  <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 8 }}>
166
  <Text style={styles.settingLabel}>
167
- {(storageStats.used / (1024 * 1024 * 1024)).toFixed(1)} GB used
168
  </Text>
169
  <Text style={styles.settingDesc}>
170
- {(storageStats.free / (1024 * 1024 * 1024)).toFixed(1)} GB free
171
  </Text>
172
  </View>
173
  <View style={{ height: 8, backgroundColor: '#e8f0fe', borderRadius: 4, overflow: 'hidden' }}>
 
9
 
10
  export const SYNC_ON_CELLULAR_KEY = '@setting_sync_cellular';
11
 
12
+ const formatStorage = (bytes: number) => {
13
+ const gb = bytes / (1024 * 1024 * 1024);
14
+ if (gb > 100) {
15
+ return `${(gb / 1024).toFixed(2)} TB`;
16
+ }
17
+ return `${gb.toFixed(1)} GB`;
18
+ };
19
+
20
  export default function SettingsScreen() {
21
  const { logout, username } = useAuth();
22
  const [syncOnCellular, setSyncOnCellular] = useState(false);
 
172
  <View style={{ padding: 16 }}>
173
  <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 8 }}>
174
  <Text style={styles.settingLabel}>
175
+ {formatStorage(storageStats.used)} used
176
  </Text>
177
  <Text style={styles.settingDesc}>
178
+ {formatStorage(storageStats.free)} free
179
  </Text>
180
  </View>
181
  <View style={{ height: 8, backgroundColor: '#e8f0fe', borderRadius: 4, overflow: 'hidden' }}>
keystone-backend/main.py CHANGED
@@ -13,6 +13,7 @@ import tempfile
13
  from fastapi.responses import FileResponse, HTMLResponse
14
  import zipfile
15
  import io
 
16
 
17
  # Load local environment variables
18
  load_dotenv()
 
13
  from fastapi.responses import FileResponse, HTMLResponse
14
  import zipfile
15
  import io
16
+ import asyncio
17
 
18
  # Load local environment variables
19
  load_dotenv()
keystone-backend/web.html CHANGED
@@ -450,9 +450,20 @@
450
 
451
  const triggerDownload = (itemPath) => {
452
  const token = localStorage.getItem('token');
453
- // We must pass token in URL since we are opening it in a new tab
454
  const url = `${API_BASE}/api/web/download?path=${encodeURIComponent(itemPath)}&token=${token}`;
455
- window.open(url, '_blank');
 
 
 
 
 
 
 
 
 
 
 
 
456
  };
457
 
458
  const renderBreadcrumbs = () => {
 
450
 
451
  const triggerDownload = (itemPath) => {
452
  const token = localStorage.getItem('token');
 
453
  const url = `${API_BASE}/api/web/download?path=${encodeURIComponent(itemPath)}&token=${token}`;
454
+
455
+ // Create an invisible anchor tag to trigger native browser download stream
456
+ // This is safer for large files/zip folders than window.open or fetch->blob
457
+ const a = document.createElement('a');
458
+ a.href = url;
459
+
460
+ const parts = itemPath.split('/');
461
+ const fallbackName = parts[parts.length - 1] || 'download';
462
+ a.download = fallbackName;
463
+
464
+ document.body.appendChild(a);
465
+ a.click();
466
+ document.body.removeChild(a);
467
  };
468
 
469
  const renderBreadcrumbs = () => {