dpv007 commited on
Commit
b763d9e
·
1 Parent(s): 435fcda

fix: Google Photos-style caching - stable URLs, recyclingKey, prefetch, proper video controls

Browse files
Files changed (1) hide show
  1. keystone-app/app/(tabs)/index.tsx +74 -41
keystone-app/app/(tabs)/index.tsx CHANGED
@@ -101,6 +101,8 @@ const GalleryRow = React.memo(({
101
  style={styles.image}
102
  contentFit="cover"
103
  cachePolicy="memory-disk"
 
 
104
  />
105
  {isVideo && (
106
  <View style={styles.videoOverlay}>
@@ -284,11 +286,39 @@ export default function PhotosScreen() {
284
  try {
285
  const cached = await AsyncStorage.getItem(CACHED_ASSETS_KEY);
286
  if (cached) {
287
- const parsedAssets = JSON.parse(cached);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  setAllAssets(parsedAssets);
289
  setSections(groupAssetsByDate(parsedAssets));
290
  setIsLoading(false); // hide spinner right away
291
  hasCached = true;
 
 
 
 
 
 
 
 
 
292
  }
293
  } catch (e) { console.warn('Cache load error', e); }
294
 
@@ -377,6 +407,15 @@ export default function PhotosScreen() {
377
  // Save to cache silently
378
  AsyncStorage.setItem(CACHED_ASSETS_KEY, JSON.stringify(unified)).catch(() => {});
379
 
 
 
 
 
 
 
 
 
 
380
  const toSync = unified.filter(a => !a.isSynced).length;
381
  setItemsToBackup(toSync);
382
  setIsLoading(false);
@@ -738,54 +777,48 @@ export default function PhotosScreen() {
738
  </View>
739
  </TouchableWithoutFeedback>
740
 
741
- {/* ImageViewer handles zooming for photos and swiping for everything */}
742
- <ImageViewer
743
- imageUrls={allAssets.map(a => ({ url: a.uri }))}
744
- index={viewerIndex || 0}
745
- onChange={(idx) => {
746
- if (idx !== undefined) {
747
- setViewerIndex(idx);
748
- setIsPlaying(true);
749
- }
 
 
 
 
 
750
  }}
751
- enableSwipeDown
752
- onSwipeDown={() => setViewerIndex(null)}
753
- renderIndicator={() => <View />}
754
- renderImage={(props) => {
755
- const uri = props.source.uri;
756
- const asset = allAssets.find(a => a.uri === uri);
757
-
758
- if (asset?.mediaType === 'video') {
759
- return (
760
- <View
761
- style={{ width, height, paddingBottom: 80, justifyContent: 'center' }}
762
- onStartShouldSetResponder={() => true}
763
- onResponderTerminationRequest={() => false}
764
- >
765
  <Video
766
- source={{ uri: asset.uri }}
767
- style={{ width: '100%', height: '100%' }}
768
  resizeMode={ResizeMode.CONTAIN}
769
  useNativeControls
770
- shouldPlay={allAssets[viewerIndex ?? 0]?.id === asset.id && isPlaying}
771
- isLooping
772
  onPlaybackStatusUpdate={(status) => {
773
  if (status.isLoaded && status.didJustFinish) setIsPlaying(false);
774
  }}
775
  />
776
  </View>
777
- );
778
- }
779
-
780
- return (
781
- <Image
782
- source={{ uri }}
783
- style={{ width: '100%', height: '100%' }}
784
- contentFit="contain"
785
- cachePolicy="memory-disk"
786
- />
787
- );
788
- }}
789
  />
790
 
791
  {/* Viewer action bar */}
@@ -926,7 +959,7 @@ const styles = StyleSheet.create({
926
  row: { flexDirection: 'row', marginBottom: SPACING },
927
  tileContainer: {
928
  width: TILE_SIZE, height: TILE_SIZE, marginRight: SPACING,
929
- borderRadius: 6, overflow: 'hidden', backgroundColor: '#e8e8e8',
930
  },
931
  tileSelected: { borderWidth: 2.5, borderColor: '#1a73e8', borderRadius: 6 },
932
  tileFiller: { width: TILE_SIZE, height: TILE_SIZE, marginRight: SPACING },
 
101
  style={styles.image}
102
  contentFit="cover"
103
  cachePolicy="memory-disk"
104
+ recyclingKey={asset.id}
105
+ transition={0}
106
  />
107
  {isVideo && (
108
  <View style={styles.videoOverlay}>
 
286
  try {
287
  const cached = await AsyncStorage.getItem(CACHED_ASSETS_KEY);
288
  if (cached) {
289
+ const parsedAssets: UnifiedAsset[] = JSON.parse(cached);
290
+
291
+ // Refresh stale token URLs for cloud-only assets so they load instantly
292
+ const freshToken = await getToken();
293
+ if (freshToken) {
294
+ for (const asset of parsedAssets) {
295
+ if (asset.isCloudOnly || asset.cloudUri) {
296
+ // Rebuild URLs with fresh token (strip old token, add new)
297
+ const refreshUrl = (url: string | undefined) => {
298
+ if (!url) return url;
299
+ const base = url.split('?token=')[0];
300
+ return `${base}?token=${freshToken}`;
301
+ };
302
+ asset.uri = refreshUrl(asset.uri) || asset.uri;
303
+ asset.cloudUri = refreshUrl(asset.cloudUri);
304
+ asset.cloudThumbUri = refreshUrl(asset.cloudThumbUri);
305
+ }
306
+ }
307
+ }
308
+
309
  setAllAssets(parsedAssets);
310
  setSections(groupAssetsByDate(parsedAssets));
311
  setIsLoading(false); // hide spinner right away
312
  hasCached = true;
313
+
314
+ // Prefetch cloud-only thumbnails in background
315
+ const cloudThumbUrls = parsedAssets
316
+ .filter(a => a.isCloudOnly && a.cloudThumbUri)
317
+ .slice(0, 30) // first 30 cloud thumbnails
318
+ .map(a => a.cloudThumbUri!);
319
+ if (cloudThumbUrls.length > 0) {
320
+ Image.prefetch(cloudThumbUrls).catch(() => {});
321
+ }
322
  }
323
  } catch (e) { console.warn('Cache load error', e); }
324
 
 
407
  // Save to cache silently
408
  AsyncStorage.setItem(CACHED_ASSETS_KEY, JSON.stringify(unified)).catch(() => {});
409
 
410
+ // Prefetch cloud-only thumbnails in background for instant display
411
+ const cloudThumbUrls = unified
412
+ .filter(a => a.isCloudOnly && a.cloudThumbUri)
413
+ .slice(0, 30)
414
+ .map(a => a.cloudThumbUri!);
415
+ if (cloudThumbUrls.length > 0) {
416
+ Image.prefetch(cloudThumbUrls).catch(() => {});
417
+ }
418
+
419
  const toSync = unified.filter(a => !a.isSynced).length;
420
  setItemsToBackup(toSync);
421
  setIsLoading(false);
 
777
  </View>
778
  </TouchableWithoutFeedback>
779
 
780
+ {/* Swipeable viewer — FlatList for smooth swiping, photos get ImageViewer zoom */}
781
+ <FlatList
782
+ ref={viewerRef}
783
+ data={allAssets}
784
+ horizontal
785
+ pagingEnabled
786
+ initialScrollIndex={viewerIndex ?? 0}
787
+ getItemLayout={(_, index) => ({ length: width, offset: width * index, index })}
788
+ keyExtractor={item => item.id}
789
+ showsHorizontalScrollIndicator={false}
790
+ onMomentumScrollEnd={(e) => {
791
+ const newIndex = Math.round(e.nativeEvent.contentOffset.x / width);
792
+ setViewerIndex(newIndex);
793
+ setIsPlaying(true);
794
  }}
795
+ renderItem={({ item, index }) => (
796
+ <View style={{ width, height, backgroundColor: '#000' }}>
797
+ {item.mediaType === 'video' ? (
798
+ <View style={{ flex: 1, paddingBottom: insets.bottom + 72 }}>
 
 
 
 
 
 
 
 
 
 
799
  <Video
800
+ source={{ uri: item.uri }}
801
+ style={{ flex: 1 }}
802
  resizeMode={ResizeMode.CONTAIN}
803
  useNativeControls
804
+ shouldPlay={index === viewerIndex && isPlaying}
805
+ isLooping={false}
806
  onPlaybackStatusUpdate={(status) => {
807
  if (status.isLoaded && status.didJustFinish) setIsPlaying(false);
808
  }}
809
  />
810
  </View>
811
+ ) : (
812
+ <Image
813
+ source={{ uri: item.uri }}
814
+ style={{ flex: 1, width: '100%' }}
815
+ contentFit="contain"
816
+ cachePolicy="memory-disk"
817
+ transition={0}
818
+ />
819
+ )}
820
+ </View>
821
+ )}
 
822
  />
823
 
824
  {/* Viewer action bar */}
 
959
  row: { flexDirection: 'row', marginBottom: SPACING },
960
  tileContainer: {
961
  width: TILE_SIZE, height: TILE_SIZE, marginRight: SPACING,
962
+ borderRadius: 6, overflow: 'hidden', backgroundColor: '#f0f0f0',
963
  },
964
  tileSelected: { borderWidth: 2.5, borderColor: '#1a73e8', borderRadius: 6 },
965
  tileFiller: { width: TILE_SIZE, height: TILE_SIZE, marginRight: SPACING },