added1
Browse files
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
|
| 168 |
</Text>
|
| 169 |
<Text style={styles.settingDesc}>
|
| 170 |
-
{(storageStats.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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = () => {
|