feat: Android TV support (#503)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-09-28 21:07:49 +02:00 committed by GitHub
parent 7ab8c015b9
commit c299492d6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
168 changed files with 12019 additions and 3073 deletions

View file

@ -10,6 +10,7 @@ import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/enum_selection.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
final clientSettings = ref.watch(clientSettingsProvider);
@ -28,10 +29,9 @@ List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
),
itemBuilder: (context) => HomeBanner.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () =>
(entry) => ItemActionButton(
label: Text(entry.label(context)),
action: () =>
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)),
),
)
@ -48,10 +48,9 @@ List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
),
itemBuilder: (context) => HomeCarouselSettings.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref
(entry) => ItemActionButton(
label: Text(entry.label(context)),
action: () => ref
.read(homeSettingsProvider.notifier)
.update((context) => context.copyWith(carouselSettings: entry)),
),
@ -70,10 +69,9 @@ List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
),
itemBuilder: (context) => HomeNextUp.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () =>
(entry) => ItemActionButton(
label: Text(entry.label(context)),
action: () =>
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)),
),
)

View file

@ -89,6 +89,21 @@ List<Widget> buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu
return SettingsListTile(
label: Text(context.localized.downloadsSyncedData),
subLabel: Text(data.byteFormat ?? ""),
onTap: () {
showDefaultAlertDialog(
context,
context.localized.downloadsClearTitle,
context.localized.downloadsClearDesc,
(context) async {
await ref.read(syncProvider.notifier).removeAllSyncedData();
setState(() {});
Navigator.of(context).pop();
},
context.localized.clear,
(context) => Navigator.of(context).pop(),
context.localized.cancel,
);
},
trailing: FilledButton(
onPressed: () {
showDefaultAlertDialog(
@ -123,21 +138,22 @@ List<Widget> buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu
label: Text(context.localized.maxConcurrentDownloadsTitle),
subLabel: Text(context.localized.maxConcurrentDownloadsDesc),
trailing: SizedBox(
width: 100,
child: IntInputField(
controller: TextEditingController(text: clientSettings.maxConcurrentDownloads.toString()),
onSubmitted: (value) {
if (value != null) {
ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(
maxConcurrentDownloads: value,
),
);
width: 150,
child: IntInputField(
controller: TextEditingController(text: clientSettings.maxConcurrentDownloads.toString()),
onSubmitted: (value) {
if (value != null) {
ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(
maxConcurrentDownloads: value,
),
);
ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value);
}
},
)),
ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value);
}
},
),
),
),
]),
const SizedBox(height: 12),

View file

