diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index fe58e8b0..95a95f2c 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -711,7 +711,13 @@ "nvscalerDescription": "NVIDIA-Bildskalierung für schärferes Video", "qualityFast": "Schnell", "qualityHQ": "Hohe Qualität", - "mode": "Modus" + "mode": "Modus", + "importShader": "Import Shader", + "customShaderDescription": "Custom GLSL shader", + "shaderImported": "Shader imported", + "shaderImportFailed": "Failed to import shader", + "deleteShader": "Delete Shader", + "deleteShaderConfirm": "Delete \"${name}\"?" }, "companionRemote": { "title": "Companion Remote", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index b0a073c3..38ce294e 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -711,7 +711,13 @@ "nvscalerDescription": "NVIDIA image scaling for sharper video", "qualityFast": "Fast", "qualityHQ": "High Quality", - "mode": "Mode" + "mode": "Mode", + "importShader": "Import Shader", + "customShaderDescription": "Custom GLSL shader", + "shaderImported": "Shader imported", + "shaderImportFailed": "Failed to import shader", + "deleteShader": "Delete Shader", + "deleteShaderConfirm": "Delete \"${name}\"?" }, "companionRemote": { "title": "Companion Remote", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index e30f4991..eea4fab1 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -711,7 +711,13 @@ "nvscalerDescription": "Escalado de imagen NVIDIA para un video más nítido", "qualityFast": "Rápido", "qualityHQ": "Alta Calidad", - "mode": "Modo" + "mode": "Modo", + "importShader": "Import Shader", + "customShaderDescription": "Custom GLSL shader", + "shaderImported": "Shader imported", + "shaderImportFailed": "Failed to import shader", + "deleteShader": "Delete Shader", + "deleteShaderConfirm": "Delete \"${name}\"?" }, "companionRemote": { "title": "Companion Remote", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index d890212e..602c9ef6 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -711,7 +711,13 @@ "nvscalerDescription": "Mise à l'échelle NVIDIA pour une vidéo plus nette", "qualityFast": "Rapide", "qualityHQ": "Haute qualité", - "mode": "Mode" + "mode": "Mode", + "importShader": "Import Shader", + "customShaderDescription": "Custom GLSL shader", + "shaderImported": "Shader imported", + "shaderImportFailed": "Failed to import shader", + "deleteShader": "Delete Shader", + "deleteShaderConfirm": "Delete \"${name}\"?" }, "companionRemote": { "title": "Companion Remote", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 2aa98fc0..f36cd95f 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -711,7 +711,13 @@ "nvscalerDescription": "Ridimensionamento NVIDIA per video più nitido", "qualityFast": "Veloce", "qualityHQ": "Alta qualità", - "mode": "Modalità" + "mode": "Modalità", + "importShader": "Import Shader", + "customShaderDescription": "Custom GLSL shader", + "shaderImported": "Shader imported", + "shaderImportFailed": "Failed to import shader", + "deleteShader": "Delete Shader", + "deleteShaderConfirm": "Delete \"${name}\"?" }, "companionRemote": { "title": "Companion Remote", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 08ced3e3..01fd2387 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -711,7 +711,13 @@ "nvscalerDescription": "더 선명한 비디오를 위한 NVIDIA 이미지 스케일링", "qualityFast": "빠름", "qualityHQ": "고품질", - "mode": "모드" + "mode": "모드", + "importShader": "Import Shader", + "customShaderDescription": "Custom GLSL shader", + "shaderImported": "Shader imported", + "shaderImportFailed": "Failed to import shader", + "deleteShader": "Delete Shader", + "deleteShaderConfirm": "Delete \"${name}\"?" }, "companionRemote": { "title": "Companion Remote", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index e63726f7..4d8674e1 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -711,7 +711,13 @@ "nvscalerDescription": "NVIDIA-beeldschaling voor scherpere video", "qualityFast": "Snel", "qualityHQ": "Hoge kwaliteit", - "mode": "Modus" + "mode": "Modus", + "importShader": "Import Shader", + "customShaderDescription": "Custom GLSL shader", + "shaderImported": "Shader imported", + "shaderImportFailed": "Failed to import shader", + "deleteShader": "Delete Shader", + "deleteShaderConfirm": "Delete \"${name}\"?" }, "companionRemote": { "title": "Companion Remote", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 590838e0..5ee5dd56 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 9 -/// Strings: 7182 (798 per locale) +/// Strings: 7236 (804 per locale) /// -/// Built on 2026-03-03 at 11:11 UTC +/// Built on 2026-03-04 at 07:38 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 6cdf117b..42730025 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -987,6 +987,12 @@ class _TranslationsShadersDe implements TranslationsShadersEn { @override String get qualityFast => 'Schnell'; @override String get qualityHQ => 'Hohe Qualität'; @override String get mode => 'Modus'; + @override String get importShader => 'Import Shader'; + @override String get customShaderDescription => 'Custom GLSL shader'; + @override String get shaderImported => 'Shader imported'; + @override String get shaderImportFailed => 'Failed to import shader'; + @override String get deleteShader => 'Delete Shader'; + @override String deleteShaderConfirm({required Object name}) => 'Delete "${name}"?'; } // Path: companionRemote @@ -1935,6 +1941,12 @@ extension on TranslationsDe { 'shaders.qualityFast' => 'Schnell', 'shaders.qualityHQ' => 'Hohe Qualität', 'shaders.mode' => 'Modus', + 'shaders.importShader' => 'Import Shader', + 'shaders.customShaderDescription' => 'Custom GLSL shader', + 'shaders.shaderImported' => 'Shader imported', + 'shaders.shaderImportFailed' => 'Failed to import shader', + 'shaders.deleteShader' => 'Delete Shader', + 'shaders.deleteShaderConfirm' => ({required Object name}) => 'Delete "${name}"?', 'companionRemote.title' => 'Companion Remote', 'companionRemote.connectToDevice' => 'Mit Gerät verbinden', 'companionRemote.hostRemoteSession' => 'Fernsteuerungssitzung starten', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index b2416943..438a853f 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -2197,6 +2197,24 @@ class TranslationsShadersEn { /// en: 'Mode' String get mode => 'Mode'; + + /// en: 'Import Shader' + String get importShader => 'Import Shader'; + + /// en: 'Custom GLSL shader' + String get customShaderDescription => 'Custom GLSL shader'; + + /// en: 'Shader imported' + String get shaderImported => 'Shader imported'; + + /// en: 'Failed to import shader' + String get shaderImportFailed => 'Failed to import shader'; + + /// en: 'Delete Shader' + String get deleteShader => 'Delete Shader'; + + /// en: 'Delete "${name}"?' + String deleteShaderConfirm({required Object name}) => 'Delete "${name}"?'; } // Path: companionRemote @@ -3538,6 +3556,12 @@ extension on Translations { 'shaders.qualityFast' => 'Fast', 'shaders.qualityHQ' => 'High Quality', 'shaders.mode' => 'Mode', + 'shaders.importShader' => 'Import Shader', + 'shaders.customShaderDescription' => 'Custom GLSL shader', + 'shaders.shaderImported' => 'Shader imported', + 'shaders.shaderImportFailed' => 'Failed to import shader', + 'shaders.deleteShader' => 'Delete Shader', + 'shaders.deleteShaderConfirm' => ({required Object name}) => 'Delete "${name}"?', 'companionRemote.title' => 'Companion Remote', 'companionRemote.connectToDevice' => 'Connect to Device', 'companionRemote.hostRemoteSession' => 'Host Remote Session', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 20fe8a6e..bc1957d2 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -987,6 +987,12 @@ class _TranslationsShadersEs implements TranslationsShadersEn { @override String get qualityFast => 'Rápido'; @override String get qualityHQ => 'Alta Calidad'; @override String get mode => 'Modo'; + @override String get importShader => 'Import Shader'; + @override String get customShaderDescription => 'Custom GLSL shader'; + @override String get shaderImported => 'Shader imported'; + @override String get shaderImportFailed => 'Failed to import shader'; + @override String get deleteShader => 'Delete Shader'; + @override String deleteShaderConfirm({required Object name}) => 'Delete "${name}"?'; } // Path: companionRemote @@ -1935,6 +1941,12 @@ extension on TranslationsEs { 'shaders.qualityFast' => 'Rápido', 'shaders.qualityHQ' => 'Alta Calidad', 'shaders.mode' => 'Modo', + 'shaders.importShader' => 'Import Shader', + 'shaders.customShaderDescription' => 'Custom GLSL shader', + 'shaders.shaderImported' => 'Shader imported', + 'shaders.shaderImportFailed' => 'Failed to import shader', + 'shaders.deleteShader' => 'Delete Shader', + 'shaders.deleteShaderConfirm' => ({required Object name}) => 'Delete "${name}"?', 'companionRemote.title' => 'Companion Remote', 'companionRemote.connectToDevice' => 'Conectar a dispositivo', 'companionRemote.hostRemoteSession' => 'Iniciar sesión remota', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 72acda2f..2d9821e6 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -987,6 +987,12 @@ class _TranslationsShadersFr implements TranslationsShadersEn { @override String get qualityFast => 'Rapide'; @override String get qualityHQ => 'Haute qualité'; @override String get mode => 'Mode'; + @override String get importShader => 'Import Shader'; + @override String get customShaderDescription => 'Custom GLSL shader'; + @override String get shaderImported => 'Shader imported'; + @override String get shaderImportFailed => 'Failed to import shader'; + @override String get deleteShader => 'Delete Shader'; + @override String deleteShaderConfirm({required Object name}) => 'Delete "${name}"?'; } // Path: companionRemote @@ -1935,6 +1941,12 @@ extension on TranslationsFr { 'shaders.qualityFast' => 'Rapide', 'shaders.qualityHQ' => 'Haute qualité', 'shaders.mode' => 'Mode', + 'shaders.importShader' => 'Import Shader', + 'shaders.customShaderDescription' => 'Custom GLSL shader', + 'shaders.shaderImported' => 'Shader imported', + 'shaders.shaderImportFailed' => 'Failed to import shader', + 'shaders.deleteShader' => 'Delete Shader', + 'shaders.deleteShaderConfirm' => ({required Object name}) => 'Delete "${name}"?', 'companionRemote.title' => 'Companion Remote', 'companionRemote.connectToDevice' => 'Se connecter à un appareil', 'companionRemote.hostRemoteSession' => 'Héberger une session distante', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 26e54259..7557767c 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -987,6 +987,12 @@ class _TranslationsShadersIt implements TranslationsShadersEn { @override String get qualityFast => 'Veloce'; @override String get qualityHQ => 'Alta qualità'; @override String get mode => 'Modalità'; + @override String get importShader => 'Import Shader'; + @override String get customShaderDescription => 'Custom GLSL shader'; + @override String get shaderImported => 'Shader imported'; + @override String get shaderImportFailed => 'Failed to import shader'; + @override String get deleteShader => 'Delete Shader'; + @override String deleteShaderConfirm({required Object name}) => 'Delete "${name}"?'; } // Path: companionRemote @@ -1935,6 +1941,12 @@ extension on TranslationsIt { 'shaders.qualityFast' => 'Veloce', 'shaders.qualityHQ' => 'Alta qualità', 'shaders.mode' => 'Modalità', + 'shaders.importShader' => 'Import Shader', + 'shaders.customShaderDescription' => 'Custom GLSL shader', + 'shaders.shaderImported' => 'Shader imported', + 'shaders.shaderImportFailed' => 'Failed to import shader', + 'shaders.deleteShader' => 'Delete Shader', + 'shaders.deleteShaderConfirm' => ({required Object name}) => 'Delete "${name}"?', 'companionRemote.title' => 'Companion Remote', 'companionRemote.connectToDevice' => 'Connetti a un dispositivo', 'companionRemote.hostRemoteSession' => 'Ospita sessione remota', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index c6164812..b97cf5dc 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -987,6 +987,12 @@ class _TranslationsShadersKo implements TranslationsShadersEn { @override String get qualityFast => '빠름'; @override String get qualityHQ => '고품질'; @override String get mode => '모드'; + @override String get importShader => 'Import Shader'; + @override String get customShaderDescription => 'Custom GLSL shader'; + @override String get shaderImported => 'Shader imported'; + @override String get shaderImportFailed => 'Failed to import shader'; + @override String get deleteShader => 'Delete Shader'; + @override String deleteShaderConfirm({required Object name}) => 'Delete "${name}"?'; } // Path: companionRemote @@ -1935,6 +1941,12 @@ extension on TranslationsKo { 'shaders.qualityFast' => '빠름', 'shaders.qualityHQ' => '고품질', 'shaders.mode' => '모드', + 'shaders.importShader' => 'Import Shader', + 'shaders.customShaderDescription' => 'Custom GLSL shader', + 'shaders.shaderImported' => 'Shader imported', + 'shaders.shaderImportFailed' => 'Failed to import shader', + 'shaders.deleteShader' => 'Delete Shader', + 'shaders.deleteShaderConfirm' => ({required Object name}) => 'Delete "${name}"?', 'companionRemote.title' => 'Companion Remote', 'companionRemote.connectToDevice' => '기기에 연결', 'companionRemote.hostRemoteSession' => '원격 세션 호스트', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index e2cb7fc8..e44561cf 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -987,6 +987,12 @@ class _TranslationsShadersNl implements TranslationsShadersEn { @override String get qualityFast => 'Snel'; @override String get qualityHQ => 'Hoge kwaliteit'; @override String get mode => 'Modus'; + @override String get importShader => 'Import Shader'; + @override String get customShaderDescription => 'Custom GLSL shader'; + @override String get shaderImported => 'Shader imported'; + @override String get shaderImportFailed => 'Failed to import shader'; + @override String get deleteShader => 'Delete Shader'; + @override String deleteShaderConfirm({required Object name}) => 'Delete "${name}"?'; } // Path: companionRemote @@ -1935,6 +1941,12 @@ extension on TranslationsNl { 'shaders.qualityFast' => 'Snel', 'shaders.qualityHQ' => 'Hoge kwaliteit', 'shaders.mode' => 'Modus', + 'shaders.importShader' => 'Import Shader', + 'shaders.customShaderDescription' => 'Custom GLSL shader', + 'shaders.shaderImported' => 'Shader imported', + 'shaders.shaderImportFailed' => 'Failed to import shader', + 'shaders.deleteShader' => 'Delete Shader', + 'shaders.deleteShaderConfirm' => ({required Object name}) => 'Delete "${name}"?', 'companionRemote.title' => 'Companion Remote', 'companionRemote.connectToDevice' => 'Verbinden met apparaat', 'companionRemote.hostRemoteSession' => 'Externe sessie hosten', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 0e68e409..858359eb 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -987,6 +987,12 @@ class _TranslationsShadersSv implements TranslationsShadersEn { @override String get qualityFast => 'Snabb'; @override String get qualityHQ => 'Hög kvalitet'; @override String get mode => 'Läge'; + @override String get importShader => 'Import Shader'; + @override String get customShaderDescription => 'Custom GLSL shader'; + @override String get shaderImported => 'Shader imported'; + @override String get shaderImportFailed => 'Failed to import shader'; + @override String get deleteShader => 'Delete Shader'; + @override String deleteShaderConfirm({required Object name}) => 'Delete "${name}"?'; } // Path: companionRemote @@ -1935,6 +1941,12 @@ extension on TranslationsSv { 'shaders.qualityFast' => 'Snabb', 'shaders.qualityHQ' => 'Hög kvalitet', 'shaders.mode' => 'Läge', + 'shaders.importShader' => 'Import Shader', + 'shaders.customShaderDescription' => 'Custom GLSL shader', + 'shaders.shaderImported' => 'Shader imported', + 'shaders.shaderImportFailed' => 'Failed to import shader', + 'shaders.deleteShader' => 'Delete Shader', + 'shaders.deleteShaderConfirm' => ({required Object name}) => 'Delete "${name}"?', 'companionRemote.title' => 'Companion Remote', 'companionRemote.connectToDevice' => 'Anslut till enhet', 'companionRemote.hostRemoteSession' => 'Starta fjärrsession', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 8b75ed8f..9363e610 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -987,6 +987,12 @@ class _TranslationsShadersZh implements TranslationsShadersEn { @override String get qualityFast => '快速'; @override String get qualityHQ => '高质量'; @override String get mode => '模式'; + @override String get importShader => 'Import Shader'; + @override String get customShaderDescription => 'Custom GLSL shader'; + @override String get shaderImported => 'Shader imported'; + @override String get shaderImportFailed => 'Failed to import shader'; + @override String get deleteShader => 'Delete Shader'; + @override String deleteShaderConfirm({required Object name}) => 'Delete "${name}"?'; } // Path: companionRemote @@ -1935,6 +1941,12 @@ extension on TranslationsZh { 'shaders.qualityFast' => '快速', 'shaders.qualityHQ' => '高质量', 'shaders.mode' => '模式', + 'shaders.importShader' => 'Import Shader', + 'shaders.customShaderDescription' => 'Custom GLSL shader', + 'shaders.shaderImported' => 'Shader imported', + 'shaders.shaderImportFailed' => 'Failed to import shader', + 'shaders.deleteShader' => 'Delete Shader', + 'shaders.deleteShaderConfirm' => ({required Object name}) => 'Delete "${name}"?', 'companionRemote.title' => 'Companion Remote', 'companionRemote.connectToDevice' => '连接到设备', 'companionRemote.hostRemoteSession' => '创建远程会话', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 5dd9557b..08e072e7 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -711,7 +711,13 @@ "nvscalerDescription": "NVIDIA-bildskalning för skarpare video", "qualityFast": "Snabb", "qualityHQ": "Hög kvalitet", - "mode": "Läge" + "mode": "Läge", + "importShader": "Import Shader", + "customShaderDescription": "Custom GLSL shader", + "shaderImported": "Shader imported", + "shaderImportFailed": "Failed to import shader", + "deleteShader": "Delete Shader", + "deleteShaderConfirm": "Delete \"${name}\"?" }, "companionRemote": { "title": "Companion Remote", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index bfa3e145..61f30a1f 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -711,7 +711,13 @@ "nvscalerDescription": "NVIDIA 图像缩放,使视频更清晰", "qualityFast": "快速", "qualityHQ": "高质量", - "mode": "模式" + "mode": "模式", + "importShader": "Import Shader", + "customShaderDescription": "Custom GLSL shader", + "shaderImported": "Shader imported", + "shaderImportFailed": "Failed to import shader", + "deleteShader": "Delete Shader", + "deleteShaderConfirm": "Delete \"${name}\"?" }, "companionRemote": { "title": "Companion Remote", diff --git a/lib/models/shader_preset.dart b/lib/models/shader_preset.dart index 95dcb254..7fba939a 100644 --- a/lib/models/shader_preset.dart +++ b/lib/models/shader_preset.dart @@ -1,5 +1,5 @@ /// Shader preset types available in the app -enum ShaderPresetType { none, nvscaler, anime4k } +enum ShaderPresetType { none, nvscaler, anime4k, custom } /// Quality tiers for Anime4K presets enum Anime4KQuality { @@ -88,12 +88,16 @@ class ShaderPreset { final Anime4KConfig? anime4kConfig; final NVScalerConfig? nvscalerConfig; + /// File name of the custom shader in the custom shaders directory + final String? fileName; + const ShaderPreset({ required this.id, required this.name, required this.type, this.anime4kConfig, this.nvscalerConfig, + this.fileName, }); /// No shader preset (off) @@ -193,6 +197,7 @@ class ShaderPreset { 'type': type.name, if (anime4kConfig != null) 'anime4kConfig': anime4kConfig!.toJson(), if (nvscalerConfig != null) 'nvscalerConfig': nvscalerConfig!.toJson(), + if (fileName != null) 'fileName': fileName, }; factory ShaderPreset.fromJson(Map json) { @@ -210,6 +215,7 @@ class ShaderPreset { type: ShaderPresetType.values.asNameMap()[json['type']] ?? ShaderPresetType.none, anime4kConfig: json['anime4kConfig'] != null ? Anime4KConfig.fromJson(json['anime4kConfig']) : null, nvscalerConfig: json['nvscalerConfig'] != null ? NVScalerConfig.fromJson(json['nvscalerConfig']) : null, + fileName: json['fileName'] as String?, ); } } diff --git a/lib/providers/shader_provider.dart b/lib/providers/shader_provider.dart index e3c0c1c7..43c7c23e 100644 --- a/lib/providers/shader_provider.dart +++ b/lib/providers/shader_provider.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import '../models/shader_preset.dart'; import '../services/settings_service.dart'; +import '../services/shader_asset_loader.dart'; /// Provider for managing shader preset state. /// @@ -11,6 +12,7 @@ class ShaderProvider extends ChangeNotifier { ShaderPreset _savedPreset = ShaderPreset.none; ShaderPreset _currentPreset = ShaderPreset.none; + List _customPresets = []; bool _initialized = false; ShaderProvider() { @@ -20,8 +22,12 @@ class ShaderProvider extends ChangeNotifier { Future _initialize() async { _settingsService = await SettingsService.getInstance(); + // Load custom presets from storage + final customData = _settingsService.getCustomShaderPresets(); + _customPresets = customData.map((json) => ShaderPreset.fromJson(json)).toList(); + final presetId = _settingsService.getGlobalShaderPreset(); - _savedPreset = ShaderPreset.fromId(presetId) ?? ShaderPreset.none; + _savedPreset = findPresetById(presetId) ?? ShaderPreset.none; _currentPreset = _savedPreset; _initialized = true; @@ -37,12 +43,20 @@ class ShaderProvider extends ChangeNotifier { /// The currently active shader preset ShaderPreset get currentPreset => _currentPreset; - /// All available shader presets - List get allPresets => ShaderPreset.allPresets; + /// All available shader presets (built-in + custom) + List get allPresets => [...ShaderPreset.allPresets, ..._customPresets]; + + /// Custom shader presets only + List get customPresets => _customPresets; /// Whether any shader is currently enabled bool get isShaderEnabled => _currentPreset.type != ShaderPresetType.none; + /// Find a preset by its ID, searching both built-in and custom presets. + ShaderPreset? findPresetById(String id) { + return ShaderPreset.fromId(id) ?? _customPresets.cast().firstWhere((p) => p!.id == id, orElse: () => null); + } + /// Apply and persist a shader preset Future setPreset(ShaderPreset preset) async { _savedPreset = preset; @@ -59,6 +73,48 @@ class ShaderProvider extends ChangeNotifier { } } + /// Import a custom shader from a file path. + /// Copies the file to the custom shaders directory and creates a preset. + Future importCustomShader(String filePath, String displayName) async { + final storedFileName = await ShaderAssetLoader.importCustomShader(filePath); + final id = 'custom_$storedFileName'; + + final preset = ShaderPreset( + id: id, + name: displayName, + type: ShaderPresetType.custom, + fileName: storedFileName, + ); + + _customPresets.add(preset); + await _saveCustomPresets(); + notifyListeners(); + return preset; + } + + /// Delete a custom shader preset and its file. + Future deleteCustomShader(ShaderPreset preset) async { + if (preset.fileName != null) { + await ShaderAssetLoader.deleteCustomShader(preset.fileName!); + } + _customPresets.removeWhere((p) => p.id == preset.id); + await _saveCustomPresets(); + + // Reset to none if the deleted preset was active + if (_currentPreset.id == preset.id || _savedPreset.id == preset.id) { + _savedPreset = ShaderPreset.none; + _currentPreset = ShaderPreset.none; + await _settingsService.setGlobalShaderPreset(ShaderPreset.none.id); + } + + notifyListeners(); + } + + Future _saveCustomPresets() async { + final data = _customPresets.map((p) => p.toJson()).toList(); + await _settingsService.setCustomShaderPresets(data); + } + /// Reset to default (no shaders) Future reset() async { await setPreset(ShaderPreset.none); diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index f73f221a..5105cd41 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -1289,12 +1289,13 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (_shaderService == null || !_shaderService!.isSupported) return; try { + final shaderProvider = context.read(); final settings = await SettingsService.getInstance(); final presetId = settings.getGlobalShaderPreset(); - final preset = ShaderPreset.fromId(presetId) ?? ShaderPreset.none; + final preset = (shaderProvider.initialized ? shaderProvider.findPresetById(presetId) : ShaderPreset.fromId(presetId)) ?? ShaderPreset.none; await _shaderService!.applyPreset(preset); if (!mounted) return; - context.read().setCurrentPreset(preset); + shaderProvider.setCurrentPreset(preset); } catch (e) { appLogger.d('Could not apply shader preset', error: e); } diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 4658817c..409e3754 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -86,6 +86,7 @@ class SettingsService extends BaseSharedPreferencesService { static const String _keyAmbientLighting = 'ambient_lighting'; static const String _keyAudioPassthrough = 'audio_passthrough'; static const String _keyAudioNormalization = 'audio_normalization'; + static const String _keyCustomShaderPresets = 'custom_shader_presets'; SettingsService._(); @@ -1153,6 +1154,22 @@ class SettingsService extends BaseSharedPreferencesService { return prefs.getString(_keyGlobalShaderPreset) ?? 'none'; // Default: no shader } + // Custom Shader Presets + List> getCustomShaderPresets() { + final jsonString = prefs.getString(_keyCustomShaderPresets); + if (jsonString == null) return []; + try { + final List decoded = json.decode(jsonString); + return decoded.cast>(); + } catch (_) { + return []; + } + } + + Future setCustomShaderPresets(List> presets) async { + await prefs.setString(_keyCustomShaderPresets, json.encode(presets)); + } + // Require Profile Selection on App Open Future setRequireProfileSelectionOnOpen(bool enabled) async { await prefs.setBool(_keyRequireProfileSelectionOnOpen, enabled); @@ -1309,6 +1326,7 @@ class SettingsService extends BaseSharedPreferencesService { prefs.remove(_keyShowUnwatchedCount), prefs.remove(_keyHideSpoilers), prefs.remove(_keyGlobalShaderPreset), + prefs.remove(_keyCustomShaderPresets), prefs.remove(_keyRequireProfileSelectionOnOpen), prefs.remove(_keyUseExternalPlayer), prefs.remove(_keySelectedExternalPlayer), diff --git a/lib/services/shader_asset_loader.dart b/lib/services/shader_asset_loader.dart index 7d037431..0f31e107 100644 --- a/lib/services/shader_asset_loader.dart +++ b/lib/services/shader_asset_loader.dart @@ -174,6 +174,47 @@ class ShaderAssetLoader { return shaders; } + /// Get the custom shader directory path, creating it if necessary. + /// Uses app support directory (persistent) rather than temp/cache. + static Future _getCustomShaderDirectory() async { + final supportDir = await getApplicationSupportDirectory(); + final customDir = Directory(path.join(supportDir.path, 'custom_shaders')); + + if (!await customDir.exists()) { + await customDir.create(recursive: true); + } + + return customDir.path; + } + + /// Import a custom shader file into the custom shaders directory. + /// Returns the stored file name (UUID-based to avoid collisions). + static Future importCustomShader(String sourcePath) async { + final customDir = await _getCustomShaderDirectory(); + final ext = path.extension(sourcePath); + final uuid = DateTime.now().millisecondsSinceEpoch.toRadixString(36); + final storedName = '$uuid$ext'; + final targetFile = File(path.join(customDir, storedName)); + + await File(sourcePath).copy(targetFile.path); + return storedName; + } + + /// Delete a custom shader file from the custom shaders directory. + static Future deleteCustomShader(String fileName) async { + final customDir = await _getCustomShaderDirectory(); + final file = File(path.join(customDir, fileName)); + if (await file.exists()) { + await file.delete(); + } + } + + /// Get the absolute path for a custom shader file. + static Future getCustomShaderPath(String fileName) async { + final customDir = await _getCustomShaderDirectory(); + return path.join(customDir, fileName); + } + /// Get shader paths for a given preset. /// Returns an empty list for ShaderPresetType.none. static Future> getShadersForPreset(ShaderPreset preset) async { @@ -185,6 +226,11 @@ class ShaderAssetLoader { case ShaderPresetType.anime4k: if (preset.anime4kConfig == null) return []; return getAnime4KShaders(preset.anime4kConfig!); + case ShaderPresetType.custom: + if (preset.fileName == null) return []; + final shaderPath = await getCustomShaderPath(preset.fileName!); + if (!await File(shaderPath).exists()) return []; + return [shaderPath]; } } diff --git a/lib/services/shader_service.dart b/lib/services/shader_service.dart index b5f64286..31ded2c4 100644 --- a/lib/services/shader_service.dart +++ b/lib/services/shader_service.dart @@ -149,8 +149,8 @@ class ShaderService { } /// Cycle to the next preset in the available list. - Future cyclePreset() async { - final presets = ShaderPreset.allPresets; + Future cyclePreset({List? presets}) async { + presets ??= ShaderPreset.allPresets; final currentIndex = presets.indexWhere((p) => p.id == _currentPreset.id); final nextIndex = (currentIndex + 1) % presets.length; final nextPreset = presets[nextIndex]; diff --git a/lib/widgets/video_controls/sheets/video_settings_sheet.dart b/lib/widgets/video_controls/sheets/video_settings_sheet.dart index 1bf81b69..4c37271f 100644 --- a/lib/widgets/video_controls/sheets/video_settings_sheet.dart +++ b/lib/widgets/video_controls/sheets/video_settings_sheet.dart @@ -1,8 +1,10 @@ import 'dart:io'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:path/path.dart' as path; import 'package:provider/provider.dart'; @@ -13,8 +15,10 @@ import '../../../services/settings_service.dart'; import '../../../services/shader_service.dart'; import '../../../services/sleep_timer_service.dart'; import '../../../focus/focusable_wrapper.dart'; +import '../../../utils/dialogs.dart'; import '../../../utils/formatters.dart'; import '../../../utils/platform_detector.dart'; +import '../../../utils/snackbar_helper.dart'; import '../../../theme/mono_tokens.dart'; import '../../../widgets/focusable_list_tile.dart'; import '../../../widgets/overlay_sheet.dart'; @@ -659,20 +663,43 @@ class _VideoSettingsSheetState extends State { return Consumer( builder: (context, shaderProvider, _) { final currentPreset = widget.shaderService!.currentPreset; - final presets = ShaderPreset.allPresets; + final presets = shaderProvider.allPresets; + // +1 for the import button at the end return ListView.builder( - itemCount: presets.length, + itemCount: presets.length + 1, itemBuilder: (context, index) { + // Import button at the end + if (index == presets.length) { + return FocusableListTile( + leading: AppIcon(Symbols.add_rounded, fill: 1, color: tokens(context).textMuted), + title: Text(t.shaders.importShader), + onTap: () => _importCustomShader(shaderProvider), + ); + } + final preset = presets[index]; final isSelected = preset.id == currentPreset.id; + final isCustom = preset.type == ShaderPresetType.custom; return FocusableListTile( title: Text(preset.name, style: TextStyle(color: isSelected ? Colors.amber : null)), subtitle: _getShaderSubtitle(preset) != null ? Text(_getShaderSubtitle(preset)!, style: TextStyle(color: tokens(context).textMuted, fontSize: 12)) : null, - trailing: isSelected ? const AppIcon(Symbols.check_rounded, fill: 1, color: Colors.amber) : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isSelected) const AppIcon(Symbols.check_rounded, fill: 1, color: Colors.amber), + if (isCustom) ...[ + if (isSelected) const SizedBox(width: 8), + GestureDetector( + onTap: () => _deleteCustomShader(shaderProvider, preset), + child: AppIcon(Symbols.delete_rounded, fill: 1, color: tokens(context).textMuted, size: 20), + ), + ], + ], + ), onTap: () async { // Disable ambient lighting when selecting a shader if (preset.type != ShaderPresetType.none && widget.isAmbientLightingEnabled) { @@ -690,6 +717,54 @@ class _VideoSettingsSheetState extends State { ); } + Future _importCustomShader(ShaderProvider shaderProvider) async { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['glsl'], + ); + + if (result == null || result.files.isEmpty || !mounted) return; + + final filePath = result.files.first.path; + if (filePath == null) return; + + try { + final displayName = path.basenameWithoutExtension(filePath); + final preset = await shaderProvider.importCustomShader(filePath, displayName); + + // Apply the imported shader immediately + if (widget.shaderService != null && mounted) { + if (preset.type != ShaderPresetType.none && widget.isAmbientLightingEnabled) { + widget.onToggleAmbientLighting?.call(); + } + await widget.shaderService!.applyPreset(preset); + await shaderProvider.setPreset(preset); + widget.onShaderChanged?.call(); + } + + if (mounted) showSuccessSnackBar(context, t.shaders.shaderImported); + } catch (_) { + if (mounted) showErrorSnackBar(context, t.shaders.shaderImportFailed); + } + } + + Future _deleteCustomShader(ShaderProvider shaderProvider, ShaderPreset preset) async { + final confirmed = await showDeleteConfirmation( + context, + title: t.shaders.deleteShader, + message: t.shaders.deleteShaderConfirm(name: preset.name), + ); + if (!confirmed || !mounted) return; + + // If the deleted shader is active, clear it from the player first + if (widget.shaderService!.currentPreset.id == preset.id) { + await widget.shaderService!.applyPreset(ShaderPreset.none); + widget.onShaderChanged?.call(); + } + + await shaderProvider.deleteCustomShader(preset); + } + String? _getShaderSubtitle(ShaderPreset preset) { switch (preset.type) { case ShaderPresetType.none: @@ -705,6 +780,8 @@ class _VideoSettingsSheetState extends State { return '$quality - ${t.shaders.mode} $mode'; } return null; + case ShaderPresetType.custom: + return t.shaders.customShaderDescription; } } diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index b652cd7f..8e026a2b 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -592,9 +592,10 @@ class _PlexVideoControlsState extends State with WindowListen // Currently off - restore saved preset final shaderProvider = context.read(); final saved = shaderProvider.savedPreset; + final allPresets = shaderProvider.allPresets; final targetPreset = saved.isEnabled ? saved - : ShaderPreset.allPresets.firstWhere((p) => p.isEnabled, orElse: () => ShaderPreset.allPresets[1]); + : allPresets.firstWhere((p) => p.isEnabled, orElse: () => allPresets[1]); shaderService.applyPreset(targetPreset).then((_) { shaderProvider.setCurrentPreset(targetPreset); // ignore: no-empty-block - setState triggers rebuild to reflect restored shader