refactor: reduce settings and media selector duplication

This commit is contained in:
edde746
2026-05-06 01:33:54 +02:00
parent fb084d3ea1
commit 25a2187bf4
16 changed files with 678 additions and 611 deletions
+3 -11
View File
@@ -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),
],
);
}
+151 -155
View File
@@ -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),
);
},
);
+22 -59
View File
@@ -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),
);
},
);
+3 -5
View File
@@ -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,
);
}
}
+57
View File
@@ -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)),
),
),
),
],
),
);
}
}