feat: UI 2.0 and other Improvements (#357)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-06-01 10:37:19 +02:00 committed by GitHub
parent 9ca06eaa37
commit e7b5bb40ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
169 changed files with 4584 additions and 3626 deletions

View file

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/screens/crash_screen/crash_screen.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart';
@ -42,75 +42,79 @@ class AboutSettingsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final applicationInfo = ref.watch(applicationInfoProvider);
return Card(
child: SettingsScaffold(
label: "",
items: [
const FladderLogo(),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)),
Text(context.localized.aboutBuild(applicationInfo.buildNumber)),
const SizedBox(height: 16),
Text(context.localized.aboutCreatedBy),
],
return SettingsScaffold(
label: "",
items: [
const FladderLogo(),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)),
Text(context.localized.aboutBuild(applicationInfo.buildNumber)),
const SizedBox(height: 16),
Text(context.localized.aboutCreatedBy),
],
),
const FractionallySizedBox(
widthFactor: 0.25,
child: Divider(
indent: 16,
endIndent: 16,
),
const Divider(),
Column(
children: [
Text(
context.localized.aboutSocials,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: socials
.map(
(e) => IconButton.filledTonal(
onPressed: () => launchUrl(context, e.url),
icon: Column(
children: [
Icon(e.icon),
Text(e.label),
],
),
),
Column(
children: [
Text(
context.localized.aboutSocials,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: socials
.map(
(e) => IconButton.filledTonal(
onPressed: () => launchUrl(context, e.url),
icon: Column(
children: [
Icon(e.icon),
Text(e.label),
],
),
)
.toList()
.addInBetween(const SizedBox(width: 16)),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(
onPressed: () => showLicensePage(
context: context,
applicationIcon: const FladderIcon(size: 55),
applicationVersion: applicationInfo.versionPlatformBuild,
applicationLegalese: "DonutWare",
),
child: Text(context.localized.aboutLicenses),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(
onPressed: () => showDialog(
context: context,
builder: (context) => const CrashScreen(),
),
child: Text(context.localized.errorLogs),
)
],
),
].addInBetween(const SizedBox(height: 16)),
),
),
)
.toList()
.addInBetween(const SizedBox(width: 16)),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(
onPressed: () => showLicensePage(
context: context,
applicationIcon: const FladderIcon(size: 55),
applicationVersion: applicationInfo.versionPlatformBuild,
applicationLegalese: "DonutWare",
),
child: Text(context.localized.aboutLicenses),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(
onPressed: () => showDialog(
context: context,
builder: (context) => const CrashScreen(),
),
child: Text(context.localized.errorLogs),
)
],
),
].addInBetween(const SizedBox(height: 16)),
);
}
}

View file

@ -2,79 +2,83 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/settings/home_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/option_dialogue.dart';
List<Widget> buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) {
return [
return settingsListGroup(
context,
SettingsLabelDivider(label: context.localized.advanced),
SettingsListTile(
label: Text(context.localized.settingsLayoutSizesTitle),
subLabel: Text(context.localized.settingsLayoutSizesDesc),
onTap: () async {
final newItems = await openMultiSelectOptions<ViewSize>(
context,
label: context.localized.settingsLayoutSizesTitle,
items: ViewSize.values,
allowMultiSelection: true,
selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())),
itemBuilder: (type, selected, tap) => CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: selected,
onChanged: (value) => tap(),
title: Text(type.label(context)),
[
SettingsListTile(
label: Text(context.localized.settingsLayoutSizesTitle),
subLabel: Text(context.localized.settingsLayoutSizesDesc),
onTap: () async {
final newItems = await openMultiSelectOptions<ViewSize>(
context,
label: context.localized.settingsLayoutSizesTitle,
items: ViewSize.values,
allowMultiSelection: true,
selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())),
itemBuilder: (type, selected, tap) => CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: selected,
onChanged: (value) => tap(),
title: Text(type.label(context)),
),
);
ref.read(homeSettingsProvider.notifier).setViewSize(newItems.toSet());
},
trailing: Card(
color: Theme.of(context).colorScheme.primaryContainer,
shadowColor: Colors.transparent,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(ref
.watch(homeSettingsProvider.select((value) => value.layoutStates.toList()))
.map((e) => e.label(context))
.join(', ')),
),
);
ref.read(homeSettingsProvider.notifier).setViewSize(newItems.toSet());
},
trailing: Card(
color: Theme.of(context).colorScheme.primaryContainer,
shadowColor: Colors.transparent,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(ref
.watch(homeSettingsProvider.select((value) => value.layoutStates.toList()))
.map((e) => e.label(context))
.join(', ')),
),
),
),
SettingsListTile(
label: Text(context.localized.settingsLayoutModesTitle),
subLabel: Text(context.localized.settingsLayoutModesDesc),
onTap: () async {
final newItems = await openMultiSelectOptions<LayoutMode>(
context,
label: context.localized.settingsLayoutModesTitle,
items: LayoutMode.values,
allowMultiSelection: true,
selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())),
itemBuilder: (type, selected, tap) => CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: selected,
onChanged: (value) => tap(),
title: Text(type.label(context)),
SettingsListTile(
label: Text(context.localized.settingsLayoutModesTitle),
subLabel: Text(context.localized.settingsLayoutModesDesc),
onTap: () async {
final newItems = await openMultiSelectOptions<LayoutMode>(
context,
label: context.localized.settingsLayoutModesTitle,
items: LayoutMode.values,
allowMultiSelection: true,
selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())),
itemBuilder: (type, selected, tap) => CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: selected,
onChanged: (value) => tap(),
title: Text(type.label(context)),
),
);
ref.read(homeSettingsProvider.notifier).setLayoutModes(newItems.toSet());
},
trailing: Card(
color: Theme.of(context).colorScheme.primaryContainer,
shadowColor: Colors.transparent,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(ref
.watch(homeSettingsProvider.select((value) => value.screenLayouts.toList()))
.map((e) => e.label(context))
.join(', ')),
),
);
ref.read(homeSettingsProvider.notifier).setLayoutModes(newItems.toSet());
},
trailing: Card(
color: Theme.of(context).colorScheme.primaryContainer,
shadowColor: Colors.transparent,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(ref
.watch(homeSettingsProvider.select((value) => value.screenLayouts.toList()))
.map((e) => e.label(context))
.join(', ')),
),
),
),
];
],
);
}

