feat: custom mpv config
This commit is contained in:
+13023
-6685
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,7 @@
|
||||
"licenses": "Licenses",
|
||||
"switchProfile": "Switch Profile",
|
||||
"subtitleStyling": "Subtitle Styling",
|
||||
"mpvConfig": "MPV Configuration",
|
||||
"search": "Search",
|
||||
"logs": "Logs"
|
||||
},
|
||||
@@ -296,6 +297,32 @@
|
||||
"backgroundOpacity": "Background Opacity",
|
||||
"backgroundColor": "Background Color"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "MPV Configuration",
|
||||
"description": "Advanced video player settings",
|
||||
"properties": "Properties",
|
||||
"presets": "Presets",
|
||||
"noProperties": "No properties configured",
|
||||
"noPresets": "No saved presets",
|
||||
"addProperty": "Add Property",
|
||||
"editProperty": "Edit Property",
|
||||
"deleteProperty": "Delete Property",
|
||||
"propertyKey": "Property Key",
|
||||
"propertyKeyHint": "e.g., hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Property Value",
|
||||
"propertyValueHint": "e.g., auto, 256000000",
|
||||
"saveAsPreset": "Save as Preset...",
|
||||
"presetName": "Preset Name",
|
||||
"presetNameHint": "Enter a name for this preset",
|
||||
"loadPreset": "Load",
|
||||
"deletePreset": "Delete",
|
||||
"presetSaved": "Preset saved",
|
||||
"presetLoaded": "Preset loaded",
|
||||
"presetDeleted": "Preset deleted",
|
||||
"confirmDeletePreset": "Are you sure you want to delete this preset?",
|
||||
"confirmDeleteProperty": "Are you sure you want to delete this property?",
|
||||
"entriesCount": "${count} entries"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Confirm Action",
|
||||
"cancel": "Cancel",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"selectServer": "Server auswählen",
|
||||
"switchProfile": "Profil wechseln",
|
||||
"subtitleStyling": "Untertitel-Stil",
|
||||
"mpvConfig": "MPV-Konfiguration",
|
||||
"search": "Suche",
|
||||
"logs": "Protokolle"
|
||||
},
|
||||
@@ -297,6 +298,32 @@
|
||||
"backgroundOpacity": "Hintergrunddeckkraft",
|
||||
"backgroundColor": "Hintergrundfarbe"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "MPV-Konfiguration",
|
||||
"description": "Erweiterte Videoplayer-Einstellungen",
|
||||
"properties": "Eigenschaften",
|
||||
"presets": "Voreinstellungen",
|
||||
"noProperties": "Keine Eigenschaften konfiguriert",
|
||||
"noPresets": "Keine gespeicherten Voreinstellungen",
|
||||
"addProperty": "Eigenschaft hinzufügen",
|
||||
"editProperty": "Eigenschaft bearbeiten",
|
||||
"deleteProperty": "Eigenschaft löschen",
|
||||
"propertyKey": "Eigenschaftsschlüssel",
|
||||
"propertyKeyHint": "z.B. hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Eigenschaftswert",
|
||||
"propertyValueHint": "z.B. auto, 256000000",
|
||||
"saveAsPreset": "Als Voreinstellung speichern...",
|
||||
"presetName": "Name der Voreinstellung",
|
||||
"presetNameHint": "Namen für diese Voreinstellung eingeben",
|
||||
"loadPreset": "Laden",
|
||||
"deletePreset": "Löschen",
|
||||
"presetSaved": "Voreinstellung gespeichert",
|
||||
"presetLoaded": "Voreinstellung geladen",
|
||||
"presetDeleted": "Voreinstellung gelöscht",
|
||||
"confirmDeletePreset": "Möchten Sie diese Voreinstellung wirklich löschen?",
|
||||
"confirmDeleteProperty": "Möchten Sie diese Eigenschaft wirklich löschen?",
|
||||
"entriesCount": "${count} Einträge"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Aktion bestätigen",
|
||||
"cancel": "Abbrechen",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"selectServer": "Seleziona server",
|
||||
"switchProfile": "Cambia profilo",
|
||||
"subtitleStyling": "Stile sottotitoli",
|
||||
"mpvConfig": "Configurazione MPV",
|
||||
"search": "Cerca",
|
||||
"logs": "Registro"
|
||||
},
|
||||
@@ -297,6 +298,32 @@
|
||||
"backgroundOpacity": "Opacità sfondo",
|
||||
"backgroundColor": "Colore sfondo"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "Configurazione MPV",
|
||||
"description": "Impostazioni avanzate del lettore video",
|
||||
"properties": "Proprietà",
|
||||
"presets": "Preset",
|
||||
"noProperties": "Nessuna proprietà configurata",
|
||||
"noPresets": "Nessun preset salvato",
|
||||
"addProperty": "Aggiungi proprietà",
|
||||
"editProperty": "Modifica proprietà",
|
||||
"deleteProperty": "Elimina proprietà",
|
||||
"propertyKey": "Chiave proprietà",
|
||||
"propertyKeyHint": "es. hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Valore proprietà",
|
||||
"propertyValueHint": "es. auto, 256000000",
|
||||
"saveAsPreset": "Salva come preset...",
|
||||
"presetName": "Nome preset",
|
||||
"presetNameHint": "Inserisci un nome per questo preset",
|
||||
"loadPreset": "Carica",
|
||||
"deletePreset": "Elimina",
|
||||
"presetSaved": "Preset salvato",
|
||||
"presetLoaded": "Preset caricato",
|
||||
"presetDeleted": "Preset eliminato",
|
||||
"confirmDeletePreset": "Sei sicuro di voler eliminare questo preset?",
|
||||
"confirmDeleteProperty": "Sei sicuro di voler eliminare questa proprietà?",
|
||||
"entriesCount": "${count} voci"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Conferma azione",
|
||||
"cancel": "Cancella",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"selectServer": "Selecteer server",
|
||||
"switchProfile": "Wissel van profiel",
|
||||
"subtitleStyling": "Ondertitel opmaak",
|
||||
"mpvConfig": "MPV-configuratie",
|
||||
"search": "Zoeken",
|
||||
"logs": "Logbestanden"
|
||||
},
|
||||
@@ -297,6 +298,32 @@
|
||||
"backgroundOpacity": "Achtergrond transparantie",
|
||||
"backgroundColor": "Achtergrondkleur"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "MPV-configuratie",
|
||||
"description": "Geavanceerde videospeler-instellingen",
|
||||
"properties": "Eigenschappen",
|
||||
"presets": "Voorinstellingen",
|
||||
"noProperties": "Geen eigenschappen geconfigureerd",
|
||||
"noPresets": "Geen opgeslagen voorinstellingen",
|
||||
"addProperty": "Eigenschap toevoegen",
|
||||
"editProperty": "Eigenschap bewerken",
|
||||
"deleteProperty": "Eigenschap verwijderen",
|
||||
"propertyKey": "Eigenschapssleutel",
|
||||
"propertyKeyHint": "bijv. hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Eigenschapswaarde",
|
||||
"propertyValueHint": "bijv. auto, 256000000",
|
||||
"saveAsPreset": "Opslaan als voorinstelling...",
|
||||
"presetName": "Naam voorinstelling",
|
||||
"presetNameHint": "Voer een naam in voor deze voorinstelling",
|
||||
"loadPreset": "Laden",
|
||||
"deletePreset": "Verwijderen",
|
||||
"presetSaved": "Voorinstelling opgeslagen",
|
||||
"presetLoaded": "Voorinstelling geladen",
|
||||
"presetDeleted": "Voorinstelling verwijderd",
|
||||
"confirmDeletePreset": "Weet je zeker dat je deze voorinstelling wilt verwijderen?",
|
||||
"confirmDeleteProperty": "Weet je zeker dat je deze eigenschap wilt verwijderen?",
|
||||
"entriesCount": "${count} items"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Bevestig actie",
|
||||
"cancel": "Annuleren",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"selectServer": "Välj server",
|
||||
"switchProfile": "Byt profil",
|
||||
"subtitleStyling": "Undertext-styling",
|
||||
"mpvConfig": "MPV-konfiguration",
|
||||
"search": "Sök",
|
||||
"logs": "Loggar"
|
||||
},
|
||||
@@ -297,6 +298,32 @@
|
||||
"backgroundOpacity": "Bakgrundsopacitet",
|
||||
"backgroundColor": "Bakgrundsfärg"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "MPV-konfiguration",
|
||||
"description": "Avancerade videospelares inställningar",
|
||||
"properties": "Egenskaper",
|
||||
"presets": "Förval",
|
||||
"noProperties": "Inga egenskaper konfigurerade",
|
||||
"noPresets": "Inga sparade förval",
|
||||
"addProperty": "Lägg till egenskap",
|
||||
"editProperty": "Redigera egenskap",
|
||||
"deleteProperty": "Ta bort egenskap",
|
||||
"propertyKey": "Egenskapsnyckel",
|
||||
"propertyKeyHint": "t.ex. hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "Egenskapsvärde",
|
||||
"propertyValueHint": "t.ex. auto, 256000000",
|
||||
"saveAsPreset": "Spara som förval...",
|
||||
"presetName": "Förvalnamn",
|
||||
"presetNameHint": "Ange ett namn för detta förval",
|
||||
"loadPreset": "Ladda",
|
||||
"deletePreset": "Ta bort",
|
||||
"presetSaved": "Förval sparat",
|
||||
"presetLoaded": "Förval laddat",
|
||||
"presetDeleted": "Förval borttaget",
|
||||
"confirmDeletePreset": "Är du säker på att du vill ta bort detta förval?",
|
||||
"confirmDeleteProperty": "Är du säker på att du vill ta bort denna egenskap?",
|
||||
"entriesCount": "${count} poster"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "Bekräfta åtgärd",
|
||||
"cancel": "Avbryt",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"selectServer": "选择服务器",
|
||||
"switchProfile": "切换用户",
|
||||
"subtitleStyling": "字幕样式",
|
||||
"mpvConfig": "MPV 配置",
|
||||
"search": "搜索",
|
||||
"logs": "日志"
|
||||
},
|
||||
@@ -297,6 +298,32 @@
|
||||
"backgroundOpacity": "背景不透明度",
|
||||
"backgroundColor": "背景颜色"
|
||||
},
|
||||
"mpvConfig": {
|
||||
"title": "MPV 配置",
|
||||
"description": "高级视频播放器设置",
|
||||
"properties": "属性",
|
||||
"presets": "预设",
|
||||
"noProperties": "未配置任何属性",
|
||||
"noPresets": "没有保存的预设",
|
||||
"addProperty": "添加属性",
|
||||
"editProperty": "编辑属性",
|
||||
"deleteProperty": "删除属性",
|
||||
"propertyKey": "属性键",
|
||||
"propertyKeyHint": "例如 hwdec, demuxer-max-bytes",
|
||||
"propertyValue": "属性值",
|
||||
"propertyValueHint": "例如 auto, 256000000",
|
||||
"saveAsPreset": "保存为预设...",
|
||||
"presetName": "预设名称",
|
||||
"presetNameHint": "输入此预设的名称",
|
||||
"loadPreset": "加载",
|
||||
"deletePreset": "删除",
|
||||
"presetSaved": "预设已保存",
|
||||
"presetLoaded": "预设已加载",
|
||||
"presetDeleted": "预设已删除",
|
||||
"confirmDeletePreset": "确定要删除此预设吗?",
|
||||
"confirmDeleteProperty": "确定要删除此属性吗?",
|
||||
"entriesCount": "${count} 条目"
|
||||
},
|
||||
"dialog": {
|
||||
"confirmAction": "确认操作",
|
||||
"cancel": "取消",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/// Represents a single MPV configuration entry
|
||||
class MpvConfigEntry {
|
||||
final String key;
|
||||
final String value;
|
||||
final bool isEnabled;
|
||||
|
||||
const MpvConfigEntry({required this.key, required this.value, this.isEnabled = true});
|
||||
|
||||
factory MpvConfigEntry.fromJson(Map<String, dynamic> json) {
|
||||
return MpvConfigEntry(
|
||||
key: json['key'] as String,
|
||||
value: json['value'] as String,
|
||||
isEnabled: json['isEnabled'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {'key': key, 'value': value, 'isEnabled': isEnabled};
|
||||
|
||||
MpvConfigEntry copyWith({String? key, String? value, bool? isEnabled}) {
|
||||
return MpvConfigEntry(key: key ?? this.key, value: value ?? this.value, isEnabled: isEnabled ?? this.isEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a saved preset of MPV configurations
|
||||
class MpvPreset {
|
||||
final String name;
|
||||
final List<MpvConfigEntry> entries;
|
||||
final DateTime createdAt;
|
||||
|
||||
const MpvPreset({required this.name, required this.entries, required this.createdAt});
|
||||
|
||||
factory MpvPreset.fromJson(Map<String, dynamic> json) {
|
||||
return MpvPreset(
|
||||
name: json['name'] as String,
|
||||
entries: (json['entries'] as List).map((e) => MpvConfigEntry.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
'entries': entries.map((e) => e.toJson()).toList(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../models/mpv_config_models.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../widgets/desktop_app_bar.dart';
|
||||
|
||||
class MpvConfigScreen extends StatefulWidget {
|
||||
const MpvConfigScreen({super.key});
|
||||
|
||||
@override
|
||||
State<MpvConfigScreen> createState() => _MpvConfigScreenState();
|
||||
}
|
||||
|
||||
class _MpvConfigScreenState extends State<MpvConfigScreen> {
|
||||
late SettingsService _settingsService;
|
||||
bool _isLoading = true;
|
||||
|
||||
List<MpvConfigEntry> _entries = [];
|
||||
List<MpvPreset> _presets = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
}
|
||||
|
||||
Future<void> _loadSettings() async {
|
||||
_settingsService = await SettingsService.getInstance();
|
||||
|
||||
setState(() {
|
||||
_entries = _settingsService.getMpvConfigEntries();
|
||||
_presets = _settingsService.getMpvPresets();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveEntries() async {
|
||||
await _settingsService.setMpvConfigEntries(_entries);
|
||||
}
|
||||
|
||||
void _toggleEntry(int index) {
|
||||
setState(() {
|
||||
_entries[index] = _entries[index].copyWith(isEnabled: !_entries[index].isEnabled);
|
||||
});
|
||||
_saveEntries();
|
||||
}
|
||||
|
||||
void _deleteEntry(int index) {
|
||||
setState(() {
|
||||
_entries.removeAt(index);
|
||||
});
|
||||
_saveEntries();
|
||||
}
|
||||
|
||||
void _addEntry(MpvConfigEntry entry) {
|
||||
setState(() {
|
||||
_entries.add(entry);
|
||||
});
|
||||
_saveEntries();
|
||||
}
|
||||
|
||||
void _updateEntry(int index, MpvConfigEntry entry) {
|
||||
setState(() {
|
||||
_entries[index] = entry;
|
||||
});
|
||||
_saveEntries();
|
||||
}
|
||||
|
||||
Future<void> _showEntryDialog({int? editIndex}) async {
|
||||
final isEdit = editIndex != null;
|
||||
final existingEntry = isEdit ? _entries[editIndex] : null;
|
||||
|
||||
final keyController = TextEditingController(text: existingEntry?.key ?? '');
|
||||
final valueController = TextEditingController(text: existingEntry?.value ?? '');
|
||||
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(isEdit ? t.mpvConfig.editProperty : t.mpvConfig.addProperty),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: keyController,
|
||||
decoration: InputDecoration(labelText: t.mpvConfig.propertyKey, hintText: t.mpvConfig.propertyKeyHint),
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: valueController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.mpvConfig.propertyValue,
|
||||
hintText: t.mpvConfig.propertyValueHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (keyController.text.isNotEmpty && valueController.text.isNotEmpty) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
},
|
||||
child: Text(t.common.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
final entry = MpvConfigEntry(
|
||||
key: keyController.text.trim(),
|
||||
value: valueController.text.trim(),
|
||||
isEnabled: existingEntry?.isEnabled ?? true,
|
||||
);
|
||||
|
||||
if (isEdit) {
|
||||
_updateEntry(editIndex, entry);
|
||||
} else {
|
||||
_addEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
keyController.dispose();
|
||||
valueController.dispose();
|
||||
}
|
||||
|
||||
Future<void> _showDeleteEntryDialog(int index) async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.mpvConfig.deleteProperty),
|
||||
content: Text(t.mpvConfig.confirmDeleteProperty),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
||||
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(t.common.delete)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
_deleteEntry(index);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showSavePresetDialog() async {
|
||||
if (_entries.isEmpty) return;
|
||||
|
||||
final nameController = TextEditingController();
|
||||
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.mpvConfig.saveAsPreset),
|
||||
content: TextField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(labelText: t.mpvConfig.presetName, hintText: t.mpvConfig.presetNameHint),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (nameController.text.isNotEmpty) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
},
|
||||
child: Text(t.common.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
await _settingsService.saveMpvPreset(nameController.text.trim(), _entries);
|
||||
setState(() {
|
||||
_presets = _settingsService.getMpvPresets();
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.mpvConfig.presetSaved)));
|
||||
}
|
||||
}
|
||||
|
||||
nameController.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadPreset(MpvPreset preset) async {
|
||||
await _settingsService.loadMpvPreset(preset.name);
|
||||
setState(() {
|
||||
_entries = _settingsService.getMpvConfigEntries();
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.mpvConfig.presetLoaded)));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deletePreset(MpvPreset preset) async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.mpvConfig.deletePreset),
|
||||
content: Text(t.mpvConfig.confirmDeletePreset),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(t.common.cancel)),
|
||||
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(t.common.delete)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
await _settingsService.deleteMpvPreset(preset.name);
|
||||
setState(() {
|
||||
_presets = _settingsService.getMpvPresets();
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.mpvConfig.presetDeleted)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
CustomAppBar(title: Text(t.screens.mpvConfig), pinned: true),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
_buildPresetsCard(),
|
||||
const SizedBox(height: 16),
|
||||
_buildEntriesCard(),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPresetsCard() {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
t.mpvConfig.presets,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const AppIcon(Symbols.save_rounded, fill: 1),
|
||||
title: Text(t.mpvConfig.saveAsPreset),
|
||||
enabled: _entries.isNotEmpty,
|
||||
onTap: _entries.isNotEmpty ? _showSavePresetDialog : null,
|
||||
),
|
||||
if (_presets.isNotEmpty) ...[
|
||||
const Divider(),
|
||||
..._presets.map(
|
||||
(preset) => ListTile(
|
||||
leading: const AppIcon(Symbols.folder_rounded, fill: 1),
|
||||
title: Text(preset.name),
|
||||
subtitle: Text(t.mpvConfig.entriesCount(count: preset.entries.length)),
|
||||
trailing: PopupMenuButton<String>(
|
||||
onSelected: (value) {
|
||||
if (value == 'load') {
|
||||
_loadPreset(preset);
|
||||
} else if (value == 'delete') {
|
||||
_deletePreset(preset);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(value: 'load', child: Text(t.mpvConfig.loadPreset)),
|
||||
PopupMenuItem(value: 'delete', child: Text(t.mpvConfig.deletePreset)),
|
||||
],
|
||||
),
|
||||
onTap: () => _loadPreset(preset),
|
||||
),
|
||||
),
|
||||
] else
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Text(
|
||||
t.mpvConfig.noPresets,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEntriesCard() {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
t.mpvConfig.properties,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const AppIcon(Symbols.add_rounded, fill: 1),
|
||||
onPressed: () => _showEntryDialog(),
|
||||
tooltip: t.mpvConfig.addProperty,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_entries.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
...List.generate(_entries.length, (index) {
|
||||
final entry = _entries[index];
|
||||
return ListTile(
|
||||
leading: Switch(value: entry.isEnabled, onChanged: (_) => _toggleEntry(index)),
|
||||
title: Text(
|
||||
entry.key,
|
||||
style: TextStyle(color: entry.isEnabled ? null : Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
subtitle: Text(
|
||||
entry.value,
|
||||
style: TextStyle(color: entry.isEnabled ? null : Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
trailing: PopupMenuButton<String>(
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
_showEntryDialog(editIndex: index);
|
||||
} else if (value == 'delete') {
|
||||
_showDeleteEntryDialog(index);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(value: 'edit', child: Text(t.mpvConfig.editProperty)),
|
||||
PopupMenuItem(value: 'delete', child: Text(t.mpvConfig.deleteProperty)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
] else
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Text(
|
||||
t.mpvConfig.noProperties,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import '../../widgets/desktop_app_bar.dart';
|
||||
import 'hotkey_recorder_widget.dart';
|
||||
import 'about_screen.dart';
|
||||
import 'logs_screen.dart';
|
||||
import 'mpv_config_screen.dart';
|
||||
import 'subtitle_styling_screen.dart';
|
||||
|
||||
/// Helper class for option selection dialog items
|
||||
@@ -250,6 +251,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const SubtitleStylingScreen()));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const AppIcon(Symbols.tune_rounded, fill: 1),
|
||||
title: Text(t.mpvConfig.title),
|
||||
subtitle: Text(t.mpvConfig.description),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const MpvConfigScreen()));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const AppIcon(Symbols.replay_10_rounded, fill: 1),
|
||||
title: Text(t.settings.smallSkipDuration),
|
||||
|
||||
@@ -313,6 +313,17 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
await player!.setProperty('sub-delay', offsetSeconds.toString());
|
||||
}
|
||||
|
||||
// Apply custom MPV config entries
|
||||
final customMpvConfig = settingsService.getEnabledMpvConfigEntries();
|
||||
for (final entry in customMpvConfig.entries) {
|
||||
try {
|
||||
await player!.setProperty(entry.key, entry.value);
|
||||
appLogger.d('Applied custom MPV property: ${entry.key}=${entry.value}');
|
||||
} catch (e) {
|
||||
appLogger.w('Failed to set MPV property ${entry.key}', error: e);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply saved volume
|
||||
final savedVolume = settingsService.getVolume();
|
||||
player!.setVolume(savedVolume);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:plezy/utils/app_logger.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../models/mpv_config_models.dart';
|
||||
import 'base_shared_preferences_service.dart';
|
||||
import '../utils/platform_detector.dart';
|
||||
|
||||
@@ -50,6 +51,8 @@ class SettingsService extends BaseSharedPreferencesService {
|
||||
static const String _keyDownloadOnWifiOnly = 'download_on_wifi_only';
|
||||
static const String _keyVideoPlayerNavigationEnabled = 'video_player_navigation_enabled';
|
||||
static const String _keyShowPerformanceOverlay = 'show_performance_overlay';
|
||||
static const String _keyMpvConfigEntries = 'mpv_config_entries';
|
||||
static const String _keyMpvConfigPresets = 'mpv_config_presets';
|
||||
|
||||
SettingsService._();
|
||||
|
||||
@@ -805,6 +808,78 @@ class SettingsService extends BaseSharedPreferencesService {
|
||||
return prefs.getBool(_keyDownloadOnWifiOnly) ?? false;
|
||||
}
|
||||
|
||||
// MPV Config Entries
|
||||
|
||||
/// Get all MPV config entries
|
||||
List<MpvConfigEntry> getMpvConfigEntries() {
|
||||
final jsonString = prefs.getString(_keyMpvConfigEntries);
|
||||
if (jsonString == null) return [];
|
||||
|
||||
try {
|
||||
final List<dynamic> decoded = json.decode(jsonString);
|
||||
return decoded.map((e) => MpvConfigEntry.fromJson(e as Map<String, dynamic>)).toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Save all MPV config entries
|
||||
Future<void> setMpvConfigEntries(List<MpvConfigEntry> entries) async {
|
||||
final jsonString = json.encode(entries.map((e) => e.toJson()).toList());
|
||||
await prefs.setString(_keyMpvConfigEntries, jsonString);
|
||||
}
|
||||
|
||||
/// Get only enabled MPV config entries (for player initialization)
|
||||
Map<String, String> getEnabledMpvConfigEntries() {
|
||||
final entries = getMpvConfigEntries();
|
||||
return Map.fromEntries(entries.where((e) => e.isEnabled).map((e) => MapEntry(e.key, e.value)));
|
||||
}
|
||||
|
||||
// MPV Presets
|
||||
|
||||
/// Get all saved presets
|
||||
List<MpvPreset> getMpvPresets() {
|
||||
final jsonString = prefs.getString(_keyMpvConfigPresets);
|
||||
if (jsonString == null) return [];
|
||||
|
||||
try {
|
||||
final List<dynamic> decoded = json.decode(jsonString);
|
||||
return decoded.map((e) => MpvPreset.fromJson(e as Map<String, dynamic>)).toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Save a new preset (overwrites existing with same name)
|
||||
Future<void> saveMpvPreset(String name, List<MpvConfigEntry> entries) async {
|
||||
final presets = getMpvPresets();
|
||||
|
||||
// Remove existing preset with same name
|
||||
presets.removeWhere((p) => p.name == name);
|
||||
|
||||
presets.add(MpvPreset(name: name, entries: entries, createdAt: DateTime.now()));
|
||||
|
||||
final jsonString = json.encode(presets.map((p) => p.toJson()).toList());
|
||||
await prefs.setString(_keyMpvConfigPresets, jsonString);
|
||||
}
|
||||
|
||||
/// Delete a preset by name
|
||||
Future<void> deleteMpvPreset(String name) async {
|
||||
final presets = getMpvPresets();
|
||||
presets.removeWhere((p) => p.name == name);
|
||||
|
||||
final jsonString = json.encode(presets.map((p) => p.toJson()).toList());
|
||||
await prefs.setString(_keyMpvConfigPresets, jsonString);
|
||||
}
|
||||
|
||||
/// Load a preset (replaces current entries)
|
||||
Future<void> loadMpvPreset(String name) async {
|
||||
final presets = getMpvPresets();
|
||||
final preset = presets.firstWhere((p) => p.name == name, orElse: () => throw Exception('Preset not found: $name'));
|
||||
|
||||
await setMpvConfigEntries(preset.entries);
|
||||
}
|
||||
|
||||
// Reset all settings to defaults
|
||||
Future<void> resetAllSettings() async {
|
||||
await Future.wait([
|
||||
@@ -841,6 +916,8 @@ class SettingsService extends BaseSharedPreferencesService {
|
||||
prefs.remove(_keyDownloadOnWifiOnly),
|
||||
prefs.remove(_keyVideoPlayerNavigationEnabled),
|
||||
prefs.remove(_keyShowPerformanceOverlay),
|
||||
prefs.remove(_keyMpvConfigEntries),
|
||||
prefs.remove(_keyMpvConfigPresets),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1306,9 +1306,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
Positioned(
|
||||
top: isMobile ? 60 : 16,
|
||||
left: 16,
|
||||
child: IgnorePointer(
|
||||
child: PlayerPerformanceOverlay(player: widget.player),
|
||||
),
|
||||
child: IgnorePointer(child: PlayerPerformanceOverlay(player: widget.player)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -13,14 +13,10 @@ import 'performance_stats_service.dart';
|
||||
class PlayerPerformanceOverlay extends StatefulWidget {
|
||||
final Player player;
|
||||
|
||||
const PlayerPerformanceOverlay({
|
||||
super.key,
|
||||
required this.player,
|
||||
});
|
||||
const PlayerPerformanceOverlay({super.key, required this.player});
|
||||
|
||||
@override
|
||||
State<PlayerPerformanceOverlay> createState() =>
|
||||
_PlayerPerformanceOverlayState();
|
||||
State<PlayerPerformanceOverlay> createState() => _PlayerPerformanceOverlayState();
|
||||
}
|
||||
|
||||
class _PlayerPerformanceOverlayState extends State<PlayerPerformanceOverlay> {
|
||||
@@ -55,12 +51,7 @@ class _PlayerPerformanceOverlayState extends State<PlayerPerformanceOverlay> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 4)],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -78,17 +69,13 @@ class _PlayerPerformanceOverlayState extends State<PlayerPerformanceOverlay> {
|
||||
_metric('FPS', _stats.videoFpsFormatted),
|
||||
_metric('Bitrate', _stats.videoBitrateFormatted),
|
||||
_metric('Decoder', _stats.hwdecFormatted),
|
||||
if (_stats.aspectName != null &&
|
||||
_stats.aspectName!.isNotEmpty)
|
||||
_metric('Aspect', _stats.aspectName!),
|
||||
if (_stats.rotate != null && _stats.rotate != 0)
|
||||
_metric('Rotation', _stats.rotateFormatted),
|
||||
if (_stats.aspectName != null && _stats.aspectName!.isNotEmpty) _metric('Aspect', _stats.aspectName!),
|
||||
if (_stats.rotate != null && _stats.rotate != 0) _metric('Rotation', _stats.rotateFormatted),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
_buildSection(Symbols.palette_rounded, 'Color', [
|
||||
_metric('Pixel Fmt', _stats.pixelformat ?? 'N/A'),
|
||||
if (_stats.hwPixelformat != null &&
|
||||
_stats.hwPixelformat != _stats.pixelformat)
|
||||
if (_stats.hwPixelformat != null && _stats.hwPixelformat != _stats.pixelformat)
|
||||
_metric('HW Fmt', _stats.hwPixelformat!),
|
||||
_metric('Matrix', _stats.colormatrix ?? 'N/A'),
|
||||
_metric('Primaries', _stats.primaries ?? 'N/A'),
|
||||
@@ -97,14 +84,10 @@ class _PlayerPerformanceOverlayState extends State<PlayerPerformanceOverlay> {
|
||||
if (_stats.hasHdrMetadata) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildSection(Symbols.hdr_on_rounded, 'HDR', [
|
||||
if (_stats.maxLuma != null)
|
||||
_metric('Max Luma', _stats.maxLumaFormatted),
|
||||
if (_stats.minLuma != null)
|
||||
_metric('Min Luma', _stats.minLumaFormatted),
|
||||
if (_stats.maxCll != null)
|
||||
_metric('MaxCLL', _stats.maxCllFormatted),
|
||||
if (_stats.maxFall != null)
|
||||
_metric('MaxFALL', _stats.maxFallFormatted),
|
||||
if (_stats.maxLuma != null) _metric('Max Luma', _stats.maxLumaFormatted),
|
||||
if (_stats.minLuma != null) _metric('Min Luma', _stats.minLumaFormatted),
|
||||
if (_stats.maxCll != null) _metric('MaxCLL', _stats.maxCllFormatted),
|
||||
if (_stats.maxFall != null) _metric('MaxFALL', _stats.maxFallFormatted),
|
||||
]),
|
||||
],
|
||||
],
|
||||
@@ -161,11 +144,7 @@ class _PlayerPerformanceOverlayState extends State<PlayerPerformanceOverlay> {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -181,13 +160,7 @@ class _PlayerPerformanceOverlayState extends State<PlayerPerformanceOverlay> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${metric.label}: ',
|
||||
style: const TextStyle(
|
||||
color: Colors.white60,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
Text('${metric.label}: ', style: const TextStyle(color: Colors.white60, fontSize: 10)),
|
||||
Flexible(
|
||||
child: Text(
|
||||
metric.value,
|
||||
|
||||
@@ -84,37 +84,37 @@ class PerformanceStats {
|
||||
|
||||
/// Creates an empty stats object (used as initial state).
|
||||
const PerformanceStats.empty()
|
||||
: videoCodec = null,
|
||||
videoWidth = null,
|
||||
videoHeight = null,
|
||||
videoFps = null,
|
||||
hwdecCurrent = null,
|
||||
videoBitrate = null,
|
||||
aspectName = null,
|
||||
rotate = null,
|
||||
pixelformat = null,
|
||||
hwPixelformat = null,
|
||||
colormatrix = null,
|
||||
primaries = null,
|
||||
gamma = null,
|
||||
maxLuma = null,
|
||||
minLuma = null,
|
||||
maxCll = null,
|
||||
maxFall = null,
|
||||
audioCodec = null,
|
||||
audioSamplerate = null,
|
||||
audioChannels = null,
|
||||
audioBitrate = null,
|
||||
actualFps = null,
|
||||
avsyncChange = null,
|
||||
displayFps = null,
|
||||
frameDropCount = null,
|
||||
decoderFrameDropCount = null,
|
||||
cacheUsed = null,
|
||||
cacheSpeed = null,
|
||||
cacheDuration = null,
|
||||
appMemoryBytes = null,
|
||||
uiFps = null;
|
||||
: videoCodec = null,
|
||||
videoWidth = null,
|
||||
videoHeight = null,
|
||||
videoFps = null,
|
||||
hwdecCurrent = null,
|
||||
videoBitrate = null,
|
||||
aspectName = null,
|
||||
rotate = null,
|
||||
pixelformat = null,
|
||||
hwPixelformat = null,
|
||||
colormatrix = null,
|
||||
primaries = null,
|
||||
gamma = null,
|
||||
maxLuma = null,
|
||||
minLuma = null,
|
||||
maxCll = null,
|
||||
maxFall = null,
|
||||
audioCodec = null,
|
||||
audioSamplerate = null,
|
||||
audioChannels = null,
|
||||
audioBitrate = null,
|
||||
actualFps = null,
|
||||
avsyncChange = null,
|
||||
displayFps = null,
|
||||
frameDropCount = null,
|
||||
decoderFrameDropCount = null,
|
||||
cacheUsed = null,
|
||||
cacheSpeed = null,
|
||||
cacheDuration = null,
|
||||
appMemoryBytes = null,
|
||||
uiFps = null;
|
||||
|
||||
/// Format video resolution as "WxH".
|
||||
String get resolution {
|
||||
|
||||
Reference in New Issue
Block a user