@ -13,6 +13,7 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/enum_selection.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
List<Widget> buildClientSettingsVisual(
BuildContext context,
@ -41,16 +42,15 @@ List<Widget> buildClientSettingsVisual(
itemBuilder: (context) {
return [
...AppLocalizations.supportedLocales.map(
(entry) => PopupMenuItem(
value: entry,
child: Localizations.override(
(entry) => ItemActionButton(
label: Localizations.override(
context: context,
locale: entry,
child: Builder(builder: (context) {
return Text("${context.localized.nativeName} (${entry.toDisplayCode()})");
}),
),
onTap: () => ref
action: () => ref
.read(clientSettingsProvider.notifier)
.update((state) => state.copyWith(selectedLocale: entry)),
),
@ -95,10 +95,9 @@ List<Widget> buildClientSettingsVisual(
current: clientSettings.backgroundImage.label(context),
itemBuilder: (context) => BackgroundType.values
.map(
(e) => PopupMenuItem(
value: e,
child: Text(e.label(context)),
onTap: () =>
(e) => ItemActionButton(
label: Text(e.label(context)),
action: () =>
ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(backgroundImage: e)),
),
)

View file

@ -94,6 +94,15 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
...buildClientSettingsAdvanced(context, ref),
if (kDebugMode) ...[
const SizedBox(height: 64),
SettingsListTile(
label: const Text(
"Clear cache",
),
contentColor: Theme.of(context).colorScheme.error,
onTap: () {
PaintingBinding.instance.imageCache.clear();
},
),
SettingsListTile(
label: Text(
context.localized.clearAllSettings,

View file

@ -9,6 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/arguments_provider.dart';
import 'package:fladder/providers/connectivity_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/user_provider.dart';
@ -27,6 +28,7 @@ import 'package:fladder/util/bitrate_helper.dart';
import 'package:fladder/util/box_fit_extension.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/enum_selection.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
@RoutePage()
class PlayerSettingsPage extends ConsumerStatefulWidget {
@ -81,10 +83,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
current: videoSettings.videoFit.label(context),
itemBuilder: (context) => BoxFit.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(entry),
(entry) => ItemActionButton(
label: Text(entry.label(context)),
action: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(entry),
),
)
.toList(),
@ -102,10 +103,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
),
itemBuilder: (context) => Bitrate.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
(entry) => ItemActionButton(
label: Text(entry.label(context)),
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(maxHomeBitrate: entry),
),
)
@ -124,10 +124,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
),
itemBuilder: (context) => Bitrate.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
(entry) => ItemActionButton(
label: Text(entry.label(context)),
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(maxInternetBitrate: entry),
),
)
@ -153,10 +152,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
current: entry.value.label(context),
itemBuilder: (context) => SegmentSkip.values
.map(
(value) => PopupMenuItem(
value: value,
child: Text(value.label(context)),
onTap: () {
(value) => ItemActionButton(
label: Text(value.label(context)),
action: () {
final newEntries = videoSettings.segmentSkipSettings.map(
(key, currentValue) => MapEntry(key, key == entry.key ? value : currentValue));
ref.read(videoPlayerSettingsProvider.notifier).state =
@ -264,145 +262,151 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
),
]),
const SizedBox(height: 12),
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.advanced), [
if (PlayerOptions.available.length != 1)
SettingsListTile(
label: Text(context.localized.playerSettingsBackendTitle),
subLabel: Text(context.localized.playerSettingsBackendDesc),
trailing: Builder(builder: (context) {
final wantedPlayer = videoSettings.wantedPlayer;
final currentPlayer = videoSettings.playerOptions;
return EnumBox(
current: currentPlayer == null
? "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"
: wantedPlayer.label(context),
itemBuilder: (context) => [
PopupMenuItem(
value: null,
child:
Text("${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(playerOptions: null),
),
...PlayerOptions.available.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(playerOptions: entry),
),
)
],
);
}),
),
AnimatedFadeSize(
child: switch (videoSettings.wantedPlayer) {
PlayerOptions.libMPV => Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
trailing: Switch(
value: videoSettings.hardwareAccel,
onChanged: (value) => provider.setHardwareAccel(value),
),
),
if (!kIsWeb)
SettingsListTile(
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
trailing: Switch(
value: videoSettings.useLibass,
onChanged: (value) => provider.setUseLibass(value),
...settingsListGroup(
context,
SettingsLabelDivider(label: context.localized.advanced),
[
if (!ref.read(argumentsStateProvider).leanBackMode) ...[
if (PlayerOptions.available.length != 1)
SettingsListTile(
label: Text(context.localized.playerSettingsBackendTitle),
subLabel: Text(context.localized.playerSettingsBackendDesc),
trailing: Builder(builder: (context) {
final wantedPlayer = videoSettings.wantedPlayer;
final currentPlayer = videoSettings.playerOptions;
return EnumBox(
current: currentPlayer == null
? "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"
: wantedPlayer.label(context),
itemBuilder: (context) => [
ItemActionButton(
label: Text(
"${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"),
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(playerOptions: null),
),
),
if (!videoSettings.useLibass)
SettingsListTile(
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
onTap: videoSettings.useLibass
? null
: () {
showDialog(
context: context,
barrierDismissible: true,
useSafeArea: false,
builder: (context) => const SubtitleEditor(),
);
},
),
AnimatedFadeSize(
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
? SettingsMessageBox(
context.localized.settingsPlayerMobileWarning,
messageType: MessageType.warning,
)
: Container(),
),
SettingsListTile(
label: Text(context.localized.settingsPlayerBufferSizeTitle),
subLabel: Text(context.localized.settingsPlayerBufferSizeDesc),
trailing: SizedBox(
width: 70,
child: IntInputField(
suffix: 'MB',
controller: TextEditingController(text: videoSettings.bufferSize.toString()),
onSubmitted: (value) {
if (value != null) {
provider.setBufferSize(value);
}
},
)),
),
],
...PlayerOptions.available.map(
(entry) => ItemActionButton(
label: Text(entry.label(context)),
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(playerOptions: entry),
),
)
],
);
}),
),
_ => SettingsMessageBox(
messageType: MessageType.info,
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}")
},
),
Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsAutoNextTitle),
subLabel: Text(context.localized.settingsAutoNextDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select(
(value) => value.nextVideoType.label(context),
),
),
itemBuilder: (context) => AutoNextType.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(nextVideoType: entry),
),
)
.toList(),
),
),
AnimatedFadeSize(
child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) {
AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)),
AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)),
_ => const SizedBox.shrink(),
child: switch (videoSettings.wantedPlayer) {
PlayerOptions.libMPV => Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
trailing: Switch(
value: videoSettings.hardwareAccel,
onChanged: (value) => provider.setHardwareAccel(value),
),
),
if (!kIsWeb)
SettingsListTile(
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
trailing: Switch(
value: videoSettings.useLibass,
onChanged: (value) => provider.setUseLibass(value),
),
),
if (!videoSettings.useLibass)
SettingsListTile(
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
onTap: videoSettings.useLibass
? null
: () {
showDialog(
context: context,
barrierDismissible: true,
useSafeArea: false,
builder: (context) => const SubtitleEditor(),
);
},
),
AnimatedFadeSize(
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
? SettingsMessageBox(
context.localized.settingsPlayerMobileWarning,
messageType: MessageType.warning,
)
: Container(),
),
SettingsListTile(
label: Text(context.localized.settingsPlayerBufferSizeTitle),
subLabel: Text(context.localized.settingsPlayerBufferSizeDesc),
trailing: SizedBox(
width: 70,
child: IntInputField(
suffix: 'MB',
controller: TextEditingController(text: videoSettings.bufferSize.toString()),
onSubmitted: (value) {
if (value != null) {
provider.setBufferSize(value);
}
},
)),
),
],
),
PlayerOptions.libMDK => SettingsMessageBox(
messageType: MessageType.info,
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}"),
_ => const SizedBox.shrink()
},
),
],
),
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
SettingsListTile(
label: Text(context.localized.playerSettingsOrientationTitle),
subLabel: Text(context.localized.playerSettingsOrientationDesc),
onTap: () => showOrientationOptions(context, ref),
),
]),
if (videoSettings.wantedPlayer != PlayerOptions.nativePlayer) ...[
Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsAutoNextTitle),
subLabel: Text(context.localized.settingsAutoNextDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select(
(value) => value.nextVideoType.label(context),
),
),
itemBuilder: (context) => AutoNextType.values
.map(
(entry) => ItemActionButton(
label: Text(entry.label(context)),
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(nextVideoType: entry),
),
)
.toList(),
),
),
AnimatedFadeSize(
child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) {
AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)),
AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)),
_ => const SizedBox.shrink(),
},
),
],
),
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb && !ref.read(argumentsStateProvider).htpcMode)
SettingsListTile(
label: Text(context.localized.playerSettingsOrientationTitle),
subLabel: Text(context.localized.playerSettingsOrientationDesc),
onTap: () => showOrientationOptions(context, ref),
),
],
],
),
],
);
}