View file

@ -7,89 +7,92 @@ import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/settings/home_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
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';
List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
final clientSettings = ref.watch(clientSettingsProvider);
return [
return settingsListGroup(
context,
SettingsLabelDivider(label: context.localized.dashboard),
SettingsListTile(
label: Text(context.localized.settingsHomeBannerTitle),
subLabel: Text(context.localized.settingsHomeBannerDescription),
trailing: EnumBox(
current: ref.watch(
homeSettingsProvider.select(
(value) => value.homeBanner.label(context),
),
),
itemBuilder: (context) => HomeBanner.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () =>
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)),
),
)
.toList(),
),
),
if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide)
[
SettingsListTile(
label: Text(context.localized.settingsHomeBannerInformationTitle),
subLabel: Text(context.localized.settingsHomeBannerInformationDesc),
label: Text(context.localized.settingsHomeBannerTitle),
subLabel: Text(context.localized.settingsHomeBannerDescription),
trailing: EnumBox(
current: ref.watch(
homeSettingsProvider.select((value) => value.carouselSettings.label(context)),
homeSettingsProvider.select(
(value) => value.homeBanner.label(context),
),
),
itemBuilder: (context) => HomeCarouselSettings.values
itemBuilder: (context) => HomeBanner.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref
.read(homeSettingsProvider.notifier)
.update((context) => context.copyWith(carouselSettings: entry)),
onTap: () =>
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)),
),
)
.toList(),
),
),
SettingsListTile(
label: Text(context.localized.settingsHomeNextUpTitle),
subLabel: Text(context.localized.settingsHomeNextUpDesc),
trailing: EnumBox(
current: ref.watch(
homeSettingsProvider.select(
(value) => value.nextUp.label(context),
if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide)
SettingsListTile(
label: Text(context.localized.settingsHomeBannerInformationTitle),
subLabel: Text(context.localized.settingsHomeBannerInformationDesc),
trailing: EnumBox(
current: ref.watch(
homeSettingsProvider.select((value) => value.carouselSettings.label(context)),
),
itemBuilder: (context) => HomeCarouselSettings.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref
.read(homeSettingsProvider.notifier)
.update((context) => context.copyWith(carouselSettings: entry)),
),
)
.toList(),
),
),
itemBuilder: (context) => HomeNextUp.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () =>
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)),
),
)
.toList(),
SettingsListTile(
label: Text(context.localized.settingsHomeNextUpTitle),
subLabel: Text(context.localized.settingsHomeNextUpDesc),
trailing: EnumBox(
current: ref.watch(
homeSettingsProvider.select(
(value) => value.nextUp.label(context),
),
),
itemBuilder: (context) => HomeNextUp.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () =>
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)),
),
)
.toList(),
),
),
),
SettingsListTile(
label: Text(context.localized.clientSettingsShowAllCollectionsTitle),
subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc),
onTap: () => ref
.read(clientSettingsProvider.notifier)
.update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)),
trailing: Switch(
value: clientSettings.showAllCollectionTypes,
onChanged: (value) => ref
SettingsListTile(
label: Text(context.localized.clientSettingsShowAllCollectionsTitle),
subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc),
onTap: () => ref
.read(clientSettingsProvider.notifier)
.update((current) => current.copyWith(showAllCollectionTypes: value)),
.update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)),
trailing: Switch(
value: clientSettings.showAllCollectionTypes,
onChanged: (value) => ref
.read(clientSettingsProvider.notifier)
.update((current) => current.copyWith(showAllCollectionTypes: value)),
),
),
),
const Divider(),
];
],
);
}

