feature: Rework responsive layout (#217)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-02-07 15:55:01 +01:00 committed by GitHub
parent e07f280124
commit 8012fdcea8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1468 additions and 1040 deletions

View file

@ -0,0 +1,80 @@
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/util/localization_helper.dart';
import 'package:fladder/util/option_dialogue.dart';
List<Widget> buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) {
return [
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.name),
),
);
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)),
),
);
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

@ -0,0 +1,95 @@
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/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/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 [
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),
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(),
),
),
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
.read(clientSettingsProvider.notifier)
.update((current) => current.copyWith(showAllCollectionTypes: value)),
),
),
const Divider(),
];
}

View file

@ -0,0 +1,122 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:ficonsax/ficonsax.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
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/shared/default_alert_dialog.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/size_formatting.dart';
List<Widget> buildClientSettingsDownload(BuildContext context, WidgetRef ref, Function setState) {
final clientSettings = ref.watch(clientSettingsProvider);
final currentFolder = ref.watch(syncProvider.notifier).savePath;
final canSync = ref.watch(userProvider.select((value) => value?.canDownload ?? false));
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),
)
],
),
)
: () 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(IconsaxOutline.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),
),
),
const Divider(),
],
];
}

View file

@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
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/util/color_extensions.dart';
import 'package:fladder/util/custom_color_themes.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/option_dialogue.dart';
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),
SettingsListTile(
label: Text(context.localized.mode),
subLabel: Text(clientSettings.themeMode.label(context)),
onTap: () => openMultiSelectOptions<ThemeMode>(
context,
label: "${context.localized.theme} ${context.localized.mode}",
items: ThemeMode.values,
selected: [ref.read(clientSettingsProvider.select((value) => value.themeMode))],
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setThemeMode(values.first),
itemBuilder: (type, selected, tap) => RadioListTile(
value: type,
title: Text(type.label(context)),
contentPadding: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
groupValue: ref.read(clientSettingsProvider.select((value) => value.themeMode)),
onChanged: (value) => tap(),
),
),
),
SettingsListTile(
label: Text(context.localized.color),
subLabel: Text(clientSettings.themeColor?.name ?? context.localized.dynamicText),
onTap: () => openMultiSelectOptions<ColorThemes?>(
context,
label: context.localized.settingsLayoutModesTitle,
items: [null, ...ColorThemes.values],
selected: [(ref.read(clientSettingsProvider.select((value) => value.themeColor)))],
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setThemeColor(values.first),
itemBuilder: (type, selected, tap) => RadioListTile<ColorThemes?>(
groupValue: ref.read(clientSettingsProvider.select((value) => value.themeColor)),
contentPadding: EdgeInsets.zero,
value: type,
onChanged: (value) => tap(),
title: Row(
children: [
Container(
height: 24,
width: 24,
decoration: BoxDecoration(
gradient: type == null
? const SweepGradient(
center: FractionalOffset.center,
colors: <Color>[
Color(0xFF4285F4), // blue
Color(0xFF34A853), // green
Color(0xFFFBBC05), // yellow
Color(0xFFEA4335), // red
Color(0xFF4285F4), // blue again to seamlessly transition to the start
],
stops: <double>[0.0, 0.25, 0.5, 0.75, 1.0],
)
: null,
color: type?.color,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(width: 8),
Text(type?.name ?? context.localized.dynamicText),
],
),
),
),
),
SettingsListTile(
label: Text(context.localized.clientSettingsSchemeVariantTitle),
subLabel: Text(clientSettings.schemeVariant.label(context)),
onTap: () async {
await openMultiSelectOptions<DynamicSchemeVariant>(
context,
label: context.localized.settingsLayoutModesTitle,
items: DynamicSchemeVariant.values,
selected: [(ref.read(clientSettingsProvider.select((value) => value.schemeVariant)))],
onChanged: (values) => ref.read(clientSettingsProvider.notifier).setSchemeVariant(values.first),
itemBuilder: (type, selected, tap) => RadioListTile<DynamicSchemeVariant>(
groupValue: selected ? type : null,
contentPadding: EdgeInsets.zero,
value: type,
onChanged: (value) => tap(),
title: Text(type.label(context)),
),
);
},
),
SettingsListTile(
label: Text(context.localized.amoledBlack),
subLabel: Text(clientSettings.amoledBlack ? context.localized.enabled : context.localized.disabled),
onTap: () => ref.read(clientSettingsProvider.notifier).setAmoledBlack(!clientSettings.amoledBlack),
trailing: Switch(
value: clientSettings.amoledBlack,
onChanged: (value) => ref.read(clientSettingsProvider.notifier).setAmoledBlack(value),
),
),
const Divider(),
];
}

View file

@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
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/shared/input_fields.dart';
import 'package:fladder/util/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';
List<Widget> buildClientSettingsVisual(
BuildContext context,
WidgetRef ref,
TextEditingController nextUpDaysEditor,
TextEditingController libraryPageSizeController,
) {
final clientSettings = ref.watch(clientSettingsProvider);
Locale currentLocale = WidgetsBinding.instance.platformDispatcher.locale;
return [
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) {
return EnumBox(
current: context.localized.nativeName,
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,
);
}),
),
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(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,
),
),
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(),
];
}