View file

@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/login/widgets/login_icon.dart';
import 'package:fladder/screens/shared/outlined_text_field.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Future<void> openQuickConnectDialog(
BuildContext context,
@ -30,18 +31,28 @@ class _QuickConnectDialogState extends ConsumerState<QuickConnectDialog> {
Widget build(BuildContext context) {
final user = ref.watch(userProvider);
return AlertDialog(
title: Text(context.localized.quickConnectTitle),
title: Text(
context.localized.quickConnectTitle,
textAlign: TextAlign.center,
),
backgroundColor: Theme.of(context).colorScheme.surface,
scrollable: true,
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 12,
children: [
Text(context.localized.quickConnectAction),
Text(
context.localized.quickConnectAction,
textAlign: TextAlign.center,
),
if (user != null) SizedBox(child: LoginIcon(user: user)),
Flexible(
child: OutlinedTextField(
label: context.localized.code,
controller: controller,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.go,
onChanged: (value) {
if (value.isNotEmpty) {
setState(() {
@ -50,6 +61,7 @@ class _QuickConnectDialogState extends ConsumerState<QuickConnectDialog> {
});
}
},
onSubmitted: (value) => tryLogin(),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
),
@ -58,50 +70,24 @@ class _QuickConnectDialogState extends ConsumerState<QuickConnectDialog> {
child: error != null || success != null
? Card(
key: Key(context.localized.error),
color: success == null ? Theme.of(context).colorScheme.errorContainer : Theme.of(context).colorScheme.surfaceContainer,
color: success == null
? Theme.of(context).colorScheme.errorContainer
: Theme.of(context).colorScheme.surfaceContainer,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
success ?? error ?? "",
style: TextStyle(
color:
success == null ? Theme.of(context).colorScheme.onErrorContainer : Theme.of(context).colorScheme.onSurface),
color: success == null
? Theme.of(context).colorScheme.onErrorContainer
: Theme.of(context).colorScheme.onSurface),
),
),
)
: null,
),
ElevatedButton(
onPressed: loading
? null
: () async {
setState(() {
error = null;
loading = true;
});
final response = await ref.read(userProvider.notifier).quickConnect(controller.text);
if (response.isSuccessful) {
setState(
() {
error = null;
success = context.localized.loggedIn;
},
);
await Future.delayed(const Duration(seconds: 2));
Navigator.of(context).pop();
} else {
if (controller.text.isEmpty) {
error = context.localized.quickConnectInputACode;
} else {
error = context.localized.quickConnectWrongCode;
}
}
loading = false;
setState(
() {},
);
controller.text = "";
},
FilledButton(
onPressed: loading ? null : () => tryLogin(),
child: loading
? const SizedBox.square(
child: CircularProgressIndicator(),
@ -109,8 +95,37 @@ class _QuickConnectDialogState extends ConsumerState<QuickConnectDialog> {
)
: Text(context.localized.login),
)
].addInBetween(const SizedBox(height: 16)),
],
),
);
}
Future<void> tryLogin() async {
setState(() {
error = null;
loading = true;
});
final response = await ref.read(userProvider.notifier).quickConnect(controller.text);
if (response.isSuccessful) {
setState(
() {
error = null;
success = context.localized.loggedIn;
},
);
await Future.delayed(const Duration(seconds: 2));
Navigator.of(context).pop();
} else {
if (controller.text.isEmpty) {
error = context.localized.quickConnectInputACode;
} else {
error = context.localized.quickConnectWrongCode;
}
}
loading = false;
setState(
() {},
);
controller.text = "";
}
}

