qdkmkkk
Browse files
keystone-app/app/(tabs)/index.tsx
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
| 13 |
Modal,
|
| 14 |
AppState,
|
| 15 |
FlatList,
|
| 16 |
-
PanResponder,
|
| 17 |
} from 'react-native';
|
| 18 |
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
| 19 |
import * as MediaLibrary from 'expo-media-library';
|
|
@@ -84,9 +83,6 @@ export default function PhotosScreen() {
|
|
| 84 |
const isSelecting = selectedIds.size > 0;
|
| 85 |
|
| 86 |
// Drag-to-select
|
| 87 |
-
const isDraggingRef = useRef(false);
|
| 88 |
-
const dragModeRef = useRef<'add' | 'remove'>('add'); // are we adding or removing?
|
| 89 |
-
const tileLayoutsRef = useRef<Map<string, { x: number; y: number; width: number; height: number }>>(new Map());
|
| 90 |
const listOffsetYRef = useRef(0);
|
| 91 |
|
| 92 |
// Sync & Network States
|
|
@@ -394,26 +390,6 @@ export default function PhotosScreen() {
|
|
| 394 |
return ids.length > 0 && ids.every(id => selectedIds.has(id));
|
| 395 |
}, [selectedIds]);
|
| 396 |
|
| 397 |
-
// Register tile layout (called in each tile's onLayout)
|
| 398 |
-
const registerTileLayout = useCallback((id: string, x: number, y: number, w: number, h: number) => {
|
| 399 |
-
tileLayoutsRef.current.set(id, { x, y, width: w, height: h });
|
| 400 |
-
}, []);
|
| 401 |
-
|
| 402 |
-
// Hit-test a point against registered tiles, return matching id
|
| 403 |
-
const getTileAtPoint = useCallback((px: number, py: number): string | null => {
|
| 404 |
-
const scrollY = listOffsetYRef.current;
|
| 405 |
-
const adjustedY = py + scrollY;
|
| 406 |
-
for (const [id, layout] of tileLayoutsRef.current.entries()) {
|
| 407 |
-
if (
|
| 408 |
-
px >= layout.x && px <= layout.x + layout.width &&
|
| 409 |
-
adjustedY >= layout.y && adjustedY <= layout.y + layout.height
|
| 410 |
-
) {
|
| 411 |
-
return id;
|
| 412 |
-
}
|
| 413 |
-
}
|
| 414 |
-
return null;
|
| 415 |
-
}, []);
|
| 416 |
-
|
| 417 |
const openViewer = (asset: UnifiedAsset) => {
|
| 418 |
if (isSelecting) { toggleSelect(asset.id); return; }
|
| 419 |
const idx = allAssets.findIndex(a => a.id === asset.id);
|
|
@@ -445,120 +421,71 @@ export default function PhotosScreen() {
|
|
| 445 |
};
|
| 446 |
|
| 447 |
// ── Render helpers ────────────────────────────────────────────
|
| 448 |
-
const renderRow = useCallback(({ item: row }: { item: UnifiedAsset[] }) =>
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
// onLayout gives position relative to the tile's parent View,
|
| 498 |
-
// so we need to measure against the page
|
| 499 |
-
}}
|
| 500 |
-
ref={(ref) => {
|
| 501 |
-
if (ref) {
|
| 502 |
-
(ref as any).measure((fx: number, fy: number, w: number, h: number, px: number, py: number) => {
|
| 503 |
-
registerTileLayout(asset.id, px, py, w, h);
|
| 504 |
-
});
|
| 505 |
}
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
<
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
</Text>
|
| 522 |
-
)}
|
| 523 |
-
</View>
|
| 524 |
-
)}
|
| 525 |
-
|
| 526 |
-
{/* Unsynced Cloud Indicator */}
|
| 527 |
-
{!asset.isSynced && !asset.isCloudOnly && (
|
| 528 |
-
<View style={styles.unsyncedOverlay}>
|
| 529 |
-
<MaterialIcons name="cloud-off" size={16} color="rgba(255,255,255,0.8)" />
|
| 530 |
-
</View>
|
| 531 |
-
)}
|
| 532 |
-
|
| 533 |
-
{/* Cloud-Only Indicator */}
|
| 534 |
-
{asset.isCloudOnly && (
|
| 535 |
-
<View style={styles.cloudOnlyOverlay}>
|
| 536 |
-
<MaterialIcons name="cloud" size={16} color="rgba(255,255,255,0.8)" />
|
| 537 |
-
</View>
|
| 538 |
-
)}
|
| 539 |
-
{/* Selection check circle — always show in selection mode */}
|
| 540 |
-
{isSelecting && (
|
| 541 |
-
<View style={styles.selectionCircleOverlay}>
|
| 542 |
-
{selected
|
| 543 |
-
? <MaterialIcons name="check-circle" size={22} color="#1a73e8" />
|
| 544 |
-
: <MaterialIcons name="radio-button-unchecked" size={22} color="rgba(255,255,255,0.8)" />
|
| 545 |
-
}
|
| 546 |
-
</View>
|
| 547 |
-
)}
|
| 548 |
-
{!isSelecting && selected && (
|
| 549 |
-
<View style={styles.selectedOverlay}>
|
| 550 |
-
<MaterialIcons name="check-circle" size={22} color="#1a73e8" />
|
| 551 |
-
</View>
|
| 552 |
-
)}
|
| 553 |
-
</TouchableOpacity>
|
| 554 |
-
);
|
| 555 |
-
})}
|
| 556 |
-
{Array.from({ length: NUM_COLUMNS - row.length }).map((_, i) => (
|
| 557 |
-
<View key={`empty-${i}`} style={styles.tileFiller} />
|
| 558 |
-
))}
|
| 559 |
-
</View>
|
| 560 |
-
);
|
| 561 |
-
}, [selectedIds, isSelecting, getTileAtPoint, registerTileLayout]);
|
| 562 |
|
| 563 |
const renderSectionHeader = useCallback(({ section }: { section: SectionData }) => {
|
| 564 |
const allDaySelected = isDayFullySelected(section);
|
|
@@ -642,9 +569,11 @@ export default function PhotosScreen() {
|
|
| 642 |
</View>
|
| 643 |
) : (
|
| 644 |
<View style={styles.header}>
|
| 645 |
-
<
|
|
|
|
|
|
|
|
|
|
| 646 |
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
| 647 |
-
<Text style={[styles.syncCountText, { color: statusUI.color }]}>{statusUI.text}</Text>
|
| 648 |
<TouchableOpacity
|
| 649 |
style={styles.syncIconContainer}
|
| 650 |
onPress={syncAssets}
|
|
@@ -719,23 +648,13 @@ export default function PhotosScreen() {
|
|
| 719 |
renderItem={({ item, index }) => (
|
| 720 |
<View style={{ width, height: '100%', justifyContent: 'center' }}>
|
| 721 |
{item.mediaType === 'video' ? (
|
| 722 |
-
<
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
/>
|
| 730 |
-
<TouchableOpacity
|
| 731 |
-
style={styles.playPauseOverlay}
|
| 732 |
-
onPress={() => setIsPlaying(!isPlaying)}
|
| 733 |
-
>
|
| 734 |
-
{!isPlaying && (
|
| 735 |
-
<MaterialIcons name="play-circle-fill" size={72} color="rgba(255,255,255,0.8)" />
|
| 736 |
-
)}
|
| 737 |
-
</TouchableOpacity>
|
| 738 |
-
</>
|
| 739 |
) : (
|
| 740 |
<Image
|
| 741 |
source={{ uri: item.uri }}
|
|
|
|
| 13 |
Modal,
|
| 14 |
AppState,
|
| 15 |
FlatList,
|
|
|
|
| 16 |
} from 'react-native';
|
| 17 |
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
| 18 |
import * as MediaLibrary from 'expo-media-library';
|
|
|
|
| 83 |
const isSelecting = selectedIds.size > 0;
|
| 84 |
|
| 85 |
// Drag-to-select
|
|
|
|
|
|
|
|
|
|
| 86 |
const listOffsetYRef = useRef(0);
|
| 87 |
|
| 88 |
// Sync & Network States
|
|
|
|
| 390 |
return ids.length > 0 && ids.every(id => selectedIds.has(id));
|
| 391 |
}, [selectedIds]);
|
| 392 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
const openViewer = (asset: UnifiedAsset) => {
|
| 394 |
if (isSelecting) { toggleSelect(asset.id); return; }
|
| 395 |
const idx = allAssets.findIndex(a => a.id === asset.id);
|
|
|
|
| 421 |
};
|
| 422 |
|
| 423 |
// ── Render helpers ────────────────────────────────────────────
|
| 424 |
+
const renderRow = useCallback(({ item: row }: { item: UnifiedAsset[] }) => (
|
| 425 |
+
<View style={styles.row}>
|
| 426 |
+
{row.map(asset => {
|
| 427 |
+
const selected = selectedIds.has(asset.id);
|
| 428 |
+
const isVideo = asset.mediaType === MediaLibrary.MediaType.video;
|
| 429 |
+
return (
|
| 430 |
+
<TouchableOpacity
|
| 431 |
+
key={asset.id}
|
| 432 |
+
style={[styles.tileContainer, selected && styles.tileSelected]}
|
| 433 |
+
onPress={() => openViewer(asset)}
|
| 434 |
+
onLongPress={() => toggleSelect(asset.id)}
|
| 435 |
+
activeOpacity={0.85}
|
| 436 |
+
>
|
| 437 |
+
<Image
|
| 438 |
+
source={{ uri: asset.uri }}
|
| 439 |
+
style={styles.image}
|
| 440 |
+
contentFit="cover"
|
| 441 |
+
cachePolicy="memory-disk"
|
| 442 |
+
/>
|
| 443 |
+
{isVideo && (
|
| 444 |
+
<View style={styles.videoOverlay}>
|
| 445 |
+
<MaterialIcons name="play-circle-fill" size={22} color="rgba(255,255,255,0.9)" />
|
| 446 |
+
{asset.duration > 0 && (
|
| 447 |
+
<Text style={styles.videoDuration}>
|
| 448 |
+
{Math.floor(asset.duration / 60)}:{String(Math.floor(asset.duration % 60)).padStart(2, '0')}
|
| 449 |
+
</Text>
|
| 450 |
+
)}
|
| 451 |
+
</View>
|
| 452 |
+
)}
|
| 453 |
+
|
| 454 |
+
{/* Unsynced Cloud Indicator */}
|
| 455 |
+
{!asset.isSynced && !asset.isCloudOnly && (
|
| 456 |
+
<View style={styles.unsyncedOverlay}>
|
| 457 |
+
<MaterialIcons name="cloud-off" size={16} color="rgba(255,255,255,0.8)" />
|
| 458 |
+
</View>
|
| 459 |
+
)}
|
| 460 |
+
|
| 461 |
+
{/* Cloud-Only Indicator */}
|
| 462 |
+
{asset.isCloudOnly && (
|
| 463 |
+
<View style={styles.cloudOnlyOverlay}>
|
| 464 |
+
<MaterialIcons name="cloud" size={16} color="rgba(255,255,255,0.8)" />
|
| 465 |
+
</View>
|
| 466 |
+
)}
|
| 467 |
+
{/* Selection check circle — always show in selection mode */}
|
| 468 |
+
{isSelecting && (
|
| 469 |
+
<View style={styles.selectionCircleOverlay}>
|
| 470 |
+
{selected
|
| 471 |
+
? <MaterialIcons name="check-circle" size={22} color="#1a73e8" />
|
| 472 |
+
: <MaterialIcons name="radio-button-unchecked" size={22} color="rgba(255,255,255,0.8)" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
}
|
| 474 |
+
</View>
|
| 475 |
+
)}
|
| 476 |
+
{!isSelecting && selected && (
|
| 477 |
+
<View style={styles.selectedOverlay}>
|
| 478 |
+
<MaterialIcons name="check-circle" size={22} color="#1a73e8" />
|
| 479 |
+
</View>
|
| 480 |
+
)}
|
| 481 |
+
</TouchableOpacity>
|
| 482 |
+
);
|
| 483 |
+
})}
|
| 484 |
+
{Array.from({ length: NUM_COLUMNS - row.length }).map((_, i) => (
|
| 485 |
+
<View key={`empty-${i}`} style={styles.tileFiller} />
|
| 486 |
+
))}
|
| 487 |
+
</View>
|
| 488 |
+
), [selectedIds, isSelecting]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
|
| 490 |
const renderSectionHeader = useCallback(({ section }: { section: SectionData }) => {
|
| 491 |
const allDaySelected = isDayFullySelected(section);
|
|
|
|
| 569 |
</View>
|
| 570 |
) : (
|
| 571 |
<View style={styles.header}>
|
| 572 |
+
<View style={{ flex: 1, marginRight: 8 }}>
|
| 573 |
+
<Text style={styles.headerLogo}>KeyStone</Text>
|
| 574 |
+
{statusUI.text && <Text style={[styles.syncCountText, { color: statusUI.color }]} numberOfLines={1}>{statusUI.text}</Text>}
|
| 575 |
+
</View>
|
| 576 |
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
|
|
| 577 |
<TouchableOpacity
|
| 578 |
style={styles.syncIconContainer}
|
| 579 |
onPress={syncAssets}
|
|
|
|
| 648 |
renderItem={({ item, index }) => (
|
| 649 |
<View style={{ width, height: '100%', justifyContent: 'center' }}>
|
| 650 |
{item.mediaType === 'video' ? (
|
| 651 |
+
<Video
|
| 652 |
+
source={{ uri: item.uri }}
|
| 653 |
+
style={styles.viewerMedia}
|
| 654 |
+
resizeMode={ResizeMode.CONTAIN}
|
| 655 |
+
useNativeControls={true}
|
| 656 |
+
shouldPlay={isPlaying && viewerIndex === index}
|
| 657 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
) : (
|
| 659 |
<Image
|
| 660 |
source={{ uri: item.uri }}
|
keystone-app/app/(tabs)/settings.tsx
CHANGED
|
@@ -109,7 +109,7 @@ export default function SettingsScreen() {
|
|
| 109 |
<View style={styles.iconBox}>
|
| 110 |
<MaterialIcons name="cloud-sync" size={24} color="#1a73e8" />
|
| 111 |
</View>
|
| 112 |
-
<View>
|
| 113 |
<Text style={styles.settingLabel}>Background Backup</Text>
|
| 114 |
<Text style={styles.settingDesc}>Upload photos quietly while the app is closed.</Text>
|
| 115 |
</View>
|
|
@@ -129,7 +129,7 @@ export default function SettingsScreen() {
|
|
| 129 |
<View style={styles.iconBox}>
|
| 130 |
<MaterialIcons name="network-cell" size={24} color="#1a73e8" />
|
| 131 |
</View>
|
| 132 |
-
<View>
|
| 133 |
<Text style={styles.settingLabel}>Sync on mobile data</Text>
|
| 134 |
<Text style={styles.settingDesc}>Use cellular data to backup photos & videos.</Text>
|
| 135 |
</View>
|
|
@@ -151,7 +151,7 @@ export default function SettingsScreen() {
|
|
| 151 |
<View style={[styles.iconBox, { backgroundColor: '#fce8e6' }]}>
|
| 152 |
<MaterialIcons name="logout" size={24} color="#d93025" />
|
| 153 |
</View>
|
| 154 |
-
<View>
|
| 155 |
<Text style={[styles.settingLabel, { color: '#d93025' }]}>Log Out</Text>
|
| 156 |
<Text style={styles.settingDesc}>Sign out of this device.</Text>
|
| 157 |
</View>
|
|
|
|
| 109 |
<View style={styles.iconBox}>
|
| 110 |
<MaterialIcons name="cloud-sync" size={24} color="#1a73e8" />
|
| 111 |
</View>
|
| 112 |
+
<View style={{ flex: 1 }}>
|
| 113 |
<Text style={styles.settingLabel}>Background Backup</Text>
|
| 114 |
<Text style={styles.settingDesc}>Upload photos quietly while the app is closed.</Text>
|
| 115 |
</View>
|
|
|
|
| 129 |
<View style={styles.iconBox}>
|
| 130 |
<MaterialIcons name="network-cell" size={24} color="#1a73e8" />
|
| 131 |
</View>
|
| 132 |
+
<View style={{ flex: 1 }}>
|
| 133 |
<Text style={styles.settingLabel}>Sync on mobile data</Text>
|
| 134 |
<Text style={styles.settingDesc}>Use cellular data to backup photos & videos.</Text>
|
| 135 |
</View>
|
|
|
|
| 151 |
<View style={[styles.iconBox, { backgroundColor: '#fce8e6' }]}>
|
| 152 |
<MaterialIcons name="logout" size={24} color="#d93025" />
|
| 153 |
</View>
|
| 154 |
+
<View style={{ flex: 1 }}>
|
| 155 |
<Text style={[styles.settingLabel, { color: '#d93025' }]}>Log Out</Text>
|
| 156 |
<Text style={styles.settingDesc}>Sign out of this device.</Text>
|
| 157 |
</View>
|