From ad25181d6763169c70eea21d5b7584c733225702 Mon Sep 17 00:00:00 2001 From: Paul Fauchon Date: Mon, 28 Aug 2023 18:15:33 +0800 Subject: [PATCH 1/2] replace easy to implement libraries with own implementation to reduce dependency hell show error symbol when playback fails update to flutter 3.13.1 --- assets/icon.svg | 2 +- lib/app/views/tv/screens/tv_home.dart | 50 +++- lib/home/views/components/home.dart | 4 +- lib/main.dart | 4 +- lib/myRouteObserver.dart | 20 -- lib/player/states/audio_player.dart | 6 +- lib/player/states/player_controls.dart | 15 +- lib/player/states/player_controls.g.dart | 11 + .../views/components/player_controls.dart | 260 +++++++++--------- lib/settings/views/screens/settings.dart | 73 +++-- .../views/screens/video_filter_setup.dart | 32 ++- lib/utils/states/select_list.dart | 38 +++ lib/utils/states/select_list.g.dart | 69 +++++ lib/utils/views/components/app_icon.dart | 18 ++ .../views/components/select_list_dialog.dart | 89 ++++++ .../views/screens/welcome_wizard.dart | 4 +- pubspec.lock | 64 +++-- pubspec.yaml | 8 +- submodules/flutter | 2 +- 19 files changed, 540 insertions(+), 229 deletions(-) create mode 100644 lib/utils/states/select_list.dart create mode 100644 lib/utils/states/select_list.g.dart create mode 100644 lib/utils/views/components/app_icon.dart create mode 100644 lib/utils/views/components/select_list_dialog.dart diff --git a/assets/icon.svg b/assets/icon.svg index 50742bb5..bcbd6ca6 100644 --- a/assets/icon.svg +++ b/assets/icon.svg @@ -81,7 +81,7 @@ inkscape:label="Layer 2" style="display:inline"> (builder: (context, expandMenu) { var cubit = context.read(); return BlocBuilder(buildWhen: (previous, current) { - return previous.server != current.server; + return previous.server != current.server; }, builder: (context, _) { var app = context.read(); return DefaultTextStyle( @@ -100,13 +101,17 @@ class TvHome extends StatelessWidget { height: 50, child: Row( children: [ - const AppIconImage(), - Visibility( - visible: expandMenu, - child: Text( - 'Clipious', - style: textTheme.titleLarge!.copyWith(color: colors.primary), - )) + const AppIcon( + width: 50, + height: 50, + ), + if (expandMenu) + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: MenuItemText( + 'Clipious', + style: textTheme.titleLarge!.copyWith(color: colors.primary), + )) ], )), ), @@ -124,7 +129,7 @@ class TvHome extends StatelessWidget { padding: EdgeInsets.only(right: 8.0), child: Icon(Icons.search), ), - expandMenu ? Text(locals.search) : const SizedBox.shrink() + if (expandMenu) MenuItemText(locals.search) ], ), ), @@ -146,7 +151,7 @@ class TvHome extends StatelessWidget { padding: EdgeInsets.only(right: 8.0), child: Icon(Icons.subscriptions), ), - expandMenu ? Text(locals.subscriptions) : const SizedBox.shrink() + if (expandMenu) MenuItemText(locals.subscriptions) ], ), ), @@ -169,7 +174,7 @@ class TvHome extends StatelessWidget { padding: EdgeInsets.only(right: 8.0), child: Icon(Icons.playlist_play), ), - expandMenu ? Text(locals.playlists) : const SizedBox.shrink() + if (expandMenu) MenuItemText(locals.playlists) ], ), ), @@ -190,7 +195,7 @@ class TvHome extends StatelessWidget { padding: EdgeInsets.only(right: 8.0), child: Icon(Icons.local_fire_department), ), - expandMenu ? Text(locals.popular) : const SizedBox.shrink() + if (expandMenu) MenuItemText(locals.popular) ], ), ), @@ -210,7 +215,7 @@ class TvHome extends StatelessWidget { padding: EdgeInsets.only(right: 8.0), child: Icon(Icons.trending_up), ), - expandMenu ? Text(locals.trending) : const SizedBox.shrink() + if (expandMenu) MenuItemText(locals.trending) ], ), ), @@ -228,7 +233,7 @@ class TvHome extends StatelessWidget { padding: EdgeInsets.only(right: 8.0), child: Icon(Icons.settings), ), - expandMenu ? Text(locals.settings) : const SizedBox.shrink() + if (expandMenu) MenuItemText(locals.settings) ], ), ), @@ -278,3 +283,18 @@ class TvHome extends StatelessWidget { ); } } + +class MenuItemText extends StatelessWidget { + final String text; + final TextStyle? style; + + const MenuItemText(this.text, {super.key, this.style}); + + @override + Widget build(BuildContext context) { + return Text( + text, + style: style, + ).animate().fadeIn(delay: animationDuration ~/ 2, duration: animationDuration).slideX(curve: Curves.easeInOutQuad); + } +} diff --git a/lib/home/views/components/home.dart b/lib/home/views/components/home.dart index 2984bf2a..83b68d04 100644 --- a/lib/home/views/components/home.dart +++ b/lib/home/views/components/home.dart @@ -1,4 +1,3 @@ -import 'package:application_icon/application_icon.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -9,6 +8,7 @@ import 'package:invidious/home/states/home.dart'; import '../../../main.dart'; import '../../../search/views/screens/search.dart'; +import '../../../utils/views/components/app_icon.dart'; const double smallVideoViewHeight = 140; @@ -78,7 +78,7 @@ class HomeView extends StatelessWidget { padding: const EdgeInsets.only(left: innerHorizontalPadding), color: colors.background, child: layout.smallSources.isEmpty && !layout.showBigSource - ? const Opacity(opacity: 0.2, child: AppIconImage()) + ? const Opacity(opacity: 0.2, child: AppIcon()) : Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/main.dart b/lib/main.dart index 6314c5a8..74b3ccb0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:after_layout/after_layout.dart'; -import 'package:application_icon/application_icon.dart'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; @@ -28,6 +27,7 @@ import 'package:invidious/settings/states/settings.dart'; import 'package:invidious/settings/views/screens/settings.dart'; import 'package:invidious/subscription_management/view/screens/manage_subscriptions.dart'; import 'package:invidious/utils.dart'; +import 'package:invidious/utils/views/components/app_icon.dart'; import 'package:invidious/videos/views/screens/video.dart'; import 'package:invidious/welcome_wizard/views/screens/welcome_wizard.dart'; import 'package:invidious/welcome_wizard/views/tv/screens/welcome_wizard.dart'; @@ -369,7 +369,7 @@ class _HomeState extends State with AfterLayoutMixin { // home handles its own padding because we don't want to cut horizontal scroll lists on the right padding: EdgeInsets.symmetric(horizontal: selectedPage == HomeDataSource.home ? 0 : innerHorizontalPadding), key: ValueKey(selectedPage), - child: selectedPage?.build(context, false) ?? const Opacity(opacity: 0.2, child: AppIconImage())), + child: selectedPage?.build(context, false) ?? const Opacity(opacity: 0.2, child: AppIcon())), /* child: [ const HomeView( diff --git a/lib/myRouteObserver.dart b/lib/myRouteObserver.dart index 6a2379b3..c7abd0ec 100644 --- a/lib/myRouteObserver.dart +++ b/lib/myRouteObserver.dart @@ -26,26 +26,6 @@ class MyRouteObserver extends RouteObserver> { stopPlayingOnPop(PageRoute? newRoute, PageRoute? poppedRoute) { newRoute?.navigator?.context.read().showMiniPlayer(); - if (newRoute != null) { - switch (newRoute.settings) { - case ROUTE_SETTINGS: - case ROUTE_PLAYLIST: - case ROUTE_SETTINGS_MANAGE_SERVERS: - case ROUTE_SETTINGS_MANAGE_ONE_SERVER: - case ROUTE_SETTINGS_SPONSOR_BLOCK: - case ROUTE_SETTINGS_VIDEO_FILTERS: - case ROUTE_VIDEO: - case ROUTE_PLAYLIST_LIST: - case ROUTE_CHANNEL: - case ROUTE_MANAGE_SUBSCRIPTIONS: - log.fine('We should stop playing video'); - // MiniPlayerController.to()?.showMiniPlayer(); - break; - default: - log.fine('keep playing video'); - break; - } - } } @override diff --git a/lib/player/states/audio_player.dart b/lib/player/states/audio_player.dart index 7b01c6a3..df8a9045 100644 --- a/lib/player/states/audio_player.dart +++ b/lib/player/states/audio_player.dart @@ -43,7 +43,10 @@ class AudioPlayerCubit extends MediaPlayerCubit { initPlayer() { if (state.player == null) { state.player = AudioPlayer(); - state.player?.playerStateStream.listen(onStateStreamChange); + state.player?.playerStateStream.listen(onStateStreamChange, onError: (e, st) { + print('ERRRRRROOOORR'); + return globalPlayer.setEvent(MediaEvent(state: MediaState.error)); + }); state.player?.positionStream.listen(onPositionChanged); state.player?.durationStream.listen(onDurationChanged); } @@ -129,6 +132,7 @@ class AudioPlayerCubit extends MediaPlayerCubit { } } catch (e) { log.severe("Couldn't play video", e); + globalPlayer.setEvent(MediaEvent(state: MediaState.error)); state.error = e.toString(); state.loading = false; if (!isClosed) emit(state); diff --git a/lib/player/states/player_controls.dart b/lib/player/states/player_controls.dart index e1900552..365c186b 100644 --- a/lib/player/states/player_controls.dart +++ b/lib/player/states/player_controls.dart @@ -40,6 +40,10 @@ class PlayerControlsCubit extends Cubit { hideControls(); state = this.state.copyWith(); break; + case MediaState.error: + hideControls(); + state = this.state.copyWith(); + state.errored = true; default: break; } @@ -47,6 +51,7 @@ class PlayerControlsCubit extends Cubit { switch (event.type) { case MediaEventType.progress: state.audioPosition = event.value; + state.errored = false; break; case MediaEventType.seek: showControls(); @@ -63,7 +68,7 @@ class PlayerControlsCubit extends Cubit { void hideControls() { var state = this.state.copyWith(); state.displayControls = false; - if(!isClosed) { + if (!isClosed) { emit(state); } } @@ -100,16 +105,22 @@ class PlayerControlsCubit extends Cubit { void setPlaybackSpeed(double d) { player.setSpeed(d); } + + void removeError() { + emit(state.copyWith(errored: false)); + } } @CopyWith(constructor: "_") class PlayerControlsState { PlayerControlsState(); + bool errored = false; + MediaEvent event = MediaEvent(state: MediaState.idle); Duration audioPosition = Duration.zero; bool displayControls = false; - PlayerControlsState._(this.event, this.audioPosition, this.displayControls); + PlayerControlsState._(this.event, this.audioPosition, this.displayControls, this.errored); } diff --git a/lib/player/states/player_controls.g.dart b/lib/player/states/player_controls.g.dart index 4bdcbfcc..0aaf6af6 100644 --- a/lib/player/states/player_controls.g.dart +++ b/lib/player/states/player_controls.g.dart @@ -13,6 +13,8 @@ abstract class _$PlayerControlsStateCWProxy { PlayerControlsState displayControls(bool displayControls); + PlayerControlsState errored(bool errored); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `PlayerControlsState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -23,6 +25,7 @@ abstract class _$PlayerControlsStateCWProxy { MediaEvent? event, Duration? audioPosition, bool? displayControls, + bool? errored, }); } @@ -43,6 +46,9 @@ class _$PlayerControlsStateCWProxyImpl implements _$PlayerControlsStateCWProxy { PlayerControlsState displayControls(bool displayControls) => this(displayControls: displayControls); + @override + PlayerControlsState errored(bool errored) => this(errored: errored); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `PlayerControlsState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. @@ -55,6 +61,7 @@ class _$PlayerControlsStateCWProxyImpl implements _$PlayerControlsStateCWProxy { Object? event = const $CopyWithPlaceholder(), Object? audioPosition = const $CopyWithPlaceholder(), Object? displayControls = const $CopyWithPlaceholder(), + Object? errored = const $CopyWithPlaceholder(), }) { return PlayerControlsState._( event == const $CopyWithPlaceholder() || event == null @@ -69,6 +76,10 @@ class _$PlayerControlsStateCWProxyImpl implements _$PlayerControlsStateCWProxy { ? _value.displayControls // ignore: cast_nullable_to_non_nullable : displayControls as bool, + errored == const $CopyWithPlaceholder() || errored == null + ? _value.errored + // ignore: cast_nullable_to_non_nullable + : errored as bool, ); } } diff --git a/lib/player/views/components/player_controls.dart b/lib/player/views/components/player_controls.dart index 06a48826..71ca93be 100644 --- a/lib/player/views/components/player_controls.dart +++ b/lib/player/views/components/player_controls.dart @@ -208,142 +208,152 @@ class PlayerControls extends StatelessWidget { onVerticalDragEnd: pc.isFullScreen() == FullScreenState.fullScreen ? null : player.videoDraggedEnd, onVerticalDragUpdate: pc.isFullScreen() == FullScreenState.fullScreen ? null : player.videoDragged, onVerticalDragStart: pc.isFullScreen() == FullScreenState.fullScreen ? null : player.videoDragStarted, - child: Padding( - padding: EdgeInsets.all(isMini ? 8 : 0.0), - child: AspectRatio( - aspectRatio: 16 / 9, - child: Stack( - alignment: Alignment.topCenter, - children: [ - Positioned( - left: 0, - right: 0, - bottom: 0, - top: 0, - child: isMini || isPip - ? const SizedBox.shrink() - : _.displayControls - ? Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(0), color: Colors.black.withOpacity(0.4)), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (pc.isFullScreen() == FullScreenState.fullScreen) + child: AspectRatio( + aspectRatio: 16 / 9, + child: Stack( + alignment: Alignment.topCenter, + children: [ + if (_.errored) + Container( + color: Colors.black.withOpacity(0.8), + child: const Center( + child: Icon(Icons.error), + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + top: 0, + child: isMini || isPip + ? const SizedBox.shrink() + : _.displayControls + ? Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(0), color: Colors.black.withOpacity(0.4)), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (pc.isFullScreen() == FullScreenState.fullScreen) + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + videoTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + )), + if (pc.supportsPip()) IconButton(onPressed: pc.enterPip, icon: const Icon(Icons.picture_in_picture)), + IconButton(onPressed: () => showOptionMenu(context, _, pc), icon: const Icon(Icons.more_vert)) + ], + ), + Expanded(child: Container()), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + pc.isMuted() + ? IconButton(onPressed: () => pc.toggleVolume(true), icon: const Icon(Icons.volume_off)) + : IconButton(onPressed: () => pc.toggleVolume(false), icon: const Icon(Icons.volume_up)), + switch (pc.isFullScreen()) { + FullScreenState.fullScreen => IconButton(onPressed: () => pc.setFullScreen((false)), icon: const Icon(Icons.fullscreen_exit)), + FullScreenState.notFullScreen => IconButton(onPressed: () => pc.setFullScreen(true), icon: const Icon(Icons.fullscreen)), + _ => const SizedBox.shrink() + } + ], + ), + if (!(player.state.currentlyPlaying?.liveNow ?? false)) + Padding( + padding: const EdgeInsets.only(top: 0.0, right: 8), + child: Row( + children: [ Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - videoTitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - )), - if (pc.supportsPip()) IconButton(onPressed: pc.enterPip, icon: const Icon(Icons.picture_in_picture)), - IconButton(onPressed: () => showOptionMenu(context, _, pc), icon: const Icon(Icons.more_vert)) - ], - ), - Expanded(child: Container()), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - pc.isMuted() - ? IconButton(onPressed: () => pc.toggleVolume(true), icon: const Icon(Icons.volume_off)) - : IconButton(onPressed: () => pc.toggleVolume(false), icon: const Icon(Icons.volume_up)), - switch (pc.isFullScreen()) { - FullScreenState.fullScreen => IconButton(onPressed: () => pc.setFullScreen((false)), icon: const Icon(Icons.fullscreen_exit)), - FullScreenState.notFullScreen => IconButton(onPressed: () => pc.setFullScreen(true), icon: const Icon(Icons.fullscreen)), - _ => const SizedBox.shrink() - } - ], - ), - if (!(player.state.currentlyPlaying?.liveNow ?? false)) - Padding( - padding: const EdgeInsets.only(top: 0.0, right: 8), - child: Row( - children: [ - Expanded( - child: SizedBox( - height: 25, - child: Slider( - min: 0, - value: min(_.audioPosition.inMilliseconds.toDouble(), pc.duration().inMilliseconds.toDouble()), - max: pc.duration().inMilliseconds.toDouble(), - secondaryTrackValue: min(pc.bufferedPosition()?.inMilliseconds.toDouble() ?? 0, pc.duration().inMilliseconds.toDouble()), - onChangeEnd: cubit.onScrubbed, - onChanged: cubit.onScrubDrag, - ), + child: SizedBox( + height: 25, + child: Slider( + min: 0, + value: min(_.audioPosition.inMilliseconds.toDouble(), pc.duration().inMilliseconds.toDouble()), + max: pc.duration().inMilliseconds.toDouble(), + secondaryTrackValue: min(pc.bufferedPosition()?.inMilliseconds.toDouble() ?? 0, pc.duration().inMilliseconds.toDouble()), + onChangeEnd: cubit.onScrubbed, + onChanged: cubit.onScrubDrag, ), ), - Text( - '${prettyDuration(pc.position())} / ${prettyDuration(pc.duration())}', - style: textTheme.bodySmall?.copyWith(color: Colors.white), - ), - ], - ), + ), + Text( + '${prettyDuration(pc.position())} / ${prettyDuration(pc.duration())}', + style: textTheme.bodySmall?.copyWith(color: Colors.white), + ), + ], ), - ], - ), - ) - : const SizedBox.expand(), - ), - if (!isMini && !isPip && _.displayControls) - Positioned( - top: 0, - left: 0, - right: 0, - bottom: 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (hasQueue) - IconButton( - onPressed: () => player.playPrevious(), - icon: const Icon( - Icons.skip_previous, - size: 20, - )), + ), + ], + ), + ) + : const SizedBox.expand(), + ), + if (!isMini && !isPip && _.displayControls) + Positioned( + top: 0, + left: 0, + right: 0, + bottom: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (hasQueue) IconButton( - onPressed: () => player.rewind(), + onPressed: () { + player.playPrevious(); + cubit.removeError(); + }, icon: const Icon( - Icons.fast_rewind, - size: 30, + Icons.skip_previous, + size: 20, )), + IconButton( + onPressed: () => player.rewind(), + icon: const Icon( + Icons.fast_rewind, + size: 30, + )), + IconButton( + onPressed: () => player.state.isPlaying ? player.pause() : player.play(), + icon: Icon(player.state.isPlaying ? Icons.pause : Icons.play_arrow, size: 55), + ), + IconButton( + onPressed: () => player.fastForward(), + icon: const Icon( + Icons.fast_forward, + size: 30, + )), + if (hasQueue) IconButton( - onPressed: () => player.state.isPlaying ? player.pause() : player.play(), - icon: Icon(player.state.isPlaying ? Icons.pause : Icons.play_arrow, size: 55), - ), - IconButton( - onPressed: () => player.fastForward(), + onPressed: () { + player.playNext(); + cubit.removeError(); + }, icon: const Icon( - Icons.fast_forward, - size: 30, + Icons.skip_next, + size: 20, )), - if (hasQueue) - IconButton( - onPressed: () => player.playNext(), - icon: const Icon( - Icons.skip_next, - size: 20, - )), - ], - ), + ], ), - if (event.state == MediaState.buffering) - const Center( - child: FractionallySizedBox( - heightFactor: 0.3, - child: AspectRatio( - aspectRatio: 1, - child: CircularProgressIndicator( - strokeWidth: 2, - )), - ), - ) - ], - ), + ), + if (event.state == MediaState.buffering) + const Center( + child: FractionallySizedBox( + heightFactor: 0.3, + child: AspectRatio( + aspectRatio: 1, + child: CircularProgressIndicator( + strokeWidth: 2, + )), + ), + ) + ], ), ), ), diff --git a/lib/settings/views/screens/settings.dart b/lib/settings/views/screens/settings.dart index bcdf475f..6d980a10 100644 --- a/lib/settings/views/screens/settings.dart +++ b/lib/settings/views/screens/settings.dart @@ -1,4 +1,3 @@ -import 'package:application_icon/application_icon.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -9,8 +8,9 @@ import 'package:invidious/settings/views/screens/app_logs.dart'; import 'package:invidious/settings/views/screens/search_history_settings.dart'; import 'package:invidious/settings/views/screens/sponsor_block_settings.dart'; import 'package:invidious/settings/views/screens/video_filter.dart'; +import 'package:invidious/utils/views/components/app_icon.dart'; +import 'package:invidious/utils/views/components/select_list_dialog.dart'; import 'package:locale_names/locale_names.dart'; -import 'package:select_dialog/select_dialog.dart'; import 'package:settings_ui/settings_ui.dart'; import '../../../globals.dart'; @@ -50,17 +50,20 @@ class Settings extends StatelessWidget { } searchCountry(BuildContext context, SettingsState controller) { - var locals = AppLocalizations.of(context); + var locals = AppLocalizations.of(context)!; var cubit = context.read(); - SelectDialog.showModal( - context, - label: locals?.selectBrowsingCountry, - selectedValue: controller.country.name, - items: countryCodes.map((e) => e.name).toList(), - onChange: (String selected) { - cubit.selectCountry(selected); - }, - ); + var colors = Theme.of(context).colorScheme; + + SelectList.show(context, + values: countryCodes.map((e) => e.name).toList(), + value: controller.country.name, + searchFilter: (filter, value) => value.toLowerCase().contains(filter.toLowerCase()), + itemBuilder: (value, selected) => Text( + value, + style: TextStyle(color: selected ? colors.primary : null), + ), + onSelect: cubit.selectCountry, + title: locals.selectBrowsingCountry); } String getNavigationLabelText(BuildContext context, NavigationDestinationLabelBehavior behavior) { @@ -100,21 +103,16 @@ class Settings extends StatelessWidget { var settings = context.read(); var colors = Theme.of(context).colorScheme; var textTheme = Theme.of(context).textTheme; - SelectDialog.showModal( - context, - label: locals.navigationBarStyle, - selectedValue: settings.state.navigationBarLabelBehavior, - itemBuilder: (context, item, isSelected) => Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - getNavigationLabelText(context, item), - style: textTheme.bodyLarge?.copyWith(color: isSelected ? colors.primary : null), - ), - ), - showSearchBox: false, - items: NavigationDestinationLabelBehavior.values, - onChange: (p0) => settings.setNavigationBarLabelBehavior(p0), - ); + + SelectList.show(context, + values: NavigationDestinationLabelBehavior.values, + value: settings.state.navigationBarLabelBehavior, + itemBuilder: (value, selected) => Text( + getNavigationLabelText(context, value), + style: textTheme.bodyLarge?.copyWith(color: selected ? colors.primary : null), + ), + onSelect: settings.setNavigationBarLabelBehavior, + title: locals.navigationBarStyle); } showSelectLanguage(BuildContext context, SettingsState controller) { @@ -122,10 +120,28 @@ class Settings extends StatelessWidget { var localsStrings = localsList.map((e) => e.nativeDisplayLanguageScript ?? '').toList(); var locals = AppLocalizations.of(context)!; var cubit = context.read(); + var colors = Theme.of(context).colorScheme; List? localeString = controller.locale?.split('_'); Locale? selected = localeString != null ? Locale.fromSubtags(languageCode: localeString[0], scriptCode: localeString.length >= 2 ? localeString[1] : null) : null; + SelectList.show(context, + values: [locals.followSystem, ...localsStrings], + value: selected?.nativeDisplayLanguageScript ?? locals.followSystem, + itemBuilder: (value, selected) => Text( + value, + style: TextStyle(color: selected ? colors.primary : null), + ), + onSelect: (value) { + if (value == locals.followSystem) { + cubit.setLocale(localsList, localsStrings, null); + } else { + cubit.setLocale(localsList, localsStrings, value); + } + }, + title: locals.appLanguage); + +/* SelectDialog.showModal( context, label: locals.appLanguage, @@ -140,6 +156,7 @@ class Settings extends StatelessWidget { } }, ); +*/ } List getCategories(BuildContext context) { @@ -333,7 +350,7 @@ class Settings extends StatelessWidget { ], ), SettingsSection(title: (Text(locals.about)), tiles: [ - SettingsTile(title: const Center(child: SizedBox(height: 150, width: 150, child: AppIconImage()))), + SettingsTile(title: const Center(child: SizedBox(height: 150, width: 150, child: AppIcon()))), SettingsTile( title: Text('${locals.name}: ${_.packageInfo.appName}'), description: Text('${locals.package}: ${_.packageInfo.packageName}'), diff --git a/lib/settings/views/screens/video_filter_setup.dart b/lib/settings/views/screens/video_filter_setup.dart index 566275ee..e273f1e8 100644 --- a/lib/settings/views/screens/video_filter_setup.dart +++ b/lib/settings/views/screens/video_filter_setup.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/settings/states/video_filter_edit.dart'; -import 'package:search_choices/search_choices.dart'; +import 'package:invidious/utils/views/components/select_list_dialog.dart'; import '../../../channels/models/channel.dart'; import '../../models/db/video_filter.dart'; @@ -64,6 +64,13 @@ class VideoFilterSetup extends StatelessWidget { ]; } + searchChannel(BuildContext context) { + var cubit = context.read(); + var locals = AppLocalizations.of(context)!; + SelectList.show(context, + itemBuilder: (value, selected) => Text(value.author), asyncSearch: (filter) => cubit.searchChannel(filter ?? ''), onSelect: (value) => cubit.selectChannel(value), title: locals.channel); + } + @override Widget build(BuildContext context) { var locals = AppLocalizations.of(context)!; @@ -87,6 +94,27 @@ class VideoFilterSetup extends StatelessWidget { child: Column( children: [ Text(locals.videoFilterEditDescription), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_.channel == null) + FilledButton.tonalIcon(onPressed: () => searchChannel(context), icon: const Icon(Icons.personal_video), label: Text('${locals.channel} (${locals.optional})')), + if (_.channel != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: RichText( + text: TextSpan(children: [ + TextSpan(text: '${locals.channel}: ', style: textTheme.bodyLarge), + TextSpan(text: _.channel?.author ?? '', style: textTheme.bodyLarge?.copyWith(color: colors.primary)) + ])), + ), + if (_.channel != null) IconButton(onPressed: () => cubit.channelClear(), icon: Icon(Icons.clear)) + ], + ), + ) +/* SearchChoices.single( isExpanded: true, value: _.channel, @@ -111,6 +139,8 @@ class VideoFilterSetup extends StatelessWidget { channels.length); }, ), +*/ + , Visibility( visible: _.filter?.channelId != null, child: SwitchListTile(title: Text(locals.videoFilterHideAllFromChannel), value: _.filter?.filterAll ?? false, onChanged: cubit.channelHideAll)), diff --git a/lib/utils/states/select_list.dart b/lib/utils/states/select_list.dart new file mode 100644 index 00000000..c9dc9491 --- /dev/null +++ b/lib/utils/states/select_list.dart @@ -0,0 +1,38 @@ +import 'package:bloc/bloc.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:flutter/cupertino.dart'; + +part 'select_list.g.dart'; + +class SelectListCubit extends Cubit> { + SelectListCubit(super.initialState); + + filterItems(Future> Function(String filter)? asyncSearch, bool Function(String filter, T value)? searchFilter, List? values, String searchQuery) async { + List result = []; + + if (searchFilter != null && values != null) { + result = values.where((element) => searchFilter(searchQuery, element)).toList(); + } else if (asyncSearch != null) { + emit(state.copyWith(loading: true)); + result = await asyncSearch(searchQuery); + } else { + result = values ?? []; + } + + emit(state.copyWith(filteredItems: result, loading: false)); + } +} + +@CopyWith(constructor: "_") +class SelectListState { + List filteredItems = []; + bool loading = false; + + SelectListState(List? values) { + if (values != null) { + filteredItems = values; + } + } + + SelectListState._(this.filteredItems, this.loading); +} diff --git a/lib/utils/states/select_list.g.dart b/lib/utils/states/select_list.g.dart new file mode 100644 index 00000000..b284f523 --- /dev/null +++ b/lib/utils/states/select_list.g.dart @@ -0,0 +1,69 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'select_list.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$SelectListStateCWProxy { + SelectListState filteredItems(List filteredItems); + + SelectListState loading(bool loading); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `SelectListState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// SelectListState(...).copyWith(id: 12, name: "My name") + /// ```` + SelectListState call({ + List? filteredItems, + bool? loading, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfSelectListState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfSelectListState.copyWith.fieldName(...)` +class _$SelectListStateCWProxyImpl implements _$SelectListStateCWProxy { + const _$SelectListStateCWProxyImpl(this._value); + + final SelectListState _value; + + @override + SelectListState filteredItems(List filteredItems) => + this(filteredItems: filteredItems); + + @override + SelectListState loading(bool loading) => this(loading: loading); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `SelectListState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// SelectListState(...).copyWith(id: 12, name: "My name") + /// ```` + SelectListState call({ + Object? filteredItems = const $CopyWithPlaceholder(), + Object? loading = const $CopyWithPlaceholder(), + }) { + return SelectListState._( + filteredItems == const $CopyWithPlaceholder() || filteredItems == null + ? _value.filteredItems + // ignore: cast_nullable_to_non_nullable + : filteredItems as List, + loading == const $CopyWithPlaceholder() || loading == null + ? _value.loading + // ignore: cast_nullable_to_non_nullable + : loading as bool, + ); + } +} + +extension $SelectListStateCopyWith on SelectListState { + /// Returns a callable class that can be used as follows: `instanceOfSelectListState.copyWith(...)` or like so:`instanceOfSelectListState.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$SelectListStateCWProxy get copyWith => + _$SelectListStateCWProxyImpl(this); +} diff --git a/lib/utils/views/components/app_icon.dart b/lib/utils/views/components/app_icon.dart new file mode 100644 index 00000000..07287290 --- /dev/null +++ b/lib/utils/views/components/app_icon.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class AppIcon extends StatelessWidget { + final double? height; + final double? width; + + const AppIcon({super.key, this.height, this.width}); + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + "assets/icon.svg", + width: width, + height: height, + ); + } +} diff --git a/lib/utils/views/components/select_list_dialog.dart b/lib/utils/views/components/select_list_dialog.dart new file mode 100644 index 00000000..cc796e52 --- /dev/null +++ b/lib/utils/views/components/select_list_dialog.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/utils/states/select_list.dart'; + +class SelectList extends StatelessWidget { + final List? values; + final T? value; + final String title; + final Widget Function(T value, bool selected) itemBuilder; + final Function(T value) onSelect; + final Future> Function(String filter)? asyncSearch; + final bool Function(String filter, T value)? searchFilter; + + SelectList({super.key, required this.title, required this.values, this.value, required this.itemBuilder, required this.onSelect, this.searchFilter, this.asyncSearch}) + : assert(values == null || asyncSearch == null, 'Cannot provide both async search and list of values'); + + static show(BuildContext context, + {List? values, + T? value, + Future> Function(String filter)? asyncSearch, + required Widget Function(T value, bool selected) itemBuilder, + required Function(T value) onSelect, + required String title, + bool search = false, + bool Function(String filter, T value)? searchFilter}) { + showDialog( + context: context, + builder: (context) => SelectList( + title: title, + asyncSearch: asyncSearch, + value: value, + values: values, + itemBuilder: itemBuilder, + onSelect: onSelect, + searchFilter: searchFilter, + ), + ); + } + + @override + Widget build(BuildContext context) { + var textTheme = Theme.of(context).textTheme; + var colors = Theme.of(context).colorScheme; + var locals = AppLocalizations.of(context)!; + + return AlertDialog( + title: Text( + title, + style: textTheme.titleLarge, + ), + content: SizedBox( + width: 300, + child: BlocProvider( + create: (BuildContext context) => SelectListCubit(SelectListState(values)), + child: BlocBuilder, SelectListState>(builder: (context, state) { + var cubit = context.read>(); + return Column( + children: [ + if (searchFilter != null || asyncSearch != null) + TextField( + decoration: InputDecoration(hintText: locals.search), + onChanged: (searchQuery) async { + cubit.filterItems(asyncSearch, searchFilter, values, searchQuery); + }, + ), + Expanded( + child: ListView.builder( + itemCount: state.filteredItems.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: GestureDetector( + key: ValueKey(state.filteredItems[index]), + onTap: () { + onSelect(state.filteredItems[index]); + Navigator.of(context).pop(); + }, + child: itemBuilder(state.filteredItems[index], state.filteredItems[index] == value)), + ), + ), + ) + ], + ); + }), + ), + ), + ); + } +} diff --git a/lib/welcome_wizard/views/screens/welcome_wizard.dart b/lib/welcome_wizard/views/screens/welcome_wizard.dart index 45a2ab0b..f72ee63f 100644 --- a/lib/welcome_wizard/views/screens/welcome_wizard.dart +++ b/lib/welcome_wizard/views/screens/welcome_wizard.dart @@ -1,4 +1,3 @@ -import 'package:application_icon/application_icon.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -6,6 +5,7 @@ import 'package:invidious/app/states/app.dart'; import 'package:invidious/main.dart'; import 'package:invidious/settings/states/server_list_settings.dart'; import 'package:invidious/settings/views/components/manager_server_inner.dart'; +import 'package:invidious/utils/views/components/app_icon.dart'; import 'package:invidious/welcome_wizard/states/welcome_wizard.dart'; import '../../../settings/models/db/server.dart'; @@ -44,7 +44,7 @@ class WelcomeWizard extends StatelessWidget { body: SafeArea( top: true, child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox(width: 150, height: 150, child: AppIconImage()), + const SizedBox(width: 150, height: 150, child: AppIcon()), Text( 'Clipious', style: textTheme.displaySmall?.copyWith(color: colors.primary), diff --git a/pubspec.lock b/pubspec.lock index 6dc82159..c6e91328 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,14 +25,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" - application_icon: - dependency: "direct main" - description: - name: application_icon - sha256: "0a05e1336050d0115dc2e5700d10e4f8956cbf7a75941005582541fb2ff52fa7" - url: "https://pub.dev" - source: hosted - version: "2.0.0" archive: dependency: transitive description: @@ -513,6 +505,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + url: "https://pub.dev" + source: hosted + version: "2.0.7" flutter_swipe_action_cell: dependency: "direct main" description: @@ -833,6 +833,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" path_provider: dependency: "direct main" description: @@ -977,22 +985,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" - search_choices: - dependency: "direct main" - description: - name: search_choices - sha256: aae91d632573e9805342c8ef4679215a81566427a80ed79ca58dfa07cc7bb00d - url: "https://pub.dev" - source: hosted - version: "2.2.6" - select_dialog: - dependency: "direct main" - description: - name: select_dialog - sha256: "48a41b3db12d998b695e369a5c035e85bf64b5d069b0a6b488d60bafbdaaf5e9" - url: "https://pub.dev" - source: hosted - version: "2.0.0" settings_ui: dependency: "direct main" description: @@ -1294,6 +1286,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" + url: "https://pub.dev" + source: hosted + version: "1.1.7" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d0547fdb..eded737b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,13 +52,12 @@ dependencies: path_provider: 2.1.0 path: 1.8.3 flutter_web_auth: 0.5.0 - select_dialog: 2.0.0 cached_network_image: 3.2.3 share_plus: 7.1.0 package_info_plus: 4.1.0 receive_sharing_intent: 1.4.5 logging: 1.2.0 - application_icon: 2.0.0 + flutter_svg: 2.0.7 flutter_native_splash: 2.3.2 flutter_linkify: 6.0.0 url_launcher: 6.1.12 @@ -70,7 +69,6 @@ dependencies: git: url: https://github.com/lamarios/locale_names.git ref: master - search_choices: 2.2.6 uuid: 3.0.7 dio: 5.3.2 just_audio: 0.9.34 @@ -131,8 +129,8 @@ flutter: generate: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg + assets: + - assets/icon.svg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see diff --git a/submodules/flutter b/submodules/flutter index e285328a..e1e47221 160000 --- a/submodules/flutter +++ b/submodules/flutter @@ -1 +1 @@ -Subproject commit e285328a6909879584a27c9b8eec5fa3621ead93 +Subproject commit e1e47221e86272429674bec4f1bd36acc4fc7b77 From 1b69fe92b8a08f8000d8e2b4a25ec0796fe3420d Mon Sep 17 00:00:00 2001 From: Paul Fauchon Date: Tue, 29 Aug 2023 17:27:45 +0800 Subject: [PATCH 2/2] reduce app icon size when there's nothing to display auto focus search text field when no search queries remove green / yellow app wide border when using mouse / keyboard --- android/app/src/main/res/values/styles.xml | 2 ++ lib/home/views/components/home.dart | 6 +++++- lib/main.dart | 7 ++++++- lib/search/views/screens/search.dart | 1 + 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 0d1fa8fc..0f345f40 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -9,6 +9,7 @@ false false shortEdges + false diff --git a/lib/home/views/components/home.dart b/lib/home/views/components/home.dart index 83b68d04..706c8a26 100644 --- a/lib/home/views/components/home.dart +++ b/lib/home/views/components/home.dart @@ -78,7 +78,11 @@ class HomeView extends StatelessWidget { padding: const EdgeInsets.only(left: innerHorizontalPadding), color: colors.background, child: layout.smallSources.isEmpty && !layout.showBigSource - ? const Opacity(opacity: 0.2, child: AppIcon()) + ? const Opacity( + opacity: 0.2, + child: AppIcon( + height: 200, + )) : Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/main.dart b/lib/main.dart index 74b3ccb0..69bc802a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -369,7 +369,12 @@ class _HomeState extends State with AfterLayoutMixin { // home handles its own padding because we don't want to cut horizontal scroll lists on the right padding: EdgeInsets.symmetric(horizontal: selectedPage == HomeDataSource.home ? 0 : innerHorizontalPadding), key: ValueKey(selectedPage), - child: selectedPage?.build(context, false) ?? const Opacity(opacity: 0.2, child: AppIcon())), + child: selectedPage?.build(context, false) ?? + const Opacity( + opacity: 0.2, + child: AppIcon( + height: 200, + ))), /* child: [ const HomeView( diff --git a/lib/search/views/screens/search.dart b/lib/search/views/screens/search.dart index c4282dd2..f317b4a8 100644 --- a/lib/search/views/screens/search.dart +++ b/lib/search/views/screens/search.dart @@ -54,6 +54,7 @@ class Search extends StatelessWidget { backgroundColor: colorScheme.background, scrolledUnderElevation: 0, title: TextField( + autofocus: query == null, controller: _.queryController, textInputAction: TextInputAction.search, onSubmitted: cubit.search,