mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
Update mobile app UI with premium design system
- Add design guidelines to CLAUDE.md - Redesign navigation with boutique styling - Add video support to chat with compression - Update matches/dares/quiz screens with gold/rose accents - Improve packs browsing with category tabs - Add SwipeCardPremium component - Update theme with premium color palette - Add video cleanup edge function and migration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
feb7657562
commit
cac7a2a31d
31 files changed
+6638
-1827
No files matched your search
@@ -1,4 +1,6 @@
|
||||
import { supabase } from './supabase';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
// Cache for signed URLs with expiration tracking
|
||||
interface CachedUrl {
|
||||
@@ -8,6 +10,12 @@ interface CachedUrl {
|
||||
|
||||
const signedUrlCache = new Map<string, CachedUrl>();
|
||||
|
||||
// In-memory tracking of videos currently being cached to prevent duplicate downloads
|
||||
const videoCacheInProgress = new Set<string>();
|
||||
|
||||
// Video cache directory
|
||||
const VIDEO_CACHE_DIR = `${FileSystem.cacheDirectory}video-cache/`;
|
||||
|
||||
// Buffer time before expiration to refresh URL (5 minutes)
|
||||
const EXPIRATION_BUFFER_MS = 5 * 60 * 1000;
|
||||
// Default signed URL expiry (1 hour)
|
||||
@@ -93,3 +101,134 @@ export async function prefetchSignedUrls(
|
||||
mediaPaths.map(path => getCachedSignedUrl(getStoragePath(path), bucket))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the video cache directory exists.
|
||||
*/
|
||||
async function ensureVideoCacheDir(): Promise<void> {
|
||||
if (Platform.OS === 'web') return;
|
||||
|
||||
const dirInfo = await FileSystem.getInfoAsync(VIDEO_CACHE_DIR);
|
||||
if (!dirInfo.exists) {
|
||||
await FileSystem.makeDirectoryAsync(VIDEO_CACHE_DIR, { intermediates: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a safe filename from a storage path.
|
||||
*/
|
||||
function getVideoCacheFilename(storagePath: string): string {
|
||||
// Replace slashes and other unsafe chars with underscores
|
||||
return storagePath.replace(/[\/\\:*?"<>|]/g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the local cache path for a video.
|
||||
*/
|
||||
export function getVideoCachePath(storagePath: string): string {
|
||||
return `${VIDEO_CACHE_DIR}${getVideoCacheFilename(storagePath)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a video is cached locally.
|
||||
* Returns the local file URI if cached, null otherwise.
|
||||
*/
|
||||
export async function getVideoCachedUri(storagePath: string): Promise<string | null> {
|
||||
if (Platform.OS === 'web') return null;
|
||||
|
||||
const cachePath = getVideoCachePath(storagePath);
|
||||
const fileInfo = await FileSystem.getInfoAsync(cachePath);
|
||||
|
||||
if (fileInfo.exists) {
|
||||
return cachePath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache a video to local storage from a signed URL.
|
||||
* Returns the local file URI on success, null on failure.
|
||||
*/
|
||||
export async function cacheVideo(
|
||||
storagePath: string,
|
||||
signedUrl: string
|
||||
): Promise<string | null> {
|
||||
if (Platform.OS === 'web') return null;
|
||||
|
||||
// Check if already cached
|
||||
const existingCache = await getVideoCachedUri(storagePath);
|
||||
if (existingCache) {
|
||||
return existingCache;
|
||||
}
|
||||
|
||||
// Check if caching is already in progress for this video
|
||||
if (videoCacheInProgress.has(storagePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
videoCacheInProgress.add(storagePath);
|
||||
await ensureVideoCacheDir();
|
||||
|
||||
const cachePath = getVideoCachePath(storagePath);
|
||||
|
||||
// Download the video file
|
||||
const downloadResult = await FileSystem.downloadAsync(signedUrl, cachePath);
|
||||
|
||||
if (downloadResult.status === 200) {
|
||||
console.log('Video cached successfully:', storagePath);
|
||||
return cachePath;
|
||||
} else {
|
||||
console.warn('Video cache download failed with status:', downloadResult.status);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to cache video:', error);
|
||||
return null;
|
||||
} finally {
|
||||
videoCacheInProgress.delete(storagePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached videos.
|
||||
*/
|
||||
export async function clearVideoCache(): Promise<void> {
|
||||
if (Platform.OS === 'web') return;
|
||||
|
||||
try {
|
||||
const dirInfo = await FileSystem.getInfoAsync(VIDEO_CACHE_DIR);
|
||||
if (dirInfo.exists) {
|
||||
await FileSystem.deleteAsync(VIDEO_CACHE_DIR, { idempotent: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to clear video cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the video cache in bytes.
|
||||
*/
|
||||
export async function getVideoCacheSize(): Promise<number> {
|
||||
if (Platform.OS === 'web') return 0;
|
||||
|
||||
try {
|
||||
const dirInfo = await FileSystem.getInfoAsync(VIDEO_CACHE_DIR);
|
||||
if (!dirInfo.exists) return 0;
|
||||
|
||||
const files = await FileSystem.readDirectoryAsync(VIDEO_CACHE_DIR);
|
||||
let totalSize = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const fileInfo = await FileSystem.getInfoAsync(`${VIDEO_CACHE_DIR}${file}`);
|
||||
if (fileInfo.exists && 'size' in fileInfo) {
|
||||
totalSize += fileInfo.size || 0;
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
} catch (error) {
|
||||
console.error('Failed to get video cache size:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user