refactor: reduce settings and media selector duplication
This commit is contained in:
@@ -14,7 +14,7 @@ import '../utils/snackbar_helper.dart';
|
||||
import '../mixins/refreshable.dart';
|
||||
import '../mixins/item_updatable.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import 'libraries/state_messages.dart';
|
||||
import 'libraries/content_state_builder.dart';
|
||||
|
||||
/// Abstract base class for screens displaying media lists (collections/playlists)
|
||||
/// Provides common state management and playback functionality
|
||||
@@ -117,11 +117,7 @@ abstract class BaseMediaListDetailScreen<T extends StatefulWidget> extends State
|
||||
/// Returns a list of slivers to display based on current state
|
||||
List<Widget> buildStateSlivers() {
|
||||
if (errorMessage != null) {
|
||||
return [
|
||||
SliverFillRemaining(
|
||||
child: ErrorStateWidget(message: errorMessage!, icon: Symbols.error_outline_rounded, onRetry: loadItems),
|
||||
),
|
||||
];
|
||||
return [SliverErrorState(message: errorMessage!, onRetry: loadItems)];
|
||||
}
|
||||
|
||||
if (items.isEmpty && isLoading) {
|
||||
@@ -129,11 +125,7 @@ abstract class BaseMediaListDetailScreen<T extends StatefulWidget> extends State
|
||||
}
|
||||
|
||||
if (items.isEmpty) {
|
||||
return [
|
||||
SliverFillRemaining(
|
||||
child: EmptyStateWidget(message: emptyMessage, icon: emptyIcon),
|
||||
),
|
||||
];
|
||||
return [SliverEmptyState(message: emptyMessage, icon: emptyIcon)];
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
@@ -25,7 +25,7 @@ class SliverErrorState extends StatelessWidget {
|
||||
/// Sliver wrapper around [EmptyStateWidget] for use in `CustomScrollView.slivers`.
|
||||
class SliverEmptyState extends StatelessWidget {
|
||||
final String message;
|
||||
final IconData icon;
|
||||
final IconData? icon;
|
||||
final String? subtitle;
|
||||
|
||||
const SliverEmptyState({super.key, required this.message, required this.icon, this.subtitle});
|
||||
|
||||
@@ -13,8 +13,8 @@ import '../../services/settings_service.dart' as settings show ThemeMode;
|
||||
import '../../focus/focusable_slider.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
import '../../widgets/app_icon.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
import '../../widgets/setting_tile.dart';
|
||||
import '../../widgets/settings_page.dart';
|
||||
import '../../widgets/settings_builder.dart';
|
||||
import '../../widgets/settings_section.dart';
|
||||
import 'settings_utils.dart';
|
||||
@@ -24,130 +24,126 @@ class AppearanceSettingsScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FocusedScrollScaffold(
|
||||
return SettingsPage(
|
||||
title: Text(t.settings.appearance),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
SettingsSectionHeader(t.settings.display),
|
||||
_themeSelector(),
|
||||
_languageSelector(context),
|
||||
_densitySelector(),
|
||||
_viewModeSelector(),
|
||||
_episodePosterModeSelector(),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showEpisodeNumberOnCards,
|
||||
icon: Symbols.tag_rounded,
|
||||
title: t.settings.showEpisodeNumberOnCards,
|
||||
subtitle: t.settings.showEpisodeNumberOnCardsDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showSeasonPostersOnTabs,
|
||||
icon: Symbols.image_rounded,
|
||||
title: t.settings.showSeasonPostersOnTabs,
|
||||
subtitle: t.settings.showSeasonPostersOnTabsDescription,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.settings.homeScreen),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showHeroSection,
|
||||
icon: Symbols.featured_play_list_rounded,
|
||||
title: t.settings.showHeroSection,
|
||||
subtitle: t.settings.showHeroSectionDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.useGlobalHubs,
|
||||
icon: Symbols.home_rounded,
|
||||
title: t.settings.useGlobalHubs,
|
||||
subtitle: t.settings.useGlobalHubsDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showServerNameOnHubs,
|
||||
icon: Symbols.dns_rounded,
|
||||
title: t.settings.showServerNameOnHubs,
|
||||
subtitle: t.settings.showServerNameOnHubsDescription,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.settings.navigation),
|
||||
if (Platform.isAndroid)
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.forceTvMode,
|
||||
icon: Symbols.tv_rounded,
|
||||
title: t.settings.forceTvMode,
|
||||
subtitle: t.settings.forceTvModeDescription,
|
||||
onAfterWrite: (value) {
|
||||
TvDetectionService.setForceTVSync(value);
|
||||
_restartApp(context);
|
||||
},
|
||||
),
|
||||
if (PlatformDetector.shouldUseSideNavigation(context))
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.alwaysKeepSidebarOpen,
|
||||
icon: Symbols.dock_to_left_rounded,
|
||||
title: t.settings.alwaysKeepSidebarOpen,
|
||||
subtitle: t.settings.alwaysKeepSidebarOpenDescription,
|
||||
),
|
||||
if (PlatformDetector.shouldUseSideNavigation(context))
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.groupLibrariesByServer,
|
||||
icon: Symbols.dns_rounded,
|
||||
title: t.settings.groupLibrariesByServer,
|
||||
subtitle: t.settings.groupLibrariesByServerDescription,
|
||||
),
|
||||
if (!PlatformDetector.shouldUseSideNavigation(context))
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showNavBarLabels,
|
||||
icon: Symbols.label_rounded,
|
||||
title: t.settings.showNavBarLabels,
|
||||
subtitle: t.settings.showNavBarLabelsDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showUnwatchedCount,
|
||||
icon: Symbols.counter_1_rounded,
|
||||
title: t.settings.showUnwatchedCount,
|
||||
subtitle: t.settings.showUnwatchedCountDescription,
|
||||
),
|
||||
|
||||
if (Platform.isWindows || Platform.isLinux) ...[
|
||||
SettingsSectionHeader(t.settings.window),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.startInFullscreen,
|
||||
icon: Symbols.fullscreen_rounded,
|
||||
title: t.settings.startInFullscreen,
|
||||
subtitle: t.settings.startInFullscreenDescription,
|
||||
),
|
||||
],
|
||||
|
||||
SettingsSectionHeader(t.settings.content),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.liveTvDefaultFavorites,
|
||||
icon: Symbols.star_rounded,
|
||||
title: t.settings.liveTvDefaultFavorites,
|
||||
subtitle: t.settings.liveTvDefaultFavoritesDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.hideSpoilers,
|
||||
icon: Symbols.visibility_off_rounded,
|
||||
title: t.settings.hideSpoilers,
|
||||
subtitle: t.settings.hideSpoilersDescription,
|
||||
),
|
||||
_requireProfileSelection(),
|
||||
if (PlatformDetector.isTV())
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.confirmExitOnBack,
|
||||
icon: Symbols.exit_to_app_rounded,
|
||||
title: t.settings.confirmExitOnBack,
|
||||
subtitle: t.settings.confirmExitOnBackDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.autoHidePerformanceOverlay,
|
||||
icon: Symbols.speed_rounded,
|
||||
title: t.settings.autoHidePerformanceOverlay,
|
||||
subtitle: t.settings.autoHidePerformanceOverlayDescription,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
children: [
|
||||
SettingsSectionHeader(t.settings.display),
|
||||
_themeSelector(),
|
||||
_languageSelector(context),
|
||||
_densitySelector(),
|
||||
_viewModeSelector(),
|
||||
_episodePosterModeSelector(),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showEpisodeNumberOnCards,
|
||||
icon: Symbols.tag_rounded,
|
||||
title: t.settings.showEpisodeNumberOnCards,
|
||||
subtitle: t.settings.showEpisodeNumberOnCardsDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showSeasonPostersOnTabs,
|
||||
icon: Symbols.image_rounded,
|
||||
title: t.settings.showSeasonPostersOnTabs,
|
||||
subtitle: t.settings.showSeasonPostersOnTabsDescription,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.settings.homeScreen),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showHeroSection,
|
||||
icon: Symbols.featured_play_list_rounded,
|
||||
title: t.settings.showHeroSection,
|
||||
subtitle: t.settings.showHeroSectionDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.useGlobalHubs,
|
||||
icon: Symbols.home_rounded,
|
||||
title: t.settings.useGlobalHubs,
|
||||
subtitle: t.settings.useGlobalHubsDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showServerNameOnHubs,
|
||||
icon: Symbols.dns_rounded,
|
||||
title: t.settings.showServerNameOnHubs,
|
||||
subtitle: t.settings.showServerNameOnHubsDescription,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.settings.navigation),
|
||||
if (Platform.isAndroid)
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.forceTvMode,
|
||||
icon: Symbols.tv_rounded,
|
||||
title: t.settings.forceTvMode,
|
||||
subtitle: t.settings.forceTvModeDescription,
|
||||
onAfterWrite: (value) {
|
||||
TvDetectionService.setForceTVSync(value);
|
||||
_restartApp(context);
|
||||
},
|
||||
),
|
||||
if (PlatformDetector.shouldUseSideNavigation(context))
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.alwaysKeepSidebarOpen,
|
||||
icon: Symbols.dock_to_left_rounded,
|
||||
title: t.settings.alwaysKeepSidebarOpen,
|
||||
subtitle: t.settings.alwaysKeepSidebarOpenDescription,
|
||||
),
|
||||
if (PlatformDetector.shouldUseSideNavigation(context))
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.groupLibrariesByServer,
|
||||
icon: Symbols.dns_rounded,
|
||||
title: t.settings.groupLibrariesByServer,
|
||||
subtitle: t.settings.groupLibrariesByServerDescription,
|
||||
),
|
||||
if (!PlatformDetector.shouldUseSideNavigation(context))
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showNavBarLabels,
|
||||
icon: Symbols.label_rounded,
|
||||
title: t.settings.showNavBarLabels,
|
||||
subtitle: t.settings.showNavBarLabelsDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.showUnwatchedCount,
|
||||
icon: Symbols.counter_1_rounded,
|
||||
title: t.settings.showUnwatchedCount,
|
||||
subtitle: t.settings.showUnwatchedCountDescription,
|
||||
),
|
||||
|
||||
if (Platform.isWindows || Platform.isLinux) ...[
|
||||
SettingsSectionHeader(t.settings.window),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.startInFullscreen,
|
||||
icon: Symbols.fullscreen_rounded,
|
||||
title: t.settings.startInFullscreen,
|
||||
subtitle: t.settings.startInFullscreenDescription,
|
||||
),
|
||||
],
|
||||
|
||||
SettingsSectionHeader(t.settings.content),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.liveTvDefaultFavorites,
|
||||
icon: Symbols.star_rounded,
|
||||
title: t.settings.liveTvDefaultFavorites,
|
||||
subtitle: t.settings.liveTvDefaultFavoritesDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.hideSpoilers,
|
||||
icon: Symbols.visibility_off_rounded,
|
||||
title: t.settings.hideSpoilers,
|
||||
subtitle: t.settings.hideSpoilersDescription,
|
||||
),
|
||||
_requireProfileSelection(),
|
||||
if (PlatformDetector.isTV())
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.confirmExitOnBack,
|
||||
icon: Symbols.exit_to_app_rounded,
|
||||
title: t.settings.confirmExitOnBack,
|
||||
subtitle: t.settings.confirmExitOnBackDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.autoHidePerformanceOverlay,
|
||||
icon: Symbols.speed_rounded,
|
||||
title: t.settings.autoHidePerformanceOverlay,
|
||||
subtitle: t.settings.autoHidePerformanceOverlayDescription,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import '../../focus/focusable_text_field.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/setting_tile.dart';
|
||||
import '../../widgets/settings_builder.dart';
|
||||
import '../../widgets/settings_page.dart';
|
||||
import '../../widgets/settings_section.dart';
|
||||
|
||||
class ExternalPlayerScreen extends StatelessWidget {
|
||||
@@ -21,46 +21,42 @@ class ExternalPlayerScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final knownPlayers = KnownPlayers.getForCurrentPlatform();
|
||||
return FocusedScrollScaffold(
|
||||
return SettingsPage(
|
||||
title: Text(t.externalPlayer.title),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.useExternalPlayer,
|
||||
icon: Symbols.open_in_new_rounded,
|
||||
title: t.externalPlayer.useExternalPlayer,
|
||||
subtitle: t.externalPlayer.useExternalPlayerDescription,
|
||||
),
|
||||
SettingsBuilder(
|
||||
prefs: [
|
||||
SettingsService.useExternalPlayer,
|
||||
SettingsService.selectedExternalPlayer,
|
||||
SettingsService.customExternalPlayers,
|
||||
],
|
||||
builder: (context) {
|
||||
final svc = SettingsService.instanceOrNull!;
|
||||
if (!svc.read(SettingsService.useExternalPlayer)) return const SizedBox.shrink();
|
||||
final selected = svc.read(SettingsService.selectedExternalPlayer);
|
||||
final custom = svc.read(SettingsService.customExternalPlayers);
|
||||
return Column(
|
||||
children: [
|
||||
SettingsSectionHeader(t.externalPlayer.selectPlayer),
|
||||
...knownPlayers.map((p) => _PlayerTile(player: p, selectedId: selected.id)),
|
||||
SettingsSectionHeader(t.externalPlayer.customPlayers),
|
||||
...custom.map((p) => _PlayerTile(player: p, selectedId: selected.id, isCustom: true)),
|
||||
ListTile(
|
||||
leading: const AppIcon(Symbols.add_rounded, fill: 1),
|
||||
title: Text(t.externalPlayer.addCustomPlayer),
|
||||
onTap: () => _showAddCustomPlayerDialog(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
children: [
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.useExternalPlayer,
|
||||
icon: Symbols.open_in_new_rounded,
|
||||
title: t.externalPlayer.useExternalPlayer,
|
||||
subtitle: t.externalPlayer.useExternalPlayerDescription,
|
||||
),
|
||||
SettingsBuilder(
|
||||
prefs: [
|
||||
SettingsService.useExternalPlayer,
|
||||
SettingsService.selectedExternalPlayer,
|
||||
SettingsService.customExternalPlayers,
|
||||
],
|
||||
builder: (context) {
|
||||
final svc = SettingsService.instanceOrNull!;
|
||||
if (!svc.read(SettingsService.useExternalPlayer)) return const SizedBox.shrink();
|
||||
final selected = svc.read(SettingsService.selectedExternalPlayer);
|
||||
final custom = svc.read(SettingsService.customExternalPlayers);
|
||||
return Column(
|
||||
children: [
|
||||
SettingsSectionHeader(t.externalPlayer.selectPlayer),
|
||||
...knownPlayers.map((p) => _PlayerTile(player: p, selectedId: selected.id)),
|
||||
SettingsSectionHeader(t.externalPlayer.customPlayers),
|
||||
...custom.map((p) => _PlayerTile(player: p, selectedId: selected.id, isCustom: true)),
|
||||
ListTile(
|
||||
leading: const AppIcon(Symbols.add_rounded, fill: 1),
|
||||
title: Text(t.externalPlayer.addCustomPlayer),
|
||||
onTap: () => _showAddCustomPlayerDialog(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import '../../services/keyboard_shortcuts_service.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
import '../../utils/snackbar_helper.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
import '../../widgets/setting_tile.dart';
|
||||
import '../../widgets/settings_builder.dart';
|
||||
import '../../widgets/settings_page.dart';
|
||||
import '../../widgets/settings_section.dart';
|
||||
import 'external_player_screen.dart';
|
||||
import 'mpv_config_screen.dart';
|
||||
@@ -45,163 +45,159 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = PlatformDetector.isMobile(context);
|
||||
|
||||
return FocusedScrollScaffold(
|
||||
return SettingsPage(
|
||||
title: Text(t.settings.videoPlayback),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
SettingsSectionHeader(t.settings.player),
|
||||
if (Platform.isAndroid) _playerBackendSelector(),
|
||||
_externalPlayerTile(),
|
||||
_hardwareDecodingTile(),
|
||||
if ((Platform.isAndroid && !PlatformDetector.isTV()) || Platform.isIOS || Platform.isMacOS) _autoPipTile(),
|
||||
if (Platform.isAndroid) _matchContentFrameRateTile(),
|
||||
if (Platform.isWindows) _matchRefreshRateTile(),
|
||||
if (Platform.isWindows) _matchDynamicRangeTile(),
|
||||
_displaySwitchDelayTile(),
|
||||
_tunneledPlaybackTile(),
|
||||
_bufferSizeTile(),
|
||||
_defaultQualityTile(),
|
||||
children: [
|
||||
SettingsSectionHeader(t.settings.player),
|
||||
if (Platform.isAndroid) _playerBackendSelector(),
|
||||
_externalPlayerTile(),
|
||||
_hardwareDecodingTile(),
|
||||
if ((Platform.isAndroid && !PlatformDetector.isTV()) || Platform.isIOS || Platform.isMacOS) _autoPipTile(),
|
||||
if (Platform.isAndroid) _matchContentFrameRateTile(),
|
||||
if (Platform.isWindows) _matchRefreshRateTile(),
|
||||
if (Platform.isWindows) _matchDynamicRangeTile(),
|
||||
_displaySwitchDelayTile(),
|
||||
_tunneledPlaybackTile(),
|
||||
_bufferSizeTile(),
|
||||
_defaultQualityTile(),
|
||||
|
||||
SettingsSectionHeader(t.settings.subtitlesAndConfig),
|
||||
SettingNavigationTile(
|
||||
icon: Symbols.subtitles_rounded,
|
||||
title: t.settings.subtitleStyling,
|
||||
subtitle: t.settings.subtitleStylingDescription,
|
||||
destinationBuilder: (_) => const SubtitleStylingScreen(),
|
||||
),
|
||||
_mpvConfigTile(),
|
||||
|
||||
SettingsSectionHeader(t.settings.seekAndTiming),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.seekTimeSmall,
|
||||
icon: Symbols.replay_10_rounded,
|
||||
title: t.settings.smallSkipDuration,
|
||||
subtitleBuilder: (v) => t.settings.secondsUnit(seconds: v.toString()),
|
||||
labelText: t.settings.secondsLabel,
|
||||
suffixText: t.settings.secondsShort,
|
||||
min: 1,
|
||||
max: 120,
|
||||
onAfterWrite: (_) => _keyboardService?.refreshFromStorage(),
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.seekTimeLarge,
|
||||
icon: Symbols.replay_30_rounded,
|
||||
title: t.settings.largeSkipDuration,
|
||||
subtitleBuilder: (v) => t.settings.secondsUnit(seconds: v.toString()),
|
||||
labelText: t.settings.secondsLabel,
|
||||
suffixText: t.settings.secondsShort,
|
||||
min: 1,
|
||||
max: 120,
|
||||
onAfterWrite: (_) => _keyboardService?.refreshFromStorage(),
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.rewindOnResume,
|
||||
icon: Symbols.replay_rounded,
|
||||
title: t.settings.rewindOnResume,
|
||||
subtitleBuilder: (v) => t.settings.secondsUnit(seconds: v.toString()),
|
||||
labelText: t.settings.secondsLabel,
|
||||
suffixText: t.settings.secondsShort,
|
||||
min: 0,
|
||||
max: 10,
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.sleepTimerDuration,
|
||||
icon: Symbols.bedtime_rounded,
|
||||
title: t.settings.defaultSleepTimer,
|
||||
subtitleBuilder: (v) => t.settings.minutesUnit(minutes: v.toString()),
|
||||
labelText: t.settings.minutesLabel,
|
||||
suffixText: t.settings.minutesShort,
|
||||
min: 5,
|
||||
max: 240,
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.maxVolume,
|
||||
icon: Symbols.volume_up_rounded,
|
||||
title: t.settings.maxVolume,
|
||||
subtitleBuilder: (v) => t.settings.maxVolumePercent(percent: v.toString()),
|
||||
labelText: t.settings.maxVolumeDescription,
|
||||
suffixText: '%',
|
||||
min: 100,
|
||||
max: 300,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.settings.behavior),
|
||||
if (DiscordRPCService.isAvailable)
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.enableDiscordRPC,
|
||||
icon: Symbols.chat_rounded,
|
||||
title: t.settings.discordRichPresence,
|
||||
subtitle: t.settings.discordRichPresenceDescription,
|
||||
onAfterWrite: (v) => DiscordRPCService.instance.setEnabled(v),
|
||||
),
|
||||
if (PlatformDetector.shouldActAsRemoteHost(context))
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.enableCompanionRemoteServer,
|
||||
icon: Symbols.phone_android_rounded,
|
||||
title: t.settings.companionRemoteServer,
|
||||
subtitle: t.settings.companionRemoteServerDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.rememberTrackSelections,
|
||||
icon: Symbols.bookmark_rounded,
|
||||
title: t.settings.rememberTrackSelections,
|
||||
subtitle: t.settings.rememberTrackSelectionsDescription,
|
||||
),
|
||||
if (!isMobile)
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.clickVideoTogglesPlayback,
|
||||
icon: Symbols.play_pause_rounded,
|
||||
title: t.settings.clickVideoTogglesPlayback,
|
||||
subtitle: t.settings.clickVideoTogglesPlaybackDescription,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.settings.autoSkip),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.autoSkipIntro,
|
||||
icon: Symbols.fast_forward_rounded,
|
||||
title: t.settings.autoSkipIntro,
|
||||
subtitle: t.settings.autoSkipIntroDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.autoSkipCredits,
|
||||
icon: Symbols.skip_next_rounded,
|
||||
title: t.settings.autoSkipCredits,
|
||||
subtitle: t.settings.autoSkipCreditsDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.forceSkipMarkerFallback,
|
||||
icon: Symbols.tune_rounded,
|
||||
title: t.settings.forceSkipMarkerFallback,
|
||||
subtitle: t.settings.forceSkipMarkerFallbackDescription,
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.autoSkipDelay,
|
||||
icon: Symbols.timer_rounded,
|
||||
title: t.settings.autoSkipDelay,
|
||||
subtitleBuilder: (v) => t.settings.autoSkipDelayDescription(seconds: v.toString()),
|
||||
labelText: t.settings.secondsLabel,
|
||||
suffixText: t.settings.secondsShort,
|
||||
min: 1,
|
||||
max: 30,
|
||||
),
|
||||
SettingRegexTile(
|
||||
pref: SettingsService.introPattern,
|
||||
icon: Symbols.match_case_rounded,
|
||||
title: t.settings.introPattern,
|
||||
subtitle: t.settings.introPatternDescription,
|
||||
defaultValue: SettingsService.defaultIntroPattern,
|
||||
),
|
||||
SettingRegexTile(
|
||||
pref: SettingsService.creditsPattern,
|
||||
icon: Symbols.match_case_rounded,
|
||||
title: t.settings.creditsPattern,
|
||||
subtitle: t.settings.creditsPatternDescription,
|
||||
defaultValue: SettingsService.defaultCreditsPattern,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
SettingsSectionHeader(t.settings.subtitlesAndConfig),
|
||||
SettingNavigationTile(
|
||||
icon: Symbols.subtitles_rounded,
|
||||
title: t.settings.subtitleStyling,
|
||||
subtitle: t.settings.subtitleStylingDescription,
|
||||
destinationBuilder: (_) => const SubtitleStylingScreen(),
|
||||
),
|
||||
_mpvConfigTile(),
|
||||
|
||||
SettingsSectionHeader(t.settings.seekAndTiming),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.seekTimeSmall,
|
||||
icon: Symbols.replay_10_rounded,
|
||||
title: t.settings.smallSkipDuration,
|
||||
subtitleBuilder: (v) => t.settings.secondsUnit(seconds: v.toString()),
|
||||
labelText: t.settings.secondsLabel,
|
||||
suffixText: t.settings.secondsShort,
|
||||
min: 1,
|
||||
max: 120,
|
||||
onAfterWrite: (_) => _keyboardService?.refreshFromStorage(),
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.seekTimeLarge,
|
||||
icon: Symbols.replay_30_rounded,
|
||||
title: t.settings.largeSkipDuration,
|
||||
subtitleBuilder: (v) => t.settings.secondsUnit(seconds: v.toString()),
|
||||
labelText: t.settings.secondsLabel,
|
||||
suffixText: t.settings.secondsShort,
|
||||
min: 1,
|
||||
max: 120,
|
||||
onAfterWrite: (_) => _keyboardService?.refreshFromStorage(),
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.rewindOnResume,
|
||||
icon: Symbols.replay_rounded,
|
||||
title: t.settings.rewindOnResume,
|
||||
subtitleBuilder: (v) => t.settings.secondsUnit(seconds: v.toString()),
|
||||
labelText: t.settings.secondsLabel,
|
||||
suffixText: t.settings.secondsShort,
|
||||
min: 0,
|
||||
max: 10,
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.sleepTimerDuration,
|
||||
icon: Symbols.bedtime_rounded,
|
||||
title: t.settings.defaultSleepTimer,
|
||||
subtitleBuilder: (v) => t.settings.minutesUnit(minutes: v.toString()),
|
||||
labelText: t.settings.minutesLabel,
|
||||
suffixText: t.settings.minutesShort,
|
||||
min: 5,
|
||||
max: 240,
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.maxVolume,
|
||||
icon: Symbols.volume_up_rounded,
|
||||
title: t.settings.maxVolume,
|
||||
subtitleBuilder: (v) => t.settings.maxVolumePercent(percent: v.toString()),
|
||||
labelText: t.settings.maxVolumeDescription,
|
||||
suffixText: '%',
|
||||
min: 100,
|
||||
max: 300,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.settings.behavior),
|
||||
if (DiscordRPCService.isAvailable)
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.enableDiscordRPC,
|
||||
icon: Symbols.chat_rounded,
|
||||
title: t.settings.discordRichPresence,
|
||||
subtitle: t.settings.discordRichPresenceDescription,
|
||||
onAfterWrite: (v) => DiscordRPCService.instance.setEnabled(v),
|
||||
),
|
||||
if (PlatformDetector.shouldActAsRemoteHost(context))
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.enableCompanionRemoteServer,
|
||||
icon: Symbols.phone_android_rounded,
|
||||
title: t.settings.companionRemoteServer,
|
||||
subtitle: t.settings.companionRemoteServerDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.rememberTrackSelections,
|
||||
icon: Symbols.bookmark_rounded,
|
||||
title: t.settings.rememberTrackSelections,
|
||||
subtitle: t.settings.rememberTrackSelectionsDescription,
|
||||
),
|
||||
if (!isMobile)
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.clickVideoTogglesPlayback,
|
||||
icon: Symbols.play_pause_rounded,
|
||||
title: t.settings.clickVideoTogglesPlayback,
|
||||
subtitle: t.settings.clickVideoTogglesPlaybackDescription,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.settings.autoSkip),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.autoSkipIntro,
|
||||
icon: Symbols.fast_forward_rounded,
|
||||
title: t.settings.autoSkipIntro,
|
||||
subtitle: t.settings.autoSkipIntroDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.autoSkipCredits,
|
||||
icon: Symbols.skip_next_rounded,
|
||||
title: t.settings.autoSkipCredits,
|
||||
subtitle: t.settings.autoSkipCreditsDescription,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.forceSkipMarkerFallback,
|
||||
icon: Symbols.tune_rounded,
|
||||
title: t.settings.forceSkipMarkerFallback,
|
||||
subtitle: t.settings.forceSkipMarkerFallbackDescription,
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.autoSkipDelay,
|
||||
icon: Symbols.timer_rounded,
|
||||
title: t.settings.autoSkipDelay,
|
||||
subtitleBuilder: (v) => t.settings.autoSkipDelayDescription(seconds: v.toString()),
|
||||
labelText: t.settings.secondsLabel,
|
||||
suffixText: t.settings.secondsShort,
|
||||
min: 1,
|
||||
max: 30,
|
||||
),
|
||||
SettingRegexTile(
|
||||
pref: SettingsService.introPattern,
|
||||
icon: Symbols.match_case_rounded,
|
||||
title: t.settings.introPattern,
|
||||
subtitle: t.settings.introPatternDescription,
|
||||
defaultValue: SettingsService.defaultIntroPattern,
|
||||
),
|
||||
SettingRegexTile(
|
||||
pref: SettingsService.creditsPattern,
|
||||
icon: Symbols.match_case_rounded,
|
||||
title: t.settings.creditsPattern,
|
||||
subtitle: t.settings.creditsPatternDescription,
|
||||
defaultValue: SettingsService.defaultCreditsPattern,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
import '../../widgets/setting_tile.dart';
|
||||
import '../../widgets/settings_page.dart';
|
||||
import '../../widgets/settings_section.dart';
|
||||
import 'settings_utils.dart';
|
||||
|
||||
@@ -29,93 +29,89 @@ class SubtitleStylingScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FocusedScrollScaffold(
|
||||
return SettingsPage(
|
||||
title: Text(t.screens.subtitleStyling),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
SettingsSectionHeader(t.subtitlingStyling.text),
|
||||
SettingSelectionTile<SubAssOverride, SubAssOverride>(
|
||||
pref: SettingsService.subAssOverride,
|
||||
icon: Symbols.subtitles_rounded,
|
||||
title: t.subtitlingStyling.assOverride,
|
||||
subtitleBuilder: _assOverrideLabel,
|
||||
options: SubAssOverride.values.map((v) => DialogOption(value: v, title: _assOverrideLabel(v))).toList(),
|
||||
decode: (v) => v,
|
||||
encode: (v) => v,
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.subtitleFontSize,
|
||||
icon: Symbols.format_size_rounded,
|
||||
title: t.subtitlingStyling.fontSize,
|
||||
subtitleBuilder: (v) => '$v',
|
||||
labelText: t.subtitlingStyling.fontSize,
|
||||
suffixText: '',
|
||||
min: 10,
|
||||
max: 80,
|
||||
),
|
||||
SettingColorTile(
|
||||
pref: SettingsService.subtitleTextColor,
|
||||
icon: Symbols.format_color_text_rounded,
|
||||
title: t.subtitlingStyling.textColor,
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.subtitlePosition,
|
||||
icon: Symbols.vertical_align_bottom_rounded,
|
||||
title: t.subtitlingStyling.position,
|
||||
subtitleBuilder: _formatPosition,
|
||||
labelText: t.subtitlingStyling.position,
|
||||
suffixText: '%',
|
||||
min: 0,
|
||||
max: 100,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.subtitleBold,
|
||||
icon: Symbols.format_bold_rounded,
|
||||
title: t.subtitlingStyling.bold,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.subtitleItalic,
|
||||
icon: Symbols.format_italic_rounded,
|
||||
title: t.subtitlingStyling.italic,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.subtitlingStyling.border),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.subtitleBorderSize,
|
||||
icon: Symbols.border_style_rounded,
|
||||
title: t.subtitlingStyling.borderSize,
|
||||
subtitleBuilder: (v) => '$v',
|
||||
labelText: t.subtitlingStyling.borderSize,
|
||||
suffixText: '',
|
||||
min: 0,
|
||||
max: 5,
|
||||
),
|
||||
SettingColorTile(
|
||||
pref: SettingsService.subtitleBorderColor,
|
||||
icon: Symbols.border_color_rounded,
|
||||
title: t.subtitlingStyling.borderColor,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.subtitlingStyling.background),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.subtitleBackgroundOpacity,
|
||||
icon: Symbols.opacity_rounded,
|
||||
title: t.subtitlingStyling.backgroundOpacity,
|
||||
subtitleBuilder: (v) => '$v%',
|
||||
labelText: t.subtitlingStyling.backgroundOpacity,
|
||||
suffixText: '%',
|
||||
min: 0,
|
||||
max: 100,
|
||||
),
|
||||
SettingColorTile(
|
||||
pref: SettingsService.subtitleBackgroundColor,
|
||||
icon: Symbols.format_color_fill_rounded,
|
||||
title: t.subtitlingStyling.backgroundColor,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
children: [
|
||||
SettingsSectionHeader(t.subtitlingStyling.text),
|
||||
SettingSelectionTile<SubAssOverride, SubAssOverride>(
|
||||
pref: SettingsService.subAssOverride,
|
||||
icon: Symbols.subtitles_rounded,
|
||||
title: t.subtitlingStyling.assOverride,
|
||||
subtitleBuilder: _assOverrideLabel,
|
||||
options: SubAssOverride.values.map((v) => DialogOption(value: v, title: _assOverrideLabel(v))).toList(),
|
||||
decode: (v) => v,
|
||||
encode: (v) => v,
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.subtitleFontSize,
|
||||
icon: Symbols.format_size_rounded,
|
||||
title: t.subtitlingStyling.fontSize,
|
||||
subtitleBuilder: (v) => '$v',
|
||||
labelText: t.subtitlingStyling.fontSize,
|
||||
suffixText: '',
|
||||
min: 10,
|
||||
max: 80,
|
||||
),
|
||||
SettingColorTile(
|
||||
pref: SettingsService.subtitleTextColor,
|
||||
icon: Symbols.format_color_text_rounded,
|
||||
title: t.subtitlingStyling.textColor,
|
||||
),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.subtitlePosition,
|
||||
icon: Symbols.vertical_align_bottom_rounded,
|
||||
title: t.subtitlingStyling.position,
|
||||
subtitleBuilder: _formatPosition,
|
||||
labelText: t.subtitlingStyling.position,
|
||||
suffixText: '%',
|
||||
min: 0,
|
||||
max: 100,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.subtitleBold,
|
||||
icon: Symbols.format_bold_rounded,
|
||||
title: t.subtitlingStyling.bold,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.subtitleItalic,
|
||||
icon: Symbols.format_italic_rounded,
|
||||
title: t.subtitlingStyling.italic,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.subtitlingStyling.border),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.subtitleBorderSize,
|
||||
icon: Symbols.border_style_rounded,
|
||||
title: t.subtitlingStyling.borderSize,
|
||||
subtitleBuilder: (v) => '$v',
|
||||
labelText: t.subtitlingStyling.borderSize,
|
||||
suffixText: '',
|
||||
min: 0,
|
||||
max: 5,
|
||||
),
|
||||
SettingColorTile(
|
||||
pref: SettingsService.subtitleBorderColor,
|
||||
icon: Symbols.border_color_rounded,
|
||||
title: t.subtitlingStyling.borderColor,
|
||||
),
|
||||
|
||||
SettingsSectionHeader(t.subtitlingStyling.background),
|
||||
SettingNumberTile(
|
||||
pref: SettingsService.subtitleBackgroundOpacity,
|
||||
icon: Symbols.opacity_rounded,
|
||||
title: t.subtitlingStyling.backgroundOpacity,
|
||||
subtitleBuilder: (v) => '$v%',
|
||||
labelText: t.subtitlingStyling.backgroundOpacity,
|
||||
suffixText: '%',
|
||||
min: 0,
|
||||
max: 100,
|
||||
),
|
||||
SettingColorTile(
|
||||
pref: SettingsService.subtitleBackgroundColor,
|
||||
icon: Symbols.format_color_fill_rounded,
|
||||
title: t.subtitlingStyling.backgroundColor,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../i18n/strings.g.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../services/trackers/tracker_constants.dart';
|
||||
import '../../widgets/app_icon.dart';
|
||||
import '../../widgets/setting_tile.dart';
|
||||
import '../../widgets/settings_builder.dart';
|
||||
import '../../widgets/settings_page.dart';
|
||||
import '../../widgets/settings_section.dart';
|
||||
import 'tracker_library_filter_screen.dart';
|
||||
|
||||
class TrackerSettingsToggle {
|
||||
final Pref<bool> pref;
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final FutureOr<void> Function(bool)? onAfterWrite;
|
||||
|
||||
const TrackerSettingsToggle({
|
||||
required this.pref,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.onAfterWrite,
|
||||
});
|
||||
}
|
||||
|
||||
class TrackerAccountSettingsBody extends StatelessWidget {
|
||||
final Widget title;
|
||||
final String accountTitle;
|
||||
final String? accountSubtitle;
|
||||
final TrackerService service;
|
||||
final List<TrackerSettingsToggle> toggles;
|
||||
final FutureOr<void> Function() onDisconnect;
|
||||
|
||||
const TrackerAccountSettingsBody({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.accountTitle,
|
||||
this.accountSubtitle,
|
||||
required this.service,
|
||||
required this.toggles,
|
||||
required this.onDisconnect,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsPage(
|
||||
title: title,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const AppIcon(Symbols.account_circle_rounded, fill: 1),
|
||||
title: Text(accountTitle),
|
||||
subtitle: accountSubtitle != null ? Text(accountSubtitle!) : null,
|
||||
),
|
||||
SettingsSectionHeader(t.settings.behavior),
|
||||
for (final toggle in toggles)
|
||||
SettingSwitchTile(
|
||||
pref: toggle.pref,
|
||||
icon: toggle.icon,
|
||||
title: toggle.title,
|
||||
subtitle: toggle.subtitle,
|
||||
onAfterWrite: toggle.onAfterWrite,
|
||||
),
|
||||
SettingsBuilder(
|
||||
prefs: [SettingsService.trackerFilterModePref(service), SettingsService.trackerFilterIdsPref(service)],
|
||||
builder: (context) {
|
||||
final settings = SettingsService.instanceOrNull!;
|
||||
return ListTile(
|
||||
leading: const AppIcon(Symbols.filter_list_rounded, fill: 1),
|
||||
title: Text(t.trackers.libraryFilter.title),
|
||||
subtitle: Text(TrackerLibraryFilterScreen.subtitleFor(settings, service)),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
onTap: () => Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute<void>(builder: (_) => TrackerLibraryFilterScreen(service: service))),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(height: 32),
|
||||
ListTile(
|
||||
leading: AppIcon(Symbols.link_off_rounded, fill: 1, color: Theme.of(context).colorScheme.error),
|
||||
title: Text(t.common.disconnect, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
onTap: () => unawaited(Future<void>.sync(onDisconnect)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import '../../services/settings_service.dart';
|
||||
import '../../services/trackers/tracker_constants.dart';
|
||||
import '../../widgets/app_icon.dart';
|
||||
import '../../widgets/focusable_list_tile.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
import '../../widgets/setting_tile.dart';
|
||||
import '../../widgets/settings_builder.dart';
|
||||
import '../../widgets/settings_page.dart';
|
||||
import '../../widgets/settings_section.dart';
|
||||
|
||||
/// Per-provider library whitelist/blacklist screen. Toggling a switch
|
||||
@@ -119,10 +119,7 @@ class TrackerLibraryFilterScreen extends StatelessWidget {
|
||||
|
||||
children.add(const SizedBox(height: 24));
|
||||
|
||||
return FocusedScrollScaffold(
|
||||
title: title,
|
||||
slivers: [SliverList(delegate: SliverChildListDelegate(children))],
|
||||
);
|
||||
return SettingsPage(title: title, children: children);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -12,15 +12,11 @@ import '../../services/trackers/simkl/simkl_tracker.dart';
|
||||
import '../../services/trackers/tracker_constants.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../utils/dialogs.dart';
|
||||
import '../../widgets/app_icon.dart';
|
||||
import '../../widgets/device_code_dialog.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
import '../../widgets/oauth_proxy_dialog.dart';
|
||||
import '../../widgets/setting_tile.dart';
|
||||
import '../../widgets/settings_builder.dart';
|
||||
import '../../widgets/settings_section.dart';
|
||||
import '../../widgets/settings_page.dart';
|
||||
import 'tracker_account_settings_body.dart';
|
||||
import 'tracker_connect_launcher.dart';
|
||||
import 'tracker_library_filter_screen.dart';
|
||||
|
||||
Future<void> startMalConnection(BuildContext context) {
|
||||
final account = context.read<TrackersProvider>();
|
||||
@@ -145,58 +141,27 @@ class TrackerSettingsScreen extends StatelessWidget {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
});
|
||||
return FocusedScrollScaffold(
|
||||
return SettingsPage.slivers(
|
||||
title: title,
|
||||
slivers: const [SliverFillRemaining(child: SizedBox.shrink())],
|
||||
);
|
||||
}
|
||||
|
||||
final username = config.username(account);
|
||||
return FocusedScrollScaffold(
|
||||
return TrackerAccountSettingsBody(
|
||||
title: title,
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
ListTile(
|
||||
leading: const AppIcon(Symbols.account_circle_rounded, fill: 1),
|
||||
title: Text(username != null ? t.trackers.connectedAs(username: username) : config.displayName),
|
||||
),
|
||||
SettingsSectionHeader(t.settings.behavior),
|
||||
SettingSwitchTile(
|
||||
pref: config.scrobblePref,
|
||||
icon: Symbols.auto_timer,
|
||||
title: t.trackers.scrobble,
|
||||
subtitle: t.trackers.scrobbleDescription,
|
||||
onAfterWrite: config.onScrobbleChanged,
|
||||
),
|
||||
SettingsBuilder(
|
||||
prefs: [
|
||||
SettingsService.trackerFilterModePref(config.service),
|
||||
SettingsService.trackerFilterIdsPref(config.service),
|
||||
],
|
||||
builder: (context) {
|
||||
final settings = SettingsService.instanceOrNull!;
|
||||
return ListTile(
|
||||
leading: const AppIcon(Symbols.filter_list_rounded, fill: 1),
|
||||
title: Text(t.trackers.libraryFilter.title),
|
||||
subtitle: Text(TrackerLibraryFilterScreen.subtitleFor(settings, config.service)),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => TrackerLibraryFilterScreen(service: config.service)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(height: 32),
|
||||
ListTile(
|
||||
leading: AppIcon(Symbols.link_off_rounded, fill: 1, color: Theme.of(context).colorScheme.error),
|
||||
title: Text(t.common.disconnect, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
onTap: () => _disconnect(context, account),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
accountTitle: username != null ? t.trackers.connectedAs(username: username) : config.displayName,
|
||||
service: config.service,
|
||||
toggles: [
|
||||
TrackerSettingsToggle(
|
||||
pref: config.scrobblePref,
|
||||
icon: Symbols.auto_timer,
|
||||
title: t.trackers.scrobble,
|
||||
subtitle: t.trackers.scrobbleDescription,
|
||||
onAfterWrite: config.onScrobbleChanged,
|
||||
),
|
||||
],
|
||||
onDisconnect: () => _disconnect(context, account),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -10,14 +10,10 @@ import '../../services/trackers/tracker_constants.dart';
|
||||
import '../../services/trakt/trakt_scrobble_service.dart';
|
||||
import '../../services/trakt/trakt_sync_service.dart';
|
||||
import '../../utils/dialogs.dart';
|
||||
import '../../widgets/app_icon.dart';
|
||||
import '../../widgets/device_code_dialog.dart';
|
||||
import '../../widgets/focused_scroll_scaffold.dart';
|
||||
import '../../widgets/setting_tile.dart';
|
||||
import '../../widgets/settings_builder.dart';
|
||||
import '../../widgets/settings_section.dart';
|
||||
import '../../widgets/settings_page.dart';
|
||||
import 'tracker_account_settings_body.dart';
|
||||
import 'tracker_connect_launcher.dart';
|
||||
import 'tracker_library_filter_screen.dart';
|
||||
|
||||
Future<void> startTraktConnection(BuildContext context) {
|
||||
final account = context.read<TraktAccountProvider>();
|
||||
@@ -61,68 +57,35 @@ class TraktSettingsScreen extends StatelessWidget {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
});
|
||||
return FocusedScrollScaffold(
|
||||
return SettingsPage.slivers(
|
||||
title: Text(t.trakt.title),
|
||||
slivers: const [SliverFillRemaining(child: SizedBox.shrink())],
|
||||
);
|
||||
}
|
||||
|
||||
final username = account.username;
|
||||
return FocusedScrollScaffold(
|
||||
return TrackerAccountSettingsBody(
|
||||
title: Text(t.trakt.title),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
ListTile(
|
||||
leading: const AppIcon(Symbols.account_circle_rounded, fill: 1),
|
||||
title: Text(username != null ? t.trakt.connectedAs(username: username) : t.trakt.connected),
|
||||
subtitle: Text(t.trakt.connected),
|
||||
),
|
||||
SettingsSectionHeader(t.settings.behavior),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.enableTraktScrobble,
|
||||
icon: Symbols.auto_timer,
|
||||
title: t.trakt.scrobble,
|
||||
subtitle: t.trakt.scrobbleDescription,
|
||||
onAfterWrite: TraktScrobbleService.instance.setEnabled,
|
||||
),
|
||||
SettingSwitchTile(
|
||||
pref: SettingsService.enableTraktWatchedSync,
|
||||
icon: Symbols.check_circle_rounded,
|
||||
title: t.trakt.watchedSync,
|
||||
subtitle: t.trakt.watchedSyncDescription,
|
||||
onAfterWrite: TraktSyncService.instance.setEnabled,
|
||||
),
|
||||
SettingsBuilder(
|
||||
prefs: [
|
||||
SettingsService.trackerFilterModePref(TrackerService.trakt),
|
||||
SettingsService.trackerFilterIdsPref(TrackerService.trakt),
|
||||
],
|
||||
builder: (context) {
|
||||
final settings = SettingsService.instanceOrNull!;
|
||||
return ListTile(
|
||||
leading: const AppIcon(Symbols.filter_list_rounded, fill: 1),
|
||||
title: Text(t.trackers.libraryFilter.title),
|
||||
subtitle: Text(TrackerLibraryFilterScreen.subtitleFor(settings, TrackerService.trakt)),
|
||||
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const TrackerLibraryFilterScreen(service: TrackerService.trakt),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(height: 32),
|
||||
ListTile(
|
||||
leading: AppIcon(Symbols.link_off_rounded, fill: 1, color: Theme.of(context).colorScheme.error),
|
||||
title: Text(t.common.disconnect, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
onTap: () => _disconnect(context, account),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
accountTitle: username != null ? t.trakt.connectedAs(username: username) : t.trakt.connected,
|
||||
accountSubtitle: t.trakt.connected,
|
||||
service: TrackerService.trakt,
|
||||
toggles: [
|
||||
TrackerSettingsToggle(
|
||||
pref: SettingsService.enableTraktScrobble,
|
||||
icon: Symbols.auto_timer,
|
||||
title: t.trakt.scrobble,
|
||||
subtitle: t.trakt.scrobbleDescription,
|
||||
onAfterWrite: TraktScrobbleService.instance.setEnabled,
|
||||
),
|
||||
TrackerSettingsToggle(
|
||||
pref: SettingsService.enableTraktWatchedSync,
|
||||
icon: Symbols.check_circle_rounded,
|
||||
title: t.trakt.watchedSync,
|
||||
subtitle: t.trakt.watchedSyncDescription,
|
||||
onAfterWrite: TraktSyncService.instance.setEnabled,
|
||||
),
|
||||
],
|
||||
onDisconnect: () => _disconnect(context, account),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../utils/desktop_window_padding.dart';
|
||||
import '../services/fullscreen_state_manager.dart';
|
||||
import 'app_bar_back_button.dart';
|
||||
@@ -54,10 +52,10 @@ class DesktopAppBarSections {
|
||||
final canPop = parentRoute?.canPop ?? false;
|
||||
|
||||
if (canPop) {
|
||||
effectiveLeading = IconButton(
|
||||
icon: const AppIcon(Symbols.arrow_back_rounded, fill: 1),
|
||||
effectiveLeading = AppBarBackButton(
|
||||
style: BackButtonStyle.plain,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
semanticLabel: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'focused_scroll_scaffold.dart';
|
||||
|
||||
/// Standard scaffold for settings pages made of ordinary list rows.
|
||||
class SettingsPage extends StatelessWidget {
|
||||
final Widget title;
|
||||
final List<Widget>? children;
|
||||
final List<Widget>? slivers;
|
||||
final List<Widget>? actions;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final bool pinned;
|
||||
final bool automaticallyImplyLeading;
|
||||
final VoidCallback? onBackPressed;
|
||||
|
||||
const SettingsPage({
|
||||
super.key,
|
||||
required this.title,
|
||||
required List<Widget> this.children,
|
||||
this.actions,
|
||||
this.padding,
|
||||
this.pinned = true,
|
||||
this.automaticallyImplyLeading = true,
|
||||
this.onBackPressed,
|
||||
}) : slivers = null;
|
||||
|
||||
const SettingsPage.slivers({
|
||||
super.key,
|
||||
required this.title,
|
||||
required List<Widget> this.slivers,
|
||||
this.actions,
|
||||
this.pinned = true,
|
||||
this.automaticallyImplyLeading = true,
|
||||
this.onBackPressed,
|
||||
}) : children = null,
|
||||
padding = null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pageSlivers = slivers ?? [_buildListSliver()];
|
||||
return FocusedScrollScaffold(
|
||||
title: title,
|
||||
actions: actions,
|
||||
pinned: pinned,
|
||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
||||
onBackPressed: onBackPressed,
|
||||
slivers: pageSlivers,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListSliver() {
|
||||
final list = SliverList(delegate: SliverChildListDelegate(children!));
|
||||
final pagePadding = padding;
|
||||
if (pagePadding == null) return list;
|
||||
return SliverPadding(padding: pagePadding, sliver: list);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import '../../../utils/provider_extensions.dart';
|
||||
import '../../../utils/scroll_utils.dart';
|
||||
import '../../../widgets/focusable_list_tile.dart';
|
||||
import '../../../widgets/overlay_sheet.dart';
|
||||
import '../widgets/media_selector_thumbnail.dart';
|
||||
import 'base_video_control_sheet.dart';
|
||||
import '../../optimized_media_image.dart';
|
||||
|
||||
@@ -96,37 +97,21 @@ class _ChapterSheetState extends State<ChapterSheet> {
|
||||
return FocusableListTile(
|
||||
key: index == 0 ? _initialScroll.firstItemKey : null,
|
||||
leading: chapter.thumb != null
|
||||
? SizedBox(
|
||||
? MediaSelectorThumbnail(
|
||||
width: 60,
|
||||
height: 34,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
child: OptimizedMediaImage.thumb(
|
||||
client: _tryGetClientForChapters(context),
|
||||
imagePath: chapter.thumb,
|
||||
localFilePath: localThumbPath,
|
||||
width: 60,
|
||||
height: 34,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context, url, error) =>
|
||||
const AppIcon(Symbols.image_rounded, fill: 1, color: Colors.white54, size: 34),
|
||||
),
|
||||
),
|
||||
if (isCurrentChapter)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
thumbnail: OptimizedMediaImage.thumb(
|
||||
client: _tryGetClientForChapters(context),
|
||||
imagePath: chapter.thumb,
|
||||
localFilePath: localThumbPath,
|
||||
width: 60,
|
||||
height: 34,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context, url, error) =>
|
||||
const AppIcon(Symbols.image_rounded, fill: 1, color: Colors.white54, size: 34),
|
||||
),
|
||||
isCurrent: isCurrentChapter,
|
||||
borderColor: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../../../utils/provider_extensions.dart';
|
||||
import '../../../utils/scroll_utils.dart';
|
||||
import '../../../widgets/focusable_list_tile.dart';
|
||||
import '../../../widgets/overlay_sheet.dart';
|
||||
import '../widgets/media_selector_thumbnail.dart';
|
||||
import 'base_video_control_sheet.dart';
|
||||
import '../../optimized_media_image.dart';
|
||||
|
||||
@@ -102,34 +103,20 @@ class _QueueSheetState extends State<QueueSheet> {
|
||||
// Try to get client for thumbnails, may fail in offline mode
|
||||
final client = context.tryGetMediaClientForServer(item.serverId);
|
||||
|
||||
return SizedBox(
|
||||
return MediaSelectorThumbnail(
|
||||
width: _kThumbWidth,
|
||||
height: _kThumbHeight,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
child: OptimizedMediaImage.thumb(
|
||||
client: client,
|
||||
imagePath: item.thumbPath,
|
||||
width: _kThumbWidth,
|
||||
height: _kThumbHeight,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context, url, error) =>
|
||||
AppIcon(Symbols.image_rounded, fill: 1, color: Colors.white54, size: _kThumbHeight),
|
||||
),
|
||||
),
|
||||
if (isCurrent)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
border: Border.fromBorderSide(BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
thumbnail: OptimizedMediaImage.thumb(
|
||||
client: client,
|
||||
imagePath: item.thumbPath,
|
||||
width: _kThumbWidth,
|
||||
height: _kThumbHeight,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context, url, error) =>
|
||||
AppIcon(Symbols.image_rounded, fill: 1, color: Colors.white54, size: _kThumbHeight),
|
||||
),
|
||||
isCurrent: isCurrent,
|
||||
borderColor: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import '../../../utils/player_utils.dart';
|
||||
import '../../../utils/provider_extensions.dart';
|
||||
import '../../app_icon.dart';
|
||||
import '../../optimized_media_image.dart';
|
||||
import 'media_selector_thumbnail.dart';
|
||||
|
||||
/// Horizontal scrollable strip of chapter/queue items shown on swipe-up.
|
||||
class ContentStrip extends StatefulWidget {
|
||||
@@ -518,33 +519,13 @@ class ContentStripState extends State<ContentStrip> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
MediaSelectorThumbnail(
|
||||
width: itemWidth,
|
||||
height: thumbHeight,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
child:
|
||||
thumbnail ??
|
||||
Container(
|
||||
color: Colors.white10,
|
||||
child: const Center(
|
||||
child: AppIcon(Symbols.movie_rounded, fill: 1, color: Colors.white38, size: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isCurrent)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
thumbnail: thumbnail,
|
||||
isCurrent: isCurrent,
|
||||
borderColor: Colors.white,
|
||||
radius: 6,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../app_icon.dart';
|
||||
|
||||
class MediaSelectorThumbnail extends StatelessWidget {
|
||||
final double width;
|
||||
final double height;
|
||||
final Widget? thumbnail;
|
||||
final bool isCurrent;
|
||||
final double radius;
|
||||
final Color borderColor;
|
||||
final Color fallbackBackgroundColor;
|
||||
final Color fallbackIconColor;
|
||||
final double fallbackIconSize;
|
||||
final IconData fallbackIcon;
|
||||
|
||||
const MediaSelectorThumbnail({
|
||||
super.key,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.thumbnail,
|
||||
required this.isCurrent,
|
||||
required this.borderColor,
|
||||
this.radius = 4,
|
||||
this.fallbackBackgroundColor = Colors.white10,
|
||||
this.fallbackIconColor = Colors.white38,
|
||||
this.fallbackIconSize = 28,
|
||||
this.fallbackIcon = Symbols.movie_rounded,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderRadius = BorderRadius.all(Radius.circular(radius));
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child:
|
||||
thumbnail ??
|
||||
Container(
|
||||
color: fallbackBackgroundColor,
|
||||
child: Center(
|
||||
child: AppIcon(fallbackIcon, fill: 1, color: fallbackIconColor, size: fallbackIconSize),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isCurrent)
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: borderRadius,
|
||||
border: Border.fromBorderSide(BorderSide(color: borderColor, width: 2)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user