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
@@ -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;
}
}