View file

@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/widgets/shared/ensure_visible.dart';
class SettingsListTile extends StatelessWidget {
final Widget label;
final Widget? subLabel;
final Widget? trailing;
final bool selected;
final bool autoFocus;
final IconData? icon;
final Widget? leading;
final Color? contentColor;
@ -16,6 +18,7 @@ class SettingsListTile extends StatelessWidget {
this.subLabel,
this.trailing,
this.selected = false,
this.autoFocus = false,
this.leading,
this.icon,
this.contentColor,
@ -52,6 +55,12 @@ class SettingsListTile extends StatelessWidget {
margin: EdgeInsets.zero,
child: FlatButton(
onTap: onTap,
autoFocus: autoFocus,
onFocusChange: (value) {
if (value) {
context.ensureVisible();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
@ -66,6 +75,7 @@ class SettingsListTile extends StatelessWidget {
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
DefaultTextStyle.merge(
@ -85,7 +95,7 @@ class SettingsListTile extends StatelessWidget {
children: [
Material(
color: Colors.transparent,
textStyle: Theme.of(context).textTheme.titleLarge,
textStyle: Theme.of(context).textTheme.titleLarge?.copyWith(color: contentColor),
child: label,
),
if (subLabel != null)
@ -93,7 +103,7 @@ class SettingsListTile extends StatelessWidget {
opacity: 0.65,
child: Material(
color: Colors.transparent,
textStyle: Theme.of(context).textTheme.labelLarge,
textStyle: Theme.of(context).textTheme.labelLarge?.copyWith(color: contentColor),
child: subLabel,
),
),
@ -101,9 +111,12 @@ class SettingsListTile extends StatelessWidget {
),
),
if (trailing != null)
Padding(
padding: const EdgeInsets.only(left: 16),
child: trailing,
ExcludeFocusTraversal(
excluding: onTap != null,
child: Padding(
padding: const EdgeInsets.only(left: 16),
child: trailing,
),
)
],
),

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/arguments_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/user_icon.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
@ -76,7 +77,7 @@ class SettingsScaffold extends ConsumerWidget {
padding: MediaQuery.paddingOf(context).copyWith(bottom: 0),
child: Row(
children: [
if (showBackButtonNested)
if (showBackButtonNested && !ref.read(argumentsStateProvider).htpcMode)
BackButton(
onPressed: () => backAction(context),
)

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -13,6 +14,7 @@ import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/settings/quick_connect_window.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart';
import 'package:fladder/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/shared/fladder_icon.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
@ -55,9 +57,9 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(flex: 1, child: _leftPane(context)),
Expanded(flex: 2, child: _leftPane(context)),
Expanded(
flex: 2,
flex: 3,
child: content,
),
],
@ -88,6 +90,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
return IconsaxPlusLinear.monitor;
case ViewSize.desktop:
return IconsaxPlusLinear.monitor;
case ViewSize.television:
return IconsaxPlusLinear.mirroring_screen;
}
}
@ -129,6 +133,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
SettingsListTile(
label: Text(context.localized.settingsClientTitle),
subLabel: Text(context.localized.settingsClientDesc),
autoFocus: true,
selected: containsRoute(const ClientSettingsRoute()),
icon: deviceIcon,
onTap: () => navigateTo(const ClientSettingsRoute()),
@ -171,83 +176,81 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
label: Text(context.localized.exitFladderTitle),
icon: IconsaxPlusLinear.close_square,
onTap: () async {
final manager = WindowManager.instance;
if (await manager.isClosable()) {
manager.close();
} else {
fladderSnackbar(context, title: context.localized.somethingWentWrong);
}
showDefaultAlertDialog(
context,
context.localized.exitFladderTitle,
context.localized.exitFladderDesc,
(context) async {
if (AdaptiveLayout.of(context).isDesktop) {
final manager = WindowManager.instance;
if (await manager.isClosable()) {
manager.close();
} else {
fladderSnackbar(context, title: context.localized.somethingWentWrong);
}
} else {
SystemNavigator.pop();
}
},
context.localized.close,
(context) => context.pop(),
context.localized.cancel,
);
},
),
],
],
floatingActionButton: Padding(
padding: EdgeInsets.symmetric(horizontal: MediaQuery.paddingOf(context).horizontal),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Spacer(),
FloatingActionButton(
key: Key(context.localized.switchUser),
tooltip: context.localized.switchUser,
onPressed: () async {
await ref.read(userProvider.notifier).logoutUser();
context.router.replaceAll([const LoginRoute()]);
},
child: const Icon(
IconsaxPlusLinear.arrow_swap_horizontal,
),
),
const SizedBox(width: 16),
FloatingActionButton(
heroTag: context.localized.logout,
key: Key(context.localized.logout),
tooltip: context.localized.logout,
backgroundColor: Theme.of(context).colorScheme.errorContainer,
onPressed: () {
final user = ref.read(userProvider);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")),
scrollable: true,
content: Text(
context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""),
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text(context.localized.cancel),
),
ElevatedButton(
style: ElevatedButton.styleFrom().copyWith(
iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer),
),
onPressed: () async {
await ref.read(authProvider.notifier).logOutUser();
if (context.mounted) {
context.router.replaceAll([const LoginRoute()]);
}
},
child: Text(context.localized.logout),
),
],
),
);
},
child: Icon(
IconsaxPlusLinear.logout,
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
],
),
const FractionallySizedBox(
widthFactor: 0.25,
child: Divider(),
),
),
SettingsListTile(
label: Text(context.localized.switchUser),
icon: IconsaxPlusLinear.arrow_swap_horizontal,
contentColor: Colors.greenAccent,
onTap: () async {
await ref.read(userProvider.notifier).logoutUser();
context.router.replaceAll([const LoginRoute()]);
},
),
SettingsListTile(
label: Text(context.localized.logout),
icon: IconsaxPlusLinear.logout,
contentColor: Theme.of(context).colorScheme.error,
onTap: () {
final user = ref.read(userProvider);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")),
scrollable: true,
content: Text(
context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""),
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text(context.localized.cancel),
),
ElevatedButton(
style: ElevatedButton.styleFrom().copyWith(
iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer),
),
onPressed: () async {
await ref.read(authProvider.notifier).logOutUser();
if (context.mounted) {
context.router.replaceAll([const LoginRoute()]);
}
},
child: Text(context.localized.logout),
),
],
),
);
},
),
],
),
),
);