View file

@ -11,9 +11,10 @@ import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/shared/input_fields.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/size_formatting.dart';
@ -24,121 +25,122 @@ List<Widget> buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu
return [
if (canSync && !kIsWeb) ...[
SettingsLabelDivider(label: context.localized.downloadsTitle),
if (AdaptiveLayout.of(context).isDesktop) ...[
SettingsListTile(
label: Text(context.localized.downloadsPath),
subLabel: Text(currentFolder ?? "-"),
onTap: currentFolder != null
? () async => await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.pathEditTitle),
content: Text(context.localized.pathEditDesc),
actions: [
ElevatedButton(
onPressed: () async {
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
if (selectedDirectory != null) {
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
}
Navigator.of(context).pop();
},
child: Text(context.localized.change),
)
],
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.downloadsTitle), [
if (AdaptiveLayout.of(context).isDesktop) ...[
SettingsListTile(
label: Text(context.localized.downloadsPath),
subLabel: Text(currentFolder ?? "-"),
onTap: currentFolder != null
? () async => await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.pathEditTitle),
content: Text(context.localized.pathEditDesc),
actions: [
ElevatedButton(
onPressed: () async {
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
if (selectedDirectory != null) {
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
}
Navigator.of(context).pop();
},
child: Text(context.localized.change),
)
],
),
)
: () async {
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
if (selectedDirectory != null) {
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
}
},
trailing: currentFolder?.isNotEmpty == true
? IconButton(
color: Theme.of(context).colorScheme.error,
onPressed: () async => await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.pathClearTitle),
content: Text(context.localized.pathEditDesc),
actions: [
ElevatedButton(
onPressed: () {
ref.read(clientSettingsProvider.notifier).setSyncPath(null);
Navigator.of(context).pop();
},
child: Text(context.localized.clear),
)
],
),
),
icon: const Icon(IconsaxPlusLinear.folder_minus),
)
: () async {
String? selectedDirectory = await FilePicker.platform
.getDirectoryPath(dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder);
if (selectedDirectory != null) {
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
: null,
),
],
FutureBuilder(
future: ref.watch(syncProvider.notifier).directorySize,
builder: (context, snapshot) {
final data = snapshot.data ?? 0;
return SettingsListTile(
label: Text(context.localized.downloadsSyncedData),
subLabel: Text(data.byteFormat ?? ""),
trailing: FilledButton(
onPressed: () {
showDefaultAlertDialog(
context,
context.localized.downloadsClearTitle,
context.localized.downloadsClearDesc,
(context) async {
await ref.read(syncProvider.notifier).clear();
setState(() {});
Navigator.of(context).pop();
},
context.localized.clear,
(context) => Navigator.of(context).pop(),
context.localized.cancel,
);
},
child: Text(context.localized.clear),
),
);
},
),
SettingsListTile(
label: Text(context.localized.clientSettingsRequireWifiTitle),
subLabel: Text(context.localized.clientSettingsRequireWifiDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi),
trailing: Switch(
value: clientSettings.requireWifi,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value),
),
),
SettingsListTile(
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,
),
);
ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value);
}
},
trailing: currentFolder?.isNotEmpty == true
? IconButton(
color: Theme.of(context).colorScheme.error,
onPressed: () async => await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.localized.pathClearTitle),
content: Text(context.localized.pathEditDesc),
actions: [
ElevatedButton(
onPressed: () {
ref.read(clientSettingsProvider.notifier).setSyncPath(null);
Navigator.of(context).pop();
},
child: Text(context.localized.clear),
)
],
),
),
icon: const Icon(IconsaxPlusLinear.folder_minus),
)
: null,
)),
),
],
FutureBuilder(
future: ref.watch(syncProvider.notifier).directorySize,
builder: (context, snapshot) {
final data = snapshot.data ?? 0;
return SettingsListTile(
label: Text(context.localized.downloadsSyncedData),
subLabel: Text(data.byteFormat ?? ""),
trailing: FilledButton(
onPressed: () {
showDefaultAlertDialog(
context,
context.localized.downloadsClearTitle,
context.localized.downloadsClearDesc,
(context) async {
await ref.read(syncProvider.notifier).clear();
setState(() {});
Navigator.of(context).pop();
},
context.localized.clear,
(context) => Navigator.of(context).pop(),
context.localized.cancel,
);
},
child: Text(context.localized.clear),
),
);
},
),
SettingsListTile(
label: Text(context.localized.clientSettingsRequireWifiTitle),
subLabel: Text(context.localized.clientSettingsRequireWifiDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi),
trailing: Switch(
value: clientSettings.requireWifi,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value),
),
),
SettingsListTile(
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,
),
);
ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value);
}
},
)),
),
const Divider(),
]),
const SizedBox(height: 12),
],
];
}

