diff --git a/lib/app_config.dart b/lib/app_config.dart index 885f8f2f7..685185b4c 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -8,3 +8,10 @@ bool allowDiscordRPC = kDebugMode || Platform.isMacOS || Platform.isWindows || bool.tryParse(const String.fromEnvironment('ALLOW_DISCORD_RPC')) == true; + +bool get yaruStyled => Platform.isLinux; + +bool get appleStyled => Platform.isMacOS || Platform.isIOS; + +// TODO(#1022): fix linux video fullscreen +bool get allowVideoFullScreen => !Platform.isLinux; diff --git a/lib/common/view/animated_like_icon.dart b/lib/common/view/animated_like_icon.dart index 698ff81f3..dc2bf67de 100644 --- a/lib/common/view/animated_like_icon.dart +++ b/lib/common/view/animated_like_icon.dart @@ -1,3 +1,4 @@ +import '../../app_config.dart'; import 'icons.dart'; import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; diff --git a/lib/common/view/audio_autocomplete.dart b/lib/common/view/audio_autocomplete.dart index 2393713e1..bce1c8da3 100644 --- a/lib/common/view/audio_autocomplete.dart +++ b/lib/common/view/audio_autocomplete.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:yaru/constants.dart'; +import '../../app_config.dart'; import '../../constants.dart'; import '../../extensions/build_context_x.dart'; import '../../extensions/theme_data_x.dart'; diff --git a/lib/common/view/audio_tile_option_button.dart b/lib/common/view/audio_tile_option_button.dart index 2734c546e..86f2dba5c 100644 --- a/lib/common/view/audio_tile_option_button.dart +++ b/lib/common/view/audio_tile_option_button.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../app_config.dart'; import '../../extensions/build_context_x.dart'; import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; @@ -10,7 +11,6 @@ import '../data/audio.dart'; import 'icons.dart'; import 'snackbars.dart'; import 'stream_provider_share_button.dart'; -import 'theme.dart'; class AudioTileOptionButton extends StatelessWidget { const AudioTileOptionButton({ diff --git a/lib/common/view/common_widgets.dart b/lib/common/view/common_widgets.dart index 9324ce7e4..7ebdc89f9 100644 --- a/lib/common/view/common_widgets.dart +++ b/lib/common/view/common_widgets.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; -import 'theme.dart'; +import '../../app_config.dart'; class CommonSwitch extends StatelessWidget { const CommonSwitch({super.key, required this.value, this.onChanged}); diff --git a/lib/common/view/country_auto_complete.dart b/lib/common/view/country_auto_complete.dart index da2ef5d0c..2b06dffc1 100644 --- a/lib/common/view/country_auto_complete.dart +++ b/lib/common/view/country_auto_complete.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:podcast_search/podcast_search.dart'; +import '../../app_config.dart'; import '../../constants.dart'; import '../../extensions/build_context_x.dart'; import '../../extensions/country_x.dart'; diff --git a/lib/common/view/drop_down_arrow.dart b/lib/common/view/drop_down_arrow.dart index e7bd380cb..2599ee21b 100644 --- a/lib/common/view/drop_down_arrow.dart +++ b/lib/common/view/drop_down_arrow.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; -import 'theme.dart'; + +import '../../app_config.dart'; class DropDownArrow extends StatelessWidget { const DropDownArrow({super.key}); diff --git a/lib/common/view/header_bar.dart b/lib/common/view/header_bar.dart index e69143ad2..f8a7f7ef0 100644 --- a/lib/common/view/header_bar.dart +++ b/lib/common/view/header_bar.dart @@ -5,6 +5,7 @@ import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; import '../../app/app_model.dart'; +import '../../app_config.dart'; import '../../extensions/build_context_x.dart'; import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; diff --git a/lib/common/view/icons.dart b/lib/common/view/icons.dart index 0f63e2d4a..2231f9014 100644 --- a/lib/common/view/icons.dart +++ b/lib/common/view/icons.dart @@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; -import 'theme.dart'; +import '../../app_config.dart'; class Iconz { static IconData get image => yaruStyled diff --git a/lib/common/view/language_autocomplete.dart b/lib/common/view/language_autocomplete.dart index 0f2b09cbe..183cc0ab7 100644 --- a/lib/common/view/language_autocomplete.dart +++ b/lib/common/view/language_autocomplete.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import '../../app_config.dart'; import '../../constants.dart'; import '../../extensions/build_context_x.dart'; import '../../extensions/string_x.dart'; diff --git a/lib/common/view/nav_back_button.dart b/lib/common/view/nav_back_button.dart index f0b1fafc3..209f6fdc4 100644 --- a/lib/common/view/nav_back_button.dart +++ b/lib/common/view/nav_back_button.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../app_config.dart'; import '../../library/library_model.dart'; -import 'theme.dart'; class NavBackButton extends StatelessWidget { const NavBackButton({super.key}); diff --git a/lib/common/view/progress.dart b/lib/common/view/progress.dart index 3b32b0e66..ca857dc62 100644 --- a/lib/common/view/progress.dart +++ b/lib/common/view/progress.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; +import '../../app_config.dart'; import '../../extensions/build_context_x.dart'; import 'theme.dart'; diff --git a/lib/common/view/search_button.dart b/lib/common/view/search_button.dart index 4f699eec9..f3e9d25b2 100644 --- a/lib/common/view/search_button.dart +++ b/lib/common/view/search_button.dart @@ -1,7 +1,9 @@ -import '../../extensions/build_context_x.dart'; -import 'icons.dart'; import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; + +import '../../app_config.dart'; +import '../../extensions/build_context_x.dart'; +import 'icons.dart'; import 'theme.dart'; class SearchButton extends StatelessWidget { diff --git a/lib/common/view/search_input.dart b/lib/common/view/search_input.dart index 5e6d3fec4..0c59d5266 100644 --- a/lib/common/view/search_input.dart +++ b/lib/common/view/search_input.dart @@ -1,6 +1,9 @@ -import '../../extensions/build_context_x.dart'; import 'dart:async'; + import 'package:flutter/material.dart'; + +import '../../app_config.dart'; +import '../../extensions/build_context_x.dart'; import 'theme.dart'; class SearchInput extends StatefulWidget { diff --git a/lib/common/view/theme.dart b/lib/common/view/theme.dart index b664349d8..e40bf0f3d 100644 --- a/lib/common/view/theme.dart +++ b/lib/common/view/theme.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; +import '../../app_config.dart'; import '../../constants.dart'; import 'icons.dart'; @@ -70,10 +71,6 @@ Color getPlayerBg(Color? surfaceTintColor, Color fallbackColor) { } } -bool get yaruStyled => Platform.isLinux; - -bool get appleStyled => Platform.isMacOS || Platform.isIOS; - const alphabetColors = { 'A': Colors.red, 'B': Colors.orange, diff --git a/lib/extensions/theme_data_x.dart b/lib/extensions/theme_data_x.dart index 7ed1703bf..9b021d218 100644 --- a/lib/extensions/theme_data_x.dart +++ b/lib/extensions/theme_data_x.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; -import '../common/view/theme.dart'; +import '../../app_config.dart'; extension ThemeDataX on ThemeData { bool get isLight => brightness == Brightness.light; diff --git a/lib/player/player_service.dart b/lib/player/player_service.dart index 1164d0277..2a39fb626 100644 --- a/lib/player/player_service.dart +++ b/lib/player/player_service.dart @@ -211,7 +211,14 @@ class PlayerService { Media? media = audio!.path != null ? Media('file://${audio!.path!}') : (audio!.url != null) - ? Media(audio!.url!) + ? Media( + audio!.url!, + httpHeaders: { + 'Accept': '*', + 'User-Agent': '$kAppTitle ($kRepoUrl)', + 'Content-Language': 'de-DE', + }, + ) : null; if (media == null) return; _player.open(media).then((_) { diff --git a/lib/player/view/full_height_video_player.dart b/lib/player/view/full_height_video_player.dart index 79fc19f3c..69b5fe8d8 100644 --- a/lib/player/view/full_height_video_player.dart +++ b/lib/player/view/full_height_video_player.dart @@ -4,6 +4,7 @@ import 'package:watch_it/watch_it.dart'; import 'package:yaru/constants.dart'; import '../../app/connectivity_model.dart'; +import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/view/icons.dart'; import '../../l10n/l10n.dart'; @@ -58,15 +59,16 @@ class FullHeightVideoPlayer extends StatelessWidget with WatchItMixin { topButtonBar: [ const Spacer(), controls, - Tooltip( - message: context.l10n.leaveFullScreen, - child: MaterialFullscreenButton( - icon: Icon( - Iconz.fullScreenExit, - color: baseColor, + if (allowVideoFullScreen) + Tooltip( + message: context.l10n.leaveFullScreen, + child: MaterialFullscreenButton( + icon: Icon( + Iconz.fullScreenExit, + color: baseColor, + ), ), ), - ), ], bottomButtonBarMargin: const EdgeInsets.all(20), bottomButtonBar: [ @@ -92,15 +94,16 @@ class FullHeightVideoPlayer extends StatelessWidget with WatchItMixin { topButtonBar: [ const Spacer(), controls, - Tooltip( - message: context.l10n.fullScreen, - child: MaterialFullscreenButton( - icon: Icon( - Iconz.fullScreen, - color: baseColor, + if (allowVideoFullScreen) + Tooltip( + message: context.l10n.fullScreen, + child: MaterialFullscreenButton( + icon: Icon( + Iconz.fullScreen, + color: baseColor, + ), ), ), - ), ], ), child: RepaintBoundary( diff --git a/lib/player/view/player_track.dart b/lib/player/view/player_track.dart index 1ea6c633a..d53616bfa 100644 --- a/lib/player/view/player_track.dart +++ b/lib/player/view/player_track.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; +import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/view/custom_track_shape.dart'; import '../../common/view/progress.dart'; -import '../../common/view/theme.dart'; import '../../extensions/build_context_x.dart'; import '../../extensions/duration_x.dart'; import '../player_model.dart'; diff --git a/lib/player/view/seek_button.dart b/lib/player/view/seek_button.dart index d08e19952..6c152329a 100644 --- a/lib/player/view/seek_button.dart +++ b/lib/player/view/seek_button.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; +import '../../app_config.dart'; import '../../common/view/icons.dart'; -import '../../common/view/theme.dart'; import '../../extensions/build_context_x.dart'; import '../player_model.dart'; diff --git a/lib/playlists/view/add_to_playlist_dialog.dart b/lib/playlists/view/add_to_playlist_dialog.dart index e462e2c26..1eaaf4ae9 100644 --- a/lib/playlists/view/add_to_playlist_dialog.dart +++ b/lib/playlists/view/add_to_playlist_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/view/common_widgets.dart'; import '../../common/view/global_keys.dart'; diff --git a/lib/playlists/view/manual_add_dialog.dart b/lib/playlists/view/manual_add_dialog.dart index da3828840..cf4ee5559 100644 --- a/lib/playlists/view/manual_add_dialog.dart +++ b/lib/playlists/view/manual_add_dialog.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/view/common_widgets.dart'; import '../../common/view/global_keys.dart'; import '../../common/view/icons.dart'; -import '../../common/view/theme.dart'; import '../../constants.dart'; import '../../external_path/external_path_service.dart'; import '../../l10n/l10n.dart'; diff --git a/lib/podcasts/view/podcast_audio_tile.dart b/lib/podcasts/view/podcast_audio_tile.dart index af9b00f96..fc862e8ae 100644 --- a/lib/podcasts/view/podcast_audio_tile.dart +++ b/lib/podcasts/view/podcast_audio_tile.dart @@ -4,6 +4,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/view/icons.dart'; import '../../common/view/share_button.dart'; @@ -14,8 +15,8 @@ import '../../extensions/duration_x.dart'; import '../../extensions/int_x.dart'; import '../../l10n/l10n.dart'; import '../../player/player_model.dart'; -import 'podcast_tile_play_button.dart'; import 'download_button.dart'; +import 'podcast_tile_play_button.dart'; class PodcastAudioTile extends StatelessWidget { const PodcastAudioTile({ diff --git a/lib/podcasts/view/podcast_genre_autocomplete.dart b/lib/podcasts/view/podcast_genre_autocomplete.dart index c06077ff0..2b3813f4c 100644 --- a/lib/podcasts/view/podcast_genre_autocomplete.dart +++ b/lib/podcasts/view/podcast_genre_autocomplete.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import '../../app_config.dart'; import '../../common/data/podcast_genre.dart'; import '../../common/view/theme.dart'; import '../../constants.dart'; diff --git a/lib/podcasts/view/podcast_tile_play_button.dart b/lib/podcasts/view/podcast_tile_play_button.dart index ca852642f..8c0a5152f 100644 --- a/lib/podcasts/view/podcast_tile_play_button.dart +++ b/lib/podcasts/view/podcast_tile_play_button.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; +import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/view/icons.dart'; import '../../common/view/theme.dart'; diff --git a/lib/radio/view/radio_history_tile.dart b/lib/radio/view/radio_history_tile.dart index a3fbd7131..729bfb9e7 100644 --- a/lib/radio/view/radio_history_tile.dart +++ b/lib/radio/view/radio_history_tile.dart @@ -3,13 +3,13 @@ import 'package:watch_it/watch_it.dart'; import 'package:yaru/constants.dart'; import 'package:yaru/yaru.dart'; +import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/view/copy_clipboard_content.dart'; import '../../common/view/icons.dart'; import '../../common/view/mpv_metadata_dialog.dart'; import '../../common/view/snackbars.dart'; import '../../common/view/tapable_text.dart'; -import '../../common/view/theme.dart'; import '../../extensions/build_context_x.dart'; import '../../extensions/theme_data_x.dart'; import '../../l10n/l10n.dart'; diff --git a/lib/radio/view/radio_lib_page.dart b/lib/radio/view/radio_lib_page.dart index aa2642182..76272d1c7 100644 --- a/lib/radio/view/radio_lib_page.dart +++ b/lib/radio/view/radio_lib_page.dart @@ -6,6 +6,7 @@ import 'package:radio_browser_api/radio_browser_api.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/view/adaptive_container.dart'; import '../../common/view/audio_card.dart'; diff --git a/lib/radio/view/tag_auto_complete.dart b/lib/radio/view/tag_auto_complete.dart index 41f39f49d..5ffc28737 100644 --- a/lib/radio/view/tag_auto_complete.dart +++ b/lib/radio/view/tag_auto_complete.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:radio_browser_api/radio_browser_api.dart' hide State; +import '../../app_config.dart'; import '../../common/view/icons.dart'; import '../../common/view/theme.dart'; import '../../constants.dart'; diff --git a/lib/search/view/search_page.dart b/lib/search/view/search_page.dart index 4da6763f6..1701f1675 100644 --- a/lib/search/view/search_page.dart +++ b/lib/search/view/search_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/rendering.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/theme.dart'; +import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/view/adaptive_container.dart'; import '../../common/view/header_bar.dart'; diff --git a/lib/settings/view/about_section.dart b/lib/settings/view/about_section.dart new file mode 100644 index 000000000..87c71779a --- /dev/null +++ b/lib/settings/view/about_section.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as p; +import 'package:url_launcher/url_launcher.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; + +import '../../app/app_model.dart'; +import '../../app/connectivity_model.dart'; +import '../../app_config.dart'; +import '../../common/view/global_keys.dart'; +import '../../common/view/progress.dart'; +import '../../common/view/snackbars.dart'; +import '../../common/view/tapable_text.dart'; +import '../../constants.dart'; +import '../../extensions/build_context_x.dart'; +import '../../extensions/theme_data_x.dart'; +import '../../l10n/l10n.dart'; + +class AboutSection extends StatelessWidget with WatchItMixin { + const AboutSection({super.key}); + + @override + Widget build(BuildContext context) { + final text = '${context.l10n.about} $kAppTitle'; + return YaruSection( + headline: Text(text), + margin: const EdgeInsets.all(kYaruPagePadding), + child: const Column( + children: [_AboutTile(), _LicenseTile()], + ), + ); + } +} + +class _AboutTile extends StatefulWidget with WatchItStatefulWidgetMixin { + const _AboutTile(); + + @override + State<_AboutTile> createState() => _AboutTileState(); +} + +class _AboutTileState extends State<_AboutTile> { + @override + void initState() { + super.initState(); + di().checkForUpdate( + isOnline: di().isOnline == true, + onError: (e) { + if (mounted) { + showSnackBar(context: context, content: Text(e)); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + final theme = context.theme; + final appModel = di(); + final updateAvailable = + watchPropertyValue((AppModel m) => m.updateAvailable); + final onlineVersion = watchPropertyValue((AppModel m) => m.onlineVersion); + final currentVersion = watchPropertyValue((AppModel m) => m.version); + + return YaruTile( + title: !di().isOnline == true || + !appModel.allowManualUpdate + ? Text(di().version ?? '') + : updateAvailable == null + ? Center( + child: SizedBox.square( + dimension: yaruStyled ? kYaruTitleBarItemHeight : 40, + child: const Progress( + padding: EdgeInsets.all(10), + ), + ), + ) + : TapAbleText( + text: updateAvailable == true + ? '${context.l10n.updateAvailable}: $onlineVersion' + : currentVersion ?? context.l10n.unknown, + style: updateAvailable == true + ? TextStyle( + color: context.theme.colorScheme.success + .scale(lightness: theme.isLight ? 0 : 0.3), + ) + : null, + onTap: () => launchUrl( + Uri.parse( + p.join( + kRepoUrl, + 'releases', + 'tag', + onlineVersion, + ), + ), + ), + ), + trailing: OutlinedButton( + onPressed: () => settingsNavigatorKey.currentState?.pushNamed('/about'), + child: Text(context.l10n.contributors), + ), + ); + } +} + +class _LicenseTile extends StatelessWidget { + const _LicenseTile(); + + @override + Widget build(BuildContext context) { + return YaruTile( + title: TapAbleText( + text: '${context.l10n.license}: GPL3', + ), + trailing: OutlinedButton( + onPressed: () => + settingsNavigatorKey.currentState?.pushNamed('/licenses'), + child: Text(context.l10n.dependencies), + ), + enabled: true, + ); + } +} diff --git a/lib/settings/view/close_action_section.dart b/lib/settings/view/close_action_section.dart new file mode 100644 index 000000000..6b7c078a0 --- /dev/null +++ b/lib/settings/view/close_action_section.dart @@ -0,0 +1,56 @@ +import '../../common/data/close_btn_action.dart'; +import '../../common/view/drop_down_arrow.dart'; +import '../../l10n/l10n.dart'; +import '../settings_model.dart'; +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; + +// TODO(#793): figure out how to show the window from clicking the dock icon in macos, windows and linux +// Also figure out how to show the window again, when the gtk window is triggered from the outside (open with) +// if we can not figure this out, we can not land this feature. +// ignore: unused_element +class CloseActionSection extends StatelessWidget with WatchItMixin { + const CloseActionSection({super.key}); + + @override + Widget build(BuildContext context) { + final model = di(); + + final closeBtnAction = + watchPropertyValue((SettingsModel m) => m.closeBtnActionIndex); + return YaruSection( + margin: const EdgeInsets.only( + left: kYaruPagePadding, + top: kYaruPagePadding, + right: kYaruPagePadding, + ), + headline: Text(context.l10n.closeBtnAction), + child: Column( + children: [ + YaruTile( + title: Text(context.l10n.whenCloseBtnClicked), + trailing: YaruPopupMenuButton( + icon: const DropDownArrow(), + initialValue: closeBtnAction, + child: Text(closeBtnAction.localize(context.l10n)), + onSelected: (value) { + model.setCloseBtnActionIndex(value); + }, + itemBuilder: (context) { + return [ + for (var i = 0; i < CloseBtnAction.values.length; ++i) + PopupMenuItem( + value: CloseBtnAction.values[i], + child: + Text(CloseBtnAction.values[i].localize(context.l10n)), + ), + ]; + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/settings/view/expose_online_section.dart b/lib/settings/view/expose_online_section.dart new file mode 100644 index 000000000..3bab645e4 --- /dev/null +++ b/lib/settings/view/expose_online_section.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; + +import '../../app/app_model.dart'; +import '../../app_config.dart'; +import '../../common/view/common_widgets.dart'; +import '../../common/view/theme.dart'; +import '../../extensions/build_context_x.dart'; +import '../../l10n/l10n.dart'; +import '../settings_model.dart'; + +class ExposeOnlineSection extends StatefulWidget + with WatchItStatefulWidgetMixin { + const ExposeOnlineSection({super.key}); + + @override + State createState() => _ExposeOnlineSectionState(); +} + +class _ExposeOnlineSectionState extends State { + late TextEditingController _lastFmApiKeyController; + late TextEditingController _lastFmSecretController; + final _formkey = GlobalKey(); + + @override + void initState() { + final model = di(); + _lastFmApiKeyController = TextEditingController(text: model.lastFmApiKey); + _lastFmSecretController = TextEditingController(text: model.lastFmSecret); + + super.initState(); + } + + @override + void dispose() { + _lastFmApiKeyController.dispose(); + _lastFmSecretController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + final discordEnabled = allowDiscordRPC + ? watchPropertyValue((SettingsModel m) => m.enableDiscordRPC) + : false; + + final lastFmEnabled = + watchPropertyValue((SettingsModel m) => m.enableLastFmScrobbling); + + return YaruSection( + headline: Text(l10n.exposeOnlineHeadline), + margin: const EdgeInsets.only( + top: kYaruPagePadding, + right: kYaruPagePadding, + left: kYaruPagePadding, + ), + child: Column( + children: [ + YaruTile( + title: Row( + children: space( + children: [ + allowDiscordRPC + ? const Icon( + TablerIcons.brand_discord_filled, + ) + : Icon( + TablerIcons.brand_discord_filled, + color: context.theme.disabledColor, + ), + Text(l10n.exposeToDiscordTitle), + ], + ), + ), + subtitle: Text( + allowDiscordRPC + ? l10n.exposeToDiscordSubTitle + : l10n.featureDisabledOnPlatform, + ), + trailing: CommonSwitch( + value: discordEnabled, + onChanged: allowDiscordRPC + ? (v) { + di().setEnableDiscordRPC(v); + final appModel = di(); + if (v) { + appModel.connectToDiscord(); + } else { + appModel.disconnectFromDiscord(); + } + } + : null, + ), + ), + YaruTile( + title: Row( + children: space( + children: [ + const Icon( + TablerIcons.brand_lastfm, + ), + if (lastFmEnabled && + watchValue((AppModel m) => m.isLastFmAuthorized)) + Text(l10n.connectedTo), + Text(l10n.exposeToLastfmTitle), + ], + ), + ), + subtitle: Column( + children: [ + Text(l10n.exposeToLastfmSubTitle), + ], + ), + trailing: CommonSwitch( + value: lastFmEnabled, + onChanged: (v) { + di().setEnableLastFmScrobbling(v); + }, + ), + ), + if (lastFmEnabled) ...[ + Padding( + padding: const EdgeInsets.all(8), + child: Form( + key: _formkey, + onChanged: _formkey.currentState?.validate, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: space( + heightGap: 10, + children: [ + TextFormField( + obscureText: true, + controller: _lastFmApiKeyController, + decoration: InputDecoration( + hintText: l10n.lastfmApiKey, + label: Text(l10n.lastfmApiKey), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.lastfmApiKeyEmpty; + } + return null; + }, + onChanged: (_) => _formkey.currentState?.validate(), + onFieldSubmitted: (value) async { + if (_formkey.currentState!.validate()) { + di().setLastFmApiKey(value); + } + }, + ), + TextFormField( + obscureText: true, + controller: _lastFmSecretController, + decoration: InputDecoration( + hintText: l10n.lastfmSecret, + label: Text(l10n.lastfmSecret), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.lastfmSecretEmpty; + } + return null; + }, + onChanged: (_) => _formkey.currentState?.validate(), + onFieldSubmitted: (value) async { + if (_formkey.currentState!.validate()) { + di().setLastFmSecret(value); + } + }, + ), + ImportantButton( + onPressed: () { + di() + ..setLastFmApiKey( + _lastFmApiKeyController.text, + ) + ..setLastFmSecret( + _lastFmSecretController.text, + ); + di().authorizeLastFm( + apiKey: _lastFmApiKeyController.text, + apiSecret: _lastFmSecretController.text, + ); + }, + child: Text(l10n.saveAndAuthorize), + ), + ], + ), + ), + ), + ), + ], + ], + ), + ); + } +} diff --git a/lib/settings/view/local_audio_section.dart b/lib/settings/view/local_audio_section.dart new file mode 100644 index 000000000..6737de446 --- /dev/null +++ b/lib/settings/view/local_audio_section.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; + +import '../../common/view/common_widgets.dart'; +import '../../l10n/l10n.dart'; +import '../../local_audio/local_audio_model.dart'; +import '../settings_model.dart'; + +class LocalAudioSection extends StatelessWidget with WatchItMixin { + const LocalAudioSection({super.key}); + + @override + Widget build(BuildContext context) { + final settingsModel = di(); + final localAudioModel = di(); + final directory = + watchPropertyValue((SettingsModel m) => m.directory ?? ''); + + Future onDirectorySelected(String directoryPath) async { + settingsModel.setDirectory(directoryPath).then( + (_) async => localAudioModel.init( + forceInit: true, + directory: directoryPath, + ), + ); + } + + return YaruSection( + headline: Text(context.l10n.localAudio), + margin: const EdgeInsets.symmetric(horizontal: kYaruPagePadding), + child: Column( + children: [ + YaruTile( + title: Text(context.l10n.musicCollectionLocation), + subtitle: Text(directory), + trailing: ImportantButton( + onPressed: () async { + final directoryPath = await settingsModel.getPathOfDirectory(); + if (directoryPath != null) { + await onDirectorySelected(directoryPath); + } + }, + child: Text( + context.l10n.select, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/settings/view/podcast_section.dart b/lib/settings/view/podcast_section.dart new file mode 100644 index 000000000..24528f903 --- /dev/null +++ b/lib/settings/view/podcast_section.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; + +import '../../common/view/common_widgets.dart'; +import '../../common/view/icons.dart'; +import '../../constants.dart'; +import '../../extensions/build_context_x.dart'; +import '../../extensions/string_x.dart'; +import '../../l10n/l10n.dart'; +import '../../podcasts/download_model.dart'; +import '../../podcasts/podcast_model.dart'; +import '../settings_model.dart'; + +class PodcastSection extends StatefulWidget with WatchItStatefulWidgetMixin { + const PodcastSection({super.key}); + + @override + State createState() => _PodcastSectionState(); +} + +class _PodcastSectionState extends State { + String? _initialKey; + String? _initialSecret; + late TextEditingController _keyController, _secretController; + + @override + void initState() { + super.initState(); + final model = di(); + _initialKey = model.podcastIndexApiKey; + _keyController = TextEditingController(text: _initialKey); + _initialSecret = model.podcastIndexApiSecret; + _secretController = TextEditingController(text: _initialSecret); + } + + @override + void dispose() { + _keyController.dispose(); + _secretController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = context.theme; + final l10n = context.l10n; + final model = di(); + final usePodcastIndex = + watchPropertyValue((SettingsModel m) => m.usePodcastIndex); + final podcastIndexApiKey = + watchPropertyValue((SettingsModel m) => m.podcastIndexApiKey); + final podcastIndexApiSecret = + watchPropertyValue((SettingsModel m) => m.podcastIndexApiSecret); + + return YaruSection( + margin: const EdgeInsets.all(kYaruPagePadding), + headline: Text(l10n.podcasts), + child: Column( + children: [ + const _DownloadsTile(), + YaruTile( + title: Text(l10n.usePodcastIndex), + trailing: CommonSwitch( + value: usePodcastIndex, + onChanged: (v) async { + await model.setUsePodcastIndex(v); + if (context.mounted) { + di().init( + forceInit: true, + updateMessage: l10n.newEpisodeAvailable, + ); + } + }, + ), + ), + if (usePodcastIndex) + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _keyController, + onChanged: (v) => setState(() => _initialKey = v), + obscureText: true, + decoration: InputDecoration( + label: Text(kPodcastIndexApiKey.camelToSentence), + suffixIcon: IconButton( + tooltip: l10n.save, + onPressed: () => + model.setPodcastIndexApiKey(_keyController.text), + icon: Icon( + Iconz.check, + color: podcastIndexApiKey == _initialKey + ? theme.colorScheme.success + : theme.colorScheme.onSurface, + ), + ), + ), + ), + ), + if (usePodcastIndex) + Padding( + padding: const EdgeInsets.only( + left: 8, + right: 8, + bottom: kYaruPagePadding, + ), + child: TextField( + controller: _secretController, + onChanged: (v) => setState(() => _initialSecret = v), + obscureText: true, + decoration: InputDecoration( + label: Text(kPodcastIndexApiSecret.camelToSentence), + suffixIcon: IconButton( + tooltip: l10n.save, + onPressed: () => + model.setPodcastIndexApiSecret(_secretController.text), + icon: Icon( + Iconz.check, + color: podcastIndexApiSecret == _initialSecret + ? theme.colorScheme.success + : theme.colorScheme.onSurface, + ), + ), + ), + ), + ), + ], + ), + ); + } +} + +class _DownloadsTile extends StatefulWidget with WatchItStatefulWidgetMixin { + const _DownloadsTile(); + + @override + State<_DownloadsTile> createState() => _DownloadsTileState(); +} + +class _DownloadsTileState extends State<_DownloadsTile> { + String? _error; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return YaruTile( + title: Text(l10n.downloadsDirectory), + subtitle: Text( + _error ?? watchPropertyValue((SettingsModel m) => m.downloadsDir ?? ''), + ), + trailing: ImportantButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + content: SizedBox( + width: 300, + child: Text( + l10n.downloadsChangeWarning, + style: context.textTheme.bodyLarge, + ), + ), + actions: [ + OutlinedButton( + onPressed: Navigator.of(context).pop, + child: Text(l10n.cancel), + ), + ImportantButton( + onPressed: () { + Navigator.of(context).pop(); + di().setDownloadsCustomDir( + onSuccess: () => di().deleteAllDownloads(), + onFail: (e) => setState(() => _error = e.toString()), + ); + }, + child: Text(l10n.ok), + ), + ], + ), + ); + }, + child: Text( + l10n.select, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } +} diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index 073d4a7d2..8d0ce9227 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -1,33 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; -import 'package:path/path.dart' as p; -import 'package:url_launcher/url_launcher.dart'; -import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; -import '../../app/app_model.dart'; -import '../../app/connectivity_model.dart'; -import '../../app_config.dart'; -import '../../common/data/close_btn_action.dart'; -import '../../common/view/common_widgets.dart'; -import '../../common/view/drop_down_arrow.dart'; -import '../../common/view/global_keys.dart'; -import '../../common/view/icons.dart'; -import '../../common/view/progress.dart'; -import '../../common/view/snackbars.dart'; -import '../../common/view/tapable_text.dart'; -import '../../common/view/theme.dart'; -import '../../constants.dart'; import '../../extensions/build_context_x.dart'; -import '../../extensions/string_x.dart'; -import '../../extensions/theme_data_x.dart'; -import '../../extensions/theme_mode_x.dart'; import '../../l10n/l10n.dart'; -import '../../local_audio/local_audio_model.dart'; -import '../../podcasts/download_model.dart'; -import '../../podcasts/podcast_model.dart'; -import '../settings_model.dart'; -import 'theme_tile.dart'; +import 'about_section.dart'; +import 'expose_online_section.dart'; +import 'local_audio_section.dart'; +import 'podcast_section.dart'; +import 'theme_section.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @@ -38,18 +18,18 @@ class SettingsPage extends StatelessWidget { children: [ YaruDialogTitleBar( border: BorderSide.none, - backgroundColor: context.theme.dialogBackgroundColor, + backgroundColor: context.theme.scaffoldBackgroundColor, onClose: (p0) => Navigator.of(rootNavigator: true, context).pop(), title: Text(context.l10n.settings), ), Expanded( child: ListView( children: const [ - _ThemeSection(), - _PodcastSection(), - _LocalAudioSection(), - _ExposeOnlineSection(), - _AboutSection(), + ThemeSection(), + PodcastSection(), + LocalAudioSection(), + ExposeOnlineSection(), + AboutSection(), ], ), ), @@ -57,636 +37,3 @@ class SettingsPage extends StatelessWidget { ); } } - -class _ThemeSection extends StatelessWidget with WatchItMixin { - const _ThemeSection(); - - @override - Widget build(BuildContext context) { - final model = di(); - final l10n = context.l10n; - final themeIndex = watchPropertyValue((SettingsModel m) => m.themeIndex); - return YaruSection( - margin: const EdgeInsets.only( - left: kYaruPagePadding, - top: kYaruPagePadding, - right: kYaruPagePadding, - ), - headline: Text(l10n.theme), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Center( - child: Padding( - padding: const EdgeInsets.only(top: kYaruPagePadding), - child: Wrap( - spacing: kYaruPagePadding, - children: [ - for (var i = 0; i < ThemeMode.values.length; ++i) - Column( - mainAxisSize: MainAxisSize.min, - children: [ - YaruSelectableContainer( - padding: const EdgeInsets.all(1), - borderRadius: BorderRadius.circular(15), - selected: themeIndex == i, - onTap: () => model.setThemeIndex(i), - selectionColor: context.theme.colorScheme.primary, - child: ThemeTile(ThemeMode.values[i]), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: - Text(ThemeMode.values[i].localize(context.l10n)), - ), - ], - ), - ], - ), - ), - ), - YaruTile( - title: Text(l10n.useMoreAnimationsTitle), - subtitle: Text(l10n.useMoreAnimationsDescription), - trailing: CommonSwitch( - onChanged: di().setUseMoreAnimations, - value: - watchPropertyValue((SettingsModel m) => m.useMoreAnimations), - ), - ), - ], - ), - ); - } -} - -// TODO: figure out how to show the window from clicking the dock icon in macos, windows and linux -// Also figure out how to show the window again, when the gtk window is triggered from the outside (open with) -// if we can not figure this out, we can not land this feature. -// ignore: unused_element -class _CloseActionSection extends StatelessWidget with WatchItMixin { - const _CloseActionSection(); - - @override - Widget build(BuildContext context) { - final model = di(); - - final closeBtnAction = - watchPropertyValue((SettingsModel m) => m.closeBtnActionIndex); - return YaruSection( - margin: const EdgeInsets.only( - left: kYaruPagePadding, - top: kYaruPagePadding, - right: kYaruPagePadding, - ), - headline: Text(context.l10n.closeBtnAction), - child: Column( - children: [ - YaruTile( - title: Text(context.l10n.whenCloseBtnClicked), - trailing: YaruPopupMenuButton( - icon: const DropDownArrow(), - initialValue: closeBtnAction, - child: Text(closeBtnAction.localize(context.l10n)), - onSelected: (value) { - model.setCloseBtnActionIndex(value); - }, - itemBuilder: (context) { - return [ - for (var i = 0; i < CloseBtnAction.values.length; ++i) - PopupMenuItem( - value: CloseBtnAction.values[i], - child: - Text(CloseBtnAction.values[i].localize(context.l10n)), - ), - ]; - }, - ), - ), - ], - ), - ); - } -} - -class _PodcastSection extends StatefulWidget with WatchItStatefulWidgetMixin { - const _PodcastSection(); - - @override - State<_PodcastSection> createState() => _PodcastSectionState(); -} - -class _PodcastSectionState extends State<_PodcastSection> { - String? _initialKey; - String? _initialSecret; - late TextEditingController _keyController, _secretController; - - @override - void initState() { - super.initState(); - final model = di(); - _initialKey = model.podcastIndexApiKey; - _keyController = TextEditingController(text: _initialKey); - _initialSecret = model.podcastIndexApiSecret; - _secretController = TextEditingController(text: _initialSecret); - } - - @override - void dispose() { - _keyController.dispose(); - _secretController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = context.theme; - final l10n = context.l10n; - final model = di(); - final usePodcastIndex = - watchPropertyValue((SettingsModel m) => m.usePodcastIndex); - final podcastIndexApiKey = - watchPropertyValue((SettingsModel m) => m.podcastIndexApiKey); - final podcastIndexApiSecret = - watchPropertyValue((SettingsModel m) => m.podcastIndexApiSecret); - - return YaruSection( - margin: const EdgeInsets.all(kYaruPagePadding), - headline: Text(l10n.podcasts), - child: Column( - children: [ - const _DownloadsTile(), - YaruTile( - title: Text(l10n.usePodcastIndex), - trailing: CommonSwitch( - value: usePodcastIndex, - onChanged: (v) async { - await model.setUsePodcastIndex(v); - if (context.mounted) { - di().init( - forceInit: true, - updateMessage: l10n.newEpisodeAvailable, - ); - } - }, - ), - ), - if (usePodcastIndex) - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: _keyController, - onChanged: (v) => setState(() => _initialKey = v), - obscureText: true, - decoration: InputDecoration( - label: Text(kPodcastIndexApiKey.camelToSentence), - suffixIcon: IconButton( - tooltip: l10n.save, - onPressed: () => - model.setPodcastIndexApiKey(_keyController.text), - icon: Icon( - Iconz.check, - color: podcastIndexApiKey == _initialKey - ? theme.colorScheme.success - : theme.colorScheme.onSurface, - ), - ), - ), - ), - ), - if (usePodcastIndex) - Padding( - padding: const EdgeInsets.only( - left: 8, - right: 8, - bottom: kYaruPagePadding, - ), - child: TextField( - controller: _secretController, - onChanged: (v) => setState(() => _initialSecret = v), - obscureText: true, - decoration: InputDecoration( - label: Text(kPodcastIndexApiSecret.camelToSentence), - suffixIcon: IconButton( - tooltip: l10n.save, - onPressed: () => - model.setPodcastIndexApiSecret(_secretController.text), - icon: Icon( - Iconz.check, - color: podcastIndexApiSecret == _initialSecret - ? theme.colorScheme.success - : theme.colorScheme.onSurface, - ), - ), - ), - ), - ), - ], - ), - ); - } -} - -class _DownloadsTile extends StatefulWidget with WatchItStatefulWidgetMixin { - const _DownloadsTile(); - - @override - State<_DownloadsTile> createState() => _DownloadsTileState(); -} - -class _DownloadsTileState extends State<_DownloadsTile> { - String? _error; - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return YaruTile( - title: Text(l10n.downloadsDirectory), - subtitle: Text( - _error ?? watchPropertyValue((SettingsModel m) => m.downloadsDir ?? ''), - ), - trailing: ImportantButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - content: SizedBox( - width: 300, - child: Text( - l10n.downloadsChangeWarning, - style: context.textTheme.bodyLarge, - ), - ), - actions: [ - OutlinedButton( - onPressed: Navigator.of(context).pop, - child: Text(l10n.cancel), - ), - ImportantButton( - onPressed: () { - Navigator.of(context).pop(); - di().setDownloadsCustomDir( - onSuccess: () => di().deleteAllDownloads(), - onFail: (e) => setState(() => _error = e.toString()), - ); - }, - child: Text(l10n.ok), - ), - ], - ), - ); - }, - child: Text( - l10n.select, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - ); - } -} - -class _LocalAudioSection extends StatelessWidget with WatchItMixin { - const _LocalAudioSection(); - - @override - Widget build(BuildContext context) { - final settingsModel = di(); - final localAudioModel = di(); - final directory = - watchPropertyValue((SettingsModel m) => m.directory ?? ''); - - Future onDirectorySelected(String directoryPath) async { - settingsModel.setDirectory(directoryPath).then( - (_) async => localAudioModel.init( - forceInit: true, - directory: directoryPath, - ), - ); - } - - return YaruSection( - headline: Text(context.l10n.localAudio), - margin: const EdgeInsets.symmetric(horizontal: kYaruPagePadding), - child: Column( - children: [ - YaruTile( - title: Text(context.l10n.musicCollectionLocation), - subtitle: Text(directory), - trailing: ImportantButton( - onPressed: () async { - final directoryPath = await settingsModel.getPathOfDirectory(); - if (directoryPath != null) { - await onDirectorySelected(directoryPath); - } - }, - child: Text( - context.l10n.select, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ), - ); - } -} - -class _AboutSection extends StatelessWidget with WatchItMixin { - const _AboutSection(); - - @override - Widget build(BuildContext context) { - final text = '${context.l10n.about} $kAppTitle'; - return YaruSection( - headline: Text(text), - margin: const EdgeInsets.all(kYaruPagePadding), - child: const Column( - children: [_AboutTile(), _LicenseTile()], - ), - ); - } -} - -class _AboutTile extends StatefulWidget with WatchItStatefulWidgetMixin { - const _AboutTile(); - - @override - State<_AboutTile> createState() => _AboutTileState(); -} - -class _AboutTileState extends State<_AboutTile> { - @override - void initState() { - super.initState(); - di().checkForUpdate( - isOnline: di().isOnline == true, - onError: (e) { - if (mounted) { - showSnackBar(context: context, content: Text(e)); - } - }, - ); - } - - @override - Widget build(BuildContext context) { - final theme = context.theme; - final appModel = di(); - final updateAvailable = - watchPropertyValue((AppModel m) => m.updateAvailable); - final onlineVersion = watchPropertyValue((AppModel m) => m.onlineVersion); - final currentVersion = watchPropertyValue((AppModel m) => m.version); - - return YaruTile( - title: !di().isOnline == true || - !appModel.allowManualUpdate - ? Text(di().version ?? '') - : updateAvailable == null - ? Center( - child: SizedBox.square( - dimension: yaruStyled ? kYaruTitleBarItemHeight : 40, - child: const Progress( - padding: EdgeInsets.all(10), - ), - ), - ) - : TapAbleText( - text: updateAvailable == true - ? '${context.l10n.updateAvailable}: $onlineVersion' - : currentVersion ?? context.l10n.unknown, - style: updateAvailable == true - ? TextStyle( - color: context.theme.colorScheme.success - .scale(lightness: theme.isLight ? 0 : 0.3), - ) - : null, - onTap: () => launchUrl( - Uri.parse( - p.join( - kRepoUrl, - 'releases', - 'tag', - onlineVersion, - ), - ), - ), - ), - trailing: OutlinedButton( - onPressed: () => settingsNavigatorKey.currentState?.pushNamed('/about'), - child: Text(context.l10n.contributors), - ), - ); - } -} - -class _LicenseTile extends StatelessWidget { - const _LicenseTile(); - - @override - Widget build(BuildContext context) { - return YaruTile( - title: TapAbleText( - text: '${context.l10n.license}: GPL3', - ), - trailing: OutlinedButton( - onPressed: () => - settingsNavigatorKey.currentState?.pushNamed('/licenses'), - child: Text(context.l10n.dependencies), - ), - enabled: true, - ); - } -} - -class _ExposeOnlineSection extends StatefulWidget - with WatchItStatefulWidgetMixin { - const _ExposeOnlineSection(); - - @override - State<_ExposeOnlineSection> createState() => _ExposeOnlineSectionState(); -} - -class _ExposeOnlineSectionState extends State<_ExposeOnlineSection> { - late TextEditingController _lastFmApiKeyController; - late TextEditingController _lastFmSecretController; - final _formkey = GlobalKey(); - - @override - void initState() { - final model = di(); - _lastFmApiKeyController = TextEditingController(text: model.lastFmApiKey); - _lastFmSecretController = TextEditingController(text: model.lastFmSecret); - - super.initState(); - } - - @override - void dispose() { - _lastFmApiKeyController.dispose(); - _lastFmSecretController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - final discordEnabled = allowDiscordRPC - ? watchPropertyValue((SettingsModel m) => m.enableDiscordRPC) - : false; - - final lastFmEnabled = - watchPropertyValue((SettingsModel m) => m.enableLastFmScrobbling); - - return YaruSection( - headline: Text(l10n.exposeOnlineHeadline), - margin: const EdgeInsets.only( - top: kYaruPagePadding, - right: kYaruPagePadding, - left: kYaruPagePadding, - ), - child: Column( - children: [ - YaruTile( - title: Row( - children: space( - children: [ - allowDiscordRPC - ? const Icon( - TablerIcons.brand_discord_filled, - ) - : Icon( - TablerIcons.brand_discord_filled, - color: context.theme.disabledColor, - ), - Text(l10n.exposeToDiscordTitle), - ], - ), - ), - subtitle: Text( - allowDiscordRPC - ? l10n.exposeToDiscordSubTitle - : l10n.featureDisabledOnPlatform, - ), - trailing: CommonSwitch( - value: discordEnabled, - onChanged: allowDiscordRPC - ? (v) { - di().setEnableDiscordRPC(v); - final appModel = di(); - if (v) { - appModel.connectToDiscord(); - } else { - appModel.disconnectFromDiscord(); - } - } - : null, - ), - ), - YaruTile( - title: Row( - children: space( - children: [ - const Icon( - TablerIcons.brand_lastfm, - ), - if (lastFmEnabled && - watchValue((AppModel m) => m.isLastFmAuthorized)) - Text(l10n.connectedTo), - Text(l10n.exposeToLastfmTitle), - ], - ), - ), - subtitle: Column( - children: [ - Text(l10n.exposeToLastfmSubTitle), - ], - ), - trailing: CommonSwitch( - value: lastFmEnabled, - onChanged: (v) { - di().setEnableLastFmScrobbling(v); - }, - ), - ), - if (lastFmEnabled) ...[ - Padding( - padding: const EdgeInsets.all(8), - child: Form( - key: _formkey, - onChanged: _formkey.currentState?.validate, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: space( - heightGap: 10, - children: [ - TextFormField( - obscureText: true, - controller: _lastFmApiKeyController, - decoration: InputDecoration( - hintText: l10n.lastfmApiKey, - label: Text(l10n.lastfmApiKey), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return l10n.lastfmApiKeyEmpty; - } - return null; - }, - onChanged: (_) => _formkey.currentState?.validate(), - onFieldSubmitted: (value) async { - if (_formkey.currentState!.validate()) { - di().setLastFmApiKey(value); - } - }, - ), - TextFormField( - obscureText: true, - controller: _lastFmSecretController, - decoration: InputDecoration( - hintText: l10n.lastfmSecret, - label: Text(l10n.lastfmSecret), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return l10n.lastfmSecretEmpty; - } - return null; - }, - onChanged: (_) => _formkey.currentState?.validate(), - onFieldSubmitted: (value) async { - if (_formkey.currentState!.validate()) { - di().setLastFmSecret(value); - } - }, - ), - ImportantButton( - onPressed: () { - di() - ..setLastFmApiKey( - _lastFmApiKeyController.text, - ) - ..setLastFmSecret( - _lastFmSecretController.text, - ); - di().authorizeLastFm( - apiKey: _lastFmApiKeyController.text, - apiSecret: _lastFmSecretController.text, - ); - }, - child: Text(l10n.saveAndAuthorize), - ), - ], - ), - ), - ), - ), - ], - ], - ), - ); - } -} diff --git a/lib/settings/view/settings_tile.dart b/lib/settings/view/settings_tile.dart index 901d98ead..2b23436bf 100644 --- a/lib/settings/view/settings_tile.dart +++ b/lib/settings/view/settings_tile.dart @@ -6,9 +6,9 @@ import 'package:yaru/yaru.dart'; import '../../app/app_model.dart'; import '../../app/connectivity_model.dart'; +import '../../app_config.dart'; import '../../common/view/icons.dart'; import '../../common/view/progress.dart'; -import '../../common/view/theme.dart'; import '../../constants.dart'; import '../../extensions/build_context_x.dart'; import '../../l10n/l10n.dart'; diff --git a/lib/settings/view/theme_section.dart b/lib/settings/view/theme_section.dart new file mode 100644 index 000000000..3940b47c1 --- /dev/null +++ b/lib/settings/view/theme_section.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; + +import '../../common/view/common_widgets.dart'; +import '../../extensions/build_context_x.dart'; +import '../../extensions/theme_mode_x.dart'; +import '../../l10n/l10n.dart'; +import '../settings_model.dart'; +import 'theme_tile.dart'; + +class ThemeSection extends StatelessWidget with WatchItMixin { + const ThemeSection({super.key}); + + @override + Widget build(BuildContext context) { + final model = di(); + final l10n = context.l10n; + final themeIndex = watchPropertyValue((SettingsModel m) => m.themeIndex); + return YaruSection( + margin: const EdgeInsets.only( + left: kYaruPagePadding, + top: kYaruPagePadding, + right: kYaruPagePadding, + ), + headline: Text(l10n.theme), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.only(top: kYaruPagePadding), + child: Wrap( + spacing: kYaruPagePadding, + children: [ + for (var i = 0; i < ThemeMode.values.length; ++i) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + YaruSelectableContainer( + padding: const EdgeInsets.all(1), + borderRadius: BorderRadius.circular(15), + selected: themeIndex == i, + onTap: () => model.setThemeIndex(i), + selectionColor: context.theme.colorScheme.primary, + child: ThemeTile(ThemeMode.values[i]), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: + Text(ThemeMode.values[i].localize(context.l10n)), + ), + ], + ), + ], + ), + ), + ), + YaruTile( + title: Text(l10n.useMoreAnimationsTitle), + subtitle: Text(l10n.useMoreAnimationsDescription), + trailing: CommonSwitch( + onChanged: di().setUseMoreAnimations, + value: + watchPropertyValue((SettingsModel m) => m.useMoreAnimations), + ), + ), + ], + ), + ); + } +}