feat: custom GLSL shader import

close #624
This commit is contained in:
edde746
2026-03-04 08:47:40 +01:00
parent 73eeac35a8
commit 3a76754af9
27 changed files with 402 additions and 23 deletions
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -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",
+2 -2
View File
@@ -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
+12
View File
@@ -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',
+24
View File
@@ -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',
+12
View File
@@ -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',
+12
View File
@@ -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',
+12
View File
@@ -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',
+12
View File
@@ -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' => '원격 세션 호스트',
+12
View File
@@ -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',
+12
View File
@@ -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',
+12
View File
@@ -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' => '创建远程会话',
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -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",
+7 -1
View File
@@ -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<String, dynamic> 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?,
);
}
}
+59 -3
View File
@@ -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<ShaderPreset> _customPresets = [];
bool _initialized = false;
ShaderProvider() {
@@ -20,8 +22,12 @@ class ShaderProvider extends ChangeNotifier {
Future<void> _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<ShaderPreset> get allPresets => ShaderPreset.allPresets;
/// All available shader presets (built-in + custom)
List<ShaderPreset> get allPresets => [...ShaderPreset.allPresets, ..._customPresets];
/// Custom shader presets only
List<ShaderPreset> 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<ShaderPreset?>().firstWhere((p) => p!.id == id, orElse: () => null);
}
/// Apply and persist a shader preset
Future<void> 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<ShaderPreset> 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<void> 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<void> _saveCustomPresets() async {
final data = _customPresets.map((p) => p.toJson()).toList();
await _settingsService.setCustomShaderPresets(data);
}
/// Reset to default (no shaders)
Future<void> reset() async {
await setPreset(ShaderPreset.none);
+3 -2
View File
@@ -1289,12 +1289,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
if (_shaderService == null || !_shaderService!.isSupported) return;
try {
final shaderProvider = context.read<ShaderProvider>();
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<ShaderProvider>().setCurrentPreset(preset);
shaderProvider.setCurrentPreset(preset);
} catch (e) {
appLogger.d('Could not apply shader preset', error: e);
}
+18
View File
@@ -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<Map<String, dynamic>> getCustomShaderPresets() {
final jsonString = prefs.getString(_keyCustomShaderPresets);
if (jsonString == null) return [];
try {
final List<dynamic> decoded = json.decode(jsonString);
return decoded.cast<Map<String, dynamic>>();
} catch (_) {
return [];
}
}
Future<void> setCustomShaderPresets(List<Map<String, dynamic>> presets) async {
await prefs.setString(_keyCustomShaderPresets, json.encode(presets));
}
// Require Profile Selection on App Open
Future<void> 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),
+46
View File
@@ -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<String> _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<String> 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<void> 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<String> 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<List<String>> 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];
}
}
+2 -2
View File
@@ -149,8 +149,8 @@ class ShaderService {
}
/// Cycle to the next preset in the available list.
Future<ShaderPreset> cyclePreset() async {
final presets = ShaderPreset.allPresets;
Future<ShaderPreset> cyclePreset({List<ShaderPreset>? presets}) async {
presets ??= ShaderPreset.allPresets;
final currentIndex = presets.indexWhere((p) => p.id == _currentPreset.id);
final nextIndex = (currentIndex + 1) % presets.length;
final nextPreset = presets[nextIndex];
@@ -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<VideoSettingsSheet> {
return Consumer<ShaderProvider>(
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<VideoSettingsSheet> {
);
}
Future<void> _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<void> _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<VideoSettingsSheet> {
return '$quality - ${t.shaders.mode} $mode';
}
return null;
case ShaderPresetType.custom:
return t.shaders.customShaderDescription;
}
}
@@ -592,9 +592,10 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
// Currently off - restore saved preset
final shaderProvider = context.read<ShaderProvider>();
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