mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-10 07:50:28 -07:00
feat: Android TV support (#503)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
7ab8c015b9
commit
c299492d6d
168 changed files with 12019 additions and 3073 deletions
|
|
@ -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)),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue