@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => '원격 세션 호스트',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => '创建远程会话',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user