View file

@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/util/color_extensions.dart';
import 'package:fladder/util/custom_color_themes.dart';
import 'package:fladder/util/localization_helper.dart';
@ -13,8 +14,7 @@ import 'package:fladder/util/theme_mode_extension.dart';
List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
final clientSettings = ref.watch(clientSettingsProvider);
return [
SettingsLabelDivider(label: context.localized.theme),
return settingsListGroup(context, SettingsLabelDivider(label: context.localized.theme), [
SettingsListTile(
label: Text(context.localized.mode),
subLabel: Text(clientSettings.themeMode.label(context)),
@ -107,6 +107,5 @@ List<Widget> buildClientSettingsTheme(BuildContext context, WidgetRef ref) {
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setAmoledBlack(value),
),
),
const Divider(),
];
]);
}

View file

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -8,8 +6,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/screens/shared/input_fields.dart';
import 'package:fladder/util/adaptive_layout.dart';
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';
@ -22,136 +21,136 @@ List<Widget> buildClientSettingsVisual(
) {
final clientSettings = ref.watch(clientSettingsProvider);
Locale currentLocale = WidgetsBinding.instance.platformDispatcher.locale;
return [
return settingsListGroup(
context,
SettingsLabelDivider(label: context.localized.settingsVisual),
SettingsListTile(
label: Text(context.localized.displayLanguage),
trailing: Localizations.override(
context: context,
locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))),
child: Builder(builder: (context) {
String language = "Unknown";
try {
language = context.localized.nativeName;
} catch (e) {
log(e.toString());
}
return EnumBox(
current: language,
itemBuilder: (context) {
return [
...AppLocalizations.supportedLocales.map(
(entry) => PopupMenuItem(
value: entry,
child: Localizations.override(
context: context,
locale: entry,
child: Builder(builder: (context) {
return Text("${context.localized.nativeName} (${entry.languageCode.toUpperCase()})");
}),
[
SettingsListTile(
label: Text(context.localized.displayLanguage),
trailing: Localizations.override(
context: context,
locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))),
child: Builder(builder: (context) {
String language = "Unknown";
try {
language = context.localized.nativeName;
} catch (_) {}
return EnumBox(
current: language,
itemBuilder: (context) {
return [
...AppLocalizations.supportedLocales.map(
(entry) => PopupMenuItem(
value: entry,
child: Localizations.override(
context: context,
locale: entry,
child: Builder(builder: (context) {
return Text("${context.localized.nativeName} (${entry.languageCode.toUpperCase()})");
}),
),
onTap: () => ref
.read(clientSettingsProvider.notifier)
.update((state) => state.copyWith(selectedLocale: entry)),
),
onTap: () => ref
.read(clientSettingsProvider.notifier)
.update((state) => state.copyWith(selectedLocale: entry)),
)
];
},
);
}),
),
),
SettingsListTile(
label: Text(context.localized.settingsBlurredPlaceholderTitle),
subLabel: Text(context.localized.settingsBlurredPlaceholderDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders),
trailing: Switch(
value: clientSettings.blurPlaceHolders,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsBlurEpisodesTitle),
subLabel: Text(context.localized.settingsBlurEpisodesDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes),
trailing: Switch(
value: clientSettings.blurUpcomingEpisodes,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsEnableOsMediaControls),
onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys),
trailing: Switch(
value: clientSettings.enableMediaKeys,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsNextUpCutoffDays),
trailing: SizedBox(
width: 100,
child: IntInputField(
suffix: context.localized.days,
controller: nextUpDaysEditor,
onSubmitted: (value) {
if (value != null) {
ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(
nextUpDateCutoff: Duration(days: value),
));
}
},
)),
),
SettingsListTile(
label: Text(context.localized.libraryPageSizeTitle),
subLabel: Text(context.localized.libraryPageSizeDesc),
trailing: SizedBox(
width: 100,
child: IntInputField(
controller: libraryPageSizeController,
placeHolder: "500",
onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(libraryPageSize: value),
),
)
];
},
);
}),
)),
),
),
SettingsListTile(
label: Text(context.localized.settingsBlurredPlaceholderTitle),
subLabel: Text(context.localized.settingsBlurredPlaceholderDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders),
trailing: Switch(
value: clientSettings.blurPlaceHolders,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsBlurEpisodesTitle),
subLabel: Text(context.localized.settingsBlurEpisodesDesc),
onTap: () => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes),
trailing: Switch(
value: clientSettings.blurUpcomingEpisodes,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsEnableOsMediaControls),
onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys),
trailing: Switch(
value: clientSettings.enableMediaKeys,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value),
),
),
SettingsListTile(
label: Text(context.localized.settingsNextUpCutoffDays),
trailing: SizedBox(
width: 100,
child: IntInputField(
suffix: context.localized.days,
controller: nextUpDaysEditor,
onSubmitted: (value) {
if (value != null) {
ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(
nextUpDateCutoff: Duration(days: value),
));
}
},
)),
),
SettingsListTile(
label: Text(context.localized.libraryPageSizeTitle),
subLabel: Text(context.localized.libraryPageSizeDesc),
trailing: SizedBox(
width: 100,
child: IntInputField(
controller: libraryPageSizeController,
placeHolder: "500",
onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(libraryPageSize: value),
),
)),
),
SettingsListTile(
label: Text(AdaptiveLayout.of(context).isDesktop
? context.localized.settingsShowScaleSlider
: context.localized.settingsPosterPinch),
onTap: () => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom),
),
trailing: Switch(
value: clientSettings.pinchPosterZoom,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(pinchPosterZoom: value),
SettingsListTile(
label: Text(AdaptiveLayout.of(context).isDesktop
? context.localized.settingsShowScaleSlider
: context.localized.settingsPosterPinch),
onTap: () => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom),
),
trailing: Switch(
value: clientSettings.pinchPosterZoom,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).update(
(current) => current.copyWith(pinchPosterZoom: value),
),
),
),
),
Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsPosterSize),
trailing: Text(
clientSettings.posterSize.toString(),
style: Theme.of(context).textTheme.bodyLarge,
Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsPosterSize),
trailing: Text(
clientSettings.posterSize.toString(),
style: Theme.of(context).textTheme.bodyLarge,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: FladderSlider(
min: 0.5,
max: 1.5,
value: clientSettings.posterSize,
divisions: 20,
onChanged: (value) =>
ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: FladderSlider(
min: 0.5,
max: 1.5,
value: clientSettings.posterSize,
divisions: 20,
onChanged: (value) =>
ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)),
),
),
),
],
),
const Divider(),
];
],
),
],
);
}

