dpv007 commited on
Commit
cd18655
·
1 Parent(s): 81b3b53
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
- // PanResponder for drag-to-select on this row
450
- const panResponder = PanResponder.create({
451
- onMoveShouldSetPanResponder: (_, gs) => Math.abs(gs.dx) > 4 || Math.abs(gs.dy) > 4,
452
- onPanResponderGrant: (e) => {
453
- isDraggingRef.current = true;
454
- const id = getTileAtPoint(e.nativeEvent.pageX, e.nativeEvent.pageY);
455
- if (id) {
456
- dragModeRef.current = selectedIds.has(id) ? 'remove' : 'add';
457
- setSelectedIds(prev => {
458
- const next = new Set(prev);
459
- dragModeRef.current === 'add' ? next.add(id) : next.delete(id);
460
- return next;
461
- });
462
- }
463
- },
464
- onPanResponderMove: (e) => {
465
- if (!isDraggingRef.current) return;
466
- const id = getTileAtPoint(e.nativeEvent.pageX, e.nativeEvent.pageY);
467
- if (id) {
468
- setSelectedIds(prev => {
469
- const next = new Set(prev);
470
- if (dragModeRef.current === 'add' && !next.has(id)) { next.add(id); return next; }
471
- if (dragModeRef.current === 'remove' && next.has(id)) { next.delete(id); return next; }
472
- return prev; // no change, avoid re-render
473
- });
474
- }
475
- },
476
- onPanResponderRelease: () => { isDraggingRef.current = false; },
477
- onPanResponderTerminate: () => { isDraggingRef.current = false; },
478
- });
479
-
480
- return (
481
- <View style={styles.row} {...panResponder.panHandlers}>
482
- {row.map(asset => {
483
- const selected = selectedIds.has(asset.id);
484
- const isVideo = asset.mediaType === MediaLibrary.MediaType.video;
485
- return (
486
- <TouchableOpacity
487
- key={asset.id}
488
- style={[styles.tileContainer, selected && styles.tileSelected]}
489
- onPress={() => openViewer(asset)}
490
- onLongPress={() => {
491
- // On first long-press, enter selection mode
492
- if (!isSelecting) dragModeRef.current = 'add';
493
- toggleSelect(asset.id);
494
- }}
495
- onLayout={(e) => {
496
- const { x, y, width, height } = e.nativeEvent.layout;
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
- activeOpacity={0.85}
508
- >
509
- <Image
510
- source={{ uri: asset.uri }}
511
- style={styles.image}
512
- contentFit="cover"
513
- cachePolicy="memory-disk"
514
- />
515
- {isVideo && (
516
- <View style={styles.videoOverlay}>
517
- <MaterialIcons name="play-circle-fill" size={22} color="rgba(255,255,255,0.9)" />
518
- {asset.duration > 0 && (
519
- <Text style={styles.videoDuration}>
520
- {Math.floor(asset.duration / 60)}:{String(Math.floor(asset.duration % 60)).padStart(2, '0')}
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
- <Text style={styles.headerLogo}>KeyStone</Text>
 
 
 
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
- <Video
724
- source={{ uri: item.uri }}
725
- style={styles.viewerMedia}
726
- resizeMode={ResizeMode.CONTAIN}
727
- useNativeControls={false}
728
- shouldPlay={isPlaying && viewerIndex === index}
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>