diff --git a/assets/shaders/shaders_new.zip b/assets/shaders/shaders_new.zip index 93f475ae..4cd610c7 100644 Binary files a/assets/shaders/shaders_new.zip and b/assets/shaders/shaders_new.zip differ diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index ac990d9e..97b7ffca 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -85,7 +85,7 @@ const AndroidVideoPlayer: React.FC = () => { const playerState = usePlayerState(); const modals = usePlayerModals(); const speedControl = useSpeedControl(); - const { settings } = useSettings(); + const { settings, updateSetting } = useSettings(); const videoRef = useRef(null); const mpvPlayerRef = useRef(null); @@ -151,7 +151,7 @@ const AndroidVideoPlayer: React.FC = () => { // Shader / Video Enhancement State const [showEnhancementModal, setShowEnhancementModal] = useState(false); - const [shaderMode, setShaderModeState] = useState('none'); + const [shaderMode, setShaderModeState] = useState(settings.defaultShaderMode || 'none'); const [glslShaders, setGlslShaders] = useState(''); // Color Profile State @@ -172,13 +172,22 @@ const AndroidVideoPlayer: React.FC = () => { setCurrentVideoSettings(visualEnhancementService.getCurrentSettings()); }, []); + // Initialize shader config from persisted setting + useEffect(() => { + if (settings.defaultShaderMode && settings.defaultShaderMode !== 'none') { + const config = shaderService.getShaderConfig(settings.defaultShaderMode, settings.shaderProfile as any || 'MID-END'); + setGlslShaders(config); + } + }, [settings.defaultShaderMode, settings.shaderProfile]); + const setShaderMode = useCallback((mode: ShaderMode) => { setShaderModeState(mode); + updateSetting('defaultShaderMode', mode); // Persist selection const config = shaderService.getShaderConfig(mode, settings.shaderProfile as any || 'MID-END'); setGlslShaders(config); // Don't close modal here, let user close it // setShowEnhancementModal(false); - }, [settings.shaderProfile]); + }, [settings.shaderProfile, updateSetting]); const handleSetProfile = useCallback(async (profile: string) => { await visualEnhancementService.setProfile(profile); diff --git a/src/components/player/modals/VisualEnhancementModal.tsx b/src/components/player/modals/VisualEnhancementModal.tsx index 535cb6be..8b5263eb 100644 --- a/src/components/player/modals/VisualEnhancementModal.tsx +++ b/src/components/player/modals/VisualEnhancementModal.tsx @@ -54,15 +54,15 @@ const ShaderTab = ({ currentMode, setMode }: { currentMode: string, setMode: (m: const { settings } = useSettings(); const selectedCategory = (settings?.shaderProfile || 'MID-END') as ShaderCategory; - const animeModes = Object.keys(SHADER_PROFILES[selectedCategory]); - const cinemaModes = Object.keys(SHADER_PROFILES['CINEMA']); + const animeModes = SHADER_PROFILES[selectedCategory] ? Object.keys(SHADER_PROFILES[selectedCategory]) : []; + const cinemaModes = SHADER_PROFILES['CINEMA'] ? Object.keys(SHADER_PROFILES['CINEMA']) : []; const getModeDescription = (name: string) => { if (name.includes('Mode A')) return 'Best for high-quality sources.'; if (name.includes('Mode B')) return 'Soft restore for noisy videos.'; if (name.includes('Mode C')) return 'Balanced restore and upscale.'; if (name.includes('FSR')) return 'Sharp upscaling for live-action.'; - if (name.includes('SSim')) return 'Natural reconstruction.'; + if (name.includes('SSimSuperRes')) return 'Natural sharpness and anti-ringing.'; return ''; }; @@ -96,20 +96,24 @@ const ShaderTab = ({ currentMode, setMode }: { currentMode: string, setMode: (m: /> ))} - - - CINEMA - + {cinemaModes.length > 0 && ( + <> + + + CINEMA + - {cinemaModes.map((mode) => ( - setMode(mode)} - /> - ))} + {cinemaModes.map((mode) => ( + setMode(mode)} + /> + ))} + + )} ); }; diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 5d2ecd79..b529286d 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -118,6 +118,7 @@ export interface AppSettings { // Upscaler settings enableShaders: boolean; // Enable/disable real-time upscalers shaderProfile: 'MID-END' | 'HIGH-END'; // Hardware profile for upscalers + defaultShaderMode: string; // Persisted shader preset (e.g. 'Anime4K: Mode A') } export const DEFAULT_SETTINGS: AppSettings = { @@ -209,6 +210,7 @@ export const DEFAULT_SETTINGS: AppSettings = { // Upscaler defaults enableShaders: false, shaderProfile: 'MID-END', + defaultShaderMode: 'none', }; const SETTINGS_STORAGE_KEY = 'app_settings'; diff --git a/src/screens/settings/PlaybackSettingsScreen.tsx b/src/screens/settings/PlaybackSettingsScreen.tsx index b743e696..c8eb8962 100644 --- a/src/screens/settings/PlaybackSettingsScreen.tsx +++ b/src/screens/settings/PlaybackSettingsScreen.tsx @@ -107,6 +107,7 @@ export const PlaybackSettingsContent: React.FC = ( if (isMounted.current) { setIsDownloadingEnhancement(false); if (success) { + shaderService.setInitialized(true); // Force update service state setIsEnhancementDownloaded(true); toastService.success(t('settings.enhancement_download_success', { defaultValue: 'Enhancement assets installed!' })); } else { @@ -338,14 +339,14 @@ export const PlaybackSettingsContent: React.FC = ( )} - {/* Experimental Settings Section - AnymeX Style */} - + {/* Shaders Section */} + {/* Enable Shaders Toggle */} ( = ( ( @@ -447,9 +448,28 @@ export const PlaybackSettingsContent: React.FC = ( )} {isEnhancementDownloaded && ( - - All required shader files are ready. You can switch profiles in the video player menu. - + + + All required shader files are ready. You can switch profiles in the video player menu. + + { + await shaderService.clearShaders(); + setIsEnhancementDownloaded(false); + toastService.success('Shader assets cleared'); + }} + > + Reset Assets + + )} diff --git a/src/services/shaderService.ts b/src/services/shaderService.ts index 86338bc3..3dc1db88 100644 --- a/src/services/shaderService.ts +++ b/src/services/shaderService.ts @@ -12,6 +12,7 @@ const ESSENTIAL_SHADERS = [ 'Anime4K_Restore_CNN_M.glsl', 'Anime4K_Upscale_CNN_x2_M.glsl', 'FSR.glsl', + 'SSimSuperRes.glsl', ]; // Exact profiles from AnymeX @@ -105,7 +106,7 @@ export const SHADER_PROFILES = { }, "CINEMA": { 'FidelityFX Super Resolution': ['FSR.glsl'], - 'SSimSuperRes': ['SSimSuperRes.glsl'], + 'SSimSuperRes (Natural)': ['SSimSuperRes.glsl'], } }; @@ -127,23 +128,16 @@ class ShaderService { } /** - * Check if all shaders are already extracted and available + * Check if shaders are available */ async checkAvailability(): Promise { try { const dirInfo = await FileSystem.getInfoAsync(this.shaderDir); if (!dirInfo.exists) return false; - // Check key files - const results = await Promise.all( - ESSENTIAL_SHADERS.map(async (filename) => { - const path = `${this.shaderDir}${filename}`; - const info = await FileSystem.getInfoAsync(path); - return info.exists; - }) - ); - - this.initialized = results.every(v => v === true); + const files = await FileSystem.readDirectoryAsync(this.shaderDir); + // As long as we have some files, consider it initialized + this.initialized = files.length >= 3; return this.initialized; } catch { return false; @@ -157,6 +151,27 @@ class ShaderService { await this.checkAvailability(); } + // Manually update initialization state (e.g. after download) + setInitialized(value: boolean) { + this.initialized = value; + } + + /** + * Delete extracted shaders (troubleshooting) + */ + async clearShaders(): Promise { + try { + const dirInfo = await FileSystem.getInfoAsync(this.shaderDir); + if (dirInfo.exists) { + await FileSystem.deleteAsync(this.shaderDir, { idempotent: true }); + this.initialized = false; + logger.info('[ShaderService] Shaders cleared'); + } + } catch (e) { + logger.error('[ShaderService] Failed to clear shaders', e); + } + } + /** * Extract shaders from bundled assets */ @@ -176,12 +191,19 @@ class ShaderService { if (onProgress) onProgress(0.3); - // 2. Read Zip - const zipContent = await FileSystem.readAsStringAsync(asset.localUri || asset.uri, { encoding: FileSystem.EncodingType.Base64 }); + // Use copyAsync to avoid loading entire binary into memory as string + const tempZip = `${FileSystem.cacheDirectory}shaders_temp.zip`; + await FileSystem.copyAsync({ + from: asset.localUri || asset.uri, + to: tempZip + }); + + // 2. Read Zip (Small file so Base64 is okay, but using safer approach) + const zipContent = await FileSystem.readAsStringAsync(tempZip, { encoding: FileSystem.EncodingType.Base64 }); const zip = await JSZip.loadAsync(zipContent, { base64: true }); if (onProgress) onProgress(0.6); - logger.info('[ShaderService] Extracting shader files...'); + logger.info(`[ShaderService] Extracting ${Object.keys(zip.files).length} files...`); // 3. Extract Files const files = Object.keys(zip.files); @@ -189,10 +211,13 @@ class ShaderService { for (const filename of files) { if (!zip.files[filename].dir) { - const content = await zip.files[filename].async('base64'); + const content = await zip.files[filename].async('uint8array'); + // For binary data, writing as base64 is safest in expo-file-system + const base64 = this.uint8ToBase64(content); + await FileSystem.writeAsStringAsync( `${this.shaderDir}${filename}`, - content, + base64, { encoding: FileSystem.EncodingType.Base64 } ); } @@ -200,6 +225,9 @@ class ShaderService { if (onProgress) onProgress(0.6 + (extractedCount / files.length) * 0.4); } + // Clean up temp zip + await FileSystem.deleteAsync(tempZip, { idempotent: true }); + this.initialized = true; logger.info('[ShaderService] Shaders installed successfully'); return true; @@ -209,6 +237,16 @@ class ShaderService { } } + // Helper for binary conversion + private uint8ToBase64(arr: Uint8Array): string { + let binary = ''; + const len = arr.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(arr[i]); + } + return btoa(binary); + } + /** * Get the MPV configuration string for the selected profile */