View file

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/shared_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart';
@ -16,7 +15,8 @@ import 'package:fladder/screens/settings/client_sections/client_settings_visual.
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/simple_duration_picker.dart';
@ -38,16 +38,12 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
@override
Widget build(BuildContext context) {
final clientSettings = ref.watch(clientSettingsProvider);
final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
return Card(
elevation: showBackground ? 2 : 0,
child: SettingsScaffold(
label: "Fladder",
items: [
...buildClientSettingsDownload(context, ref, setState),
SettingsLabelDivider(label: context.localized.lockscreen),
return SettingsScaffold(
label: "Fladder",
items: [
...buildClientSettingsDownload(context, ref, setState),
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.lockscreen), [
SettingsListTile(
label: Text(context.localized.timeOut),
subLabel: Text(timePickerString(context, clientSettings.timeOut)),
@ -64,12 +60,16 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
: null);
},
),
const Divider(),
...buildClientSettingsDashboard(context, ref),
...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController),
...buildClientSettingsTheme(context, ref),
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
SettingsLabelDivider(label: context.localized.controls),
]),
const SizedBox(height: 12),
...buildClientSettingsDashboard(context, ref),
const SizedBox(height: 12),
...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController),
const SizedBox(height: 12),
...buildClientSettingsTheme(context, ref),
const SizedBox(height: 12),
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.controls), [
SettingsListTile(
label: Text(context.localized.mouseDragSupport),
subLabel: Text(clientSettings.mouseDragSupport ? context.localized.enabled : context.localized.disabled),
@ -83,61 +83,61 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
.update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)),
),
),
const Divider(),
],
...buildClientSettingsAdvanced(context, ref),
if (kDebugMode) ...[
const SizedBox(height: 64),
SettingsListTile(
label: Text(
context.localized.clearAllSettings,
),
contentColor: Theme.of(context).colorScheme.error,
onTap: () {
showDialog(
context: context,
builder: (context) => Dialog(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.localized.clearAllSettingsQuestion,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
context.localized.unableToReverseAction,
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(context.localized.cancel),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () async {
await ref.read(sharedPreferencesProvider).clear();
context.router.push(const LoginRoute());
},
child: Text(context.localized.clear),
)
],
),
],
),
]),
const SizedBox(height: 12),
],
...buildClientSettingsAdvanced(context, ref),
if (kDebugMode) ...[
const SizedBox(height: 64),
SettingsListTile(
label: Text(
context.localized.clearAllSettings,
),
contentColor: Theme.of(context).colorScheme.error,
onTap: () {
showDialog(
context: context,
builder: (context) => Dialog(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.localized.clearAllSettingsQuestion,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
context.localized.unableToReverseAction,
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(context.localized.cancel),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () async {
await ref.read(sharedPreferencesProvider).clear();
context.router.push(const LoginRoute());
},
child: Text(context.localized.clear),
)
],
),
],
),
),
);
},
),
],
),
);
},
),
],
),
],
);
}
}

