import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../focus/focusable_button.dart'; import '../../i18n/strings.g.dart'; import '../../models/external_player_models.dart'; import '../../services/settings_service.dart'; import '../../widgets/focused_scroll_scaffold.dart'; import '../../widgets/settings_section.dart'; class ExternalPlayerScreen extends StatefulWidget { const ExternalPlayerScreen({super.key}); @override State createState() => _ExternalPlayerScreenState(); } class _ExternalPlayerScreenState extends State { late SettingsService _settingsService; bool _isLoading = true; bool _useExternalPlayer = false; ExternalPlayer _selectedPlayer = KnownPlayers.systemDefault; List _customPlayers = []; @override void initState() { super.initState(); _loadSettings(); } Future _loadSettings() async { _settingsService = await SettingsService.getInstance(); if (!mounted) return; setState(() { _useExternalPlayer = _settingsService.getUseExternalPlayer(); _selectedPlayer = _settingsService.getSelectedExternalPlayer(); _customPlayers = _settingsService.getCustomExternalPlayers(); _isLoading = false; }); } @override Widget build(BuildContext context) { if (_isLoading) { return FocusedScrollScaffold( title: Text(t.externalPlayer.title), slivers: [const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))], ); } final knownPlayers = KnownPlayers.getForCurrentPlatform(); return FocusedScrollScaffold( title: Text(t.externalPlayer.title), slivers: [ SliverList( delegate: SliverChildListDelegate([ SwitchListTile( secondary: const AppIcon(Symbols.open_in_new_rounded, fill: 1), title: Text(t.externalPlayer.useExternalPlayer), subtitle: Text(t.externalPlayer.useExternalPlayerDescription), value: _useExternalPlayer, onChanged: (value) async { setState(() => _useExternalPlayer = value); await _settingsService.setUseExternalPlayer(value); }, ), if (_useExternalPlayer) ...[ SettingsSectionHeader(t.externalPlayer.selectPlayer), ...knownPlayers.map((player) => _buildPlayerTile(player)), SettingsSectionHeader(t.externalPlayer.customPlayers), ..._customPlayers.map((player) => _buildPlayerTile(player, isCustom: true)), ListTile( leading: const AppIcon(Symbols.add_rounded, fill: 1), title: Text(t.externalPlayer.addCustomPlayer), onTap: _showAddCustomPlayerDialog, ), ], const SizedBox(height: 24), ]), ), ], ); } Widget _buildPlayerTile(ExternalPlayer player, {bool isCustom = false}) { final isSelected = _selectedPlayer.id == player.id; Widget leading; if (player.iconAsset != null) { leading = ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(6)), child: player.iconAsset!.endsWith('.svg') ? SvgPicture.asset(player.iconAsset!, width: 32, height: 32) : Image.asset( player.iconAsset!, width: 32, height: 32, errorBuilder: (_, _, _) { return const AppIcon(Symbols.play_circle_rounded, fill: 1, size: 32); }, ), ); } else if (player.id == 'system_default') { leading = const AppIcon(Symbols.open_in_new_rounded, fill: 1, size: 32); } else { leading = const AppIcon(Symbols.play_circle_rounded, fill: 1, size: 32); } return ListTile( leading: leading, title: Text(player.id == 'system_default' ? t.externalPlayer.systemDefault : player.name), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (isCustom) IconButton( icon: const AppIcon(Symbols.delete_rounded, fill: 1, size: 20), onPressed: () => _deleteCustomPlayer(player), ), AppIcon( isSelected ? Symbols.radio_button_checked_rounded : Symbols.radio_button_unchecked_rounded, fill: 1, color: isSelected ? Theme.of(context).colorScheme.primary : null, ), ], ), onTap: () async { setState(() => _selectedPlayer = player); await _settingsService.setSelectedExternalPlayer(player); }, ); } Future _deleteCustomPlayer(ExternalPlayer player) async { await _settingsService.removeCustomExternalPlayer(player.id); if (!mounted) return; setState(() { _customPlayers.removeWhere((p) => p.id == player.id); _selectedPlayer = _settingsService.getSelectedExternalPlayer(); }); } Future _showAddCustomPlayerDialog() async { final nameController = TextEditingController(); final valueController = TextEditingController(); final valueFocusNode = FocusNode(); final saveFocusNode = FocusNode(); var selectedType = CustomPlayerType.command; final result = await showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setDialogState) { final isUrlScheme = selectedType == CustomPlayerType.urlScheme; final String fieldLabel; final String fieldHint; if (isUrlScheme) { fieldLabel = t.externalPlayer.playerUrlScheme; fieldHint = 'myplayer://play?url='; } else if (Platform.isAndroid) { fieldLabel = t.externalPlayer.playerPackage; fieldHint = 'com.example.player'; } else { fieldLabel = t.externalPlayer.playerCommand; fieldHint = Platform.isMacOS ? 'mpv' : '/usr/bin/player'; } return AlertDialog( title: Text(t.externalPlayer.addCustomPlayer), content: SizedBox( width: 300, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: nameController, decoration: InputDecoration(labelText: t.externalPlayer.playerName, hintText: 'My Player'), autofocus: true, textInputAction: TextInputAction.next, onSubmitted: (_) => primaryFocus?.nextFocus(), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: SegmentedButton( segments: [ ButtonSegment( value: CustomPlayerType.command, label: Text( Platform.isAndroid ? t.externalPlayer.playerPackage : t.externalPlayer.playerCommand, ), ), ButtonSegment(value: CustomPlayerType.urlScheme, label: Text(t.externalPlayer.playerUrlScheme)), ], selected: {selectedType}, onSelectionChanged: (value) { setDialogState(() => selectedType = value.first); }, ), ), const SizedBox(height: 16), TextField( controller: valueController, focusNode: valueFocusNode, decoration: InputDecoration(labelText: fieldLabel, hintText: fieldHint), textInputAction: TextInputAction.done, onSubmitted: (_) => saveFocusNode.requestFocus(), ), ], ), ), actions: [ FocusableButton( onPressed: () => Navigator.pop(context), child: TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), ), FocusableButton( focusNode: saveFocusNode, onPressed: () { if (nameController.text.isNotEmpty && valueController.text.isNotEmpty) { Navigator.pop(context, true); } }, child: FilledButton( onPressed: () { if (nameController.text.isNotEmpty && valueController.text.isNotEmpty) { Navigator.pop(context, true); } }, child: Text(t.common.save), ), ), ], ); }, ), ); valueFocusNode.dispose(); saveFocusNode.dispose(); if (result != true) return; final id = 'custom_${DateTime.now().millisecondsSinceEpoch}'; final newPlayer = ExternalPlayer.custom( id: id, name: nameController.text, value: valueController.text, type: selectedType, ); await _settingsService.addCustomExternalPlayer(newPlayer); if (!mounted) return; setState(() { _customPlayers.add(newPlayer); }); } }