View file

@ -8,20 +8,20 @@ import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/user_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';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/screens/settings/widgets/settings_message_box.dart';
import 'package:fladder/screens/settings/widgets/subtitle_editor.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/screens/shared/input_fields.dart';
import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/bitrate_helper.dart';
import 'package:fladder/util/box_fit_extension.dart';
import 'package:fladder/util/localization_helper.dart';
@ -41,100 +41,105 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
Widget build(BuildContext context) {
final videoSettings = ref.watch(videoPlayerSettingsProvider);
final provider = ref.read(videoPlayerSettingsProvider.notifier);
final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
final connectionState = ref.watch(connectivityStatusProvider);
return Card(
elevation: showBackground ? 2 : 0,
child: SettingsScaffold(
label: context.localized.settingsPlayerTitle,
items: [
return SettingsScaffold(
label: context.localized.settingsPlayerTitle,
items: [
...settingsListGroup(
context,
SettingsLabelDivider(label: context.localized.video),
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
[
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
Column(
children: [
SettingsListTile(
label: Text(context.localized.videoScalingFillScreenTitle),
subLabel: Text(context.localized.videoScalingFillScreenDesc),
onTap: () => provider.setFillScreen(!videoSettings.fillScreen),
trailing: Switch(
value: videoSettings.fillScreen,
onChanged: (value) => provider.setFillScreen(value),
),
),
AnimatedFadeSize(
child: videoSettings.fillScreen
? SettingsMessageBox(
context.localized.videoScalingFillScreenNotif,
messageType: MessageType.warning,
)
: Container(),
),
],
),
SettingsListTile(
label: Text(context.localized.videoScalingFillScreenTitle),
subLabel: Text(context.localized.videoScalingFillScreenDesc),
onTap: () => provider.setFillScreen(!videoSettings.fillScreen),
trailing: Switch(
value: videoSettings.fillScreen,
onChanged: (value) => provider.setFillScreen(value),
subLabel: Text(videoSettings.videoFit.label(context)),
onTap: () => openMultiSelectOptions(
context,
label: context.localized.videoScalingFillScreenTitle,
items: BoxFit.values,
selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))],
onChanged: (values) => ref.read(videoPlayerSettingsProvider.notifier).setFitType(values.first),
itemBuilder: (type, selected, tap) => RadioListTile(
groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)),
title: Text(type.label(context)),
value: type,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
contentPadding: EdgeInsets.zero,
onChanged: (value) => tap(),
),
),
),
AnimatedFadeSize(
child: videoSettings.fillScreen
? SettingsMessageBox(
context.localized.videoScalingFillScreenNotif,
messageType: MessageType.warning,
)
: Container(),
),
SettingsListTile(
label: Text(context.localized.videoScalingFillScreenTitle),
subLabel: Text(videoSettings.videoFit.label(context)),
onTap: () => openMultiSelectOptions(
context,
label: context.localized.videoScalingFillScreenTitle,
items: BoxFit.values,
selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))],
onChanged: (values) => ref.read(videoPlayerSettingsProvider.notifier).setFitType(values.first),
itemBuilder: (type, selected, tap) => RadioListTile(
groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)),
title: Text(type.label(context)),
value: type,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
contentPadding: EdgeInsets.zero,
onChanged: (value) => tap(),
SettingsListTile(
label: _StatusIndicator(
homeInternet: connectionState.homeInternet,
label: Text(context.localized.homeStreamingQualityTitle),
),
subLabel: Text(context.localized.homeStreamingQualityDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)),
),
itemBuilder: (context) => Bitrate.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry),
),
)
.toList(),
),
),
),
SettingsListTile(
label: _StatusIndicator(
homeInternet: connectionState.homeInternet,
label: Text(context.localized.homeStreamingQualityTitle),
),
subLabel: Text(context.localized.homeStreamingQualityDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)),
SettingsListTile(
label: _StatusIndicator(
homeInternet: !connectionState.homeInternet,
label: Text(context.localized.internetStreamingQualityTitle),
),
itemBuilder: (context) => Bitrate.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry),
),
)
.toList(),
),
),
SettingsListTile(
label: _StatusIndicator(
homeInternet: !connectionState.homeInternet,
label: Text(context.localized.internetStreamingQualityTitle),
),
subLabel: Text(context.localized.internetStreamingQualityDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)),
subLabel: Text(context.localized.internetStreamingQualityDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)),
),
itemBuilder: (context) => Bitrate.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(maxInternetBitrate: entry),
),
)
.toList(),
),
itemBuilder: (context) => Bitrate.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(maxInternetBitrate: entry),
),
)
.toList(),
),
),
const Divider(),
SettingsLabelDivider(label: context.localized.mediaSegmentActions),
],
),
const SizedBox(height: 12),
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.mediaSegmentActions), [
...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map(
(entry) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@ -167,7 +172,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
),
),
),
SettingsLabelDivider(label: context.localized.playbackTrackSelection),
]),
const SizedBox(height: 12),
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.playbackTrackSelection), [
SettingsListTile(
label: Text(context.localized.rememberAudioSelections),
subLabel: Text(context.localized.rememberAudioSelectionsDesc),
@ -190,8 +197,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
onChanged: (_) => ref.read(userProvider.notifier).setRememberSubtitleSelections(),
),
),
const Divider(),
SettingsLabelDivider(label: context.localized.advanced),
]),
const SizedBox(height: 12),
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.advanced), [
if (PlayerOptions.available.length != 1)
SettingsListTile(
label: Text(context.localized.playerSettingsBackendTitle),
@ -236,7 +244,7 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
onChanged: (value) => provider.setHardwareAccel(value),
),
),
if (!kIsWeb) ...[
if (!kIsWeb)
SettingsListTile(
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
@ -246,15 +254,14 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
onChanged: (value) => provider.setUseLibass(value),
),
),
AnimatedFadeSize(
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
? SettingsMessageBox(
context.localized.settingsPlayerMobileWarning,
messageType: MessageType.warning,
)
: Container(),
),
],
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),
@ -291,33 +298,37 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}")
},
),
SettingsListTile(
label: Text(context.localized.settingsAutoNextTitle),
subLabel: Text(context.localized.settingsAutoNextDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select(
(value) => value.nextVideoType.label(context),
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 =
ref.read(videoPlayerSettingsProvider).copyWith(nextVideoType: entry),
),
)
.toList(),
),
),
itemBuilder: (context) => AutoNextType.values
.map(
(entry) => PopupMenuItem(
value: entry,
child: Text(entry.label(context)),
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).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(),
},
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)
SettingsListTile(
@ -325,8 +336,8 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
subLabel: Text(context.localized.playerSettingsOrientationDesc),
onTap: () => showOrientationOptions(context, ref),
),
],
),
]),
],
);
}
}

View file

@ -1,14 +1,15 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart';
import 'package:fladder/screens/settings/settings_scaffold.dart';
import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
import 'package:fladder/screens/shared/authenticate_button_options.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@RoutePage()
class SecuritySettingsPage extends ConsumerStatefulWidget {
@ -22,14 +23,10 @@ class _UserSettingsPageState extends ConsumerState<SecuritySettingsPage> {
@override
Widget build(BuildContext context) {
final user = ref.watch(userProvider);
final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone &&
AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
return Card(
elevation: showBackground ? 2 : 0,
child: SettingsScaffold(
label: context.localized.settingsProfileTitle,
items: [
SettingsLabelDivider(label: context.localized.settingSecurityApplockTitle),
return SettingsScaffold(
label: context.localized.settingsProfileTitle,
items: [
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.settingSecurityApplockTitle), [
SettingsListTile(
label: Text(context.localized.settingSecurityApplockTitle),
subLabel: Text(user?.authMethod.name(context) ?? ""),
@ -37,8 +34,8 @@ class _UserSettingsPageState extends ConsumerState<SecuritySettingsPage> {
ref.read(userProvider.notifier).updateUser(newUser);
}),
),
],
),
]),
],
);
}
}

View file

@ -90,7 +90,7 @@ class SettingsListTile extends StatelessWidget {
),
if (subLabel != null)
Opacity(
opacity: 0.75,
opacity: 0.65,
child: Material(
color: Colors.transparent,
textStyle: Theme.of(context).textTheme.labelLarge,

View file

@ -4,10 +4,9 @@ import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/user_icon.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/router_extension.dart';
class SettingsScaffold extends ConsumerWidget {
@ -34,7 +33,6 @@ class SettingsScaffold extends ConsumerWidget {
final padding = MediaQuery.of(context).padding;
final singleLayout = AdaptiveLayout.layoutModeOf(context) == LayoutMode.single;
return Scaffold(
backgroundColor: AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual ? Colors.transparent : null,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: floatingActionButton,
body: Column(
@ -87,9 +85,10 @@ class SettingsScaffold extends ConsumerWidget {
),
),
SliverPadding(
padding: MediaQuery.paddingOf(context).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 0 : 8),
sliver: SliverList(
delegate: SliverChildListDelegate(items),
padding: MediaQuery.paddingOf(context).copyWith(top: 0),
sliver: SliverList.builder(
itemBuilder: (context, index) => items[index],
itemCount: items.length,
),
),
if (bottomActions.isEmpty)

View file

@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/providers/auth_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart';
@ -12,7 +11,7 @@ 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/fladder_icon.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/theme_extensions.dart';
@ -97,119 +96,122 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
final quickConnectAvailable =
ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false));
return Container(
color: context.colors.surface,
child: SettingsScaffold(
label: context.localized.settings,
scrollController: scrollController,
showBackButtonNested: true,
showUserIcon: true,
items: [
SettingsListTile(
label: Text(context.localized.settingsClientTitle),
subLabel: Text(context.localized.settingsClientDesc),
selected: containsRoute(const ClientSettingsRoute()),
icon: deviceIcon,
onTap: () => navigateTo(const ClientSettingsRoute()),
),
if (quickConnectAvailable)
return Padding(
padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
child: Container(
color: context.colors.surface,
child: SettingsScaffold(
label: context.localized.settings,
scrollController: scrollController,
showBackButtonNested: true,
showUserIcon: true,
items: [
SettingsListTile(
label: Text(context.localized.settingsQuickConnectTitle),
icon: IconsaxPlusLinear.password_check,
onTap: () => openQuickConnectDialog(context),
label: Text(context.localized.settingsClientTitle),
subLabel: Text(context.localized.settingsClientDesc),
selected: containsRoute(const ClientSettingsRoute()),
icon: deviceIcon,
onTap: () => navigateTo(const ClientSettingsRoute()),
),
SettingsListTile(
label: Text(context.localized.settingsProfileTitle),
subLabel: Text(context.localized.settingsProfileDesc),
selected: containsRoute(const SecuritySettingsRoute()),
icon: IconsaxPlusLinear.security_user,
onTap: () => navigateTo(const SecuritySettingsRoute()),
),
SettingsListTile(
label: Text(context.localized.settingsPlayerTitle),
subLabel: Text(context.localized.settingsPlayerDesc),
selected: containsRoute(const PlayerSettingsRoute()),
icon: IconsaxPlusLinear.video_play,
onTap: () => navigateTo(const PlayerSettingsRoute()),
),
SettingsListTile(
label: Text(context.localized.about),
subLabel: const Text("Fladder"),
selected: containsRoute(const AboutSettingsRoute()),
suffix: Opacity(
opacity: 1,
child: FladderIconOutlined(
size: 24,
color: context.colors.onSurfaceVariant,
if (quickConnectAvailable)
SettingsListTile(
label: Text(context.localized.settingsQuickConnectTitle),
icon: IconsaxPlusLinear.password_check,
onTap: () => openQuickConnectDialog(context),
),
SettingsListTile(
label: Text(context.localized.settingsProfileTitle),
subLabel: Text(context.localized.settingsProfileDesc),
selected: containsRoute(const SecuritySettingsRoute()),
icon: IconsaxPlusLinear.security_user,
onTap: () => navigateTo(const SecuritySettingsRoute()),
),
onTap: () => navigateTo(const AboutSettingsRoute()),
),
],
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,
),
SettingsListTile(
label: Text(context.localized.settingsPlayerTitle),
subLabel: Text(context.localized.settingsPlayerDesc),
selected: containsRoute(const PlayerSettingsRoute()),
icon: IconsaxPlusLinear.video_play,
onTap: () => navigateTo(const PlayerSettingsRoute()),
),
SettingsListTile(
label: Text(context.localized.about),
subLabel: const Text("Fladder"),
selected: containsRoute(const AboutSettingsRoute()),
suffix: Opacity(
opacity: 1,
child: FladderIconOutlined(
size: 24,
color: context.colors.onSurfaceVariant,
),
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),
),
onTap: () => navigateTo(const AboutSettingsRoute()),
),
],
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 ?? ""),
),
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),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text(context.localized.cancel),
),
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,
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,
),
),
),
],
],
),
),
),
),

View file

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart';
List<Widget> settingsListGroup(BuildContext context, Widget label, List<Widget> children) {
final radius = BorderRadius.circular(24);
final radiusSmall = const Radius.circular(6);
final color = Theme.of(context).colorScheme.surfaceContainerLow.harmonizeWith(Colors.red);
return [
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
color: color,
shape: RoundedRectangleBorder(
borderRadius: radius.copyWith(
bottomLeft: radiusSmall,
bottomRight: radiusSmall,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: label,
),
),
...children.map(
(e) {
return Card(
elevation: 0,
color: color,
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
shape: RoundedRectangleBorder(
borderRadius: radius.copyWith(
topLeft: radiusSmall,
topRight: radiusSmall,
bottomLeft: e != children.last ? radiusSmall : null,
bottomRight: e != children.last ? radiusSmall : null,
)),
child: e,
);
},
)
];
}

View file

@ -7,7 +7,7 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart';
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart';