diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 375b5d84..5417450e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + diff --git a/android/app/src/main/res/drawable/app_icon.png b/android/app/src/main/res/drawable/app_icon.png new file mode 100644 index 00000000..c715d7b0 Binary files /dev/null and b/android/app/src/main/res/drawable/app_icon.png differ diff --git a/android/app/src/main/res/drawable/ic_bg_service_small.png b/android/app/src/main/res/drawable/ic_bg_service_small.png new file mode 100644 index 00000000..c715d7b0 Binary files /dev/null and b/android/app/src/main/res/drawable/ic_bg_service_small.png differ diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_bg_service_small.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_bg_service_small.xml new file mode 100644 index 00000000..0be1a476 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_bg_service_small.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/4029.txt b/fastlane/metadata/android/en-US/changelogs/4029.txt new file mode 100644 index 00000000..c19ad7f2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4029.txt @@ -0,0 +1,3 @@ +Add foreground service to get notified on new content (subscriptions / channel / playlists) +Fix search +Split settings in multiple settings sub pages as it was getting too big \ No newline at end of file diff --git a/lib/app/states/app.dart b/lib/app/states/app.dart index e30e42c5..b07fc1fc 100644 --- a/lib/app/states/app.dart +++ b/lib/app/states/app.dart @@ -2,22 +2,21 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:flutter/material.dart'; +import 'package:invidious/router.dart'; import 'package:logging/logging.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import '../../database.dart'; import '../../globals.dart'; import '../../home/models/db/home_layout.dart'; -import '../../main.dart'; import '../../settings/models/db/server.dart'; -import '../../videos/views/screens/video.dart'; part 'app.g.dart'; final log = Logger('HomeState'); class AppCubit extends Cubit { + AppCubit(super.initialState) { onReady(); } @@ -51,19 +50,15 @@ class AppCubit extends Cubit { try { Uri uri = Uri.parse(url); if (YOUTUBE_HOSTS.contains(uri.host)) { - if (uri.pathSegments.length == 1 && uri.pathSegments.contains("watch") && uri.queryParameters.containsKey('v')) { + if (uri.pathSegments.length == 1 && + uri.pathSegments.contains("watch") && + uri.queryParameters.containsKey('v')) { String videoId = uri.queryParameters['v']!; - navigatorKey.currentState?.push(MaterialPageRoute( - builder: (context) => VideoView( - videoId: videoId, - ))); + appRouter.push(VideoRoute(videoId: videoId)); } if (uri.host == 'youtu.be' && uri.pathSegments.length == 1) { String videoId = uri.pathSegments[0]; - navigatorKey.currentState?.push(MaterialPageRoute( - builder: (context) => VideoView( - videoId: videoId, - ))); + appRouter.push(VideoRoute(videoId: videoId)); } } } catch (err, stacktrace) { @@ -88,7 +83,8 @@ class AppCubit extends Cubit { emit(state.copyWith(homeLayout: db.getHomeLayout())); } - bool get isLoggedIn => (state.server?.authToken?.isNotEmpty ?? false) || (state.server?.sidCookie?.isNotEmpty ?? false); + bool get isLoggedIn => + (state.server?.authToken?.isNotEmpty ?? false) || (state.server?.sidCookie?.isNotEmpty ?? false); } @CopyWith(constructor: "_") diff --git a/lib/app/states/tv_home.dart b/lib/app/states/tv_home.dart index 93a97289..2c969cfd 100644 --- a/lib/app/states/tv_home.dart +++ b/lib/app/states/tv_home.dart @@ -20,17 +20,17 @@ class TvHomeCubit extends Cubit { } } - scrollToTop(){ + scrollToTop() { state.scrollController.animateTo(0, duration: animationDuration, curve: Curves.easeInOutQuad); } } @CopyWith(constructor: "_") -class TvHomeState{ - bool expandMenu = false; - ScrollController scrollController = ScrollController(); +class TvHomeState { + bool expandMenu = false; + ScrollController scrollController = ScrollController(); - TvHomeState(); + TvHomeState(); TvHomeState._(this.expandMenu, this.scrollController); } diff --git a/lib/app/views/screens/main.dart b/lib/app/views/screens/main.dart new file mode 100644 index 00000000..c4597233 --- /dev/null +++ b/lib/app/views/screens/main.dart @@ -0,0 +1,23 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:invidious/player/views/components/player.dart'; + +import '../../../player/views/components/mini_player_aware.dart'; + +@RoutePage() +class MainScreen extends StatelessWidget { + const MainScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Stack( + children: [ + MiniPlayerAware( + child: AutoRouter(), + ), + Player() + ], + ); + } +} diff --git a/lib/app/views/tv/screens/tv_home.dart b/lib/app/views/tv/screens/tv_home.dart index 11810def..30c6d10e 100644 --- a/lib/app/views/tv/screens/tv_home.dart +++ b/lib/app/views/tv/screens/tv_home.dart @@ -1,4 +1,7 @@ +import 'package:auto_route/annotations.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -13,7 +16,9 @@ import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; import 'package:invidious/videos/views/components/subscriptions.dart'; import 'package:invidious/videos/views/components/trending.dart'; import 'package:invidious/videos/views/tv/screens/video_grid_view.dart'; +import 'package:invidious/welcome_wizard/views/tv/components/welcome_wizard.dart'; +import '../../../../router.dart'; import '../../../../utils/views/components/app_icon.dart'; import '../../../../videos/views/components/popular.dart'; import '../../../states/app.dart'; @@ -23,50 +28,35 @@ const double overlayBlur = 25.0; GlobalKey popularTitle = GlobalKey(debugLabel: 'popular title'); GlobalKey subscriptionTitle = GlobalKey(debugLabel: 'subscription title'); -class TvHome extends StatelessWidget { - const TvHome({Key? key}) : super(key: key); +@RoutePage() +class TvHomeScreen extends StatelessWidget { + const TvHomeScreen({Key? key}) : super(key: key); openSettings(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute(builder: (builder) => const TVSettings())); + AutoRouter.of(context).push(const TVSettingsRoute()); } openPopular(BuildContext context) { var locals = AppLocalizations.of(context)!; - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvGridView(paginatedVideoList: SingleEndpointList(service.getPopular), title: locals.popular), - )); + AutoRouter.of(context).push(TvGridRoute(paginatedVideoList: SingleEndpointList(service.getPopular), title: locals.popular)); } openTrending(BuildContext context) { var locals = AppLocalizations.of(context)!; - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvGridView( - paginatedVideoList: SingleEndpointList(service.getTrending), - title: locals.trending, - ), - )); + AutoRouter.of(context).push(TvGridRoute(paginatedVideoList: SingleEndpointList(service.getTrending), title: locals.trending)); } openSubscriptions(BuildContext context) { var locals = AppLocalizations.of(context)!; - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvGridView( - paginatedVideoList: SubscriptionVideoList(), - title: locals.subscriptions, - ), - )); + AutoRouter.of(context).push(TvGridRoute(paginatedVideoList: SubscriptionVideoList(), title: locals.subscriptions)); } openSearch(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute(builder: (builder) => const TvSearch())); + AutoRouter.of(context).push(const TvSearchRoute()); } openPlaylists(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvPlaylistGridView( - playlistList: SingleEndpointList(service.getUserPlaylists), - ), - )); + AutoRouter.of(context).push(TvPlaylistGridRoute(playlistList: SingleEndpointList(service.getUserPlaylists))); } @override @@ -74,76 +64,123 @@ class TvHome extends StatelessWidget { ColorScheme colors = Theme.of(context).colorScheme; TextTheme textTheme = Theme.of(context).textTheme; var locals = AppLocalizations.of(context)!; - - return BlocProvider( - create: (BuildContext context) => TvHomeCubit(TvHomeState()), - child: Scaffold( - body: BlocBuilder(builder: (context, homeState) { - var homeCubit = context.read(); - return BlocBuilder(buildWhen: (previous, current) { - return previous.server != current.server; - }, builder: (context, _) { - var app = context.read(); - return DefaultTextStyle( - style: textTheme.bodyLarge!, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - AnimatedContainer( - width: homeState.expandMenu ? 250 : 118, - duration: animationDuration ~/ 2, - curve: Curves.easeInOutQuad, - decoration: BoxDecoration(color: homeState.expandMenu ? colors.secondaryContainer.withOpacity(0.5) : Colors.transparent), - child: Padding( - padding: EdgeInsets.only(top: TvOverscan.vertical, left: TvOverscan.horizontal, bottom: TvOverscan.vertical, right: homeState.expandMenu ? TvOverscan.horizontal : 8), - child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only(bottom: 30.0), - child: SizedBox( - height: 50, - child: Row( - children: [ - const AppIcon( - width: 50, - height: 50, + return Shortcuts( + shortcuts: { + LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), + }, + child: BlocProvider( + create: (BuildContext context) => TvHomeCubit(TvHomeState()), + child: Scaffold( + body: BlocBuilder(builder: (context, homeState) { + var homeCubit = context.read(); + return BlocBuilder(buildWhen: (previous, current) { + return previous.server != current.server; + }, builder: (context, _) { + var app = context.read(); + return DefaultTextStyle( + style: textTheme.bodyLarge!, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedContainer( + width: homeState.expandMenu ? 250 : 118, + duration: animationDuration ~/ 2, + curve: Curves.easeInOutQuad, + decoration: BoxDecoration(color: homeState.expandMenu ? colors.secondaryContainer.withOpacity(0.5) : Colors.transparent), + child: Padding( + padding: EdgeInsets.only(top: TvOverscan.vertical, left: TvOverscan.horizontal, bottom: TvOverscan.vertical, right: homeState.expandMenu ? TvOverscan.horizontal : 8), + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.only(bottom: 30.0), + child: SizedBox( + height: 50, + child: Row( + children: [ + const AppIcon( + width: 50, + height: 50, + ), + if (homeState.expandMenu) + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: MenuItemText( + 'Clipious', + style: textTheme.titleLarge!.copyWith(color: colors.primary), + )) + ], + )), + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: TvButton( + onFocusChanged: homeCubit.menuItemFocusChanged, + onPressed: openSearch, + unfocusedColor: colors.secondaryContainer.withOpacity(0.0), + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Icon(Icons.search), + ), + if (homeState.expandMenu) MenuItemText(locals.search) + ], + ), + ), + ), + ), + Visibility( + visible: app.isLoggedIn, + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: TvButton( + onFocusChanged: homeCubit.menuItemFocusChanged, + onPressed: openSubscriptions, + unfocusedColor: colors.secondaryContainer.withOpacity(0.0), + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Icon(Icons.subscriptions), + ), + if (homeState.expandMenu) MenuItemText(locals.subscriptions) + ], ), - if (homeState.expandMenu) - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: MenuItemText( - 'Clipious', - style: textTheme.titleLarge!.copyWith(color: colors.primary), - )) - ], - )), - ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: TvButton( - onFocusChanged: homeCubit.menuItemFocusChanged, - onPressed: openSearch, - unfocusedColor: colors.secondaryContainer.withOpacity(0.0), + ), + ), + ), + ), + Visibility( + visible: app.isLoggedIn, child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Icon(Icons.search), + padding: const EdgeInsets.only(bottom: 8.0), + child: TvButton( + onFocusChanged: homeCubit.menuItemFocusChanged, + onPressed: openPlaylists, + unfocusedColor: colors.secondaryContainer.withOpacity(0.0), + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Icon(Icons.playlist_play), + ), + if (homeState.expandMenu) MenuItemText(locals.playlists) + ], ), - if (homeState.expandMenu) MenuItemText(locals.search) - ], + ), ), ), ), - ), - Visibility( - visible: app.isLoggedIn, - child: Padding( + Padding( padding: const EdgeInsets.only(bottom: 8.0), child: TvButton( onFocusChanged: homeCubit.menuItemFocusChanged, - onPressed: openSubscriptions, + onPressed: openPopular, unfocusedColor: colors.secondaryContainer.withOpacity(0.0), child: Padding( padding: const EdgeInsets.all(8), @@ -151,22 +188,19 @@ class TvHome extends StatelessWidget { children: [ const Padding( padding: EdgeInsets.only(right: 8.0), - child: Icon(Icons.subscriptions), + child: Icon(Icons.local_fire_department), ), - if (homeState.expandMenu) MenuItemText(locals.subscriptions) + if (homeState.expandMenu) MenuItemText(locals.popular) ], ), ), ), ), - ), - Visibility( - visible: app.isLoggedIn, - child: Padding( + Padding( padding: const EdgeInsets.only(bottom: 8.0), child: TvButton( onFocusChanged: homeCubit.menuItemFocusChanged, - onPressed: openPlaylists, + onPressed: openTrending, unfocusedColor: colors.secondaryContainer.withOpacity(0.0), child: Padding( padding: const EdgeInsets.all(8), @@ -174,20 +208,17 @@ class TvHome extends StatelessWidget { children: [ const Padding( padding: EdgeInsets.only(right: 8.0), - child: Icon(Icons.playlist_play), + child: Icon(Icons.trending_up), ), - if (homeState.expandMenu) MenuItemText(locals.playlists) + if (homeState.expandMenu) MenuItemText(locals.trending) ], ), ), ), ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: TvButton( + TvButton( onFocusChanged: homeCubit.menuItemFocusChanged, - onPressed: openPopular, + onPressed: (context) => openSettings(context), unfocusedColor: colors.secondaryContainer.withOpacity(0.0), child: Padding( padding: const EdgeInsets.all(8), @@ -195,117 +226,80 @@ class TvHome extends StatelessWidget { children: [ const Padding( padding: EdgeInsets.only(right: 8.0), - child: Icon(Icons.local_fire_department), + child: Icon(Icons.settings), ), - if (homeState.expandMenu) MenuItemText(locals.popular) + if (homeState.expandMenu) MenuItemText(locals.settings) ], ), ), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: TvButton( - onFocusChanged: homeCubit.menuItemFocusChanged, - onPressed: openTrending, - unfocusedColor: colors.secondaryContainer.withOpacity(0.0), - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Icon(Icons.trending_up), - ), - if (homeState.expandMenu) MenuItemText(locals.trending) - ], + ) + ])), + ), + Expanded( + // terrible work around to be able to scroll to all the global keys + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: TvOverscan.vertical, bottom: TvOverscan.vertical, right: TvOverscan.horizontal, left: 8), + child: ListView( + controller: homeState.scrollController, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + // crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Visibility( + key: subscriptionTitle, + visible: app.isLoggedIn, + child: Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + locals.subscriptions, + style: textTheme.titleLarge, + ), ), ), - ), - ), - TvButton( - onFocusChanged: homeCubit.menuItemFocusChanged, - onPressed: (context) => openSettings(context), - unfocusedColor: colors.secondaryContainer.withOpacity(0.0), - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Icon(Icons.settings), - ), - if (homeState.expandMenu) MenuItemText(locals.settings) - ], + Visibility( + visible: app.isLoggedIn, + child: Subscriptions( + onItemFocus: (video, index, focus) { + if (focus) { + Scrollable.ensureVisible(subscriptionTitle.currentContext!, + duration: animationDuration, curve: Curves.easeInOutQuad, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart); + } + }, + ), ), - ), - ) - ])), - ), - Expanded( - // terrible work around to be able to scroll to all the global keys - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only(top: TvOverscan.vertical, bottom: TvOverscan.vertical, right: TvOverscan.horizontal, left: 8), - child: ListView( - controller: homeState.scrollController, - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - // crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Visibility( - key: subscriptionTitle, - visible: app.isLoggedIn, - child: Padding( + Padding( + key: popularTitle, padding: const EdgeInsets.only(top: 16.0), - child: Text( - locals.subscriptions, - style: textTheme.titleLarge, - ), + child: Text(locals.popular, style: textTheme.titleLarge), ), - ), - Visibility( - visible: app.isLoggedIn, - child: Subscriptions( + Popular( onItemFocus: (video, index, focus) { if (focus) { - Scrollable.ensureVisible(subscriptionTitle.currentContext!, + Scrollable.ensureVisible(popularTitle.currentContext!, duration: animationDuration, curve: Curves.easeInOutQuad, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart); } }, ), - ), - Padding( - key: popularTitle, - padding: const EdgeInsets.only(top: 16.0), - child: Text(locals.popular, style: textTheme.titleLarge), - ), - Popular( - onItemFocus: (video, index, focus) { - if (focus) { - Scrollable.ensureVisible(popularTitle.currentContext!, - duration: animationDuration, curve: Curves.easeInOutQuad, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart); - } - }, - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Text( - locals.trending, - style: textTheme.titleLarge, + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + locals.trending, + style: textTheme.titleLarge, + ), ), - ), - const Trending(), - ], + const Trending(), + ], + ), ), ), ), - ), - ], - ), - ); - }); - }), + ], + ), + ); + }); + }), + ), ), ); } diff --git a/lib/channels/models/channel.dart b/lib/channels/models/channel.dart index b235f71e..0f8caa72 100644 --- a/lib/channels/models/channel.dart +++ b/lib/channels/models/channel.dart @@ -24,8 +24,20 @@ class Channel implements ShareLinks { List? allowedRegions; List? latestVideos; - Channel(this.author, this.authorId, this.authorUrl, this.authorBanners, this.authorThumbnails, this.subCount, this.totalViews, this.joined, this.autoGenerated, this.isFamilyFriendly, - this.description, this.allowedRegions, this.latestVideos); + Channel( + this.author, + this.authorId, + this.authorUrl, + this.authorBanners, + this.authorThumbnails, + this.subCount, + this.totalViews, + this.joined, + this.autoGenerated, + this.isFamilyFriendly, + this.description, + this.allowedRegions, + this.latestVideos); factory Channel.fromJson(Map json) => _$ChannelFromJson(json); diff --git a/lib/channels/states/channel.dart b/lib/channels/states/channel.dart index 291d7b9c..93a67a1d 100644 --- a/lib/channels/states/channel.dart +++ b/lib/channels/states/channel.dart @@ -65,5 +65,6 @@ class ChannelController { ChannelController(this.channelId); - ChannelController._(this.channelId, this.isSubscribed, this.selectedIndex, this.channel, this.loading, this.smallHeader, this.barHeight, this.barOpacity); + ChannelController._(this.channelId, this.isSubscribed, this.selectedIndex, this.channel, this.loading, + this.smallHeader, this.barHeight, this.barOpacity); } diff --git a/lib/channels/states/tv_channel.dart b/lib/channels/states/tv_channel.dart index b698ef5e..b59a1ede 100644 --- a/lib/channels/states/tv_channel.dart +++ b/lib/channels/states/tv_channel.dart @@ -62,7 +62,10 @@ class TvChannelCubit extends Cubit { scrollTo(GlobalKey key, bool focus) { if (key.currentContext != null && focus) { - Scrollable.ensureVisible(key.currentContext!, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, duration: animationDuration, curve: Curves.easeInOutQuad); + Scrollable.ensureVisible(key.currentContext!, + alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, + duration: animationDuration, + curve: Curves.easeInOutQuad); } } @@ -91,6 +94,6 @@ class TvChannelController { TvChannelController(); - TvChannelController._( - this.scrollController, this.showBackground, this.hasShorts, this.hasStreams, this.hasVideos, this.hasPlaylist, this.videosTitle, this.shortTitle, this.streamTitle, this.playlistsTitle); + TvChannelController._(this.scrollController, this.showBackground, this.hasShorts, this.hasStreams, this.hasVideos, + this.hasPlaylist, this.videosTitle, this.shortTitle, this.streamTitle, this.playlistsTitle); } diff --git a/lib/channels/views/components/info.dart b/lib/channels/views/components/info.dart index efa6f127..58589a2a 100644 --- a/lib/channels/views/components/info.dart +++ b/lib/channels/views/components/info.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:invidious/channels/models/channel.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/notifications/views/components/bell_icon.dart'; import 'package:invidious/utils.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -22,9 +24,12 @@ class ChannelInfo extends StatelessWidget { ColorScheme colors = Theme.of(context).colorScheme; var textTheme = Theme.of(context).textTheme; List widgets = [ - Text( - channel.author ?? '', - style: textTheme.titleLarge?.copyWith(color: colors.primary), + Container( + padding: const EdgeInsets.only(top: 10), + child: Text( + channel.author ?? '', + style: textTheme.titleLarge?.copyWith(color: colors.primary), + ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), @@ -60,7 +65,8 @@ class ChannelInfo extends StatelessWidget { mainAxisSpacing: 5, childAspectRatio: getGridAspectRatio(context), children: channel.latestVideos?.map((e) { - VideoInList videoInList = VideoInList(e.title, e.videoId, e.lengthSeconds, 0, e.author, channel.authorId, channel.authorId, 0, '', e.videoThumbnails); + VideoInList videoInList = VideoInList(e.title, e.videoId, e.lengthSeconds, 0, e.author, channel.authorId, + channel.authorId, 0, '', e.videoThumbnails); videoInList.filtered = e.filtered; videoInList.matchedFilters = e.matchedFilters; return VideoListItem( @@ -70,8 +76,8 @@ class ChannelInfo extends StatelessWidget { [])); return SingleChildScrollView( - child: Stack( - alignment: Alignment.topCenter, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 230, @@ -83,16 +89,13 @@ class ChannelInfo extends StatelessWidget { color: colors.secondaryContainer, )), ), - Container( - padding: const EdgeInsets.only(top: 200, left: 16, right: 16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment(0, Alignment.topCenter.y + 0.033), end: Alignment(0, Alignment.topCenter.y + 0.045), colors: [colors.background.withOpacity(0), colors.background])), + Padding( + padding: const EdgeInsets.symmetric(horizontal: innerHorizontalPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: widgets, ), - ), + ) ], ), ); diff --git a/lib/channels/views/components/playlists.dart b/lib/channels/views/components/playlists.dart index 4d675b10..40e52b39 100644 --- a/lib/channels/views/components/playlists.dart +++ b/lib/channels/views/components/playlists.dart @@ -19,7 +19,8 @@ class ChannelPlayListsView extends StatelessWidget { Widget build(BuildContext context) { return PlaylistList( canDeleteVideos: canDeleteVideos, - paginatedList: ContinuationList((continuation) => service.getChannelPlaylists(channelId, continuation: continuation)), + paginatedList: ContinuationList( + (continuation) => service.getChannelPlaylists(channelId, continuation: continuation)), ); } } diff --git a/lib/channels/views/components/videos.dart b/lib/channels/views/components/videos.dart index e028d4ef..d954bdfa 100644 --- a/lib/channels/views/components/videos.dart +++ b/lib/channels/views/components/videos.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:invidious/channels/models/channelVideos.dart'; import 'package:invidious/globals.dart'; import 'package:invidious/videos/models/video_in_list.dart'; -import 'package:invidious/videos/views/components/video_in_list.dart'; import 'package:invidious/videos/views/components/video_list.dart'; import '../../../utils/models/paginatedList.dart'; @@ -25,7 +24,8 @@ class ChannelVideosView extends StatelessWidget { color: colorScheme.background, child: VideoList( key: const ValueKey('channel-videos'), - paginatedVideoList: ContinuationList((continuation) => getVideos(channel.authorId, continuation)), + paginatedVideoList: + ContinuationList((continuation) => getVideos(channel.authorId, continuation)), // tags: 'channel-video-list-${(key as ValueKey).value}' ), ), diff --git a/lib/channels/views/screens/channel.dart b/lib/channels/views/screens/channel.dart index d40c0c7e..5e7c1a32 100644 --- a/lib/channels/views/screens/channel.dart +++ b/lib/channels/views/screens/channel.dart @@ -1,4 +1,5 @@ // import 'package:video_player/video_player.dart'; +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_fadein/flutter_fadein.dart'; @@ -8,16 +9,16 @@ import 'package:invidious/channels/views/components/info.dart'; import 'package:invidious/channels/views/components/playlists.dart'; import 'package:invidious/channels/views/components/videos.dart'; import 'package:invidious/globals.dart'; -import 'package:invidious/videos/views/components/video_in_list.dart'; import '../../../settings/states/settings.dart'; import '../../../utils.dart'; import '../../../utils/views/components/placeholders.dart'; -class ChannelView extends StatelessWidget { +@RoutePage() +class ChannelScreen extends StatelessWidget { final String channelId; - const ChannelView({super.key, required this.channelId}); + const ChannelScreen({super.key, required this.channelId}); @override Widget build(BuildContext context) { @@ -74,7 +75,9 @@ class ChannelView extends StatelessWidget { child: AnimatedSwitcher( duration: animationDuration, child: [ - _.loading ? const ChannelPlaceHolder() : ChannelInfo(key: const ValueKey('info'), channel: _.channel!), + _.loading + ? const ChannelPlaceHolder() + : ChannelInfo(key: const ValueKey('info'), channel: _.channel!), if (!_.loading) ChannelVideosView( key: const ValueKey('videos'), @@ -93,7 +96,9 @@ class ChannelView extends StatelessWidget { channel: _.channel!, getVideos: service.getChannelStreams, ), - if (!_.loading) ChannelPlayListsView(key: const ValueKey('playlists'), channelId: _.channel!.authorId, canDeleteVideos: false) + if (!_.loading) + ChannelPlayListsView( + key: const ValueKey('playlists'), channelId: _.channel!.authorId, canDeleteVideos: false) ][_.selectedIndex], )), ); diff --git a/lib/channels/views/tv/screens/channel.dart b/lib/channels/views/tv/screens/channel.dart index 41949423..bc3a2ac8 100644 --- a/lib/channels/views/tv/screens/channel.dart +++ b/lib/channels/views/tv/screens/channel.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:auto_route/annotations.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -22,10 +23,12 @@ import '../../../../utils.dart'; import '../../../../videos/views/components/video_thumbnail.dart'; import '../../../states/tv_channel.dart'; -class TvChannelView extends StatelessWidget { + +@RoutePage() +class TvChannelScreen extends StatelessWidget { final String channelId; - const TvChannelView({Key? key, required this.channelId}) : super(key: key); + const TvChannelScreen({Key? key, required this.channelId}) : super(key: key); @override Widget build(BuildContext context) { @@ -55,7 +58,12 @@ class TvChannelView extends StatelessWidget { style: textTheme.bodyLarge!, child: Stack( children: [ - Positioned(top: 0, left: 0, right: 0, child: CachedNetworkImage(imageUrl: ImageObject.getBestThumbnail(channel.channel?.authorBanners)?.url ?? '')), + Positioned( + top: 0, + left: 0, + right: 0, + child: CachedNetworkImage( + imageUrl: ImageObject.getBestThumbnail(channel.channel?.authorBanners)?.url ?? '')), TweenAnimationBuilder( tween: Tween(begin: 0, end: tv.showBackground ? overlayBlur : 0), duration: animationDuration, @@ -67,123 +75,149 @@ class TvChannelView extends StatelessWidget { sigmaY: value, ), child: AnimatedContainer( - color: colors.background.withOpacity(tv.showBackground ? overlayBackgroundOpacity : 0), + color: + colors.background.withOpacity(tv.showBackground ? overlayBackgroundOpacity : 0), duration: animationDuration, child: TvOverscan( child: SingleChildScrollView( controller: tv.scrollController, - child: ListView(physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, children: [ - Padding( - padding: const EdgeInsets.only(top: 100.0), - child: Align( - alignment: Alignment.centerLeft, - child: AnimatedContainer( - decoration: BoxDecoration( - color: tv.showBackground ? colors.background.withOpacity(0) : colors.background.withOpacity(1), borderRadius: BorderRadius.circular(35)), - duration: animationDuration, - child: Row(mainAxisSize: MainAxisSize.min, children: [ - Thumbnail( - thumbnailUrl: ImageObject.getBestThumbnail(channel.channel?.authorThumbnails)?.url ?? '', - width: 70, - height: 70, - id: 'author-big-${channel.channel?.authorId}', - decoration: BoxDecoration(borderRadius: BorderRadius.circular(35)), + child: ListView( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.only(top: 100.0), + child: Align( + alignment: Alignment.centerLeft, + child: AnimatedContainer( + decoration: BoxDecoration( + color: tv.showBackground + ? colors.background.withOpacity(0) + : colors.background.withOpacity(1), + borderRadius: BorderRadius.circular(35)), + duration: animationDuration, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Thumbnail( + thumbnailUrl: ImageObject.getBestThumbnail( + channel.channel?.authorThumbnails) + ?.url ?? + '', + width: 70, + height: 70, + id: 'author-big-${channel.channel?.authorId}', + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(35)), + ), + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 20), + child: Text( + channel.channel?.author ?? '', + style: textTheme.displaySmall, + ), + ) + ]), ), - Padding( - padding: const EdgeInsets.only(left: 8.0, right: 20), - child: Text( - channel.channel?.author ?? '', - style: textTheme.displaySmall, - ), - ) - ]), + ), ), - ), - ), - TvSubscribeButton( - autoFocus: true, - channelId: channelId, - subCount: compactCurrency.format(channel.channel!.subCount), - onFocusChanged: tvCubit.scrollToTop, - ), - TvExpandableText( - text: channel.channel?.description ?? '', - maxLines: 3, - ), - tv.hasVideos - ? Padding( - key: tv.videosTitle, - padding: const EdgeInsets.only(top: 20.0), - child: Text( - locals.videos, - style: textTheme.titleLarge, - ), - ) - : const SizedBox.shrink(), - TvHorizontalVideoList( - onItemFocus: (video, index, focus) => tvCubit.scrollTo(tv.videosTitle, focus), - paginatedVideoList: ContinuationList((continuation) => service.getChannelVideos(channel.channel?.authorId ?? '', continuation).then((value) { - tvCubit.setHasVideos(value.videos.isNotEmpty); - return value; - }))), - tv.hasShorts - ? Padding( - key: tv.shortTitle, - padding: const EdgeInsets.only(top: 20.0), - child: Text( - locals.shorts, - style: textTheme.titleLarge, - ), - ) - : const SizedBox.shrink(), - TvHorizontalVideoList( - onItemFocus: (video, index, focus) => tvCubit.scrollTo(tv.shortTitle, focus), - paginatedVideoList: ContinuationList((continuation) => service.getChannelShorts(channel.channel?.authorId ?? '', continuation).then((value) { - tvCubit.setHasShorts(value.videos.isNotEmpty); - return value; - }))), - tv.hasStreams - ? Padding( - key: tv.streamTitle, - padding: const EdgeInsets.only(top: 20.0), - child: Text( - locals.streams, - style: textTheme.titleLarge, - ), - ) - : const SizedBox.shrink(), - TvHorizontalVideoList( - onItemFocus: (video, index, focus) => tvCubit.scrollTo(tv.streamTitle, focus), - paginatedVideoList: ContinuationList((continuation) => service.getChannelStreams(channel.channel?.authorId ?? '', continuation).then((value) { - tvCubit.setHasStreams(value.videos.isNotEmpty); - return value; - }))), - tv.hasPlaylist - ? Padding( - padding: const EdgeInsets.only(top: 20.0), - child: Text( - locals.playlists, - style: textTheme.titleLarge, - ), - ) - : const SizedBox.shrink(), - TvHorizontalItemList( - getPlaceholder: () => const TvPlaylistPlaceHolder(), - paginatedList: - ContinuationList((continuation) => service.getChannelPlaylists(channel.channel?.authorId ?? '', continuation: continuation).then((value) { + TvSubscribeButton( + autoFocus: true, + channelId: channelId, + subCount: compactCurrency.format(channel.channel!.subCount), + onFocusChanged: tvCubit.scrollToTop, + ), + TvExpandableText( + text: channel.channel?.description ?? '', + maxLines: 3, + ), + tv.hasVideos + ? Padding( + key: tv.videosTitle, + padding: const EdgeInsets.only(top: 20.0), + child: Text( + locals.videos, + style: textTheme.titleLarge, + ), + ) + : const SizedBox.shrink(), + TvHorizontalVideoList( + onItemFocus: (video, index, focus) => + tvCubit.scrollTo(tv.videosTitle, focus), + paginatedVideoList: ContinuationList((continuation) => + service + .getChannelVideos(channel.channel?.authorId ?? '', continuation) + .then((value) { + tvCubit.setHasVideos(value.videos.isNotEmpty); + return value; + }))), + tv.hasShorts + ? Padding( + key: tv.shortTitle, + padding: const EdgeInsets.only(top: 20.0), + child: Text( + locals.shorts, + style: textTheme.titleLarge, + ), + ) + : const SizedBox.shrink(), + TvHorizontalVideoList( + onItemFocus: (video, index, focus) => + tvCubit.scrollTo(tv.shortTitle, focus), + paginatedVideoList: ContinuationList((continuation) => + service + .getChannelShorts(channel.channel?.authorId ?? '', continuation) + .then((value) { + tvCubit.setHasShorts(value.videos.isNotEmpty); + return value; + }))), + tv.hasStreams + ? Padding( + key: tv.streamTitle, + padding: const EdgeInsets.only(top: 20.0), + child: Text( + locals.streams, + style: textTheme.titleLarge, + ), + ) + : const SizedBox.shrink(), + TvHorizontalVideoList( + onItemFocus: (video, index, focus) => + tvCubit.scrollTo(tv.streamTitle, focus), + paginatedVideoList: ContinuationList((continuation) => + service + .getChannelStreams( + channel.channel?.authorId ?? '', continuation) + .then((value) { + tvCubit.setHasStreams(value.videos.isNotEmpty); + return value; + }))), + tv.hasPlaylist + ? Padding( + padding: const EdgeInsets.only(top: 20.0), + child: Text( + locals.playlists, + style: textTheme.titleLarge, + ), + ) + : const SizedBox.shrink(), + TvHorizontalItemList( + getPlaceholder: () => const TvPlaylistPlaceHolder(), + paginatedList: ContinuationList((continuation) => service + .getChannelPlaylists(channel.channel?.authorId ?? '', + continuation: continuation) + .then((value) { tvCubit.setHasPlaylists(value.playlists.isNotEmpty); return value; })), - buildItem: (context, index, item) => Padding( - padding: const EdgeInsets.all(8.0), - child: PlaylistInList( - playlist: item, - canDeleteVideos: false, - isTv: true, - // cameFromSearch: true, - )), - ), - ]), + buildItem: (context, index, item) => Padding( + padding: const EdgeInsets.all(8.0), + child: PlaylistInList( + playlist: item, + canDeleteVideos: false, + isTv: true, + // cameFromSearch: true, + )), + ), + ]), ), ), ), diff --git a/lib/comments/models/comment.dart b/lib/comments/models/comment.dart index 74617835..a3045725 100644 --- a/lib/comments/models/comment.dart +++ b/lib/comments/models/comment.dart @@ -21,8 +21,8 @@ class Comment { CreatorHeart? creatorHeart; CommentReplies? replies; - Comment(this.author, this.authorThumbnails, this.authorId, this.authorUrl, this.isEdited, this.content, this.publishedText, this.likeCount, this.commentId, this.authorIsChannelOwner, - this.creatorHeart, this.replies); + Comment(this.author, this.authorThumbnails, this.authorId, this.authorUrl, this.isEdited, this.content, + this.publishedText, this.likeCount, this.commentId, this.authorIsChannelOwner, this.creatorHeart, this.replies); factory Comment.fromJson(Map json) => _$CommentFromJson(json); diff --git a/lib/comments/states/comments.dart b/lib/comments/states/comments.dart index 9f1ba644..9c653922 100644 --- a/lib/comments/states/comments.dart +++ b/lib/comments/states/comments.dart @@ -41,7 +41,8 @@ class CommentsCubit extends Cubit { state = this.state.copyWith(); try { - VideoComments comments = await service.getComments(state.video.videoId, continuation: state.continuation, sortBy: state.sortBy, source: state.source); + VideoComments comments = await service.getComments(state.video.videoId, + continuation: state.continuation, sortBy: state.sortBy, source: state.source); state.comments = comments; state.loadingComments = false; state.continuation = comments.continuation; @@ -75,5 +76,6 @@ class CommentsState { comments = VideoComments(0, video.videoId, '', []); } - CommentsState._(this.video, this.loadingComments, this.comments, this.continuationLoaded, this.continuation, this.error, this.source, this.sortBy); + CommentsState._(this.video, this.loadingComments, this.comments, this.continuationLoaded, this.continuation, + this.error, this.source, this.sortBy); } diff --git a/lib/comments/views/components/comment.dart b/lib/comments/views/components/comment.dart index 588db97c..4bac6e23 100644 --- a/lib/comments/views/components/comment.dart +++ b/lib/comments/views/components/comment.dart @@ -1,14 +1,13 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:invidious/channels/views/screens/channel.dart'; import 'package:invidious/comments/states/single_comment.dart'; import 'package:invidious/comments/views/components/comments.dart'; -import 'package:invidious/myRouteObserver.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/utils/views/components/text_linkified.dart'; import 'package:invidious/videos/views/components/video_thumbnail.dart'; -import '../../../main.dart'; import '../../../player/states/player.dart'; import '../../../utils/models/image_object.dart'; import '../../../videos/models/base_video.dart'; @@ -21,7 +20,7 @@ class SingleCommentView extends StatelessWidget { const SingleCommentView({super.key, required this.comment, required this.video}); openChannel(BuildContext context, String authorId) { - navigatorKey.currentState?.push(MaterialPageRoute(settings: ROUTE_CHANNEL, builder: (context) => ChannelView(channelId: authorId))); + AutoRouter.of(context).push(ChannelRoute(channelId: authorId)); } @override @@ -72,7 +71,8 @@ class SingleCommentView extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Container( - decoration: BoxDecoration(color: colors.primaryContainer, borderRadius: BorderRadius.circular(20)), + decoration: BoxDecoration( + color: colors.primaryContainer, borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8), child: Row( diff --git a/lib/comments/views/components/comments.dart b/lib/comments/views/components/comments.dart index cb13ba2c..fb65b31d 100644 --- a/lib/comments/views/components/comments.dart +++ b/lib/comments/views/components/comments.dart @@ -19,7 +19,8 @@ class CommentsView extends StatelessWidget { var locals = AppLocalizations.of(context)!; var textTheme = Theme.of(context).textTheme; return BlocProvider( - create: (context) => CommentsCubit(CommentsState(video: video, sortBy: sortBy, source: source, continuation: continuation)), + create: (context) => + CommentsCubit(CommentsState(video: video, sortBy: sortBy, source: source, continuation: continuation)), child: BlocBuilder(builder: (context, _) { var cubit = context.read(); List widgets = []; diff --git a/lib/comments/views/components/comments_container.dart b/lib/comments/views/components/comments_container.dart index 159a67d6..1ae66ce9 100644 --- a/lib/comments/views/components/comments_container.dart +++ b/lib/comments/views/components/comments_container.dart @@ -71,7 +71,11 @@ class CommentsContainer extends StatelessWidget { */ ], ), - CommentsView(key: ValueKey('comments-${_.sortBy}-${_.source}'), video: video, source: _.source, sortBy: _.sortBy), + CommentsView( + key: ValueKey('comments-${_.sortBy}-${_.source}'), + video: video, + source: _.source, + sortBy: _.sortBy), ], ); }, diff --git a/lib/database.dart b/lib/database.dart index dc4c3123..e49ecb01 100644 --- a/lib/database.dart +++ b/lib/database.dart @@ -1,5 +1,7 @@ import 'package:easy_debounce/easy_debounce.dart'; import 'package:invidious/home/models/db/home_layout.dart'; +import 'package:invidious/notifications/models/db/channel_notifications.dart'; +import 'package:invidious/notifications/models/db/subscription_notifications.dart'; import 'package:invidious/search/models/db/searchHistoryItem.dart'; import 'package:invidious/settings/models/db/settings.dart'; import 'package:invidious/settings/models/errors/noServerSelected.dart'; @@ -11,6 +13,7 @@ import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'downloads/models/downloaded_video.dart'; +import 'notifications/models/db/playlist_notifications.dart'; import 'objectbox.g.dart'; // created by `flutter pub run build_runner build` import 'settings/models/db/app_logs.dart'; import 'settings/models/db/server.dart'; @@ -45,6 +48,9 @@ const FILL_FULLSCREEN = 'fill-fullscreen'; const APP_LAYOUT = 'app-layout'; const NAVIGATION_BAR_LABEL_BEHAVIOR = 'navigation-bar-label-behavior'; const DISTRACTION_FREE_MODE = 'distraction-free-mode'; +const BACKGROUND_NOTIFICATIONS = 'background-notifications'; +const SUBSCRIPTION_NOTIFICATIONS = 'subscriptions-notifications'; +const BACKGROUND_CHECK_FREQUENCY = "background-check-frequency"; const ON_OPEN = "on-open"; @@ -61,10 +67,22 @@ class DbClient { static Future create() async { final docsDir = await getApplicationDocumentsDirectory(); // Future openStore() {...} is defined in the generated objectbox.g.dart - final store = await openStore(directory: p.join(docsDir.path, "impuc-data")); + var dbPath = p.join(docsDir.path, "impuc-data"); + Store? store; + if (Store.isOpen(dbPath)) { + store = Store.attach(getObjectBoxModel(), dbPath); + } else { + store = await openStore(directory: dbPath); + } return DbClient._create(store); } + bool get isClosed => store.isClosed(); + + close() { + store.close(); + } + Server? getServer(String url) { return store.box().query(Server_.url.equals(url)).build().findFirst(); } @@ -130,7 +148,8 @@ class DbClient { bool isLoggedInToCurrentServer() { var currentlySelectedServer = getCurrentlySelectedServer(); - return (currentlySelectedServer.authToken?.isNotEmpty ?? false) || (currentlySelectedServer.sidCookie?.isNotEmpty ?? false); + return (currentlySelectedServer.authToken?.isNotEmpty ?? false) || + (currentlySelectedServer.sidCookie?.isNotEmpty ?? false); } double getVideoProgress(String videoId) { @@ -159,7 +178,9 @@ class DbClient { } List _getSearchHistory() { - return (store.box().query()..order(SearchHistoryItem_.time, flags: Order.descending)).build().find(); + return (store.box().query()..order(SearchHistoryItem_.time, flags: Order.descending)) + .build() + .find(); } void addToSearchHistory(SearchHistoryItem searchHistoryItem) { @@ -247,4 +268,67 @@ class DbClient { var all = store.box().getAll(); return all.firstOrNull ?? HomeLayout(); } + + SubscriptionNotification? getLastSubscriptionNotification() { + return store.box().getAll().lastOrNull; + } + + void setLastSubscriptionNotification(SubscriptionNotification sub) { + store.box().removeAll(); + store.box().put(sub); + } + + ChannelNotification? getChannelNotification(String channelId) { + return store.box().query(ChannelNotification_.channelId.equals(channelId)).build().findFirst(); + } + + List getAllChannelNotifications() { + return store.box().getAll(); + } + + void deleteChannelNotification(ChannelNotification notif) { + store.box().remove(notif.id); + } + + void upsertChannelNotification(ChannelNotification notif) { + store.box().put(notif); + } + + void setChannelNotificationLastViewedVideo(String channelId, String videoId) { + var notif = getChannelNotification(channelId); + if (notif != null) { + notif.lastSeenVideoId = videoId; + notif.timestamp = DateTime.now().millisecondsSinceEpoch; + upsertChannelNotification(notif); + } + } + + PlaylistNotification? getPlaylistNotification(String channelId) { + return store + .box() + .query(PlaylistNotification_.playlistId.equals(channelId)) + .build() + .findFirst(); + } + + List getAllPlaylistNotifications() { + return store.box().getAll(); + } + + void deletePlaylistNotification(PlaylistNotification notif) { + store.box().remove(notif.id); + } + + void upsertPlaylistNotification(PlaylistNotification notif) { + store.box().put(notif); + } + + void setPlaylistNotificationLastViewedVideo(String playlistId, int videoCount) { + var notif = getPlaylistNotification(playlistId); + if (notif != null) { + notif.lastVideoCount = videoCount; + notif.timestamp = DateTime.now().millisecondsSinceEpoch; + upsertPlaylistNotification(notif); + } + } } diff --git a/lib/downloads/states/download_manager.dart b/lib/downloads/states/download_manager.dart index 8ebc9b29..20ac8182 100644 --- a/lib/downloads/states/download_manager.dart +++ b/lib/downloads/states/download_manager.dart @@ -58,7 +58,6 @@ class DownloadManagerCubit extends Cubit { db.upsertDownload(v); } } - print('setting videos'); state.videos = vids; emit(state); @@ -66,7 +65,8 @@ class DownloadManagerCubit extends Cubit { void playAll() { setVideos(); - player.playOfflineVideos(state.videos.where((element) => element.downloadComplete && !element.downloadFailed).toList()); + player.playOfflineVideos( + state.videos.where((element) => element.downloadComplete && !element.downloadFailed).toList()); } onProgress(int count, int total, DownloadedVideo video) { @@ -95,8 +95,14 @@ class DownloadManagerCubit extends Cubit { return false; } else { Video vid = await service.getVideo(videoId); - var downloadedVideo = - DownloadedVideo(videoId: vid.videoId, title: vid.title, author: vid.author, authorUrl: vid.authorUrl, audioOnly: audioOnly, lengthSeconds: vid.lengthSeconds, quality: quality); + var downloadedVideo = DownloadedVideo( + videoId: vid.videoId, + title: vid.title, + author: vid.author, + authorUrl: vid.authorUrl, + audioOnly: audioOnly, + lengthSeconds: vid.lengthSeconds, + quality: quality); db.upsertDownload(downloadedVideo); String contentUrl; @@ -105,7 +111,9 @@ class DownloadManagerCubit extends Cubit { FormatStream stream = vid.formatStreams.firstWhere((element) => element.resolution == quality); contentUrl = stream.url; } else { - AdaptiveFormat audio = vid.adaptiveFormats.sortByReversed((e) => int.parse(e.bitrate ?? "0")).firstWhere((element) => element.type.contains("audio")); + AdaptiveFormat audio = vid.adaptiveFormats + .sortByReversed((e) => int.parse(e.bitrate ?? "0")) + .firstWhere((element) => element.type.contains("audio")); contentUrl = audio.url; } @@ -130,9 +138,11 @@ class DownloadManagerCubit extends Cubit { // download video var videoPath = await downloadedVideo.mediaPath; - log.info("Downloading video ${vid.title}, audioOnly ? $audioOnly, quality: $quality (if not only audio) to path: $videoPath"); + log.info( + "Downloading video ${vid.title}, audioOnly ? $audioOnly, quality: $quality (if not only audio) to path: $videoPath"); dio - .download(contentUrl, videoPath, onReceiveProgress: (count, total) => onProgress(count, total, downloadedVideo), cancelToken: cancelToken) + .download(contentUrl, videoPath, + onReceiveProgress: (count, total) => onProgress(count, total, downloadedVideo), cancelToken: cancelToken) .catchError((err) => onDownloadError(err, downloadedVideo)); return true; diff --git a/lib/downloads/views/components/download_app_bar_button.dart b/lib/downloads/views/components/download_app_bar_button.dart index b8388727..0c4aab12 100644 --- a/lib/downloads/views/components/download_app_bar_button.dart +++ b/lib/downloads/views/components/download_app_bar_button.dart @@ -1,10 +1,9 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:invidious/router.dart'; -import '../../../main.dart'; -import '../../../myRouteObserver.dart'; import '../../states/download_manager.dart'; -import '../screens/download_manager.dart'; class AppBarDownloadButton extends StatelessWidget { const AppBarDownloadButton({Key? key}) : super(key: key); @@ -19,7 +18,7 @@ class AppBarDownloadButton extends StatelessWidget { alignment: Alignment.center, children: [ IconButton( - onPressed: openDownloadManager, + onPressed: () => openDownloadManager(context), icon: Icon( Icons.download, color: _.downloadProgresses.isNotEmpty ? colors.background : null, @@ -27,7 +26,7 @@ class AppBarDownloadButton extends StatelessWidget { ), _.downloadProgresses.isNotEmpty ? InkWell( - onTap: openDownloadManager, + onTap: () => openDownloadManager(context), child: SizedBox( width: 15, height: 15, @@ -42,7 +41,7 @@ class AppBarDownloadButton extends StatelessWidget { right: 1, child: _.videos.isNotEmpty ? GestureDetector( - onTap: openDownloadManager, + onTap: () => openDownloadManager(context), child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration(color: colors.secondaryContainer, shape: BoxShape.circle), @@ -57,7 +56,7 @@ class AppBarDownloadButton extends StatelessWidget { ); } - void openDownloadManager() { - navigatorKey.currentState?.push(MaterialPageRoute(settings: ROUTE_DOWNLOAD_MANAGER, builder: (context) => const DownloadManager())); + void openDownloadManager(BuildContext context) { + AutoRouter.of(context).push(const DownloadManagerRoute()); } } diff --git a/lib/downloads/views/components/downloaded_video.dart b/lib/downloads/views/components/downloaded_video.dart index c21784a7..05958565 100644 --- a/lib/downloads/views/components/downloaded_video.dart +++ b/lib/downloads/views/components/downloaded_video.dart @@ -62,7 +62,8 @@ class DownloadedVideoView extends StatelessWidget { right: 10, bottom: 5, child: Container( - decoration: BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(20)), + decoration: BoxDecoration( + color: colors.secondaryContainer, borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.all(4), child: Row( diff --git a/lib/downloads/views/screens/download_manager.dart b/lib/downloads/views/screens/download_manager.dart index 96a1fcee..2008be52 100644 --- a/lib/downloads/views/screens/download_manager.dart +++ b/lib/downloads/views/screens/download_manager.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -7,8 +8,9 @@ import 'package:invidious/globals.dart'; import '../../states/download_manager.dart'; -class DownloadManager extends StatelessWidget { - const DownloadManager({Key? key}) : super(key: key); +@RoutePage() +class DownloadManagerScreen extends StatelessWidget { + const DownloadManagerScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/extensions.dart b/lib/extensions.dart index b1671f57..dfdcf4eb 100644 --- a/lib/extensions.dart +++ b/lib/extensions.dart @@ -1,5 +1,6 @@ extension Iterables on Iterable { - Map> groupBy(K Function(E) keyFunction) => fold(>{}, (Map> map, E element) => map..putIfAbsent(keyFunction(element), () => []).add(element)); + Map> groupBy(K Function(E) keyFunction) => fold(>{}, + (Map> map, E element) => map..putIfAbsent(keyFunction(element), () => []).add(element)); Iterable sortBy(Comparable Function(E e) key) => toList()..sort((a, b) => key(a).compareTo(key(b))); diff --git a/lib/foreground_service.dart b/lib/foreground_service.dart new file mode 100644 index 00000000..555eb61e --- /dev/null +++ b/lib/foreground_service.dart @@ -0,0 +1,231 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_background_service/flutter_background_service.dart'; +import 'package:flutter_background_service_android/flutter_background_service_android.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:http/http.dart'; +import 'package:intl/intl.dart'; +import 'package:invidious/database.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/notifications/models/db/subscription_notifications.dart'; +import 'package:invidious/settings/states/settings.dart'; +import 'package:invidious/videos/models/video_in_list.dart'; +import 'package:logging/logging.dart'; + +import 'notifications/notifications.dart'; + +const restartTimerMethod = 'restart-timer'; + +final backgroundService = FlutterBackgroundService(); + +final log = Logger('Background service'); + +const debugMode = kDebugMode; +// const debugMode = true; + +Timer? timer; + +void configureBackgroundService(SettingsCubit settings) async { + var notif = NotificationTypes.foregroundService; + + var locals = await getLocalization(); + + await backgroundService.configure( + iosConfiguration: IosConfiguration(), + androidConfiguration: AndroidConfiguration( + onStart: onStart, + autoStart: settings.state.backgroundNotifications, + autoStartOnBoot: settings.state.backgroundNotifications, + isForegroundMode: true, + foregroundServiceNotificationId: notif.idSpace, + initialNotificationTitle: locals.foregroundServiceNotificationTitle, + initialNotificationContent: locals.foregroundServiceNotificationContent(refreshRate), + notificationChannelId: notif.id)); +} + +String get refreshRate => db.getSettings(BACKGROUND_CHECK_FREQUENCY)?.value ?? "1"; + +@pragma('vm:entry-point') +onStart(ServiceInstance service) async { + print("Background service started"); + + DartPluginRegistrant.ensureInitialized(); + + if (service is AndroidServiceInstance) { + service.on('setAsForeground').listen((event) { + service.setAsForegroundService(); + }); + + service.on('setAsBackground').listen((event) { + service.setAsBackgroundService(); + }); + } + + service.on('stopService').listen((event) { + print('foreground service stopped'); + service.stopSelf(); + }); + + service.on(restartTimerMethod).listen((event) async { + await _restartTimer(); + }); + + _restartTimer(); +} + +_restartTimer() async { + print('setting background timer'); + db = await DbClient.create(); + var locals = await getLocalization(); + var title = locals.foregroundServiceNotificationTitle; + sendNotification(title, locals.foregroundServiceNotificationContent(refreshRate), type: NotificationTypes.foregroundService); + timer?.cancel(); + timer = Timer.periodic(debugMode ? const Duration(seconds: 60) : Duration(hours: int.parse(refreshRate)), (timer) { + print('foreground service running'); + _backgroundCheck(); + }); + + db.close(); +} + +_backgroundCheck() async { + try { + db = await DbClient.create(); + + var locals = await getLocalization(); + var title = locals.foregroundServiceNotificationTitle; + print('we have a db ${db.isClosed}'); + sendNotification(title, locals.foregroundServiceUpdatingSubscriptions, type: NotificationTypes.foregroundService); + await _handleSubscriptionsNotifications(); + sendNotification(title, locals.foregroundServiceUpdatingChannels, type: NotificationTypes.foregroundService); + await _handleChannelNotifications(); + sendNotification(title, locals.foregroundServiceUpdatingPlaylist, type: NotificationTypes.foregroundService); + await _handlePlaylistNotifications(); + sendNotification(title, locals.foregroundServiceNotificationContent(refreshRate), type: NotificationTypes.foregroundService); + } catch (e) { + print('we have a background service error: ${e}'); + } finally { + db.close(); + } +} + +Future getLocalization() async { + List? localeString; + String dbLocale = db.getSettings(LOCALE)?.value ?? Intl.getCurrentLocale(); + localeString = dbLocale.split('_'); + + print('Locale to use: $dbLocale'); + Locale locale = Locale.fromSubtags(languageCode: localeString[0], scriptCode: localeString.length >= 2 ? localeString[1] : null); + + return await AppLocalizations.delegate.load(locale); +} + +_handlePlaylistNotifications() async { + var notifs = db.getAllPlaylistNotifications(); + print('Watching ${notifs.length} playlists'); + for (var n in notifs) { + // we get the latest video, + var videos = await service.getPublicPlaylists(n.playlistId, saveLastSeen: false); + + if ((videos.videos ?? []).isNotEmpty) { + if (n.lastVideoCount > 0) { + // if in list, we calculate + int videosToNotifyAbout = n.lastVideoCount - videos.videoCount; + + // if not we tell that list.size+ new videos are available + var locals = await getLocalization(); + + print('$videosToNotifyAbout videos from playlist ${n.playlistName} to notify about'); + if (debugMode || videosToNotifyAbout > 0) { + sendNotification(locals.playlistNotificationTitle(n.playlistName), locals.playlistNotificationContent(n.playlistName, videosToNotifyAbout), + type: NotificationTypes.playlist, + payload: { + playlistId: n.playlistId, + }, + id: n.id); + } + } + } + } +} + +_handleChannelNotifications() async { + var notifs = db.getAllChannelNotifications(); + + print('Watching ${notifs.length} channels'); + for (var n in notifs) { + // we get the latest video, + var videos = await service.getChannelVideos(n.channelId, null, saveLastSeen: false); + + if ((videos.videos ?? []).isNotEmpty) { + if (n.lastSeenVideoId.isNotEmpty) { + // if in list, we calculate + int videosToNotifyAbout = 0; + + int index = videos.videos.indexWhere((element) => element.videoId == n.lastSeenVideoId); + + if (index >= 0) { + videosToNotifyAbout = index; + } else { + videosToNotifyAbout = videos.videos.length; + } + + // if not we tell that list.size+ new videos are available + var locals = await getLocalization(); + + print('$videosToNotifyAbout videos from channel ${n.channelName} to notify about'); + if (debugMode || videosToNotifyAbout > 0) { + sendNotification(locals.channelNotificationTitle(n.channelName), locals.channelNotificationContent(n.channelName, videosToNotifyAbout), + type: NotificationTypes.channel, payload: {channelId: n.channelId, lastSeenVideo: videos.videos.first.videoId}, id: n.id); + } + } + } + } +} + +_handleSubscriptionsNotifications() async { + bool isEnabled = db.getSettings(SUBSCRIPTION_NOTIFICATIONS)?.value == 'true'; + if (isEnabled && db.isLoggedInToCurrentServer()) { + // we need to get the last notification before we call the feed endpoint as it is going to save the last seen video + final lastNotification = db.getLastSubscriptionNotification(); + print('getting feed...'); + var feed = await service.getUserFeed(maxResults: 100, saveLastSeen: false); + + List videos = []; + videos.addAll(feed.notifications ?? []); + videos.addAll(feed.videos ?? []); + + print('we have a feed with ${videos.length} videos'); + + if (videos.isNotEmpty) { + // we don't send notification for the first run ever to avoid weird behavior + if (lastNotification == null) { + var toSave = SubscriptionNotification(videos.last.videoId, DateTime.now().millisecondsSinceEpoch); + db.setLastSubscriptionNotification(toSave); + print('first time run'); + } else { + late int videosToNotifyAbout; + int index = videos.indexWhere((element) => element.videoId == lastNotification.lastSeenVideoId); + //more than 100 videos + if (index == -1) { + videosToNotifyAbout = videos.length; + } else { + videosToNotifyAbout = index; + } + + var locals = await getLocalization(); + + print('$videosToNotifyAbout videos to notify about'); + if (debugMode || videosToNotifyAbout > 0) { + sendNotification(locals.subscriptionNotificationTitle, locals.subscriptionNotificationContent(videosToNotifyAbout), + type: NotificationTypes.subscription, payload: {lastSeenVideo: videos.first.videoId}); + } + } + } + } else { + print('Subscription notifications not enabled'); + } +} diff --git a/lib/home/models/db/home_layout.dart b/lib/home/models/db/home_layout.dart index 9df5bb0e..e386fe4b 100644 --- a/lib/home/models/db/home_layout.dart +++ b/lib/home/models/db/home_layout.dart @@ -48,7 +48,8 @@ enum HomeDataSource { bool isPermitted(BuildContext context) { return switch (this) { - (HomeDataSource.subscription || HomeDataSource.playlist || HomeDataSource.history) => context.read().isLoggedIn, + (HomeDataSource.subscription || HomeDataSource.playlist || HomeDataSource.history) => + context.read().isLoggedIn, (HomeDataSource.searchHistory) => context.read().state.useSearchHistory, (_) => true }; @@ -58,12 +59,16 @@ enum HomeDataSource { var locals = AppLocalizations.of(context)!; return switch (this) { (HomeDataSource.trending) => NavigationDestination(icon: const Icon(Icons.trending_up), label: getLabel(locals)), - (HomeDataSource.popular) => NavigationDestination(icon: const Icon(Icons.local_fire_department), label: getLabel(locals)), - (HomeDataSource.playlist) => NavigationDestination(icon: const Icon(Icons.playlist_play), label: getLabel(locals)), + (HomeDataSource.popular) => + NavigationDestination(icon: const Icon(Icons.local_fire_department), label: getLabel(locals)), + (HomeDataSource.playlist) => + NavigationDestination(icon: const Icon(Icons.playlist_play), label: getLabel(locals)), (HomeDataSource.history) => NavigationDestination(icon: const Icon(Icons.history), label: getLabel(locals)), (HomeDataSource.downloads) => NavigationDestination(icon: const Icon(Icons.download), label: getLabel(locals)), - (HomeDataSource.searchHistory) => NavigationDestination(icon: const Icon(Icons.saved_search), label: getLabel(locals)), - (HomeDataSource.subscription) => NavigationDestination(icon: const Icon(Icons.subscriptions), label: getLabel(locals)), + (HomeDataSource.searchHistory) => + NavigationDestination(icon: const Icon(Icons.saved_search), label: getLabel(locals)), + (HomeDataSource.subscription) => + NavigationDestination(icon: const Icon(Icons.subscriptions), label: getLabel(locals)), (HomeDataSource.home) => NavigationDestination(icon: const Icon(Icons.home), label: getLabel(locals)), }; } @@ -137,9 +142,10 @@ enum HomeDataSource { paginatedVideoList: PageBasedPaginatedList( getItemsFunc: (page, maxResults) => // we get the data for each video - service - .getUserHistory(page, maxResults) - .then((value) => Future.wait(value.map((e) async => (await HistoryVideoCache.fromVideoIdToVideo(e)).toBaseVideo().toVideoInList()).toList())), + service.getUserHistory(page, maxResults).then((value) => Future.wait(value + .map((e) async => + (await HistoryVideoCache.fromVideoIdToVideo(e)).toBaseVideo().toVideoInList()) + .toList())), maxResults: 20), ) : const HistoryView(), @@ -177,6 +183,8 @@ class HomeLayout { List get dbSmallSources => smallSources.map((e) => e.name).toList(); set dbSmallSources(List values) { - smallSources = values.map((e) => HomeDataSource.values.where((element) => element.name == e).firstOrNull ?? HomeDataSource.trending).toList(); + smallSources = values + .map((e) => HomeDataSource.values.where((element) => element.name == e).firstOrNull ?? HomeDataSource.trending) + .toList(); } } diff --git a/lib/home/states/edit_layout.dart b/lib/home/states/edit_layout.dart index 42322c2d..8b60500f 100644 --- a/lib/home/states/edit_layout.dart +++ b/lib/home/states/edit_layout.dart @@ -17,7 +17,9 @@ class EditLayoutCubit extends Cubit { var state = this.state.copyWith(); if (state.smallSources.length < maxSmallSources) { - state.smallSources.add(HomeDataSource.values.where((element) => element.small).firstWhere((e) => e != state.bigSource && !state.smallSources.contains(e))); + state.smallSources.add(HomeDataSource.values + .where((element) => element.small) + .firstWhere((e) => e != state.bigSource && !state.smallSources.contains(e))); } state.smallSources = List.of(state.smallSources); emit(state); diff --git a/lib/home/views/components/home.dart b/lib/home/views/components/home.dart index 706c8a26..88b5156f 100644 --- a/lib/home/views/components/home.dart +++ b/lib/home/views/components/home.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -5,9 +6,8 @@ import 'package:invidious/app/states/app.dart'; import 'package:invidious/globals.dart'; import 'package:invidious/home/models/db/home_layout.dart'; import 'package:invidious/home/states/home.dart'; +import 'package:invidious/router.dart'; -import '../../../main.dart'; -import '../../../search/views/screens/search.dart'; import '../../../utils/views/components/app_icon.dart'; const double smallVideoViewHeight = 140; @@ -16,12 +16,11 @@ class HomeView extends StatelessWidget { const HomeView({super.key}); static openSearch(BuildContext context, String search) { - navigatorKey.currentState - ?.push(MaterialPageRoute( - builder: (context) => Search( - query: search, - searchNow: true, - ))) + AutoRouter.of(context) + .push(SearchRoute( + query: search, + searchNow: true, + )) .then((value) => context.read().updateLayout()); } @@ -68,7 +67,9 @@ class HomeView extends StatelessWidget { return NotificationListener( onNotification: (notificationInfo) { - if (notificationInfo is ScrollUpdateNotification && (notificationInfo.metrics.axisDirection == AxisDirection.down || notificationInfo.metrics.axisDirection == AxisDirection.up)) { + if (notificationInfo is ScrollUpdateNotification && + (notificationInfo.metrics.axisDirection == AxisDirection.down || + notificationInfo.metrics.axisDirection == AxisDirection.up)) { home.setScroll(notificationInfo.metrics.pixels > 100); } return true; @@ -93,7 +94,8 @@ class HomeView extends StatelessWidget { secondCurve: Curves.easeInOutQuad, sizeCurve: Curves.easeInOutQuad, duration: animationDuration, - firstChild: Column(mainAxisSize: MainAxisSize.min, children: getSmallSources(context, layout)), + firstChild: + Column(mainAxisSize: MainAxisSize.min, children: getSmallSources(context, layout)), secondChild: const Row( children: [ SizedBox.shrink(), diff --git a/lib/home/views/screens/edit_layout.dart b/lib/home/views/screens/edit_layout.dart index c7edb933..b9d1c3b0 100644 --- a/lib/home/views/screens/edit_layout.dart +++ b/lib/home/views/screens/edit_layout.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -8,8 +9,9 @@ import 'package:invidious/utils/views/components/placeholders.dart'; import '../../../utils.dart'; -class EditHomeLayout extends StatelessWidget { - const EditHomeLayout({super.key}); +@RoutePage() +class EditHomeLayoutScreen extends StatelessWidget { + const EditHomeLayoutScreen({super.key}); List buildSmallSources(BuildContext context, HomeLayout layout, EditLayoutCubit editLayout) { var widgets = []; diff --git a/lib/home/views/screens/home.dart b/lib/home/views/screens/home.dart new file mode 100644 index 00000000..cded82d5 --- /dev/null +++ b/lib/home/views/screens/home.dart @@ -0,0 +1,193 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:back_button_interceptor/back_button_interceptor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/app/states/app.dart'; +import 'package:invidious/downloads/views/components/download_app_bar_button.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/home/models/db/home_layout.dart'; +import 'package:invidious/router.dart'; +import 'package:invidious/settings/states/settings.dart'; +import 'package:invidious/utils.dart'; +import 'package:invidious/utils/views/components/app_icon.dart'; + +import '../../../main.dart'; +import '../../../notifications/notifications.dart'; + +@RoutePage() +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + openSettings(BuildContext context) { + AutoRouter.of(context).push(const SettingsRoute()); + } + + openSubscriptionManagement(BuildContext context) { + AutoRouter.of(context).push(const ManageSubscriptionsRoute()); + } + + openLayoutEditor(BuildContext context) { + var app = context.read(); + AutoRouter.of(context).push(const EditHomeLayoutRoute()).then((value) => app.updateLayout()); + } + + @override + void initState() { + super.initState(); + BackButtonInterceptor.add((stopDefaultButtonEvent, RouteInfo routeInfo) { + var currentRoute = routeInfo.currentRoute(context); + var settings2 = currentRoute?.settings; + if (settings2?.name != 'HomeRoute') { + AutoRouter.of(context).pop(); + // navigatorKey.currentState?.pop(); + return true; + } else { + return false; + } + }, name: 'mainNavigator', zIndex: 0, ifNotYetIntercepted: true); + + // Only after at least the action method is set, the notification events are delivered + AwesomeNotifications().setListeners( + onActionReceivedMethod: NotificationController.onActionReceivedMethod, + onNotificationCreatedMethod: NotificationController.onNotificationCreatedMethod, + onNotificationDisplayedMethod: NotificationController.onNotificationDisplayedMethod, + onDismissActionReceivedMethod: NotificationController.onDismissActionReceivedMethod); + } + + @override + void dispose() { + BackButtonInterceptor.removeByName('mainNavigator'); + super.dispose(); + } + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + var locals = AppLocalizations.of(context)!; + + return BlocBuilder(buildWhen: (previous, current) { + return previous.selectedIndex != current.selectedIndex || previous.server != current.server; + }, builder: (context, _) { + var app = context.read(); + var settings = context.watch().state; + + var allowedPages = settings.appLayout.where((element) => element.isPermitted(context)).toList(); + var navigationWidgets = allowedPages.map((e) => e.getBottomBarNavigationWidget(context)).toList(); + + var selectedIndex = _.selectedIndex; + if (selectedIndex >= allowedPages.length) { + selectedIndex = 0; + } + + HomeDataSource? selectedPage; + if (selectedIndex < allowedPages.length) { + selectedPage = allowedPages[selectedIndex]; + } + + return Scaffold( + key: ValueKey(_.server?.url), + // so we rebuild the view if the server changes + backgroundColor: colorScheme.background, + bottomNavigationBar: allowedPages.length >= 2 + ? NavigationBar( + backgroundColor: colorScheme.background, + labelBehavior: settings.navigationBarLabelBehavior, + elevation: 0, + onDestinationSelected: app.selectIndex, + selectedIndex: selectedIndex, + destinations: navigationWidgets, + ) + : null, + appBar: AppBar( + systemOverlayStyle: getUiOverlayStyle(context), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(selectedPage?.getLabel(locals) ?? 'Clipious'), + if (selectedPage == HomeDataSource.home) + IconButton( + iconSize: 15, + onPressed: () => openLayoutEditor(context), + icon: Icon( + Icons.edit, + color: colorScheme.secondary, + )) + ], + ), + scrolledUnderElevation: 0, + // backgroundColor: Colors.pink, + backgroundColor: colorScheme.background, + actions: [ + selectedPage == HomeDataSource.subscription ? IconButton(onPressed: () => openSubscriptionManagement(context), icon: const Icon(Icons.checklist)) : const SizedBox.shrink(), + const AppBarDownloadButton(), + IconButton( + onPressed: () { + AutoRouter.of(context).push(SearchRoute()); + }, + icon: const Icon(Icons.search), + ), + IconButton( + onPressed: () => openSettings(context), + icon: const Icon(Icons.settings), + ), + ], + ), + body: SafeArea( + bottom: false, + child: Stack(children: [ + Positioned( + top: 0, + left: 0, + right: 0, + bottom: 0, + child: AnimatedSwitcher( + switchInCurve: Curves.easeInOutQuad, + switchOutCurve: Curves.easeInOutQuad, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition(opacity: animation, child: child); + }, + duration: animationDuration, + child: Container( + // 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( + height: 200, + ))), +/* + child: [ + const HomeView( + key: ValueKey(0), + ), + const Trending( + key: ValueKey(1), + ), + const Subscriptions( + key: ValueKey(2), + ), + const AddToPlaylistList( + key: ValueKey(3), + canDeleteVideos: true, + ), + const HistoryView( + key: ValueKey(4), + ), + ][_.selectedIndex], +*/ + ), + ) + ]))); + }); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 171ad408..6f9211ad 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1007,6 +1007,164 @@ } } }, + "notifications": "Notifications", + "@notifications": { + "description": "Notification settings title" + }, + "notificationsDescription": "Enable and review what you are notified about", + "@notificationsDescription": { + "description": "Setting description for notifications" + }, + "enableNotificationDescriptions": "Runs foreground service to check and notify you on the changes you are monitoring", + "@enableNotificationDescriptions": { + "description": "" + }, + "subscriptionNotification": "Subscription notifications", + "@subscriptionNotification": { + "description": "Title for subscriptions notifications" + }, + "subscriptionNotificationDescription": "Get notified of new videos from your subscription feed if you are logged in to your current instance", + "@subscriptionNotificationDescription": { + "description": "Description for subscription notifications" + }, + "subscriptionNotificationTitle": "New videos from your subscriptions", + "@subscriptionNotificationTitle": { + "description": "Title for the notification showing that there are new videos from the subscription feed" + }, + "subscriptionNotificationContent": "There are {count, plural, =0{no new videos} =1{1 new video} other{{count} new videos}} in your subscription feed", + "@subscriptionNotificationContent": { + "description": "Content for subscription notification", + "placeholders": { + "count": { + "type": "num", + "format": "compact" + } + } + }, + "askForDisableBatteryOptimizationTitle": "Disabling battery optimization required", + "@askForDisableBatteryOptimizationTitle": { + "description": "Title for the dialog asking the user to turn off disabling battery optimization when turning on notifications" + }, + "askForDisableBatteryOptimizationContent": "In order to send notification Clipious needs to run a background service. For it to run smoothly it is required that Clipious is given unrestricted battery usage, tapping ok will open the battery optimization settings.", + "@askForDisableBatteryOptimizationContent": { + "description": "Content for the dialog asking the user to turn off disabling battery optimization when turning on notifications" + }, + "askToEnableBackgroundServiceTitle": "Notifications turned off", + "@askToEnableBackgroundServiceTitle": { + "description": "If the users tries to turn on notifications for a channel but hasn't enable notifications in the app we need to turn it on for them" + }, + "askToEnableBackgroundServiceContent": "To get notifications, Clipious notifications need to be enabled, press OK to enable it.", + "@askToEnableBackgroundServiceContent": { + "description": "If the users tries to turn on notifications for a channel but hasn't enable notifications in the app we need to turn it on for them" + }, + "otherNotifications": "Other notifications sources (bell icons)", + "@otherNotifications": { + "description": "Title for settings section in the notification settings" + }, + "deleteChannelNotificationTitle": "Delete channel notification ?", + "@deleteChannelNotificationTitle": { + "description": "Title for dialog to confirm whether to delete channel notifications" + }, + "deleteChannelNotificationContent": "You won''t receive anymore notifications from this channel.", + "@deleteChannelNotificationContent": { + "description": "Title for dialog to confirm whether to delete channel notifications" + }, + "deletePlaylistNotificationTitle": "Delete playlist notification ?", + "@deletePlaylistNotificationTitle": { + "description": "Title for dialog to confirm whether to delete playlist notifications" + }, + "deletePlaylistNotificationContent": "You won''t receive anymore notifications from this playlist.", + "@deletePlaylistNotificationContent": { + "description": "Title for dialog to confirm whether to delete playlist notifications" + }, + "channelNotificationTitle": "New videos from {channel}", + "@channelNotificationTitle": { + "description": "Title for the channel notifications when there are new videos", + "placeholders": { + "channel": { + "type": "String", + "example": "MKBHD" + } + } + }, + "channelNotificationContent": "There are {count, plural, =0{no new videos} =1{1 new video} other{{count} new videos}} from {channel}", + "@channelNotificationContent": { + "description": "Content for channel notification when there are new videos", + "placeholders": { + "channel": { + "type": "String", + "example": "MKBHD" + }, + "count": { + "type": "num", + "format": "compact" + } + } + }, + "playlistNotificationTitle": "New videos in {playlist} playlist", + "@playlistNotificationTitle": { + "description": "Title for the playlist notifications when there are new videos", + "placeholders": { + "playlist": { + "type": "String", + "example": "Lo-Fi girl" + } + } + }, + "playlistNotificationContent": "There are {count, plural, =0{no new videos} =1{1 new video} other{{count} new videos}} in the {playlist} playlist", + "@playlistNotificationContent": { + "description": "Content for playlist notification when there are new videos", + "placeholders": { + "playlist": { + "type": "String", + "example": "Lo-Fi girl" + }, + "count": { + "type": "num", + "format": "compact" + } + } + }, + "foregroundServiceNotificationTitle": "Video monitoring", + "@foregroundServiceNotificationTitle": { + "description": "Title for the foreground service running notification when the user wants to receive notifications" + }, + "foregroundServiceNotificationContent": "Will check for new videos once {hours, select, 1{per hour} 24{a day} other{every {hours} hours}}", + "@foregroundServiceNotificationContent": { + "description": "Content for the foreground service running notification when the user wants to receive notifications", + "hours": { + "type": "num", + "format": "compact" + } + }, + "foregroundServiceUpdatingSubscriptions": "Checking subscriptions...", + "@foregroundServiceUpdatingSubscriptions": { + "description": "Foreground service notification text when checking for new subscription videos" + }, + "foregroundServiceUpdatingPlaylist": "Checking playlists...", + "@foregroundServiceUpdatingPlaylist": { + "description": "Foreground service notification text when checking for new playlist videos" + }, + "foregroundServiceUpdatingChannels": "Checking channels...", + "@foregroundServiceUpdatingChannels": { + "description": "Foreground service notification text when checking for new channel videos" + }, + "notificationFrequencySettingsTitle": "New video check frequency", + "@notificationFrequencySettingsTitle": { + "description": "Title for frequency settings" + }, + "notificationFrequencySettingsDescription": "How often the application will check for new videos", + "@notificationFrequencySettingsDescription": { + "description": "Description for frequency settings" + }, + "notificationFrequencySliderLabel": "{hours, select, 24{1d} other{{hours}h}}", + "@notificationFrequencySliderLabel": { + "description": "Short form for a number of hours going up to 1 day", + "hours": { + "type": "num", + "format": "compact" + } + }, "history": "History", "@history": { "description": "User view history label" diff --git a/lib/main.dart b/lib/main.dart index 69bc802a..a59d086b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,47 +1,31 @@ import 'dart:async'; import 'dart:io'; -import 'package:after_layout/after_layout.dart'; -import 'package:back_button_interceptor/back_button_interceptor.dart'; +import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/app/states/app.dart'; -import 'package:invidious/app/views/tv/screens/tv_home.dart'; -import 'package:invidious/channels/views/screens/channel.dart'; import 'package:invidious/downloads/states/download_manager.dart'; -import 'package:invidious/downloads/views/components/download_app_bar_button.dart'; +import 'package:invidious/foreground_service.dart'; import 'package:invidious/globals.dart'; -import 'package:invidious/home/models/db/home_layout.dart'; -import 'package:invidious/home/views/screens/edit_layout.dart'; import 'package:invidious/httpOverrides.dart'; import 'package:invidious/mediaHander.dart'; +import 'package:invidious/notifications/notifications.dart'; import 'package:invidious/player/states/player.dart'; -import 'package:invidious/player/views/components/mini_player_aware.dart'; -import 'package:invidious/player/views/components/player.dart'; -import 'package:invidious/search/views/screens/search.dart'; +import 'package:invidious/router.dart'; 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'; import 'package:logging/logging.dart'; import 'database.dart'; -import 'myRouteObserver.dart'; import 'settings/models/db/app_logs.dart'; const brandColor = Color(0xFF4f0096); final scaffoldKey = GlobalKey(); -final GlobalKey navigatorKey = GlobalKey(); -final GlobalKey globalNavigator = GlobalKey(); bool isTv = false; late MediaHandler mediaHandler; @@ -54,21 +38,36 @@ Future main() async { debugPrint('[${record.level.name}] [${record.loggerName}] ${record.message}'); // we don't want debug if (record.level == Level.INFO || record.level == Level.SEVERE) { - db.insertLogs(AppLog(logger: record.loggerName, level: record.level.name, time: record.time, message: record.message, stacktrace: record.stackTrace?.toString())); + db.insertLogs(AppLog( + logger: record.loggerName, + level: record.level.name, + time: record.time, + message: record.message, + stacktrace: record.stackTrace?.toString())); } }); HttpOverrides.global = MyHttpOverrides(); WidgetsFlutterBinding.ensureInitialized(); - isTv = await isDeviceTv(); + // FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); db = await DbClient.create(); + + initializeNotifications(); + var initialNotification = await AwesomeNotifications().getInitialNotificationAction(); + print('Initial notification ${initialNotification?.payload}'); + + isTv = await isDeviceTv(); runApp(MultiBlocProvider(providers: [ BlocProvider( create: (context) => AppCubit(AppState()), ), BlocProvider( - create: (context) => SettingsCubit(SettingsState(), context.read()), + create: (context) { + var settingsCubit = SettingsCubit(SettingsState(), context.read()); + configureBackgroundService(settingsCubit); + return settingsCubit; + }, ), BlocProvider( create: (context) => PlayerCubit(PlayerState(), context.read()), @@ -87,17 +86,13 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( - buildWhen: (previous, current) => previous.selectedIndex == current.selectedIndex || previous.server != current.server, + buildWhen: (previous, current) => + previous.selectedIndex == current.selectedIndex || previous.server != current.server, // we want to rebuild only when anything other than the navigation index is changed builder: (context, _) { var app = context.read(); var settings = context.read(); bool useDynamicTheme = settings.state.useDynamicTheme; - bool showWizard = false; - - if (app.state.server == null) { - showWizard = true; - } return DynamicColorBuilder(builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { ColorScheme lightColorScheme; @@ -140,267 +135,63 @@ class MyApp extends StatelessWidget { } log.fine('locale from db ${db.getSettings(LOCALE)?.value} from cubit: ${dbLocale}, ${localeString}'); - Locale? savedLocale = localeString != null ? Locale.fromSubtags(languageCode: localeString[0], scriptCode: localeString.length >= 2 ? localeString[1] : null) : null; - - return MaterialApp( - locale: savedLocale, - localizationsDelegates: AppLocalizations.localizationsDelegates, - localeListResolutionCallback: (locales, supportedLocales) { - log.info('device locales=$locales supported locales=$supportedLocales, saved: $savedLocale'); - if (savedLocale != null) { - log.info("using saved locale, $savedLocale"); - return savedLocale; - } - if (locales != null) { - for (Locale locale in locales) { - // if device language is supported by the app, - // just return it to set it as current app language - if (supportedLocales.contains(locale)) { - log.info("Locale match found, $locale"); - return locale; - } else { - Locale? match = supportedLocales.where((element) => element.languageCode == locale.languageCode).firstOrNull; - if (match != null) { - log.info("found partial match $locale with $match"); - return match; - } + Locale? savedLocale = localeString != null + ? Locale.fromSubtags( + languageCode: localeString[0], scriptCode: localeString.length >= 2 ? localeString[1] : null) + : null; + + return MaterialApp.router( + routerConfig: appRouter.config( + navigatorObservers: () => [MyRouteObserver()], + ), + locale: savedLocale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + localeListResolutionCallback: (locales, supportedLocales) { + log.info('device locales=$locales supported locales=$supportedLocales, saved: $savedLocale'); + if (savedLocale != null) { + log.info("using saved locale, $savedLocale"); + return savedLocale; + } + if (locales != null) { + for (Locale locale in locales) { + // if device language is supported by the app, + // just return it to set it as current app language + if (supportedLocales.contains(locale)) { + log.info("Locale match found, $locale"); + return locale; + } else { + Locale? match = + supportedLocales.where((element) => element.languageCode == locale.languageCode).firstOrNull; + if (match != null) { + log.info("found partial match $locale with $match"); + return match; } } } - // if device language is not supported by the app, - // the app will set it to english but return this to set to Bahasa instead - log.info("locale not supported, returning english"); - return const Locale('en', 'US'); - }, - supportedLocales: AppLocalizations.supportedLocales, - scaffoldMessengerKey: scaffoldKey, - navigatorKey: globalNavigator, - debugShowCheckedModeBanner: false, - themeMode: ThemeMode.values.firstWhere((element) => element.name == settings.state.themeMode.name, orElse: () => ThemeMode.system), - title: 'Clipious', - theme: ThemeData( - useMaterial3: true, colorScheme: lightColorScheme, progressIndicatorTheme: ProgressIndicatorThemeData(circularTrackColor: lightColorScheme.secondaryContainer.withOpacity(0.8))), - darkTheme: ThemeData( - useMaterial3: true, colorScheme: darkColorScheme, progressIndicatorTheme: ProgressIndicatorThemeData(circularTrackColor: darkColorScheme.secondaryContainer.withOpacity(0.8))), - home: Shortcuts( - shortcuts: { - LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), - }, - child: isTv - ? showWizard - ? const TvWelcomeWizard() - : const TvHome() - : Stack( - children: [ - MiniPlayerAware( - child: Navigator( - observers: [MyRouteObserver()], - key: navigatorKey, - initialRoute: '/', - onGenerateRoute: (settings) { - switch (settings.name) { - case "/": - return MaterialPageRoute(builder: (context) => showWizard ? const WelcomeWizard() : const Home()); - case PATH_MANAGE_SUBS: - return MaterialPageRoute(builder: (context) => const ManageSubscriptions(), settings: ROUTE_MANAGE_SUBSCRIPTIONS); - case PATH_VIDEO: - VideoRouteArguments args = settings.arguments as VideoRouteArguments; - return MaterialPageRoute( - builder: (context) => VideoView( - videoId: args.videoId, - playNow: args.playNow, - )); - case PATH_CHANNEL: - if (settings.arguments is String) { - return MaterialPageRoute( - builder: (context) => ChannelView(channelId: settings.arguments! as String), - settings: ROUTE_CHANNEL, - ); - } - break; - case PATH_LAYOUT_EDITOR: - return MaterialPageRoute( - builder: (context) => const EditHomeLayout(), - settings: ROUTE_CHANNEL, - ); - break; - } - }), - ), - const Player() - ], - ), - )); + } + // if device language is not supported by the app, + // the app will set it to english but return this to set to Bahasa instead + log.info("locale not supported, returning english"); + return const Locale('en', 'US'); + }, + supportedLocales: AppLocalizations.supportedLocales, + scaffoldMessengerKey: scaffoldKey, + debugShowCheckedModeBanner: false, + themeMode: ThemeMode.values.firstWhere((element) => element.name == settings.state.themeMode.name, + orElse: () => ThemeMode.system), + title: 'Clipious', + theme: ThemeData( + useMaterial3: true, + colorScheme: lightColorScheme, + progressIndicatorTheme: ProgressIndicatorThemeData( + circularTrackColor: lightColorScheme.secondaryContainer.withOpacity(0.8))), + darkTheme: ThemeData( + useMaterial3: true, + colorScheme: darkColorScheme, + progressIndicatorTheme: ProgressIndicatorThemeData( + circularTrackColor: darkColorScheme.secondaryContainer.withOpacity(0.8))), + ); }); }); } } - -class Home extends StatefulWidget { - const Home({super.key}); - - @override - State createState() => _HomeState(); -} - -class _HomeState extends State with AfterLayoutMixin { - openSettings(BuildContext context) { - navigatorKey.currentState?.push(MaterialPageRoute(settings: ROUTE_SETTINGS, builder: (context) => const Settings())); - } - - openSubscriptionManagement(BuildContext context) { - navigatorKey.currentState?.pushNamed(PATH_MANAGE_SUBS); - } - - openLayoutEditor(BuildContext context) { - var app = context.read(); - navigatorKey.currentState?.pushNamed(PATH_LAYOUT_EDITOR).then((value) => app.updateLayout()); - } - - @override - void initState() { - super.initState(); - BackButtonInterceptor.add((stopDefaultButtonEvent, RouteInfo routeInfo) { - if (routeInfo.currentRoute(context)?.settings.name != null) { - navigatorKey.currentState?.pop(); - return true; - } else { - return false; - } - }, name: 'mainNavigator', zIndex: 0, ifNotYetIntercepted: true); - } - - @override - void dispose() { - BackButtonInterceptor.removeByName('mainNavigator'); - super.dispose(); - } - - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - ColorScheme colorScheme = Theme.of(context).colorScheme; - var locals = AppLocalizations.of(context)!; - - return BlocBuilder(buildWhen: (previous, current) { - return previous.selectedIndex != current.selectedIndex || previous.server != current.server; - }, builder: (context, _) { - var app = context.read(); - var settings = context.watch().state; - - var allowedPages = settings.appLayout.where((element) => element.isPermitted(context)).toList(); - var navigationWidgets = allowedPages.map((e) => e.getBottomBarNavigationWidget(context)).toList(); - - var selectedIndex = _.selectedIndex; - if (selectedIndex >= allowedPages.length) { - selectedIndex = 0; - } - - HomeDataSource? selectedPage; - if (selectedIndex < allowedPages.length) { - selectedPage = allowedPages[selectedIndex]; - } - - return Scaffold( - key: ValueKey(_.server?.url), - // so we rebuild the view if the server changes - backgroundColor: colorScheme.background, - bottomNavigationBar: allowedPages.length >= 2 - ? NavigationBar( - backgroundColor: colorScheme.background, - labelBehavior: settings.navigationBarLabelBehavior, - elevation: 0, - onDestinationSelected: app.selectIndex, - selectedIndex: selectedIndex, - destinations: navigationWidgets, - ) - : null, - appBar: AppBar( - systemOverlayStyle: getUiOverlayStyle(context), - title: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(selectedPage?.getLabel(locals) ?? 'Clipious'), - if (selectedPage == HomeDataSource.home) - IconButton( - iconSize: 15, - onPressed: () => openLayoutEditor(context), - icon: Icon( - Icons.edit, - color: colorScheme.secondary, - )) - ], - ), - scrolledUnderElevation: 0, - // backgroundColor: Colors.pink, - backgroundColor: colorScheme.background, - actions: [ - selectedPage == HomeDataSource.subscription ? IconButton(onPressed: () => openSubscriptionManagement(context), icon: const Icon(Icons.checklist)) : const SizedBox.shrink(), - const AppBarDownloadButton(), - IconButton( - onPressed: () { - // showSearch(context: context, delegate: MySearchDelegate()); - navigatorKey.currentState?.push(MaterialPageRoute(settings: ROUTE_SETTINGS, builder: (context) => const Search())); - }, - icon: const Icon(Icons.search), - ), - IconButton( - onPressed: () => openSettings(context), - icon: const Icon(Icons.settings), - ), - ], - ), - body: SafeArea( - bottom: false, - child: Stack(children: [ - Positioned( - top: 0, - left: 0, - right: 0, - bottom: 0, - child: AnimatedSwitcher( - switchInCurve: Curves.easeInOutQuad, - switchOutCurve: Curves.easeInOutQuad, - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition(opacity: animation, child: child); - }, - duration: animationDuration, - child: Container( - // 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( - height: 200, - ))), -/* - child: [ - const HomeView( - key: ValueKey(0), - ), - const Trending( - key: ValueKey(1), - ), - const Subscriptions( - key: ValueKey(2), - ), - const AddToPlaylistList( - key: ValueKey(3), - canDeleteVideos: true, - ), - const HistoryView( - key: ValueKey(4), - ), - ][_.selectedIndex], -*/ - ), - ) - ]))); - }); - } - - @override - FutureOr afterFirstLayout(BuildContext context) {} -} diff --git a/lib/myRouteObserver.dart b/lib/myRouteObserver.dart deleted file mode 100644 index c7abd0ec..00000000 --- a/lib/myRouteObserver.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:invidious/player/states/player.dart'; -import 'package:logging/logging.dart'; - -const String PATH_CHANNEL = "/channel"; -const String PATH_VIDEO = "/video"; -const String PATH_MANAGE_SUBS = "/manage-subscription_management"; -const String PATH_LAYOUT_EDITOR = '/edit-layout'; - -const RouteSettings ROUTE_SETTINGS = RouteSettings(name: 'settings'); -const RouteSettings ROUTE_DOWNLOAD_MANAGER = RouteSettings(name: 'download-manager'); -const RouteSettings ROUTE_SETTINGS_MANAGE_SERVERS = RouteSettings(name: 'settings-manage-servers'); -const RouteSettings ROUTE_SETTINGS_MANAGE_ONE_SERVER = RouteSettings(name: 'settings-manage-one-server'); -const RouteSettings ROUTE_SETTINGS_SPONSOR_BLOCK = RouteSettings(name: 'settings-sponsor-block'); -const RouteSettings ROUTE_SETTINGS_VIDEO_FILTERS = RouteSettings(name: 'settings-video-filters'); -const RouteSettings ROUTE_SETTINGS_SEARCH_HISTORY = RouteSettings(name: 'settings-search-history'); -const RouteSettings ROUTE_VIDEO = RouteSettings(name: PATH_VIDEO); -const RouteSettings ROUTE_CHANNEL = RouteSettings(name: PATH_CHANNEL); -const RouteSettings ROUTE_PLAYLIST_LIST = RouteSettings(name: 'playlist-list'); -const RouteSettings ROUTE_PLAYLIST = RouteSettings(name: 'playlist'); -const RouteSettings ROUTE_MANAGE_SUBSCRIPTIONS = RouteSettings(name: PATH_MANAGE_SUBS); - -class MyRouteObserver extends RouteObserver> { - var log = Logger('MyRouteObserver'); - - stopPlayingOnPop(PageRoute? newRoute, PageRoute? poppedRoute) { - newRoute?.navigator?.context.read().showMiniPlayer(); - } - - @override - void didPush(Route route, Route? previousRoute) { - log.fine("New route context: ${route.navigator?.context}"); - route.navigator?.context.read().showMiniPlayer(); - super.didPush(route, previousRoute); - if (route is PageRoute) { - if (previousRoute is PageRoute) { - stopPlayingOnPop(route, previousRoute); - } - } - } - - @override - void didReplace({Route? newRoute, Route? oldRoute}) { - super.didReplace(newRoute: newRoute, oldRoute: oldRoute); - if (newRoute is PageRoute) { - // _sendScreenView(newRoute); - } - } - - @override - void didPop(Route route, Route? previousRoute) { - super.didPop(route, previousRoute); - if (previousRoute is PageRoute && route is PageRoute) { - stopPlayingOnPop(route, previousRoute); - } - } -} diff --git a/lib/notifications/models/db/channel_notifications.dart b/lib/notifications/models/db/channel_notifications.dart new file mode 100644 index 00000000..5d95cefa --- /dev/null +++ b/lib/notifications/models/db/channel_notifications.dart @@ -0,0 +1,23 @@ +import 'package:objectbox/objectbox.dart'; + +@Entity() +class ChannelNotification { + @Id() + int id = 0; + + @Unique(onConflict: ConflictStrategy.replace) + String channelId; + + String lastSeenVideoId; + + int timestamp; + + String channelName; + + ChannelNotification( + this.channelId, + this.channelName, + this.lastSeenVideoId, + this.timestamp, + ); +} diff --git a/lib/notifications/models/db/playlist_notifications.dart b/lib/notifications/models/db/playlist_notifications.dart new file mode 100644 index 00000000..c597fce8 --- /dev/null +++ b/lib/notifications/models/db/playlist_notifications.dart @@ -0,0 +1,18 @@ +import 'package:objectbox/objectbox.dart'; + +@Entity() +class PlaylistNotification { + @Id() + int id = 0; + + @Unique(onConflict: ConflictStrategy.replace) + String playlistId; + + int lastVideoCount = 0; + + int timestamp; + + String playlistName; + + PlaylistNotification(this.playlistId, this.lastVideoCount, this.timestamp, this.playlistName); +} diff --git a/lib/notifications/models/db/subscription_notifications.dart b/lib/notifications/models/db/subscription_notifications.dart new file mode 100644 index 00000000..16a3143b --- /dev/null +++ b/lib/notifications/models/db/subscription_notifications.dart @@ -0,0 +1,13 @@ +import 'package:objectbox/objectbox.dart'; + +@Entity() +class SubscriptionNotification { + @Id() + int id = 0; + + String lastSeenVideoId; + + int timestamp; + + SubscriptionNotification(this.lastSeenVideoId, this.timestamp); +} diff --git a/lib/notifications/notifications.dart b/lib/notifications/notifications.dart new file mode 100644 index 00000000..2751cf87 --- /dev/null +++ b/lib/notifications/notifications.dart @@ -0,0 +1,89 @@ +import 'dart:ffi'; + +import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:flutter/foundation.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/main.dart'; +import 'package:invidious/notifications/models/db/subscription_notifications.dart'; +import 'package:invidious/router.dart'; +import 'package:logging/logging.dart'; + +final log = Logger('notifications'); + +const String playlistId = "playlistId", lastSeenVideo = "lastSeenVideo", channelId = "channelId"; + +enum NotificationTypes { + foregroundService( + id: 'foreground-service', description: 'Checks for new videos from sources you selected', name: 'Foreground service', idSpace: 4000000, playSound: false, importance: NotificationImportance.Min), + subscription(id: 'subscription-notifications', description: 'Get notifications about your subscriptions', name: 'New subscription videos', idSpace: 1000000), + channel(id: 'channel-notifications', description: 'Get notifications from selected channels (bell icon)', name: 'Channel new videos', idSpace: 2000000), + playlist(id: 'playlist-notifications', description: 'Get notification from selected playlists (bell icon)', name: 'Playlist new videos', idSpace: 3000000); + + final NotificationImportance importance; + final String id, name, description; + final bool playSound; + + // to prevent notifications with the same id, when sending notifications this will do idSpace+id + final int idSpace; + + const NotificationTypes({required this.id, required this.name, required this.description, required this.idSpace, this.playSound = true, this.importance = NotificationImportance.Default}); +} + +initializeNotifications() { + var defaultIcon = 'resource://drawable/app_icon'; + AwesomeNotifications().initialize( + defaultIcon, + NotificationTypes.values + .map((e) => NotificationChannel( + icon: defaultIcon, channelKey: e.id, channelName: e.name, channelDescription: e.description, playSound: e.playSound, enableVibration: e.playSound, importance: e.importance)) + .toList(), + debug: kDebugMode); +} + +class NotificationController { + /// Use this method to detect when a new notification or a schedule is created + @pragma("vm:entry-point") + static Future onNotificationCreatedMethod(ReceivedNotification receivedNotification) async { + // Your code goes here + } + + /// Use this method to detect every time that a new notification is displayed + @pragma("vm:entry-point") + static Future onNotificationDisplayedMethod(ReceivedNotification receivedNotification) async { + // Your code goes here + } + + /// Use this method to detect if the user dismissed a notification + @pragma("vm:entry-point") + static Future onDismissActionReceivedMethod(ReceivedAction receivedAction) async { + // Your code goes here + } + + /// Use this method to detect when the user taps on a notification or action button + @pragma("vm:entry-point") + static Future onActionReceivedMethod(ReceivedAction receivedAction) async { + print("notification tapped ${receivedAction.payload} "); + if (receivedAction.payload != null && receivedAction.payload!.isNotEmpty) { + var payload = receivedAction.payload!; + if (receivedAction.channelKey == NotificationTypes.channel.id && payload.containsKey(channelId) && payload.containsKey(lastSeenVideo)) { + log.fine('Launching channel screen ${receivedAction.payload}'); + appRouter.push(ChannelRoute(channelId: payload[channelId]!)); + db.setChannelNotificationLastViewedVideo(payload[channelId]!, payload[lastSeenVideo]!); + } else if (receivedAction.channelKey == NotificationTypes.playlist.id && payload.containsKey(playlistId)) { + log.fine('Launching playlist screen ${receivedAction.payload}'); + service.getPublicPlaylists(payload[playlistId]!).then((value) { + appRouter.push(PlaylistViewRoute(playlist: value, canDeleteVideos: false)); + db.setPlaylistNotificationLastViewedVideo(value.playlistId, value.videoCount); + }); + } else if (receivedAction.channelKey == NotificationTypes.subscription.id && payload.containsKey(lastSeenVideo)) { + appRouter.push(const SubscriptionRoute()); + db.setLastSubscriptionNotification(SubscriptionNotification(payload[lastSeenVideo]!, DateTime.now().millisecondsSinceEpoch)); + } + } + } +// Your code goes here +} + +sendNotification(String title, String message, {required NotificationTypes type, Map? payload, int id = 0}) async { + AwesomeNotifications().createNotification(content: NotificationContent(id: type.idSpace + id, payload: payload, channelKey: type.id, title: title, body: message, actionType: ActionType.Default)); +} diff --git a/lib/notifications/state/bell_icon.dart b/lib/notifications/state/bell_icon.dart new file mode 100644 index 00000000..59cdb877 --- /dev/null +++ b/lib/notifications/state/bell_icon.dart @@ -0,0 +1,85 @@ +import 'package:bloc/bloc.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/notifications/models/db/channel_notifications.dart'; +import 'package:invidious/notifications/models/db/playlist_notifications.dart'; + +import '../../settings/states/settings.dart'; +import '../views/components/bell_icon.dart'; + +enum TurnOnStatus { ok, needToEnableBackGroundServices, needToEnableBatteryOptimization } + +class BellIconCubit extends Cubit { + final SettingsCubit settings; + final String itemId; + final BellIconType type; + + BellIconCubit(super.initialState, this.settings, this.itemId, this.type) { + onInit(); + } + + void onInit() { + if (settings.state.backgroundNotifications) { + emit(getNotification()); + } else { + emit(false); + } + } + + bool getNotification() { + switch (type) { + case BellIconType.channel: + return db.getChannelNotification(itemId) != null; + case BellIconType.playlist: + return db.getPlaylistNotification(itemId) != null; + } + } + + Future toggle() async { + if (!state) { + var backgroundServices = settings.state.backgroundNotifications; + if (!backgroundServices) { + return TurnOnStatus.needToEnableBackGroundServices; + } + + if (!settings.state.backgroundNotifications) { + var settingsResponse = await settings.setBackgroundNotifications(true); + if (settingsResponse == EnableBackGroundNotificationResponse.needBatteryOptimization) { + return TurnOnStatus.needToEnableBatteryOptimization; + } + } + + emit(true); + switch (type) { + case BellIconType.channel: + var channel = await service.getChannel(itemId); + db.upsertChannelNotification(ChannelNotification(itemId, channel.author, + channel.latestVideos?.firstOrNull?.videoId ?? '', DateTime.now().millisecondsSinceEpoch)); + break; + case BellIconType.playlist: + var playlist = await service.getPublicPlaylists(itemId); + db.upsertPlaylistNotification( + PlaylistNotification(itemId, playlist.videoCount, DateTime.now().millisecondsSinceEpoch, playlist.title)); + + break; + } + } else { + switch(type){ + case BellIconType.channel: + var notif = db.getChannelNotification(itemId); + if (notif != null) { + db.deleteChannelNotification(notif); + emit(false); + } + break; + case BellIconType.playlist: + var notif = db.getPlaylistNotification(itemId); + if (notif != null) { + db.deletePlaylistNotification(notif); + emit(false); + } + } + } + + return TurnOnStatus.ok; + } +} diff --git a/lib/notifications/views/components/bell_icon.dart b/lib/notifications/views/components/bell_icon.dart new file mode 100644 index 00000000..4c20423b --- /dev/null +++ b/lib/notifications/views/components/bell_icon.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/notifications/state/bell_icon.dart'; +import 'package:invidious/settings/states/settings.dart'; + +import '../../../utils.dart'; + +enum BellIconType { + playlist, + channel; +} + +class BellIcon extends StatelessWidget { + final BellIconType type; + final String itemId; + + const BellIcon({super.key, required this.itemId, required this.type}); + + toggleNotifications(BuildContext context) async { + var cubit = context.read(); + var result = await cubit.toggle(); + switch (result) { + case TurnOnStatus.needToEnableBackGroundServices: + if (context.mounted) { + var locals = AppLocalizations.of(context)!; + okCancelDialog(context, locals.askToEnableBackgroundServiceTitle, locals.askToEnableBackgroundServiceContent, () async { + var settings = context.read(); + var res = await settings.setBackgroundNotifications(true); + if (context.mounted) { + if (res == EnableBackGroundNotificationResponse.needBatteryOptimization) { + showBatteryOptimizationDialog(context); + } else { + cubit.toggle(); + } + } + }); + } + break; + case TurnOnStatus.needToEnableBatteryOptimization: + if(context.mounted) { + showBatteryOptimizationDialog(context); + } + break; + default: + break; + } + } + + @override + Widget build(BuildContext context) { + var colors = Theme.of(context).colorScheme; + return BlocProvider( + create: (context) => BellIconCubit(false, context.read(), itemId, type), + child: BlocBuilder( + builder: (context, state) { + return IconButton( + onPressed: () => toggleNotifications(context), + icon: Icon(state ? Icons.notifications_active : Icons.notifications), + color: state ? colors.primary : null, + ).animate(target: state ? 1 : 0, effects: state ? [const ShakeEffect()] : []); + }, + ), + ); + } +} diff --git a/lib/objectbox-model.json b/lib/objectbox-model.json index 960bdc0e..8245f3bf 100644 --- a/lib/objectbox-model.json +++ b/lib/objectbox-model.json @@ -341,10 +341,106 @@ } ], "relations": [] + }, + { + "id": "11:3657792956132207980", + "lastPropertyId": "3:1941341505549292694", + "name": "SubscriptionNotification", + "properties": [ + { + "id": "1:2430225471686599517", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:2835271477274198093", + "name": "lastSeenVideoId", + "type": 9 + }, + { + "id": "3:1941341505549292694", + "name": "timestamp", + "type": 6 + } + ], + "relations": [] + }, + { + "id": "12:2070539588161609146", + "lastPropertyId": "5:2886321232475223602", + "name": "ChannelNotification", + "properties": [ + { + "id": "1:8845605462686818448", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:2512902621149037266", + "name": "channelId", + "type": 9, + "flags": 34848, + "indexId": "6:1543728882665646543" + }, + { + "id": "3:1308895344490646098", + "name": "lastSeenVideoId", + "type": 9 + }, + { + "id": "4:4272759280615528314", + "name": "timestamp", + "type": 6 + }, + { + "id": "5:2886321232475223602", + "name": "channelName", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "13:8331886434292283747", + "lastPropertyId": "5:5316267761398216511", + "name": "PlaylistNotification", + "properties": [ + { + "id": "1:7681992295553062859", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:8328838064368170718", + "name": "playlistId", + "type": 9, + "flags": 34848, + "indexId": "7:4488140487903335246" + }, + { + "id": "3:7229926964587744944", + "name": "lastVideoCount", + "type": 6 + }, + { + "id": "4:6825540046607174830", + "name": "timestamp", + "type": 6 + }, + { + "id": "5:5316267761398216511", + "name": "playlistName", + "type": 9 + } + ], + "relations": [] } ], - "lastEntityId": "10:6821162325360407377", - "lastIndexId": "5:7262786699272501249", + "lastEntityId": "13:8331886434292283747", + "lastIndexId": "7:4488140487903335246", "lastRelationId": "0:0", "lastSequenceId": "0:0", "modelVersion": 5, diff --git a/lib/objectbox.g.dart b/lib/objectbox.g.dart index 655050f9..2ac7d610 100644 --- a/lib/objectbox.g.dart +++ b/lib/objectbox.g.dart @@ -16,6 +16,9 @@ import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart'; import 'downloads/models/downloaded_video.dart'; import 'home/models/db/home_layout.dart'; +import 'notifications/models/db/channel_notifications.dart'; +import 'notifications/models/db/playlist_notifications.dart'; +import 'notifications/models/db/subscription_notifications.dart'; import 'search/models/db/searchHistoryItem.dart'; import 'settings/models/db/app_logs.dart'; import 'settings/models/db/server.dart'; @@ -361,6 +364,100 @@ final _entities = [ flags: 0) ], relations: [], + backlinks: []), + ModelEntity( + id: const IdUid(11, 3657792956132207980), + name: 'SubscriptionNotification', + lastPropertyId: const IdUid(3, 1941341505549292694), + flags: 0, + properties: [ + ModelProperty( + id: const IdUid(1, 2430225471686599517), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 2835271477274198093), + name: 'lastSeenVideoId', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(3, 1941341505549292694), + name: 'timestamp', + type: 6, + flags: 0) + ], + relations: [], + backlinks: []), + ModelEntity( + id: const IdUid(12, 2070539588161609146), + name: 'ChannelNotification', + lastPropertyId: const IdUid(5, 2886321232475223602), + flags: 0, + properties: [ + ModelProperty( + id: const IdUid(1, 8845605462686818448), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 2512902621149037266), + name: 'channelId', + type: 9, + flags: 34848, + indexId: const IdUid(6, 1543728882665646543)), + ModelProperty( + id: const IdUid(3, 1308895344490646098), + name: 'lastSeenVideoId', + type: 9, + flags: 0), + ModelProperty( + id: const IdUid(4, 4272759280615528314), + name: 'timestamp', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(5, 2886321232475223602), + name: 'channelName', + type: 9, + flags: 0) + ], + relations: [], + backlinks: []), + ModelEntity( + id: const IdUid(13, 8331886434292283747), + name: 'PlaylistNotification', + lastPropertyId: const IdUid(5, 5316267761398216511), + flags: 0, + properties: [ + ModelProperty( + id: const IdUid(1, 7681992295553062859), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 8328838064368170718), + name: 'playlistId', + type: 9, + flags: 34848, + indexId: const IdUid(7, 4488140487903335246)), + ModelProperty( + id: const IdUid(3, 7229926964587744944), + name: 'lastVideoCount', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(4, 6825540046607174830), + name: 'timestamp', + type: 6, + flags: 0), + ModelProperty( + id: const IdUid(5, 5316267761398216511), + name: 'playlistName', + type: 9, + flags: 0) + ], + relations: [], backlinks: []) ]; @@ -391,8 +488,8 @@ Future openStore( ModelDefinition getObjectBoxModel() { final model = ModelInfo( entities: _entities, - lastEntityId: const IdUid(10, 6821162325360407377), - lastIndexId: const IdUid(5, 7262786699272501249), + lastEntityId: const IdUid(13, 8331886434292283747), + lastIndexId: const IdUid(7, 4488140487903335246), lastRelationId: const IdUid(0, 0), lastSequenceId: const IdUid(0, 0), retiredEntityUids: const [6897417709810972885], @@ -805,6 +902,115 @@ ModelDefinition getObjectBoxModel() { lazy: false) .vTableGet(buffer, rootOffset, 10, []); + return object; + }), + SubscriptionNotification: EntityDefinition( + model: _entities[9], + toOneRelations: (SubscriptionNotification object) => [], + toManyRelations: (SubscriptionNotification object) => {}, + getId: (SubscriptionNotification object) => object.id, + setId: (SubscriptionNotification object, int id) { + object.id = id; + }, + objectToFB: (SubscriptionNotification object, fb.Builder fbb) { + final lastSeenVideoIdOffset = fbb.writeString(object.lastSeenVideoId); + fbb.startTable(4); + fbb.addInt64(0, object.id); + fbb.addOffset(1, lastSeenVideoIdOffset); + fbb.addInt64(2, object.timestamp); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final lastSeenVideoIdParam = + const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 6, ''); + final timestampParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 8, 0); + final object = SubscriptionNotification( + lastSeenVideoIdParam, timestampParam) + ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + + return object; + }), + ChannelNotification: EntityDefinition( + model: _entities[10], + toOneRelations: (ChannelNotification object) => [], + toManyRelations: (ChannelNotification object) => {}, + getId: (ChannelNotification object) => object.id, + setId: (ChannelNotification object, int id) { + object.id = id; + }, + objectToFB: (ChannelNotification object, fb.Builder fbb) { + final channelIdOffset = fbb.writeString(object.channelId); + final lastSeenVideoIdOffset = fbb.writeString(object.lastSeenVideoId); + final channelNameOffset = fbb.writeString(object.channelName); + fbb.startTable(6); + fbb.addInt64(0, object.id); + fbb.addOffset(1, channelIdOffset); + fbb.addOffset(2, lastSeenVideoIdOffset); + fbb.addInt64(3, object.timestamp); + fbb.addOffset(4, channelNameOffset); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final channelIdParam = const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 6, ''); + final channelNameParam = + const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 12, ''); + final lastSeenVideoIdParam = + const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 8, ''); + final timestampParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0); + final object = ChannelNotification(channelIdParam, channelNameParam, + lastSeenVideoIdParam, timestampParam) + ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + + return object; + }), + PlaylistNotification: EntityDefinition( + model: _entities[11], + toOneRelations: (PlaylistNotification object) => [], + toManyRelations: (PlaylistNotification object) => {}, + getId: (PlaylistNotification object) => object.id, + setId: (PlaylistNotification object, int id) { + object.id = id; + }, + objectToFB: (PlaylistNotification object, fb.Builder fbb) { + final playlistIdOffset = fbb.writeString(object.playlistId); + final playlistNameOffset = fbb.writeString(object.playlistName); + fbb.startTable(6); + fbb.addInt64(0, object.id); + fbb.addOffset(1, playlistIdOffset); + fbb.addInt64(2, object.lastVideoCount); + fbb.addInt64(3, object.timestamp); + fbb.addOffset(4, playlistNameOffset); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final playlistIdParam = const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 6, ''); + final lastVideoCountParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 8, 0); + final timestampParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0); + final playlistNameParam = + const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 12, ''); + final object = PlaylistNotification(playlistIdParam, + lastVideoCountParam, timestampParam, playlistNameParam) + ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + return object; }) }; @@ -1030,3 +1236,64 @@ class HomeLayout_ { static final dbSmallSources = QueryStringVectorProperty(_entities[8].properties[3]); } + +/// [SubscriptionNotification] entity fields to define ObjectBox queries. +class SubscriptionNotification_ { + /// see [SubscriptionNotification.id] + static final id = QueryIntegerProperty( + _entities[9].properties[0]); + + /// see [SubscriptionNotification.lastSeenVideoId] + static final lastSeenVideoId = + QueryStringProperty(_entities[9].properties[1]); + + /// see [SubscriptionNotification.timestamp] + static final timestamp = QueryIntegerProperty( + _entities[9].properties[2]); +} + +/// [ChannelNotification] entity fields to define ObjectBox queries. +class ChannelNotification_ { + /// see [ChannelNotification.id] + static final id = + QueryIntegerProperty(_entities[10].properties[0]); + + /// see [ChannelNotification.channelId] + static final channelId = + QueryStringProperty(_entities[10].properties[1]); + + /// see [ChannelNotification.lastSeenVideoId] + static final lastSeenVideoId = + QueryStringProperty(_entities[10].properties[2]); + + /// see [ChannelNotification.timestamp] + static final timestamp = + QueryIntegerProperty(_entities[10].properties[3]); + + /// see [ChannelNotification.channelName] + static final channelName = + QueryStringProperty(_entities[10].properties[4]); +} + +/// [PlaylistNotification] entity fields to define ObjectBox queries. +class PlaylistNotification_ { + /// see [PlaylistNotification.id] + static final id = + QueryIntegerProperty(_entities[11].properties[0]); + + /// see [PlaylistNotification.playlistId] + static final playlistId = + QueryStringProperty(_entities[11].properties[1]); + + /// see [PlaylistNotification.lastVideoCount] + static final lastVideoCount = + QueryIntegerProperty(_entities[11].properties[2]); + + /// see [PlaylistNotification.timestamp] + static final timestamp = + QueryIntegerProperty(_entities[11].properties[3]); + + /// see [PlaylistNotification.playlistName] + static final playlistName = + QueryStringProperty(_entities[11].properties[4]); +} diff --git a/lib/player/models/mediaCommand.dart b/lib/player/models/mediaCommand.dart index 20e03694..0af168ed 100644 --- a/lib/player/models/mediaCommand.dart +++ b/lib/player/models/mediaCommand.dart @@ -1,8 +1,18 @@ import '../../videos/models/video.dart'; -enum MediaCommandType { play, pause, seek, mute, unmute, speed, switchVideo, switchToOfflineVideo, +enum MediaCommandType { + play, + pause, + seek, + mute, + unmute, + speed, + switchVideo, + switchToOfflineVideo, // those are more to let the player know if they need to do anything when the full screen status changes - enterFullScreen, exitFullScreen} + enterFullScreen, + exitFullScreen +} class MediaCommand { MediaCommandType type; diff --git a/lib/player/states/audio_player.dart b/lib/player/states/audio_player.dart index 2a79f157..ce1081ed 100644 --- a/lib/player/states/audio_player.dart +++ b/lib/player/states/audio_player.dart @@ -78,7 +78,10 @@ class AudioPlayerCubit extends MediaPlayerCubit { var state = this.state.copyWith(); state.loading = false; if (!isClosed) emit(state); - player.setEvent(MediaEvent(state: MediaState.playing, type: MediaEventType.durationChanged, value: duration ?? const Duration(seconds: 1))); + player.setEvent(MediaEvent( + state: MediaState.playing, + type: MediaEventType.durationChanged, + value: duration ?? const Duration(seconds: 1))); } onPositionChanged(Duration position) { @@ -105,7 +108,10 @@ class AudioPlayerCubit extends MediaPlayerCubit { AudioSource? source; if (!offline) { - AdaptiveFormat? audio = state.video?.adaptiveFormats.where((element) => element.type.contains("audio")).sortByReversed((e) => int.parse(e.bitrate ?? "0")).first; + AdaptiveFormat? audio = state.video?.adaptiveFormats + .where((element) => element.type.contains("audio")) + .sortByReversed((e) => int.parse(e.bitrate ?? "0")) + .first; if (audio != null) { if (startAt == null) { double progress = db.getVideoProgress(state.video!.videoId); @@ -329,7 +335,6 @@ class AudioPlayerCubit extends MediaPlayerCubit { EasyThrottle.throttle('audio-buffering', const Duration(seconds: 1), () { player.setEvent(MediaEvent(state: MediaState.playing, type: MediaEventType.bufferChanged, value: event)); }); - } } @@ -348,7 +353,8 @@ class AudioPlayerState extends MediaPlayerState { bool loading = false; String? error; - AudioPlayerState._(this.player, this.audioLength, this.audioPosition, this.previousSponsorCheck, this.loading, this.error, + AudioPlayerState._( + this.player, this.audioLength, this.audioPosition, this.previousSponsorCheck, this.loading, this.error, {Video? video, DownloadedVideo? offlineVideo, bool? disableControls, bool? playNow}) : super(video: video, offlineVideo: offlineVideo, disableControls: disableControls, playNow: playNow); } diff --git a/lib/player/states/interfaces/media_player.dart b/lib/player/states/interfaces/media_player.dart index e0958c37..365bb3f2 100644 --- a/lib/player/states/interfaces/media_player.dart +++ b/lib/player/states/interfaces/media_player.dart @@ -30,8 +30,10 @@ abstract class MediaPlayerCubit extends Cubit { @mustCallSuper void playVideo(bool offline) { var duration = Duration(seconds: state.offlineVideo?.lengthSeconds ?? state.video?.lengthSeconds ?? 1); - player.setEvent(MediaEvent(state: MediaState.ready, type: MediaEventType.durationChanged, value: duration)); - player.setEvent(MediaEvent(state: MediaState.ready, type: MediaEventType.aspectRatioChanged, value: getAspectRatio())); + player + .setEvent(MediaEvent(state: MediaState.ready, type: MediaEventType.durationChanged, value: duration)); + player.setEvent( + MediaEvent(state: MediaState.ready, type: MediaEventType.aspectRatioChanged, value: getAspectRatio())); } void switchToOfflineVideo(DownloadedVideo v); diff --git a/lib/player/states/player.dart b/lib/player/states/player.dart index c604ea63..a9956d53 100644 --- a/lib/player/states/player.dart +++ b/lib/player/states/player.dart @@ -9,7 +9,6 @@ import 'package:easy_debounce/easy_debounce.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/globals.dart'; import 'package:invidious/player/models/mediaCommand.dart'; import 'package:invidious/player/models/mediaEvent.dart'; @@ -67,7 +66,13 @@ class PlayerCubit extends Cubit { MediaControl.stop, state.hasQueue ? MediaControl.skipToNext : MediaControl.fastForward, ], - systemActions: const {MediaAction.seek, MediaAction.seekForward, MediaAction.seekBackward, MediaAction.setShuffleMode, MediaAction.setRepeatMode}, + systemActions: const { + MediaAction.seek, + MediaAction.seekForward, + MediaAction.seekBackward, + MediaAction.setShuffleMode, + MediaAction.setRepeatMode + }, androidCompactActionIndices: const [0, 1, 3], processingState: const { MediaState.idle: AudioProcessingState.idle, @@ -198,7 +203,8 @@ class PlayerCubit extends Cubit { hide() { var state = this.state.copyWith(); state.isMini = true; - state.mediaEvent = MediaEvent(state: MediaState.playing, type: MediaEventType.miniDisplayChanged, value: state.isMini); + state.mediaEvent = + MediaEvent(state: MediaState.playing, type: MediaEventType.miniDisplayChanged, value: state.isMini); state.top = null; state.height = targetHeight; state.isHidden = true; @@ -245,7 +251,9 @@ class PlayerCubit extends Cubit { state.offlineVideos = []; if (videos.isNotEmpty) { //removing videos that are already in the queue - state.videos.addAll(videos.where((v) => state.videos.indexWhere((v2) => v2.videoId == v.videoId) == -1).where((element) => !element.filtered)); + state.videos.addAll(videos + .where((v) => state.videos.indexWhere((v2) => v2.videoId == v.videoId) == -1) + .where((element) => !element.filtered)); } else { playVideo(videos); } @@ -256,7 +264,8 @@ class PlayerCubit extends Cubit { showBigPlayer() { var state = this.state.copyWith(); state.isMini = false; - state.mediaEvent = MediaEvent(state: MediaState.playing, type: MediaEventType.miniDisplayChanged, value: state.isMini); + state.mediaEvent = + MediaEvent(state: MediaState.playing, type: MediaEventType.miniDisplayChanged, value: state.isMini); state.top = 0; state.opacity = 1; state.isHidden = false; @@ -267,7 +276,8 @@ class PlayerCubit extends Cubit { if (state.currentlyPlaying != null || state.offlineCurrentlyPlaying != null) { var state = this.state.copyWith(); state.isMini = true; - state.mediaEvent = MediaEvent(state: MediaState.playing, type: MediaEventType.miniDisplayChanged, value: state.isMini); + state.mediaEvent = + MediaEvent(state: MediaState.playing, type: MediaEventType.miniDisplayChanged, value: state.isMini); state.top = null; state.isHidden = false; state.opacity = 1; @@ -286,7 +296,8 @@ class PlayerCubit extends Cubit { if (state.sponsorSegments.isNotEmpty) { double positionInMs = currentPosition * 1000; - Pair nextSegment = state.sponsorSegments.firstWhere((e) => e.first <= positionInMs && positionInMs <= e.last, orElse: () => Pair(-1, -1)); + Pair nextSegment = state.sponsorSegments + .firstWhere((e) => e.first <= positionInMs && positionInMs <= e.last, orElse: () => Pair(-1, -1)); if (nextSegment.first != -1) { emit(state.copyWith(mediaEvent: MediaEvent(state: MediaState.playing, type: MediaEventType.sponsorSkipped))); //for some reasons this needs to be last @@ -315,7 +326,8 @@ class PlayerCubit extends Cubit { var state = this.state.copyWith(); var allVideos = state.videos.isNotEmpty ? state.videos : state.offlineVideos; - log.fine('Play next: played length: ${state.playedVideos.length} videos: ${state.videos.length} Repeat mode: ${settings.state.playerRepeatMode}'); + log.fine( + 'Play next: played length: ${state.playedVideos.length} videos: ${state.videos.length} Repeat mode: ${settings.state.playerRepeatMode}'); if (settings.state.playerRepeatMode == PlayerRepeat.repeatOne) { if (state.videos.isNotEmpty) { switchToVideo(state.currentlyPlaying!, startAt: Duration.zero); @@ -462,7 +474,8 @@ class PlayerCubit extends Cubit { v = await service.getVideo(video.videoId); } state.currentlyPlaying = v; - state.mediaCommand = MediaCommand(MediaCommandType.switchVideo, value: SwitchVideoValue(video: v, startAt: startAt)); + state.mediaCommand = + MediaCommand(MediaCommandType.switchVideo, value: SwitchVideoValue(video: v, startAt: startAt)); } else { state.offlineCurrentlyPlaying = video; state.mediaCommand = MediaCommand(MediaCommandType.switchToOfflineVideo, value: video); @@ -491,7 +504,8 @@ class PlayerCubit extends Cubit { playVideo(List v, {bool? goBack, bool? audio, Duration? startAt}) async { List videos = v.where((element) => !element.filtered).toList(); - if (goBack ?? false) navigatorKey.currentState?.pop(); + // TODO: find how to do this with auto router + // if (goBack ?? false) navigatorKey.currentState?.pop(); log.fine('Playing ${videos.length} videos'); setAudio(audio); @@ -531,7 +545,9 @@ class PlayerCubit extends Cubit { if (listToUpdate.length == 1) { hide(); } else { - int index = state.videos.isNotEmpty ? state.videos.indexWhere((element) => element.videoId == videoId) : state.offlineVideos.indexWhere((element) => element.videoId == videoId); + int index = state.videos.isNotEmpty + ? state.videos.indexWhere((element) => element.videoId == videoId) + : state.offlineVideos.indexWhere((element) => element.videoId == videoId); state.playedVideos.remove(videoId); if (index >= 0) { if (index < state.currentIndex) { @@ -551,7 +567,8 @@ class PlayerCubit extends Cubit { // we change the display mode if there's a big enough drag movement to avoid jittery behavior when dragging slow if (details.delta.dy.abs() > 3) { state.isMini = details.delta.dy > 0; - state.mediaEvent = MediaEvent(state: MediaState.playing, type: MediaEventType.miniDisplayChanged, value: state.isMini); + state.mediaEvent = + MediaEvent(state: MediaState.playing, type: MediaEventType.miniDisplayChanged, value: state.isMini); } state.dragDistance += details.delta.dy; // we're going down, putting threshold high easier to switch to mini player @@ -613,7 +630,8 @@ class PlayerCubit extends Cubit { setSponsorBlock() async { var state = this.state.copyWith(); if (state.currentlyPlaying != null) { - List types = SponsorSegmentType.values.where((e) => db.getSettings(e.settingsName())?.value == 'true').toList(); + List types = + SponsorSegmentType.values.where((e) => db.getSettings(e.settingsName())?.value == 'true').toList(); if (types.isNotEmpty) { List sponsorSegments = await service.getSponsorSegments(state.currentlyPlaying!.videoId, types); @@ -640,7 +658,8 @@ class PlayerCubit extends Cubit { duration = Duration.zero; } - var videoLength = this.state.currentlyPlaying?.lengthSeconds ?? this.state.offlineCurrentlyPlaying?.lengthSeconds ?? 1; + var videoLength = + this.state.currentlyPlaying?.lengthSeconds ?? this.state.offlineCurrentlyPlaying?.lengthSeconds ?? 1; if (duration.inSeconds > (videoLength)) { duration = Duration(seconds: videoLength); } @@ -685,11 +704,22 @@ class PlayerCubit extends Cubit { if (state.videos.isNotEmpty) { var e = state.videos[index]; return MediaItem( - id: e.videoId, title: e.title, artist: e.author, duration: Duration(seconds: e.lengthSeconds), album: '', artUri: Uri.parse(ImageObject.getBestThumbnail(e.videoThumbnails)?.url ?? '')); + id: e.videoId, + title: e.title, + artist: e.author, + duration: Duration(seconds: e.lengthSeconds), + album: '', + artUri: Uri.parse(ImageObject.getBestThumbnail(e.videoThumbnails)?.url ?? '')); } else if (state.offlineVideos.isNotEmpty) { var e = state.offlineVideos[index]; var path = await e.thumbnailPath; - return MediaItem(id: e.videoId, title: e.title, artist: e.author, duration: Duration(seconds: e.lengthSeconds), album: '', artUri: Uri.file(path)); + return MediaItem( + id: e.videoId, + title: e.title, + artist: e.author, + duration: Duration(seconds: e.lengthSeconds), + album: '', + artUri: Uri.file(path)); } return null; } @@ -704,7 +734,9 @@ class PlayerCubit extends Cubit { // emit(state.copyWith(mediaCommand: MediaCommand(MediaCommandType.fullScreen, value: fsState))); emit(state.copyWith( fullScreenState: fsState, - mediaCommand: MediaCommand(fsState == FullScreenState.notFullScreen ? MediaCommandType.exitFullScreen : MediaCommandType.enterFullScreen), + mediaCommand: MediaCommand(fsState == FullScreenState.notFullScreen + ? MediaCommandType.exitFullScreen + : MediaCommandType.enterFullScreen), mediaEvent: MediaEvent(state: state.mediaEvent.state, type: MediaEventType.fullScreenChanged, value: fsState))); switch (fsState) { @@ -747,7 +779,8 @@ class PlayerCubit extends Cubit { } } - Duration get duration => Duration(seconds: (state.currentlyPlaying?.lengthSeconds ?? state.offlineCurrentlyPlaying?.lengthSeconds ?? 1)); + Duration get duration => + Duration(seconds: (state.currentlyPlaying?.lengthSeconds ?? state.offlineCurrentlyPlaying?.lengthSeconds ?? 1)); double get progress => state.position.inMilliseconds / duration.inMilliseconds; } diff --git a/lib/player/states/player_controls.dart b/lib/player/states/player_controls.dart index d52821ce..914d2dde 100644 --- a/lib/player/states/player_controls.dart +++ b/lib/player/states/player_controls.dart @@ -23,7 +23,8 @@ class PlayerControlsCubit extends Cubit { void onReady() { log.fine("Controls ready!"); - emit(state.copyWith(duration: player.duration, muted: player.state.muted, fullScreenState: player.state.fullScreenState)); + emit(state.copyWith( + duration: player.duration, muted: player.state.muted, fullScreenState: player.state.fullScreenState)); showControls(); } @@ -199,6 +200,18 @@ class PlayerControlsState { bool justDoubleTappedSkip = false; bool showSponsorBlocked = false; - PlayerControlsState._(this.buffering, this.justDoubleTappedSkip, this.position, this.displayControls, this.errored, this.duration, this.fullScreenState, this.muted, this.buffer, - this.draggingPositionSlider, this.doubleTapFastForwardedOpacity, this.doubleTapRewindedOpacity, this.showSponsorBlocked); + PlayerControlsState._( + this.buffering, + this.justDoubleTappedSkip, + this.position, + this.displayControls, + this.errored, + this.duration, + this.fullScreenState, + this.muted, + this.buffer, + this.draggingPositionSlider, + this.doubleTapFastForwardedOpacity, + this.doubleTapRewindedOpacity, + this.showSponsorBlocked); } diff --git a/lib/player/states/tv_player_controls.dart b/lib/player/states/tv_player_controls.dart index 6b1212f1..bee5bf08 100644 --- a/lib/player/states/tv_player_controls.dart +++ b/lib/player/states/tv_player_controls.dart @@ -46,7 +46,8 @@ class TvPlayerControlsCubit extends Cubit { KeyEventResult handleRemoteEvents(FocusNode node, KeyEvent event) { bool timeLineControl = !state.showQueue && !state.showSettings && !state.displayControls; - log.fine('Key: ${event.logicalKey}, Timeline control: $timeLineControl, showQueue: ${state.showQueue}, showSettings: ${state.showSettings}, showControls: ${state.displayControls}'); + log.fine( + 'Key: ${event.logicalKey}, Timeline control: $timeLineControl, showQueue: ${state.showQueue}, showSettings: ${state.showSettings}, showControls: ${state.displayControls}'); showUi(); // looks like back is activate on pressdown and not press up diff --git a/lib/player/states/tv_player_settings.dart b/lib/player/states/tv_player_settings.dart index 33f61f38..d95ccf67 100644 --- a/lib/player/states/tv_player_settings.dart +++ b/lib/player/states/tv_player_settings.dart @@ -1,17 +1,26 @@ import 'package:better_player/better_player.dart'; import 'package:bloc/bloc.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:invidious/database.dart'; -import 'package:invidious/globals.dart'; import 'package:invidious/player/states/video_player.dart'; -import 'package:invidious/settings/models/db/settings.dart'; import 'package:logging/logging.dart'; import '../../settings/states/settings.dart'; part 'tv_player_settings.g.dart'; -const List tvAvailablePlaybackSpeeds = ['0.5x', '0.75x', '1x', '1.25x', '1.5x', '1.75x', '2x', '2.25x', '2.5x', '2.75x', '3x']; +const List tvAvailablePlaybackSpeeds = [ + '0.5x', + '0.75x', + '1x', + '1.25x', + '1.5x', + '1.75x', + '2x', + '2.25x', + '2.5x', + '2.75x', + '3x' +]; final Logger log = Logger('TvSettingsController'); enum Tabs { @@ -31,9 +40,12 @@ class TvPlayerSettingsCubit extends Cubit { ? player.state.videoController?.betterPlayerAsmsTracks.map((e) => '${e.height}p').toSet().toList() ?? [] : player.state.videoController?.betterPlayerDataSource?.resolutions?.keys.toList() ?? []; - List get audioTrackNames => settings.state.useDash ? player.state.videoController?.betterPlayerAsmsAudioTracks?.map((e) => '${e.label}').toList() ?? [] : []; + List get audioTrackNames => settings.state.useDash + ? player.state.videoController?.betterPlayerAsmsAudioTracks?.map((e) => '${e.label}').toList() ?? [] + : []; - List get availableCaptions => player.state.videoController?.betterPlayerSubtitlesSourceList.map((e) => '${e.name}').toList() ?? []; + List get availableCaptions => + player.state.videoController?.betterPlayerSubtitlesSourceList.map((e) => '${e.name}').toList() ?? []; BetterPlayerController? get videoController => player.state.videoController; @@ -73,7 +85,8 @@ class TvPlayerSettingsCubit extends Cubit { log.fine('Video quality selected $selected'); if (settings.state.useDash) { - BetterPlayerAsmsTrack? track = videoController?.betterPlayerAsmsTracks.firstWhere((element) => '${element.height}p' == selected); + BetterPlayerAsmsTrack? track = + videoController?.betterPlayerAsmsTracks.firstWhere((element) => '${element.height}p' == selected); if (track != null) { log.fine('Changing video track to $selected'); @@ -89,7 +102,8 @@ class TvPlayerSettingsCubit extends Cubit { changeChangeAudioTrack(String selected) { log.fine('Audio quality selected $selected'); - BetterPlayerAsmsAudioTrack? track = videoController?.betterPlayerAsmsAudioTracks?.firstWhere((e) => '${e.label}' == selected); + BetterPlayerAsmsAudioTrack? track = + videoController?.betterPlayerAsmsAudioTracks?.firstWhere((e) => '${e.label}' == selected); if (track != null) { log.fine('Changing audio track to $selected'); @@ -99,7 +113,8 @@ class TvPlayerSettingsCubit extends Cubit { changeSubtitles(String selected) { log.fine('Subtitles selected $selected'); - BetterPlayerSubtitlesSource? track = videoController?.betterPlayerSubtitlesSourceList.firstWhere((e) => '${e.name}' == selected); + BetterPlayerSubtitlesSource? track = + videoController?.betterPlayerSubtitlesSourceList.firstWhere((e) => '${e.name}' == selected); settings.setLastSubtitle(selected); diff --git a/lib/player/states/video_player.dart b/lib/player/states/video_player.dart index f1e4fb7b..a89c3cb3 100644 --- a/lib/player/states/video_player.dart +++ b/lib/player/states/video_player.dart @@ -75,11 +75,17 @@ class VideoPlayerCubit extends MediaPlayerCubit { case BetterPlayerEventType.progress: EasyThrottle.throttle('video-player-progress', Duration(seconds: 1), () { - player.setEvent(MediaEvent(state: MediaState.playing, type: MediaEventType.progress, value: state.videoController?.videoPlayerController?.value.position ?? Duration.zero)); + player.setEvent(MediaEvent( + state: MediaState.playing, + type: MediaEventType.progress, + value: state.videoController?.videoPlayerController?.value.position ?? Duration.zero)); }); case BetterPlayerEventType.seekTo: // we bypass the rest so we can send the current progress - player.setEvent(MediaEvent(state: MediaState.playing, type: MediaEventType.progress, value: state.videoController?.videoPlayerController?.value.position ?? Duration.zero)); + player.setEvent(MediaEvent( + state: MediaState.playing, + type: MediaEventType.progress, + value: state.videoController?.videoPlayerController?.value.position ?? Duration.zero)); return; case BetterPlayerEventType.bufferingEnd: mediaState = MediaState.playing; @@ -99,7 +105,8 @@ class VideoPlayerCubit extends MediaPlayerCubit { case BetterPlayerEventType.openFullscreen: break; case BetterPlayerEventType.initialized: - player.setEvent(MediaEvent(state: MediaState.ready, type: MediaEventType.aspectRatioChanged, value: getAspectRatio())); + player.setEvent(MediaEvent( + state: MediaState.ready, type: MediaEventType.aspectRatioChanged, value: getAspectRatio())); mediaState = MediaState.ready; break; case BetterPlayerEventType.hideFullscreen: @@ -137,7 +144,8 @@ class VideoPlayerCubit extends MediaPlayerCubit { EasyThrottle.throttle('video-buffering', Duration(seconds: 1), () { List durations = event.parameters?['buffered'] ?? []; state.bufferPosition = durations.sortBy((e) => e.end).map((e) => e.end).last; - player.setEvent(MediaEvent(state: MediaState.playing, type: MediaEventType.bufferChanged, value: state.bufferPosition)); + player.setEvent( + MediaEvent(state: MediaState.playing, type: MediaEventType.bufferChanged, value: state.bufferPosition)); }); break; case BetterPlayerEventType.play: @@ -216,7 +224,8 @@ class VideoPlayerCubit extends MediaPlayerCubit { if (startAt == null && !offline) { double progress = db.getVideoProgress(idedVideo.videoId); if (progress > 0 && progress < 0.90) { - startAt = Duration(seconds: (offline ? state.offlineVideo!.lengthSeconds : state.video!.lengthSeconds * progress).floor()); + startAt = Duration( + seconds: (offline ? state.offlineVideo!.lengthSeconds : state.video!.lengthSeconds * progress).floor()); } } @@ -274,7 +283,11 @@ class VideoPlayerCubit extends MediaPlayerCubit { videoFormat: format, liveStream: state.video!.liveNow, subtitles: state.video!.captions - .map((s) => BetterPlayerSubtitlesSource(type: BetterPlayerSubtitlesSourceType.network, urls: ['${baseUrl}${s.url}'], name: s.label, selectedByDefault: s.label == lastSubtitle)) + .map((s) => BetterPlayerSubtitlesSource( + type: BetterPlayerSubtitlesSourceType.network, + urls: ['${baseUrl}${s.url}'], + name: s.label, + selectedByDefault: s.label == lastSubtitle)) .toList(), resolutions: resolutions.isNotEmpty ? resolutions : null, ); @@ -288,8 +301,18 @@ class VideoPlayerCubit extends MediaPlayerCubit { state.videoController = BetterPlayerController( BetterPlayerConfiguration( overlay: isTv ? const TvPlayerControls() : PlayerControls(mediaPlayerCubit: this), - deviceOrientationsOnFullScreen: [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, DeviceOrientation.portraitDown, DeviceOrientation.portraitUp], - deviceOrientationsAfterFullScreen: [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, DeviceOrientation.portraitDown, DeviceOrientation.portraitUp], + deviceOrientationsOnFullScreen: [ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + DeviceOrientation.portraitDown, + DeviceOrientation.portraitUp + ], + deviceOrientationsAfterFullScreen: [ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + DeviceOrientation.portraitDown, + DeviceOrientation.portraitUp + ], handleLifecycle: false, startAt: startAt, autoPlay: true, @@ -435,7 +458,9 @@ class VideoPlayerCubit extends MediaPlayerCubit { @override List getSubtitles() { - return state.video != null ? state.videoController?.betterPlayerSubtitlesSourceList.map(_subtitleToString).toList() ?? [] : []; + return state.video != null + ? state.videoController?.betterPlayerSubtitlesSourceList.map(_subtitleToString).toList() ?? [] + : []; } @override @@ -534,10 +559,18 @@ class VideoPlayerState extends MediaPlayerState { String selectedNonDashTrack = ''; Duration? bufferPosition = Duration.zero; - VideoPlayerState({required this.colors, required this.overFlowTextColor, required this.key, Video? video, DownloadedVideo? offlineVideo, bool? disableControls, this.startAt}) + VideoPlayerState( + {required this.colors, + required this.overFlowTextColor, + required this.key, + Video? video, + DownloadedVideo? offlineVideo, + bool? disableControls, + this.startAt}) : super(video: video, offlineVideo: offlineVideo, disableControls: disableControls); - VideoPlayerState._(this.videoController, this.colors, this.overFlowTextColor, this.key, this.startAt, this.selectedNonDashTrack, this.bufferPosition, + VideoPlayerState._(this.videoController, this.colors, this.overFlowTextColor, this.key, this.startAt, + this.selectedNonDashTrack, this.bufferPosition, {Video? video, DownloadedVideo? offlineVideo, bool? disableControls, bool? playNow}) : super(video: video, offlineVideo: offlineVideo, disableControls: disableControls, playNow: playNow); } diff --git a/lib/player/states/video_queue.dart b/lib/player/states/video_queue.dart index d1f3d9b1..28a25bce 100644 --- a/lib/player/states/video_queue.dart +++ b/lib/player/states/video_queue.dart @@ -1,7 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:flutter/cupertino.dart'; -class VideoQueueCubit extends Cubit{ +class VideoQueueCubit extends Cubit { VideoQueueCubit(super.initialState); - -} \ No newline at end of file +} diff --git a/lib/player/views/components/audio_player.dart b/lib/player/views/components/audio_player.dart index 04ce8c4c..ad477f42 100644 --- a/lib/player/views/components/audio_player.dart +++ b/lib/player/views/components/audio_player.dart @@ -15,7 +15,8 @@ class AudioPlayer extends StatelessWidget { final DownloadedVideo? offlineVideo; final bool miniPlayer; - const AudioPlayer({super.key, this.video, required this.miniPlayer, this.offlineVideo}) : assert(video == null || offlineVideo == null, 'cannot provide both video and offline video\n'); + const AudioPlayer({super.key, this.video, required this.miniPlayer, this.offlineVideo}) + : assert(video == null || offlineVideo == null, 'cannot provide both video and offline video\n'); @override Widget build(BuildContext context) { @@ -23,10 +24,12 @@ class AudioPlayer extends StatelessWidget { var settings = context.read(); return BlocProvider( - create: (context) => AudioPlayerCubit(AudioPlayerState(offlineVideo: offlineVideo, video: video), player, settings), + create: (context) => + AudioPlayerCubit(AudioPlayerState(offlineVideo: offlineVideo, video: video), player, settings), child: BlocBuilder( builder: (context, _) => BlocListener( - listenWhen: (previous, current) => previous.mediaCommand != current.mediaCommand && current.mediaCommand != null, + listenWhen: (previous, current) => + previous.mediaCommand != current.mediaCommand && current.mediaCommand != null, listener: (context, state) => context.read().handleCommand(state.mediaCommand!), child: Padding( padding: EdgeInsets.all(miniPlayer ? 8 : 0.0), @@ -40,7 +43,8 @@ class AudioPlayer extends StatelessWidget { thumbnailUrl: _.video?.getBestThumbnail()?.url ?? '', ) : _.offlineVideo != null - ? OfflineVideoThumbnail(borderRadius: 0, key: ValueKey(_.offlineVideo?.videoId ?? ''), video: _.offlineVideo!) + ? OfflineVideoThumbnail( + borderRadius: 0, key: ValueKey(_.offlineVideo?.videoId ?? ''), video: _.offlineVideo!) : const SizedBox.shrink(), PlayerControls(mediaPlayerCubit: context.read()) ], diff --git a/lib/player/views/components/expanded_player.dart b/lib/player/views/components/expanded_player.dart index 1afc21d6..241f78a0 100644 --- a/lib/player/views/components/expanded_player.dart +++ b/lib/player/views/components/expanded_player.dart @@ -68,21 +68,23 @@ class ExpandedPlayer { ), )), Visibility( - visible: !settings.distractionFreeMode && !controller.isMini && video != null && !isFullScreen, + visible: !settings.distractionFreeMode && !controller.isMini && video != null && !isFullScreen, child: SizedBox( // height: 80, child: Builder(builder: (context) { - - var selectedIndex = context.select((PlayerCubit value) => value.state.selectedFullScreenIndex); - return NavigationBar(backgroundColor: colors.background, elevation: 0, selectedIndex: selectedIndex, + return NavigationBar( + backgroundColor: colors.background, + elevation: 0, + selectedIndex: selectedIndex, labelBehavior: settings.navigationBarLabelBehavior, - onDestinationSelected: player.selectTab, destinations: [ - NavigationDestination(icon: const Icon(Icons.info), label: locals.info), - NavigationDestination(icon: const Icon(Icons.chat_bubble), label: locals.comments), - NavigationDestination(icon: const Icon(Icons.schema), label: locals.recommended), - NavigationDestination(icon: const Icon(Icons.playlist_play), label: locals.videoQueue) - ]); + onDestinationSelected: player.selectTab, + destinations: [ + NavigationDestination(icon: const Icon(Icons.info), label: locals.info), + NavigationDestination(icon: const Icon(Icons.chat_bubble), label: locals.comments), + NavigationDestination(icon: const Icon(Icons.schema), label: locals.recommended), + NavigationDestination(icon: const Icon(Icons.playlist_play), label: locals.videoQueue) + ]); }), ), ) diff --git a/lib/player/views/components/mini_player.dart b/lib/player/views/components/mini_player.dart index d0d99e33..4d27e759 100644 --- a/lib/player/views/components/mini_player.dart +++ b/lib/player/views/components/mini_player.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:invidious/downloads/models/downloaded_video.dart'; -import 'package:invidious/player/states/interfaces/media_player.dart'; import 'package:invidious/player/states/player.dart'; import 'package:invidious/player/views/components/mini_player_controls.dart'; import 'package:invidious/player/views/components/mini_player_progress.dart'; @@ -26,7 +25,8 @@ class MiniPlayer { padding: const EdgeInsets.all(4.0), child: Builder(builder: (context) { return Builder(builder: (context) { - DownloadedVideo? offlineVid = context.select((PlayerCubit value) => value.state.offlineCurrentlyPlaying); + DownloadedVideo? offlineVid = + context.select((PlayerCubit value) => value.state.offlineCurrentlyPlaying); Video? vid = context.select((PlayerCubit value) => value.state.currentlyPlaying); String title = vid?.title ?? offlineVid?.title ?? ''; diff --git a/lib/player/views/components/mini_player_controls.dart b/lib/player/views/components/mini_player_controls.dart index 56c5d4a6..ca45a290 100644 --- a/lib/player/views/components/mini_player_controls.dart +++ b/lib/player/views/components/mini_player_controls.dart @@ -23,12 +23,11 @@ class MiniPlayerControls extends StatelessWidget { child: Builder(builder: (context) { bool isMini = context.select((PlayerCubit value) => value.state.isMini); bool hasQueue = context.select((PlayerCubit value) => value.state.hasQueue); - bool isPlaying = context.select((PlayerCubit value)=> value.state.isPlaying); + bool isPlaying = context.select((PlayerCubit value) => value.state.isPlaying); return Padding( padding: isMini ? EdgeInsets.zero : const EdgeInsets.all(8.0), child: Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: isMini ? colors.secondaryContainer : colors.background), constraints: BoxConstraints( maxWidth: tabletMaxVideoWidth, ), @@ -79,10 +78,9 @@ class MiniPlayerControls extends StatelessWidget { ], ), if (!isMini) - Builder( - builder: (context) { - var playerRepeatMode = context.select((SettingsCubit s) => s.state.playerRepeatMode); - var shuffleMode = context.select((SettingsCubit s) => s.state.playerShuffleMode); + Builder(builder: (context) { + var playerRepeatMode = context.select((SettingsCubit s) => s.state.playerRepeatMode); + var shuffleMode = context.select((SettingsCubit s) => s.state.playerShuffleMode); var cubit = context.read(); return Row( mainAxisAlignment: MainAxisAlignment.end, diff --git a/lib/player/views/components/mini_player_progress.dart b/lib/player/views/components/mini_player_progress.dart index 2b3b341f..0e48da63 100644 --- a/lib/player/views/components/mini_player_progress.dart +++ b/lib/player/views/components/mini_player_progress.dart @@ -22,7 +22,7 @@ class MiniPlayerProgress extends StatelessWidget { width: double.infinity, height: 2, decoration: BoxDecoration( - color: colors.secondaryContainer, + color: colors.primary.withOpacity(0.2), borderRadius: BorderRadius.circular(20), ), child: AnimatedFractionallySizedBox( diff --git a/lib/player/views/components/player.dart b/lib/player/views/components/player.dart index d54b270c..18e7af74 100644 --- a/lib/player/views/components/player.dart +++ b/lib/player/views/components/player.dart @@ -8,7 +8,6 @@ import 'package:invidious/player/views/components/audio_player.dart'; import 'package:invidious/player/views/components/expanded_player.dart'; import 'package:invidious/player/views/components/mini_player.dart'; import 'package:invidious/player/views/components/video_player.dart'; -import 'package:invidious/settings/states/settings.dart'; import '../../../utils.dart'; import '../../../videos/models/video.dart'; @@ -42,7 +41,9 @@ class Player extends StatelessWidget { Widget videoPlayer = showPlayer ? BlocBuilder( buildWhen: (previous, current) => - previous.isAudio != current.isAudio || previous.currentlyPlaying != current.currentlyPlaying || previous.offlineCurrentlyPlaying != current.offlineCurrentlyPlaying, + previous.isAudio != current.isAudio || + previous.currentlyPlaying != current.currentlyPlaying || + previous.offlineCurrentlyPlaying != current.offlineCurrentlyPlaying, builder: (context, _) { return AspectRatio( aspectRatio: isFullScreen ? aspectRatio : 16 / 9, diff --git a/lib/player/views/components/player_controls.dart b/lib/player/views/components/player_controls.dart index 840c3728..1629e706 100644 --- a/lib/player/views/components/player_controls.dart +++ b/lib/player/views/components/player_controls.dart @@ -42,14 +42,18 @@ class PlayerControls extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - IconButton(onPressed: () => setState(() => player.setSpeed(max(minValue, player.getSpeed() - minValue))), icon: const Icon(Icons.remove)), + IconButton( + onPressed: () => setState(() => player.setSpeed(max(minValue, player.getSpeed() - minValue))), + icon: const Icon(Icons.remove)), SizedBox( width: 50, child: Text( '${player.getSpeed().toStringAsFixed(2)}x', textAlign: TextAlign.center, )), - IconButton(onPressed: () => setState(() => player.setSpeed(min(maxValue, player.getSpeed() + minValue))), icon: const Icon(Icons.add)), + IconButton( + onPressed: () => setState(() => player.setSpeed(min(maxValue, player.getSpeed() + minValue))), + icon: const Icon(Icons.add)), ], ) ], @@ -59,7 +63,8 @@ class PlayerControls extends StatelessWidget { ); } - showPlayerTrackSelection(BuildContext context, PlayerControlsState _, {required List tracks, required int selected, required Function(int index) onSelected}) { + showPlayerTrackSelection(BuildContext context, PlayerControlsState _, + {required List tracks, required int selected, required Function(int index) onSelected}) { List widgets = []; for (int i = 0; i < tracks.length; i++) { @@ -187,7 +192,11 @@ class PlayerControls extends StatelessWidget { var player = context.read(); var colors = Theme.of(context).colorScheme; return Theme( - data: ThemeData(useMaterial3: true, colorScheme: darkColorScheme, progressIndicatorTheme: ProgressIndicatorThemeData(circularTrackColor: darkColorScheme.secondaryContainer.withOpacity(0.8))), + data: ThemeData( + useMaterial3: true, + colorScheme: darkColorScheme, + progressIndicatorTheme: + ProgressIndicatorThemeData(circularTrackColor: darkColorScheme.secondaryContainer.withOpacity(0.8))), child: BlocProvider( create: (context) => PlayerControlsCubit(PlayerControlsState(), player), child: BlocBuilder( @@ -197,7 +206,8 @@ class PlayerControls extends StatelessWidget { bool isPip = context.select((PlayerCubit cubit) => cubit.state.isPip); int totalFastForward = context.select((PlayerCubit cubit) => cubit.state.totalFastForward); int totalRewind = context.select((PlayerCubit cubit) => cubit.state.totalRewind); - String videoTitle = context.select((PlayerCubit cubit) => cubit.state.currentlyPlaying?.title ?? cubit.state.offlineCurrentlyPlaying?.title ?? ''); + String videoTitle = context.select((PlayerCubit cubit) => + cubit.state.currentlyPlaying?.title ?? cubit.state.offlineCurrentlyPlaying?.title ?? ''); var cubit = context.read(); return BlocListener( @@ -207,9 +217,14 @@ class PlayerControls extends StatelessWidget { }, child: GestureDetector( behavior: HitTestBehavior.translucent, - onVerticalDragEnd: _.fullScreenState == FullScreenState.fullScreen || _.displayControls ? null : player.videoDraggedEnd, - onVerticalDragUpdate: _.fullScreenState == FullScreenState.fullScreen || _.displayControls ? null : player.videoDragged, - onVerticalDragStart: _.fullScreenState == FullScreenState.fullScreen || _.displayControls ? null : player.videoDragStarted, + onVerticalDragEnd: _.fullScreenState == FullScreenState.fullScreen || _.displayControls + ? null + : player.videoDraggedEnd, + onVerticalDragUpdate: + _.fullScreenState == FullScreenState.fullScreen || _.displayControls ? null : player.videoDragged, + onVerticalDragStart: _.fullScreenState == FullScreenState.fullScreen || _.displayControls + ? null + : player.videoDragStarted, child: AspectRatio( aspectRatio: 16 / 9, child: Stack( @@ -221,7 +236,8 @@ class PlayerControls extends StatelessWidget { left: 10, child: Container( padding: const EdgeInsets.all(8), - decoration: BoxDecoration(color: Colors.black.withOpacity(0.5), borderRadius: BorderRadius.circular(20)), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), borderRadius: BorderRadius.circular(20)), child: Row( children: [ const Icon( @@ -243,7 +259,11 @@ class PlayerControls extends StatelessWidget { .slideX(duration: animationDuration, curve: Curves.easeInOutQuad) .fadeIn(duration: animationDuration) .fadeOut(delay: const Duration(seconds: 1), duration: animationDuration) - .slideX(end: -0.5, duration: animationDuration, curve: Curves.easeInOutQuad, delay: const Duration(seconds: 1))), + .slideX( + end: -0.5, + duration: animationDuration, + curve: Curves.easeInOutQuad, + delay: const Duration(seconds: 1))), if (!isMini && !isPip) Positioned( left: 0, @@ -261,7 +281,10 @@ class PlayerControls extends StatelessWidget { ? cubit.hideControls : cubit.showControls, onDoubleTap: _.justDoubleTappedSkip ? null : cubit.doubleTapRewind, - child: DoubleTapButton(stepText: '-$totalRewind ${locals.secondsShortForm}', opacity: _.doubleTapRewindedOpacity, icon: Icons.fast_rewind))), + child: DoubleTapButton( + stepText: '-$totalRewind ${locals.secondsShortForm}', + opacity: _.doubleTapRewindedOpacity, + icon: Icons.fast_rewind))), Expanded( child: GestureDetector( onTap: _.justDoubleTappedSkip @@ -296,7 +319,8 @@ class PlayerControls extends StatelessWidget { ? GestureDetector( onTap: cubit.hideControls, child: Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(0), color: Colors.black.withOpacity(0.4)), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(0), color: Colors.black.withOpacity(0.4)), child: Column( children: [ Row( @@ -310,11 +334,16 @@ class PlayerControls extends StatelessWidget { videoTitle, maxLines: 1, overflow: TextOverflow.ellipsis, - style: textTheme.bodyMedium?.copyWith(color: Colors.white.withOpacity(0.8)), + style: textTheme.bodyMedium + ?.copyWith(color: Colors.white.withOpacity(0.8)), ), )), - IconButton(onPressed: () => player.enterPip(), icon: const Icon(Icons.picture_in_picture)), - IconButton(onPressed: () => showOptionMenu(context, _), icon: const Icon(Icons.more_vert)) + IconButton( + onPressed: () => player.enterPip(), + icon: const Icon(Icons.picture_in_picture)), + IconButton( + onPressed: () => showOptionMenu(context, _), + icon: const Icon(Icons.more_vert)) ], ), Expanded(child: Container()), @@ -335,8 +364,13 @@ class PlayerControls extends StatelessWidget { }, icon: const Icon(Icons.volume_up)), switch (_.fullScreenState) { - FullScreenState.fullScreen => IconButton(onPressed: () => player.setFullScreen(FullScreenState.notFullScreen), icon: const Icon(Icons.fullscreen_exit)), - FullScreenState.notFullScreen => IconButton(onPressed: () => player.setFullScreen(FullScreenState.fullScreen), icon: const Icon(Icons.fullscreen)), + FullScreenState.fullScreen => IconButton( + onPressed: () => + player.setFullScreen(FullScreenState.notFullScreen), + icon: const Icon(Icons.fullscreen_exit)), + FullScreenState.notFullScreen => IconButton( + onPressed: () => player.setFullScreen(FullScreenState.fullScreen), + icon: const Icon(Icons.fullscreen)), } ], ), @@ -349,7 +383,8 @@ class PlayerControls extends StatelessWidget { ) : const SizedBox.expand(), ), - if ((_.displayControls || _.justDoubleTappedSkip) && !(player.state.currentlyPlaying?.liveNow ?? false)) + if ((_.displayControls || _.justDoubleTappedSkip) && + !(player.state.currentlyPlaying?.liveNow ?? false)) Positioned( bottom: 0, left: 0, @@ -359,7 +394,10 @@ class PlayerControls extends StatelessWidget { Container( decoration: _.justDoubleTappedSkip ? BoxDecoration( - gradient: LinearGradient(begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Colors.black.withOpacity(1), Colors.black.withOpacity(0)])) + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [Colors.black.withOpacity(1), Colors.black.withOpacity(0)])) : null, child: Padding( padding: const EdgeInsets.only(top: 16.0, right: 8), @@ -370,9 +408,11 @@ class PlayerControls extends StatelessWidget { height: 25, child: Slider( min: 0, - value: min(_.position.inMilliseconds.toDouble(), _.duration.inMilliseconds.toDouble()), + value: min(_.position.inMilliseconds.toDouble(), + _.duration.inMilliseconds.toDouble()), max: _.duration.inMilliseconds.toDouble(), - secondaryTrackValue: min(_.buffer.inMilliseconds.toDouble(), _.duration.inMilliseconds.toDouble()), + secondaryTrackValue: min(_.buffer.inMilliseconds.toDouble(), + _.duration.inMilliseconds.toDouble()), onChangeEnd: cubit.onScrubbed, onChanged: cubit.onScrubDrag, )), diff --git a/lib/player/views/components/video_player.dart b/lib/player/views/components/video_player.dart index 8145361f..74d63de8 100644 --- a/lib/player/views/components/video_player.dart +++ b/lib/player/views/components/video_player.dart @@ -17,7 +17,14 @@ class VideoPlayer extends StatefulWidget { final bool? disableControls; final Duration? startAt; - const VideoPlayer({super.key, this.video, required this.miniPlayer, this.playNow, this.disableControls, this.offlineVideo, this.startAt}) + const VideoPlayer( + {super.key, + this.video, + required this.miniPlayer, + this.playNow, + this.disableControls, + this.offlineVideo, + this.startAt}) : assert(video == null || offlineVideo == null, 'cannot provide both video and offline video\n'); @override @@ -46,10 +53,12 @@ class _VideoPlayerState extends State { video: widget.video, offlineVideo: widget.offlineVideo, disableControls: widget.disableControls), - player, settings), + player, + settings), child: BlocBuilder( builder: (context, _) => BlocListener( - listenWhen: (previous, current) => previous.mediaCommand != current.mediaCommand && current.mediaCommand != null, + listenWhen: (previous, current) => + previous.mediaCommand != current.mediaCommand && current.mediaCommand != null, listener: (context, state) => context.read().handleCommand(state.mediaCommand!), child: _.videoController == null ? const Text('nullll') diff --git a/lib/player/views/components/video_queue.dart b/lib/player/views/components/video_queue.dart index f36b8b7d..bfe39123 100644 --- a/lib/player/views/components/video_queue.dart +++ b/lib/player/views/components/video_queue.dart @@ -116,7 +116,9 @@ class VideoQueue extends StatelessWidget { scrollController: scrollController, itemCount: state.videos.isNotEmpty ? state.videos.length : state.offlineVideos.length, onReorder: controller.onQueueReorder, - itemBuilder: (context, index) => state.videos.isNotEmpty ? onlineVideoQueue(context, index, state.videos[index]) : offlineVideoQueue(context, index, state.offlineVideos[index])), + itemBuilder: (context, index) => state.videos.isNotEmpty + ? onlineVideoQueue(context, index, state.videos[index]) + : offlineVideoQueue(context, index, state.offlineVideos[index])), ); }), ) diff --git a/lib/player/views/tv/components/player_controls.dart b/lib/player/views/tv/components/player_controls.dart index c667288e..9bf544e9 100644 --- a/lib/player/views/tv/components/player_controls.dart +++ b/lib/player/views/tv/components/player_controls.dart @@ -2,19 +2,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/player/states/tv_player_controls.dart'; +import 'package:invidious/player/views/tv/components/player_settings.dart'; import 'package:invidious/utils/views/tv/components/tv_button.dart'; import 'package:invidious/utils/views/tv/components/tv_horizontal_item_list.dart'; import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; -import 'package:invidious/player/views/tv/components/player_settings.dart'; -import '../../../states/interfaces/media_player.dart'; -import '../../../states/player.dart'; import '../../../../globals.dart'; +import '../../../../utils.dart'; import '../../../../utils/models/image_object.dart'; import '../../../../utils/models/paginatedList.dart'; import '../../../../videos/models/video_in_list.dart'; -import '../../../../utils.dart'; import '../../../../videos/views/components/video_thumbnail.dart'; +import '../../../states/player.dart'; class TvPlayerControls extends StatelessWidget { const TvPlayerControls({super.key}); @@ -56,8 +55,11 @@ class TvPlayerControls extends StatelessWidget { duration: animationDuration, child: Container( decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.black.withOpacity(1), Colors.black.withOpacity(0), Colors.black.withOpacity(1)])), + gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ + Colors.black.withOpacity(1), + Colors.black.withOpacity(0), + Colors.black.withOpacity(1) + ])), ), ), ), @@ -89,7 +91,10 @@ class TvPlayerControls extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Thumbnail( - thumbnailUrl: ImageObject.getBestThumbnail(mpc.currentlyPlaying?.authorThumbnails)?.url ?? '', + thumbnailUrl: + ImageObject.getBestThumbnail(mpc.currentlyPlaying?.authorThumbnails) + ?.url ?? + '', width: 40, height: 40, id: 'author-small-${mpc.currentlyPlaying?.authorId}', @@ -267,14 +272,17 @@ class TvPlayerControls extends StatelessWidget { : Expanded( child: player.progress >= 0 ? Container( - decoration: BoxDecoration(color: Colors.black.withOpacity(0.5), borderRadius: BorderRadius.circular(5)), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(5)), child: AnimatedFractionallySizedBox( alignment: Alignment.centerLeft, duration: animationDuration, widthFactor: player.progress, child: Container( height: 8, - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(5)), + decoration: BoxDecoration( + color: Colors.white, borderRadius: BorderRadius.circular(5)), ), )) : const SizedBox.shrink()), @@ -313,7 +321,8 @@ class TvPlayerControls extends StatelessWidget { TvHorizontalVideoList( onSelect: (ctx, video) => onVideoQueueSelected(ctx, cubit, video), paginatedVideoList: FixedItemList(mpc!.videos - .map((e) => VideoInList(e.title, e.videoId, e.lengthSeconds, null, e.author, e.authorId, e.authorUrl, null, null, e.videoThumbnails)) + .map((e) => VideoInList(e.title, e.videoId, e.lengthSeconds, null, + e.author, e.authorId, e.authorUrl, null, null, e.videoThumbnails)) .toList())), ], ), diff --git a/lib/player/views/tv/screens/tvPlayerView.dart b/lib/player/views/tv/screens/tvPlayerView.dart index c5f3def7..1bf8a5a2 100644 --- a/lib/player/views/tv/screens/tvPlayerView.dart +++ b/lib/player/views/tv/screens/tvPlayerView.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -8,10 +9,11 @@ import 'package:invidious/settings/states/settings.dart'; import '../../../../main.dart'; import '../../../../videos/models/base_video.dart'; -class TvPlayerView extends StatelessWidget { +@RoutePage() +class TvPlayerScreen extends StatelessWidget { final List videos; - const TvPlayerView({Key? key, required this.videos}) : super(key: key); + const TvPlayerScreen({Key? key, required this.videos}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/playlists/models/playlist.dart b/lib/playlists/models/playlist.dart index 955a9614..977d6d36 100644 --- a/lib/playlists/models/playlist.dart +++ b/lib/playlists/models/playlist.dart @@ -107,7 +107,8 @@ class Playlist { @JsonKey(includeToJson: false, includeFromJson: false) int removedByFilter = 0; - Playlist(this.type, this.title, this.playlistId, this.author, this.authordId, this.authorUrl, this.description, this.videoCount); + Playlist(this.type, this.title, this.playlistId, this.author, this.authordId, this.authorUrl, this.description, + this.videoCount); factory Playlist.fromJson(Map json) => _$PlaylistFromJson(json); diff --git a/lib/playlists/states/playlist.dart b/lib/playlists/states/playlist.dart index 2900f948..2e25cc28 100644 --- a/lib/playlists/states/playlist.dart +++ b/lib/playlists/states/playlist.dart @@ -54,7 +54,8 @@ class PlaylistCubit extends Cubit { do { pl = await service.getPublicPlaylists(state.playlist.playlistId, page: page); - var toAdd = pl.videos.where((v) => state.playlist.videos.indexWhere((v2) => v2.videoId == v.videoId) == -1).toList(); + var toAdd = + pl.videos.where((v) => state.playlist.videos.indexWhere((v2) => v2.videoId == v.videoId) == -1).toList(); state.playlist.videos.addAll(toAdd); @@ -106,5 +107,6 @@ class PlaylistState { PlaylistState({required this.playlist, required this.playlistItemHeight}); - PlaylistState._(this.showImage, this.loadingProgress, this.playlist, this.loading, this.scrollController, this.playlistItemHeight); + PlaylistState._(this.showImage, this.loadingProgress, this.playlist, this.loading, this.scrollController, + this.playlistItemHeight); } diff --git a/lib/playlists/states/playlist_list.dart b/lib/playlists/states/playlist_list.dart index 5a50a85e..677c0450 100644 --- a/lib/playlists/states/playlist_list.dart +++ b/lib/playlists/states/playlist_list.dart @@ -87,5 +87,5 @@ class PlaylistListState { PlaylistListState(this.paginatedList); - PlaylistListState._(this.paginatedList, this.playlists, this.loading, this.scrollController, this.error); + PlaylistListState._(this.paginatedList, this.playlists, this.loading, this.scrollController, this.error); } diff --git a/lib/playlists/views/components/add_to_playlist_list.dart b/lib/playlists/views/components/add_to_playlist_list.dart index a7ceb7ff..c5852f21 100644 --- a/lib/playlists/views/components/add_to_playlist_list.dart +++ b/lib/playlists/views/components/add_to_playlist_list.dart @@ -70,7 +70,7 @@ class _AddPlayListFormState extends State { var id = await service.createPlayList(nameController.value.text, privacyValue); if (context.mounted) { - Navigator.of(context).pop(); + Navigator.pop(context); } if (context.mounted && id != null && widget.afterAdd != null) { diff --git a/lib/playlists/views/components/playlist_in_list.dart b/lib/playlists/views/components/playlist_in_list.dart index f28ac886..0db363a9 100644 --- a/lib/playlists/views/components/playlist_in_list.dart +++ b/lib/playlists/views/components/playlist_in_list.dart @@ -1,14 +1,13 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/globals.dart'; -import 'package:invidious/main.dart'; -import 'package:invidious/myRouteObserver.dart'; import 'package:invidious/playlists/models/playlist.dart'; import 'package:invidious/playlists/states/playlist_in_list.dart'; import 'package:invidious/playlists/views/components/playlist_thumbnail.dart'; -import 'package:invidious/playlists/views/screens/playlist.dart'; import 'package:invidious/playlists/views/tv/screens/playlist.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/utils.dart'; import '../../states/playlist_list.dart'; @@ -25,20 +24,11 @@ class PlaylistInList extends StatelessWidget { openPlayList(BuildContext context) { var cubit = context.read(); - navigatorKey.currentState - ?.push(MaterialPageRoute( - settings: ROUTE_PLAYLIST, - builder: (context) => PlaylistView( - playlist: playlist, - canDeleteVideos: canDeleteVideos, - ))) - .then((value) => cubit.refreshPlaylists()); + AutoRouter.of(context).push(PlaylistViewRoute(playlist: playlist, canDeleteVideos: canDeleteVideos)).then((value) => cubit.refreshPlaylists()); } openTvPlaylist(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvPlaylistView(playlist: playlist, canDeleteVideos: false), - )); + AutoRouter.of(context).push(TvPlaylistRoute(playlist: playlist, canDeleteVideos: false)); } @override diff --git a/lib/playlists/views/components/playlist_list.dart b/lib/playlists/views/components/playlist_list.dart index fdebff5c..da6ed72f 100644 --- a/lib/playlists/views/components/playlist_list.dart +++ b/lib/playlists/views/components/playlist_list.dart @@ -34,8 +34,11 @@ class PlaylistList extends StatelessWidget { ? Container( alignment: Alignment.center, color: colorScheme.background, - child: - Visibility(visible: _.error.isNotEmpty, child: InkWell(onTap: () => cubit.getPlaylists(), child: Text(_.error == couldNotGetPlaylits ? locals.couldntFetchVideos : _.error))), + child: Visibility( + visible: _.error.isNotEmpty, + child: InkWell( + onTap: () => cubit.getPlaylists(), + child: Text(_.error == couldNotGetPlaylits ? locals.couldntFetchVideos : _.error))), ) : Padding( padding: const EdgeInsets.all(8.0), @@ -49,14 +52,20 @@ class PlaylistList extends StatelessWidget { controller: _.scrollController, itemBuilder: (context, index) => index >= _.playlists.length ? PlaylistPlaceHolder(small: small) - : PlaylistInList(key: ValueKey(_.playlists[index].playlistId), playlist: _.playlists[index], canDeleteVideos: canDeleteVideos, small: small), + : PlaylistInList( + key: ValueKey(_.playlists[index].playlistId), + playlist: _.playlists[index], + canDeleteVideos: canDeleteVideos, + small: small), // separatorBuilder: (context, index) => const Divider(), itemCount: _.playlists.length + (_.loading ? 7 : 0)), ), ), ), Visibility(visible: _.loading && !small, child: const TopListLoading()), - Visibility(visible: !small && canDeleteVideos, child: const Positioned(bottom: 15, right: 15, child: AddPlayListButton())) + Visibility( + visible: !small && canDeleteVideos, + child: const Positioned(bottom: 15, right: 15, child: AddPlayListButton())) ], ); }, diff --git a/lib/playlists/views/components/playlist_thumbnail.dart b/lib/playlists/views/components/playlist_thumbnail.dart index f086b905..dc1e41b2 100644 --- a/lib/playlists/views/components/playlist_thumbnail.dart +++ b/lib/playlists/views/components/playlist_thumbnail.dart @@ -14,7 +14,13 @@ class PlaylistThumbnails extends StatelessWidget { final int maxThumbs; final bool isPlaceHolder; - const PlaylistThumbnails({super.key, required this.videos, this.children, this.bestThumbnails = false, this.maxThumbs = 4, this.isPlaceHolder = false}); + const PlaylistThumbnails( + {super.key, + required this.videos, + this.children, + this.bestThumbnails = false, + this.maxThumbs = 4, + this.isPlaceHolder = false}); List getThumbs(BuildContext context, BoxConstraints constraints) { var thumbs = []; @@ -44,12 +50,15 @@ class PlaylistThumbnails extends StatelessWidget { ? VideoThumbnailView( cacheKey: 'v-${bestThumbnails ? 'best' : 'worst'}/${videosToUse[i].videoId}', videoId: videosToUse[i].videoId, - thumbnailUrl: - (bestThumbnails ? ImageObject.getBestThumbnail(videosToUse[i].videoThumbnails)?.url : ImageObject.getWorstThumbnail(videosToUse[i].videoThumbnails)?.url) ?? '', + thumbnailUrl: (bestThumbnails + ? ImageObject.getBestThumbnail(videosToUse[i].videoThumbnails)?.url + : ImageObject.getWorstThumbnail(videosToUse[i].videoThumbnails)?.url) ?? + '', ) : const SizedBox.shrink(), secondChild: Container( - decoration: BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(10)), + decoration: + BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(10)), ), crossFadeState: isPlaceHolder ? CrossFadeState.showFirst diff --git a/lib/playlists/views/screens/playlist.dart b/lib/playlists/views/screens/playlist.dart index 85c47b1a..941f9d4f 100644 --- a/lib/playlists/views/screens/playlist.dart +++ b/lib/playlists/views/screens/playlist.dart @@ -1,17 +1,17 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_swipe_action_cell/core/cell.dart'; -import 'package:invidious/main.dart'; -import 'package:invidious/myRouteObserver.dart'; +import 'package:invidious/notifications/views/components/bell_icon.dart'; import 'package:invidious/player/states/player.dart'; import 'package:invidious/playlists/views/components/playlist_thumbnail.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/settings/models/errors/invidiousServiceError.dart'; import 'package:invidious/utils.dart'; import 'package:invidious/utils/views/components/placeholders.dart'; import 'package:invidious/videos/models/video_in_list.dart'; import 'package:invidious/videos/views/components/compact_video.dart'; -import 'package:invidious/videos/views/screens/video.dart'; import '../../../globals.dart'; import '../../../videos/views/components/add_to_queue_button.dart'; @@ -19,11 +19,12 @@ import '../../../videos/views/components/play_button.dart'; import '../../models/playlist.dart'; import '../../states/playlist.dart'; -class PlaylistView extends StatelessWidget { +@RoutePage() +class PlaylistViewScreen extends StatelessWidget { final Playlist playlist; final bool canDeleteVideos; - const PlaylistView({super.key, required this.playlist, required this.canDeleteVideos}); + const PlaylistViewScreen({super.key, required this.playlist, required this.canDeleteVideos}); deletePlayList(BuildContext context) { var cubit = context.read(); @@ -37,12 +38,8 @@ class PlaylistView extends StatelessWidget { }); } - openVideo(String videoId) { - navigatorKey.currentState?.push(MaterialPageRoute( - settings: ROUTE_VIDEO, - builder: (context) => VideoView( - videoId: videoId, - ))); + openVideo(BuildContext context, String videoId) { + AutoRouter.of(context).push(VideoRoute(videoId: videoId)); } removeVideoFromPlayList(BuildContext context, VideoInList v) async { @@ -90,7 +87,7 @@ class PlaylistView extends StatelessWidget { ), ), ) - : const SizedBox.shrink() + : BellIcon(itemId: playlist.playlistId, type: BellIconType.playlist) ], ), backgroundColor: colors.background, @@ -115,7 +112,9 @@ class PlaylistView extends StatelessWidget { Center( child: Container( padding: const EdgeInsets.all(5), - decoration: BoxDecoration(color: colors.background.withOpacity(0.5), shape: BoxShape.circle), + decoration: BoxDecoration( + color: colors.background.withOpacity(0.5), + shape: BoxShape.circle), child: TweenAnimationBuilder( tween: Tween(begin: 0, end: _.loadingProgress), duration: animationDuration, @@ -166,7 +165,7 @@ class PlaylistView extends StatelessWidget { : null, child: CompactVideo( video: v, - onTap: () => openVideo(v.videoId), + onTap: () => openVideo(context, v.videoId), key: ValueKey(v.videoId), ))) .toList(), diff --git a/lib/playlists/views/tv/screens/playlist.dart b/lib/playlists/views/tv/screens/playlist.dart index 8a7e3e92..3f46c9b0 100644 --- a/lib/playlists/views/tv/screens/playlist.dart +++ b/lib/playlists/views/tv/screens/playlist.dart @@ -1,5 +1,7 @@ import 'dart:ui'; +import 'package:auto_route/annotations.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -9,6 +11,7 @@ import 'package:invidious/globals.dart'; import 'package:invidious/player/views/tv/screens/tvPlayerView.dart'; import 'package:invidious/playlists/states/playlist.dart'; import 'package:invidious/playlists/views/screens/playlist.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/utils/views/components/placeholders.dart'; import 'package:invidious/utils/views/tv/components/tv_button.dart'; import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; @@ -19,11 +22,12 @@ import '../../../../utils/models/image_object.dart'; import '../../../../videos/models/base_video.dart'; import '../../../../videos/views/components/video_thumbnail.dart'; -class TvPlaylistView extends PlaylistView { - const TvPlaylistView({super.key, required super.playlist, required super.canDeleteVideos}); +@RoutePage() +class TvPlaylistScreen extends PlaylistViewScreen { + const TvPlaylistScreen({super.key, required super.playlist, required super.canDeleteVideos}); playPlaylist(BuildContext context, PlaylistState _) { - Navigator.of(context).push(MaterialPageRoute(builder: (ctx) => TvPlayerView(videos: _.playlist.videos))); + AutoRouter.of(context).push(TvPlayerRoute(videos: _.playlist.videos)); } @override @@ -45,7 +49,10 @@ class TvPlaylistView extends PlaylistView { itemCount: _.playlist.videos.length, itemBuilder: (BuildContext context, int itemIndex, int pageViewIndex) { BaseVideo video = _.playlist.videos[itemIndex]; - return VideoThumbnailView(videoId: video.videoId, decoration: BoxDecoration(), thumbnailUrl: ImageObject.getBestThumbnail(video.videoThumbnails)?.url ?? ''); + return VideoThumbnailView( + videoId: video.videoId, + decoration: BoxDecoration(), + thumbnailUrl: ImageObject.getBestThumbnail(video.videoThumbnails)?.url ?? ''); }, options: CarouselOptions( autoPlayCurve: Curves.easeInOutQuad, @@ -143,20 +150,24 @@ class TvPlaylistView extends PlaylistView { ), ], ), - if(!_.loading)Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 16.0), - child: GridView.count( - controller: _.scrollController, - childAspectRatio: 16 / 13, - crossAxisCount: 3, - children: [ - ..._.playlist.videos.map((e) => TvVideoItem(video: e, autoFocus: false)).toList(), - if (_.loading) ...repeatWidget(() => const TvVideoItemPlaceHolder(), count: 10) - ], + if (!_.loading) + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 16.0), + child: GridView.count( + controller: _.scrollController, + childAspectRatio: 16 / 13, + crossAxisCount: 3, + children: [ + ..._.playlist.videos + .map((e) => TvVideoItem(video: e, autoFocus: false)) + .toList(), + if (_.loading) + ...repeatWidget(() => const TvVideoItemPlaceHolder(), count: 10) + ], + ), ), ), - ), ], ), ), diff --git a/lib/playlists/views/tv/screens/playlist_grid.dart b/lib/playlists/views/tv/screens/playlist_grid.dart index ce9c8821..5be3b2c0 100644 --- a/lib/playlists/views/tv/screens/playlist_grid.dart +++ b/lib/playlists/views/tv/screens/playlist_grid.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -8,11 +9,13 @@ import 'package:invidious/utils/models/paginatedList.dart'; import 'package:invidious/utils/views/components/placeholders.dart'; import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; -class TvPlaylistGridView extends StatelessWidget { + +@RoutePage() +class TvPlaylistGridScreen extends StatelessWidget { final PaginatedList playlistList; final String? tags; - const TvPlaylistGridView({Key? key, required this.playlistList, this.tags}) : super(key: key); + const TvPlaylistGridScreen({Key? key, required this.playlistList, this.tags}) : super(key: key); @override Widget build(BuildContext context) { @@ -45,7 +48,10 @@ class TvPlaylistGridView extends StatelessWidget { childAspectRatio: 16 / 13, crossAxisCount: 3, children: [ - ..._.playlists.map((e) => PlaylistInList(key: ValueKey(e.playlistId), playlist: e, canDeleteVideos: false, isTv: true)).toList(), + ..._.playlists + .map((e) => PlaylistInList( + key: ValueKey(e.playlistId), playlist: e, canDeleteVideos: false, isTv: true)) + .toList(), if (_.loading) ...repeatWidget(() => const TvPlaylistPlaceHolder(), count: 10) ], )) diff --git a/lib/router.dart b/lib/router.dart new file mode 100644 index 00000000..b967a569 --- /dev/null +++ b/lib/router.dart @@ -0,0 +1,154 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:invidious/app/views/screens/main.dart'; +import 'package:invidious/app/views/tv/screens/tv_home.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/player/states/player.dart'; +import 'package:invidious/player/views/tv/screens/tvPlayerView.dart'; +import 'package:invidious/playlists/models/playlist.dart'; +import 'package:invidious/playlists/views/screens/playlist.dart'; +import 'package:invidious/playlists/views/tv/screens/playlist.dart'; +import 'package:invidious/playlists/views/tv/screens/playlist_grid.dart'; +import 'package:invidious/search/views/screens/search.dart'; +import 'package:invidious/search/views/tv/screens/search.dart'; +import 'package:invidious/settings/models/db/server.dart'; +import 'package:invidious/settings/models/db/video_filter.dart'; +import 'package:invidious/settings/views/screens/app_logs.dart'; +import 'package:invidious/settings/views/screens/appearance.dart'; +import 'package:invidious/settings/views/screens/browsing.dart'; +import 'package:invidious/settings/views/screens/manage_servers.dart'; +import 'package:invidious/settings/views/screens/manage_single_server.dart'; +import 'package:invidious/settings/views/screens/notifications.dart'; +import 'package:invidious/settings/views/screens/search_history_settings.dart'; +import 'package:invidious/settings/views/screens/settings.dart'; +import 'package:invidious/settings/views/screens/sponsor_block_settings.dart'; +import 'package:invidious/settings/views/screens/video_filter.dart'; +import 'package:invidious/settings/views/screens/video_filter_setup.dart'; +import 'package:invidious/settings/views/screens/video_player.dart'; +import 'package:invidious/settings/views/tv/screens/manage_servers.dart'; +import 'package:invidious/settings/views/tv/screens/manage_single_server.dart'; +import 'package:invidious/settings/views/tv/screens/search_history_settings.dart'; +import 'package:invidious/settings/views/tv/screens/settings.dart'; +import 'package:invidious/settings/views/tv/screens/sponsor_block_settings.dart'; +import 'package:invidious/subscription_management/view/screens/manage_subscriptions.dart'; +import 'package:invidious/utils/models/paginatedList.dart'; +import 'package:invidious/utils/views/tv/components/select_from_list.dart'; +import 'package:invidious/utils/views/tv/components/tv_plain_text.dart'; +import 'package:invidious/utils/views/tv/components/tv_text_field.dart'; +import 'package:invidious/videos/models/base_video.dart'; +import 'package:invidious/videos/models/video_in_list.dart'; +import 'package:invidious/videos/views/screens/subscriptions.dart'; +import 'package:invidious/videos/views/screens/video.dart'; +import 'package:invidious/videos/views/tv/screens/video.dart'; +import 'package:invidious/videos/views/tv/screens/video_grid_view.dart'; +import 'package:invidious/welcome_wizard/views/screens/welcome_wizard.dart'; +import 'package:invidious/welcome_wizard/views/tv/components/welcome_wizard.dart'; +import 'package:logging/logging.dart'; + +import 'channels/views/screens/channel.dart'; +import 'channels/views/tv/screens/channel.dart'; +import 'downloads/views/screens/download_manager.dart'; +import 'home/views/screens/edit_layout.dart'; +import 'home/views/screens/home.dart'; +import 'main.dart'; + +part 'router.gr.dart'; + +const pathManageSingleServerFromWizard = '/wizard/manage-single-server'; +const pathManageSingleServerFromSettings = 'manage-single-server'; + +final appRouter = AppRouter(); + +final log = Logger('Router'); + +@AutoRouterConfig(replaceInRouteName: 'Screen,Route') +class AppRouter extends _$AppRouter { + @override + List get routes { + bool hasServer = false; + try { + db.getCurrentlySelectedServer(); + hasServer = true; + } catch (e) { + hasServer = false; + } + return isTv + ? [ + AutoRoute( + path: '/', + page: TvHomeRoute.page, + initial: hasServer, + ), + AutoRoute(page: TvWelcomeWizardRoute.page), + AutoRoute(page: TvChannelRoute.page), + AutoRoute(page: TvGridRoute.page), + AutoRoute(page: TvVideoRoute.page), + AutoRoute(page: TvSearchRoute.page), + AutoRoute(page: TvPlayerRoute.page), + AutoRoute(page: TvPlaylistGridRoute.page), + AutoRoute(page: TvPlaylistRoute.page), + AutoRoute(page: TVSettingsRoute.page), + AutoRoute(page: TvSettingsManageServersRoute.page), + AutoRoute(page: TvSearchHistorySettingsRoute.page), + AutoRoute(page: TvSponsorBlockSettingsRoute.page), + AutoRoute(page: TvManageSingleServerRoute.page), + AutoRoute(page: TvSelectFromListRoute.page), + AutoRoute(page: TvTextFieldRoute.page) + ] + : [ + AutoRoute( + page: MainRoute.page, + initial: hasServer, + children: [ + AutoRoute(page: HomeRoute.page, initial: true), + AutoRoute(page: VideoRoute.page), + AutoRoute(page: ChannelRoute.page), + AutoRoute(page: DownloadManagerRoute.page), + AutoRoute(page: SearchRoute.page), + AutoRoute(page: EditHomeLayoutRoute.page), + AutoRoute(page: SettingsRoute.page), + AutoRoute(page: BrowsingSettingsRoute.page), + AutoRoute(page: ManageServersRoute.page), + AutoRoute(page: ManageSingleServerRoute.page, path: pathManageSingleServerFromSettings), + AutoRoute(page: VideoPlayerSettingsRoute.page), + AutoRoute(page: AppearanceSettingsRoute.page), + AutoRoute(page: SearchHistorySettingsRoute.page), + AutoRoute(page: VideoFilterSettingsRoute.page), + AutoRoute(page: VideoFilterSetupRoute.page), + AutoRoute(page: SponsorBlockSettingsRoute.page), + AutoRoute(page: NotificationSettingsRoute.page), + AutoRoute(page: ManageSubscriptionsRoute.page), + AutoRoute(page: AppLogsRoute.page), + AutoRoute(page: PlaylistViewRoute.page), + AutoRoute(page: SubscriptionRoute.page) + ], + ), + AutoRoute(page: ManageSingleServerRoute.page, path: pathManageSingleServerFromWizard), + AutoRoute(page: WelcomeWizardRoute.page, initial: !hasServer) + ]; + } +} + +class MyRouteObserver extends AutoRouterObserver { + @override + void didPush(Route route, Route? previousRoute) { + log.fine('New route pushed: ${route.settings.name}, ${route.runtimeType}'); + if (route.settings.name != null) { + route.navigator?.context.read().showMiniPlayer(); + } + } + +/* + // only override to observer tab routes + @override + void didInitTabRoute(TabPageRoute route, TabPageRoute? previousRoute) { + print('Tab route visited: ${route.name}'); + } + + @override + void didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) { + print('Tab route re-visited: ${route.name}'); + } +*/ +} diff --git a/lib/router.gr.dart b/lib/router.gr.dart new file mode 100644 index 00000000..316954cb --- /dev/null +++ b/lib/router.gr.dart @@ -0,0 +1,1382 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AutoRouterGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +// coverage:ignore-file + +part of 'router.dart'; + +abstract class _$AppRouter extends RootStackRouter { + // ignore: unused_element + _$AppRouter({super.navigatorKey}); + + @override + final Map pagesMap = { + AppLogsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const AppLogsScreen(), + ); + }, + AppearanceSettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const AppearanceSettingsScreen(), + ); + }, + BrowsingSettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const BrowsingSettingsScreen(), + ); + }, + ChannelRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: ChannelScreen( + key: args.key, + channelId: args.channelId, + ), + ); + }, + DownloadManagerRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const DownloadManagerScreen(), + ); + }, + EditHomeLayoutRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const EditHomeLayoutScreen(), + ); + }, + HomeRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const HomeScreen(), + ); + }, + MainRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const MainScreen(), + ); + }, + ManageServersRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ManageServersScreen(), + ); + }, + ManageSingleServerRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: ManageSingleServerScreen( + key: args.key, + server: args.server, + ), + ); + }, + ManageSubscriptionsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ManageSubscriptionsScreen(), + ); + }, + NotificationSettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const NotificationSettingsScreen(), + ); + }, + PlaylistViewRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: PlaylistViewScreen( + key: args.key, + playlist: args.playlist, + canDeleteVideos: args.canDeleteVideos, + ), + ); + }, + SearchHistorySettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const SearchHistorySettingsScreen(), + ); + }, + SearchRoute.name: (routeData) { + final args = routeData.argsAs( + orElse: () => const SearchRouteArgs()); + return AutoRoutePage( + routeData: routeData, + child: SearchScreen( + key: args.key, + query: args.query, + searchNow: args.searchNow, + ), + ); + }, + SettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const SettingsScreen(), + ); + }, + SponsorBlockSettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const SponsorBlockSettingsScreen(), + ); + }, + SubscriptionRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const SubscriptionScreen(), + ); + }, + TVSettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const TVSettingsScreen(), + ); + }, + TvChannelRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: TvChannelScreen( + key: args.key, + channelId: args.channelId, + ), + ); + }, + TvGridRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: TvGridScreen( + key: args.key, + paginatedVideoList: args.paginatedVideoList, + tags: args.tags, + title: args.title, + ), + ); + }, + TvHomeRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const TvHomeScreen(), + ); + }, + TvManageSingleServerRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: TvManageSingleServerScreen( + key: args.key, + server: args.server, + ), + ); + }, + TvPlainTextRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: TvPlainTextScreen( + key: args.key, + text: args.text, + ), + ); + }, + TvPlayerRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: TvPlayerScreen( + key: args.key, + videos: args.videos, + ), + ); + }, + TvPlaylistGridRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: TvPlaylistGridScreen( + key: args.key, + playlistList: args.playlistList, + tags: args.tags, + ), + ); + }, + TvPlaylistRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: TvPlaylistScreen( + key: args.key, + playlist: args.playlist, + canDeleteVideos: args.canDeleteVideos, + ), + ); + }, + TvSearchHistorySettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const TvSearchHistorySettingsScreen(), + ); + }, + TvSearchRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const TvSearchScreen(), + ); + }, + TvSelectFromListRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: TvSelectFromListScreen( + key: args.key, + options: args.options, + selected: args.selected, + onSelect: args.onSelect, + title: args.title, + ), + ); + }, + TvSettingsManageServersRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const TvSettingsManageServersScreen(), + ); + }, + TvSponsorBlockSettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const TvSponsorBlockSettingsScreen(), + ); + }, + TvTextFieldRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: TvTextFieldScreen( + key: args.key, + controller: args.controller, + autofocus: args.autofocus, + autocorrect: args.autocorrect, + focusNode: args.focusNode, + onSubmitted: args.onSubmitted, + textInputAction: args.textInputAction, + obscureText: args.obscureText, + autofillHints: args.autofillHints, + decoration: args.decoration, + ), + ); + }, + TvVideoRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: TvVideoScreen( + key: args.key, + videoId: args.videoId, + ), + ); + }, + TvWelcomeWizardRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const TvWelcomeWizardScreen(), + ); + }, + VideoFilterSettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const VideoFilterSettingsScreen(), + ); + }, + VideoFilterSetupRoute.name: (routeData) { + final args = routeData.argsAs( + orElse: () => const VideoFilterSetupRouteArgs()); + return AutoRoutePage( + routeData: routeData, + child: VideoFilterSetupScreen( + key: args.key, + channelId: args.channelId, + filter: args.filter, + ), + ); + }, + VideoPlayerSettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const VideoPlayerSettingsScreen(), + ); + }, + VideoRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: VideoScreen( + key: args.key, + videoId: args.videoId, + playNow: args.playNow, + ), + ); + }, + WelcomeWizardRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const WelcomeWizardScreen(), + ); + }, + }; +} + +/// generated route for +/// [AppLogsScreen] +class AppLogsRoute extends PageRouteInfo { + const AppLogsRoute({List? children}) + : super( + AppLogsRoute.name, + initialChildren: children, + ); + + static const String name = 'AppLogsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [AppearanceSettingsScreen] +class AppearanceSettingsRoute extends PageRouteInfo { + const AppearanceSettingsRoute({List? children}) + : super( + AppearanceSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'AppearanceSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [BrowsingSettingsScreen] +class BrowsingSettingsRoute extends PageRouteInfo { + const BrowsingSettingsRoute({List? children}) + : super( + BrowsingSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'BrowsingSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ChannelScreen] +class ChannelRoute extends PageRouteInfo { + ChannelRoute({ + Key? key, + required String channelId, + List? children, + }) : super( + ChannelRoute.name, + args: ChannelRouteArgs( + key: key, + channelId: channelId, + ), + initialChildren: children, + ); + + static const String name = 'ChannelRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ChannelRouteArgs { + const ChannelRouteArgs({ + this.key, + required this.channelId, + }); + + final Key? key; + + final String channelId; + + @override + String toString() { + return 'ChannelRouteArgs{key: $key, channelId: $channelId}'; + } +} + +/// generated route for +/// [DownloadManagerScreen] +class DownloadManagerRoute extends PageRouteInfo { + const DownloadManagerRoute({List? children}) + : super( + DownloadManagerRoute.name, + initialChildren: children, + ); + + static const String name = 'DownloadManagerRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [EditHomeLayoutScreen] +class EditHomeLayoutRoute extends PageRouteInfo { + const EditHomeLayoutRoute({List? children}) + : super( + EditHomeLayoutRoute.name, + initialChildren: children, + ); + + static const String name = 'EditHomeLayoutRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [HomeScreen] +class HomeRoute extends PageRouteInfo { + const HomeRoute({List? children}) + : super( + HomeRoute.name, + initialChildren: children, + ); + + static const String name = 'HomeRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [MainScreen] +class MainRoute extends PageRouteInfo { + const MainRoute({List? children}) + : super( + MainRoute.name, + initialChildren: children, + ); + + static const String name = 'MainRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ManageServersScreen] +class ManageServersRoute extends PageRouteInfo { + const ManageServersRoute({List? children}) + : super( + ManageServersRoute.name, + initialChildren: children, + ); + + static const String name = 'ManageServersRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ManageSingleServerScreen] +class ManageSingleServerRoute + extends PageRouteInfo { + ManageSingleServerRoute({ + Key? key, + required Server server, + List? children, + }) : super( + ManageSingleServerRoute.name, + args: ManageSingleServerRouteArgs( + key: key, + server: server, + ), + initialChildren: children, + ); + + static const String name = 'ManageSingleServerRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ManageSingleServerRouteArgs { + const ManageSingleServerRouteArgs({ + this.key, + required this.server, + }); + + final Key? key; + + final Server server; + + @override + String toString() { + return 'ManageSingleServerRouteArgs{key: $key, server: $server}'; + } +} + +/// generated route for +/// [ManageSubscriptionsScreen] +class ManageSubscriptionsRoute extends PageRouteInfo { + const ManageSubscriptionsRoute({List? children}) + : super( + ManageSubscriptionsRoute.name, + initialChildren: children, + ); + + static const String name = 'ManageSubscriptionsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [NotificationSettingsScreen] +class NotificationSettingsRoute extends PageRouteInfo { + const NotificationSettingsRoute({List? children}) + : super( + NotificationSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'NotificationSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [PlaylistViewScreen] +class PlaylistViewRoute extends PageRouteInfo { + PlaylistViewRoute({ + Key? key, + required Playlist playlist, + required bool canDeleteVideos, + List? children, + }) : super( + PlaylistViewRoute.name, + args: PlaylistViewRouteArgs( + key: key, + playlist: playlist, + canDeleteVideos: canDeleteVideos, + ), + initialChildren: children, + ); + + static const String name = 'PlaylistViewRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class PlaylistViewRouteArgs { + const PlaylistViewRouteArgs({ + this.key, + required this.playlist, + required this.canDeleteVideos, + }); + + final Key? key; + + final Playlist playlist; + + final bool canDeleteVideos; + + @override + String toString() { + return 'PlaylistViewRouteArgs{key: $key, playlist: $playlist, canDeleteVideos: $canDeleteVideos}'; + } +} + +/// generated route for +/// [SearchHistorySettingsScreen] +class SearchHistorySettingsRoute extends PageRouteInfo { + const SearchHistorySettingsRoute({List? children}) + : super( + SearchHistorySettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'SearchHistorySettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [SearchScreen] +class SearchRoute extends PageRouteInfo { + SearchRoute({ + Key? key, + String? query, + bool? searchNow, + List? children, + }) : super( + SearchRoute.name, + args: SearchRouteArgs( + key: key, + query: query, + searchNow: searchNow, + ), + initialChildren: children, + ); + + static const String name = 'SearchRoute'; + + static const PageInfo page = PageInfo(name); +} + +class SearchRouteArgs { + const SearchRouteArgs({ + this.key, + this.query, + this.searchNow, + }); + + final Key? key; + + final String? query; + + final bool? searchNow; + + @override + String toString() { + return 'SearchRouteArgs{key: $key, query: $query, searchNow: $searchNow}'; + } +} + +/// generated route for +/// [SettingsScreen] +class SettingsRoute extends PageRouteInfo { + const SettingsRoute({List? children}) + : super( + SettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'SettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [SponsorBlockSettingsScreen] +class SponsorBlockSettingsRoute extends PageRouteInfo { + const SponsorBlockSettingsRoute({List? children}) + : super( + SponsorBlockSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'SponsorBlockSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [SubscriptionScreen] +class SubscriptionRoute extends PageRouteInfo { + const SubscriptionRoute({List? children}) + : super( + SubscriptionRoute.name, + initialChildren: children, + ); + + static const String name = 'SubscriptionRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [TVSettingsScreen] +class TVSettingsRoute extends PageRouteInfo { + const TVSettingsRoute({List? children}) + : super( + TVSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'TVSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [TvChannelScreen] +class TvChannelRoute extends PageRouteInfo { + TvChannelRoute({ + Key? key, + required String channelId, + List? children, + }) : super( + TvChannelRoute.name, + args: TvChannelRouteArgs( + key: key, + channelId: channelId, + ), + initialChildren: children, + ); + + static const String name = 'TvChannelRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class TvChannelRouteArgs { + const TvChannelRouteArgs({ + this.key, + required this.channelId, + }); + + final Key? key; + + final String channelId; + + @override + String toString() { + return 'TvChannelRouteArgs{key: $key, channelId: $channelId}'; + } +} + +/// generated route for +/// [TvGridScreen] +class TvGridRoute extends PageRouteInfo { + TvGridRoute({ + Key? key, + required PaginatedList paginatedVideoList, + String? tags, + required String title, + List? children, + }) : super( + TvGridRoute.name, + args: TvGridRouteArgs( + key: key, + paginatedVideoList: paginatedVideoList, + tags: tags, + title: title, + ), + initialChildren: children, + ); + + static const String name = 'TvGridRoute'; + + static const PageInfo page = PageInfo(name); +} + +class TvGridRouteArgs { + const TvGridRouteArgs({ + this.key, + required this.paginatedVideoList, + this.tags, + required this.title, + }); + + final Key? key; + + final PaginatedList paginatedVideoList; + + final String? tags; + + final String title; + + @override + String toString() { + return 'TvGridRouteArgs{key: $key, paginatedVideoList: $paginatedVideoList, tags: $tags, title: $title}'; + } +} + +/// generated route for +/// [TvHomeScreen] +class TvHomeRoute extends PageRouteInfo { + const TvHomeRoute({List? children}) + : super( + TvHomeRoute.name, + initialChildren: children, + ); + + static const String name = 'TvHomeRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [TvManageSingleServerScreen] +class TvManageSingleServerRoute + extends PageRouteInfo { + TvManageSingleServerRoute({ + Key? key, + required Server server, + List? children, + }) : super( + TvManageSingleServerRoute.name, + args: TvManageSingleServerRouteArgs( + key: key, + server: server, + ), + initialChildren: children, + ); + + static const String name = 'TvManageSingleServerRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class TvManageSingleServerRouteArgs { + const TvManageSingleServerRouteArgs({ + this.key, + required this.server, + }); + + final Key? key; + + final Server server; + + @override + String toString() { + return 'TvManageSingleServerRouteArgs{key: $key, server: $server}'; + } +} + +/// generated route for +/// [TvPlainTextScreen] +class TvPlainTextRoute extends PageRouteInfo { + TvPlainTextRoute({ + Key? key, + required String text, + List? children, + }) : super( + TvPlainTextRoute.name, + args: TvPlainTextRouteArgs( + key: key, + text: text, + ), + initialChildren: children, + ); + + static const String name = 'TvPlainTextRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class TvPlainTextRouteArgs { + const TvPlainTextRouteArgs({ + this.key, + required this.text, + }); + + final Key? key; + + final String text; + + @override + String toString() { + return 'TvPlainTextRouteArgs{key: $key, text: $text}'; + } +} + +/// generated route for +/// [TvPlayerScreen] +class TvPlayerRoute extends PageRouteInfo { + TvPlayerRoute({ + Key? key, + required List videos, + List? children, + }) : super( + TvPlayerRoute.name, + args: TvPlayerRouteArgs( + key: key, + videos: videos, + ), + initialChildren: children, + ); + + static const String name = 'TvPlayerRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class TvPlayerRouteArgs { + const TvPlayerRouteArgs({ + this.key, + required this.videos, + }); + + final Key? key; + + final List videos; + + @override + String toString() { + return 'TvPlayerRouteArgs{key: $key, videos: $videos}'; + } +} + +/// generated route for +/// [TvPlaylistGridScreen] +class TvPlaylistGridRoute extends PageRouteInfo { + TvPlaylistGridRoute({ + Key? key, + required PaginatedList playlistList, + String? tags, + List? children, + }) : super( + TvPlaylistGridRoute.name, + args: TvPlaylistGridRouteArgs( + key: key, + playlistList: playlistList, + tags: tags, + ), + initialChildren: children, + ); + + static const String name = 'TvPlaylistGridRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class TvPlaylistGridRouteArgs { + const TvPlaylistGridRouteArgs({ + this.key, + required this.playlistList, + this.tags, + }); + + final Key? key; + + final PaginatedList playlistList; + + final String? tags; + + @override + String toString() { + return 'TvPlaylistGridRouteArgs{key: $key, playlistList: $playlistList, tags: $tags}'; + } +} + +/// generated route for +/// [TvPlaylistScreen] +class TvPlaylistRoute extends PageRouteInfo { + TvPlaylistRoute({ + Key? key, + required Playlist playlist, + required bool canDeleteVideos, + List? children, + }) : super( + TvPlaylistRoute.name, + args: TvPlaylistRouteArgs( + key: key, + playlist: playlist, + canDeleteVideos: canDeleteVideos, + ), + initialChildren: children, + ); + + static const String name = 'TvPlaylistRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class TvPlaylistRouteArgs { + const TvPlaylistRouteArgs({ + this.key, + required this.playlist, + required this.canDeleteVideos, + }); + + final Key? key; + + final Playlist playlist; + + final bool canDeleteVideos; + + @override + String toString() { + return 'TvPlaylistRouteArgs{key: $key, playlist: $playlist, canDeleteVideos: $canDeleteVideos}'; + } +} + +/// generated route for +/// [TvSearchHistorySettingsScreen] +class TvSearchHistorySettingsRoute extends PageRouteInfo { + const TvSearchHistorySettingsRoute({List? children}) + : super( + TvSearchHistorySettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'TvSearchHistorySettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [TvSearchScreen] +class TvSearchRoute extends PageRouteInfo { + const TvSearchRoute({List? children}) + : super( + TvSearchRoute.name, + initialChildren: children, + ); + + static const String name = 'TvSearchRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [TvSelectFromListScreen] +class TvSelectFromListRoute extends PageRouteInfo { + TvSelectFromListRoute({ + Key? key, + required List options, + required String selected, + required dynamic Function(String) onSelect, + required String title, + List? children, + }) : super( + TvSelectFromListRoute.name, + args: TvSelectFromListRouteArgs( + key: key, + options: options, + selected: selected, + onSelect: onSelect, + title: title, + ), + initialChildren: children, + ); + + static const String name = 'TvSelectFromListRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class TvSelectFromListRouteArgs { + const TvSelectFromListRouteArgs({ + this.key, + required this.options, + required this.selected, + required this.onSelect, + required this.title, + }); + + final Key? key; + + final List options; + + final String selected; + + final dynamic Function(String) onSelect; + + final String title; + + @override + String toString() { + return 'TvSelectFromListRouteArgs{key: $key, options: $options, selected: $selected, onSelect: $onSelect, title: $title}'; + } +} + +/// generated route for +/// [TvSettingsManageServersScreen] +class TvSettingsManageServersRoute extends PageRouteInfo { + const TvSettingsManageServersRoute({List? children}) + : super( + TvSettingsManageServersRoute.name, + initialChildren: children, + ); + + static const String name = 'TvSettingsManageServersRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [TvSponsorBlockSettingsScreen] +class TvSponsorBlockSettingsRoute extends PageRouteInfo { + const TvSponsorBlockSettingsRoute({List? children}) + : super( + TvSponsorBlockSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'TvSponsorBlockSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [TvTextFieldScreen] +class TvTextFieldRoute extends PageRouteInfo { + TvTextFieldRoute({ + Key? key, + required TextEditingController controller, + bool? autofocus, + bool? autocorrect, + FocusNode? focusNode, + void Function(String)? onSubmitted, + TextInputAction? textInputAction, + bool? obscureText, + Iterable? autofillHints, + InputDecoration? decoration, + List? children, + }) : super( + TvTextFieldRoute.name, + args: TvTextFieldRouteArgs( + key: key, + controller: controller, + autofocus: autofocus, + autocorrect: autocorrect, + focusNode: focusNode, + onSubmitted: onSubmitted, + textInputAction: textInputAction, + obscureText: obscureText, + autofillHints: autofillHints, + decoration: decoration, + ), + initialChildren: children, + ); + + static const String name = 'TvTextFieldRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class TvTextFieldRouteArgs { + const TvTextFieldRouteArgs({ + this.key, + required this.controller, + this.autofocus, + this.autocorrect, + this.focusNode, + this.onSubmitted, + this.textInputAction, + this.obscureText, + this.autofillHints, + this.decoration, + }); + + final Key? key; + + final TextEditingController controller; + + final bool? autofocus; + + final bool? autocorrect; + + final FocusNode? focusNode; + + final void Function(String)? onSubmitted; + + final TextInputAction? textInputAction; + + final bool? obscureText; + + final Iterable? autofillHints; + + final InputDecoration? decoration; + + @override + String toString() { + return 'TvTextFieldRouteArgs{key: $key, controller: $controller, autofocus: $autofocus, autocorrect: $autocorrect, focusNode: $focusNode, onSubmitted: $onSubmitted, textInputAction: $textInputAction, obscureText: $obscureText, autofillHints: $autofillHints, decoration: $decoration}'; + } +} + +/// generated route for +/// [TvVideoScreen] +class TvVideoRoute extends PageRouteInfo { + TvVideoRoute({ + Key? key, + required String videoId, + List? children, + }) : super( + TvVideoRoute.name, + args: TvVideoRouteArgs( + key: key, + videoId: videoId, + ), + initialChildren: children, + ); + + static const String name = 'TvVideoRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class TvVideoRouteArgs { + const TvVideoRouteArgs({ + this.key, + required this.videoId, + }); + + final Key? key; + + final String videoId; + + @override + String toString() { + return 'TvVideoRouteArgs{key: $key, videoId: $videoId}'; + } +} + +/// generated route for +/// [TvWelcomeWizardScreen] +class TvWelcomeWizardRoute extends PageRouteInfo { + const TvWelcomeWizardRoute({List? children}) + : super( + TvWelcomeWizardRoute.name, + initialChildren: children, + ); + + static const String name = 'TvWelcomeWizardRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [VideoFilterSettingsScreen] +class VideoFilterSettingsRoute extends PageRouteInfo { + const VideoFilterSettingsRoute({List? children}) + : super( + VideoFilterSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'VideoFilterSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [VideoFilterSetupScreen] +class VideoFilterSetupRoute extends PageRouteInfo { + VideoFilterSetupRoute({ + Key? key, + String? channelId, + VideoFilter? filter, + List? children, + }) : super( + VideoFilterSetupRoute.name, + args: VideoFilterSetupRouteArgs( + key: key, + channelId: channelId, + filter: filter, + ), + initialChildren: children, + ); + + static const String name = 'VideoFilterSetupRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class VideoFilterSetupRouteArgs { + const VideoFilterSetupRouteArgs({ + this.key, + this.channelId, + this.filter, + }); + + final Key? key; + + final String? channelId; + + final VideoFilter? filter; + + @override + String toString() { + return 'VideoFilterSetupRouteArgs{key: $key, channelId: $channelId, filter: $filter}'; + } +} + +/// generated route for +/// [VideoPlayerSettingsScreen] +class VideoPlayerSettingsRoute extends PageRouteInfo { + const VideoPlayerSettingsRoute({List? children}) + : super( + VideoPlayerSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'VideoPlayerSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [VideoScreen] +class VideoRoute extends PageRouteInfo { + VideoRoute({ + Key? key, + required String videoId, + bool? playNow, + List? children, + }) : super( + VideoRoute.name, + args: VideoRouteArgs( + key: key, + videoId: videoId, + playNow: playNow, + ), + initialChildren: children, + ); + + static const String name = 'VideoRoute'; + + static const PageInfo page = PageInfo(name); +} + +class VideoRouteArgs { + const VideoRouteArgs({ + this.key, + required this.videoId, + this.playNow, + }); + + final Key? key; + + final String videoId; + + final bool? playNow; + + @override + String toString() { + return 'VideoRouteArgs{key: $key, videoId: $videoId, playNow: $playNow}'; + } +} + +/// generated route for +/// [WelcomeWizardScreen] +class WelcomeWizardRoute extends PageRouteInfo { + const WelcomeWizardRoute({List? children}) + : super( + WelcomeWizardRoute.name, + initialChildren: children, + ); + + static const String name = 'WelcomeWizardRoute'; + + static const PageInfo page = PageInfo(name); +} diff --git a/lib/search/states/search.dart b/lib/search/states/search.dart index 69eb00ba..8ca217fc 100644 --- a/lib/search/states/search.dart +++ b/lib/search/states/search.dart @@ -2,22 +2,19 @@ import 'package:bloc/bloc.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:flutter/material.dart'; -import 'package:invidious/database.dart'; import 'package:invidious/globals.dart'; -import 'package:invidious/main.dart'; -import 'package:invidious/search/models/search_results.dart'; import 'package:invidious/search/models/search_sort_by.dart'; import '../../channels/models/channel.dart'; import '../../playlists/models/playlist.dart'; import '../../settings/states/settings.dart'; import '../../videos/models/video_in_list.dart'; -import '../models/search_type.dart'; part 'search.g.dart'; class SearchCubit extends Cubit { final SettingsCubit settings; + SearchCubit(super.initialState, this.settings) { onInit(); } @@ -42,25 +39,28 @@ class SearchCubit extends Cubit { search(state.queryController.value.text); } - void searchCleared() { + // returns true search is already cleared + bool searchCleared() { if (state.queryController.value.text.isEmpty) { - navigatorKey.currentState?.pop(); + return true; } else { var state = this.state.copyWith(); state.queryController.clear(); state.showResults = false; emit(state); + return false; } } + clearSearch(){ + emit(state.copyWith(showResults: false)); + } + void getSuggestions({bool hideResult = true}) { - var state = this.state.copyWith(); - state.showResults = !hideResult; - emit(state); + emit(state.copyWith(showResults: !hideResult)); EasyDebounce.debounce('search-suggestions', const Duration(milliseconds: 500), () async { - var state = this.state.copyWith(); - state.suggestions = (await service.getSearchSuggestion(state.queryController.value.text)).suggestions; - emit(state); + var suggestions = (await service.getSearchSuggestion(state.queryController.value.text)).suggestions; + emit(state.copyWith(suggestions: suggestions)); }); } @@ -69,26 +69,7 @@ class SearchCubit extends Cubit { } search(String value) async { - var state = this.state.copyWith(); - state.showResults = true; - state.loading = true; - state.videos = []; - state.channels = []; - state.playlists = []; - emit(state); - - state = state.copyWith(); - List results = await Future.wait([ - service.search(state.queryController.value.text, type: SearchType.video, sortBy: state.sortBy), - service.search(state.queryController.value.text, type: SearchType.channel, sortBy: state.sortBy), - service.search(state.queryController.value.text, type: SearchType.playlist, sortBy: state.sortBy) - ]); - - state.videos = results[0].videos; - state.channels = results[1].channels; - state.playlists = results[2].playlists; - state.loading = false; - emit(state); + emit(state.copyWith(showResults: true)); } setSearchQuery(String e) { @@ -113,13 +94,6 @@ class SearchState extends Clonable { int selectedIndex; - List videos; - - List channels; - - List playlists; - - bool searchNow; List suggestions; @@ -128,8 +102,6 @@ class SearchState extends Clonable { bool showResults; - bool loading; - int videoPage, channelPage, playlistPage; SearchState( @@ -150,20 +122,16 @@ class SearchState extends Clonable { String? query}) : queryController = queryController ?? TextEditingController(text: query ?? ''), selectedIndex = selectedIndex ?? 0, - channels = channels ?? [], - videos = videos ?? [], - playlists = playlists ?? [], searchNow = searchNow ?? false, suggestions = suggestions ?? [], sortBy = sortBy ?? SearchSortBy.relevance, showResults = showResults ?? false, - loading = loading ?? false, videoPage = videoPage ?? 1, channelPage = channelPage ?? 1, playlistPage = playlistPage ?? 1; - SearchState.inLine(this.queryController, this.selectedIndex, this.videos, this.channels, this.playlists, this.searchNow, this.suggestions, this.sortBy, this.showResults, - this.loading, this.videoPage, this.channelPage, this.playlistPage); + SearchState.inLine(this.queryController, this.selectedIndex, this.searchNow, this.suggestions, this.sortBy, + this.showResults, this.videoPage, this.channelPage, this.playlistPage); @override SearchState clone() { diff --git a/lib/search/states/search.g.dart b/lib/search/states/search.g.dart index 8b93d7e4..4ab7457d 100644 --- a/lib/search/states/search.g.dart +++ b/lib/search/states/search.g.dart @@ -11,12 +11,6 @@ abstract class _$SearchStateCWProxy { SearchState selectedIndex(int selectedIndex); - SearchState videos(List videos); - - SearchState channels(List channels); - - SearchState playlists(List playlists); - SearchState searchNow(bool searchNow); SearchState suggestions(List suggestions); @@ -25,8 +19,6 @@ abstract class _$SearchStateCWProxy { SearchState showResults(bool showResults); - SearchState loading(bool loading); - SearchState videoPage(int videoPage); SearchState channelPage(int channelPage); @@ -42,14 +34,10 @@ abstract class _$SearchStateCWProxy { SearchState call({ TextEditingController? queryController, int? selectedIndex, - List? videos, - List? channels, - List? playlists, bool? searchNow, List? suggestions, SearchSortBy? sortBy, bool? showResults, - bool? loading, int? videoPage, int? channelPage, int? playlistPage, @@ -70,15 +58,6 @@ class _$SearchStateCWProxyImpl implements _$SearchStateCWProxy { SearchState selectedIndex(int selectedIndex) => this(selectedIndex: selectedIndex); - @override - SearchState videos(List videos) => this(videos: videos); - - @override - SearchState channels(List channels) => this(channels: channels); - - @override - SearchState playlists(List playlists) => this(playlists: playlists); - @override SearchState searchNow(bool searchNow) => this(searchNow: searchNow); @@ -92,9 +71,6 @@ class _$SearchStateCWProxyImpl implements _$SearchStateCWProxy { @override SearchState showResults(bool showResults) => this(showResults: showResults); - @override - SearchState loading(bool loading) => this(loading: loading); - @override SearchState videoPage(int videoPage) => this(videoPage: videoPage); @@ -116,14 +92,10 @@ class _$SearchStateCWProxyImpl implements _$SearchStateCWProxy { SearchState call({ Object? queryController = const $CopyWithPlaceholder(), Object? selectedIndex = const $CopyWithPlaceholder(), - Object? videos = const $CopyWithPlaceholder(), - Object? channels = const $CopyWithPlaceholder(), - Object? playlists = const $CopyWithPlaceholder(), Object? searchNow = const $CopyWithPlaceholder(), Object? suggestions = const $CopyWithPlaceholder(), Object? sortBy = const $CopyWithPlaceholder(), Object? showResults = const $CopyWithPlaceholder(), - Object? loading = const $CopyWithPlaceholder(), Object? videoPage = const $CopyWithPlaceholder(), Object? channelPage = const $CopyWithPlaceholder(), Object? playlistPage = const $CopyWithPlaceholder(), @@ -137,18 +109,6 @@ class _$SearchStateCWProxyImpl implements _$SearchStateCWProxy { ? _value.selectedIndex // ignore: cast_nullable_to_non_nullable : selectedIndex as int, - videos == const $CopyWithPlaceholder() || videos == null - ? _value.videos - // ignore: cast_nullable_to_non_nullable - : videos as List, - channels == const $CopyWithPlaceholder() || channels == null - ? _value.channels - // ignore: cast_nullable_to_non_nullable - : channels as List, - playlists == const $CopyWithPlaceholder() || playlists == null - ? _value.playlists - // ignore: cast_nullable_to_non_nullable - : playlists as List, searchNow == const $CopyWithPlaceholder() || searchNow == null ? _value.searchNow // ignore: cast_nullable_to_non_nullable @@ -165,10 +125,6 @@ class _$SearchStateCWProxyImpl implements _$SearchStateCWProxy { ? _value.showResults // ignore: cast_nullable_to_non_nullable : showResults as bool, - loading == const $CopyWithPlaceholder() || loading == null - ? _value.loading - // ignore: cast_nullable_to_non_nullable - : loading as bool, videoPage == const $CopyWithPlaceholder() || videoPage == null ? _value.videoPage // ignore: cast_nullable_to_non_nullable diff --git a/lib/search/states/tv_search.dart b/lib/search/states/tv_search.dart index 077504ae..3b08d1a4 100644 --- a/lib/search/states/tv_search.dart +++ b/lib/search/states/tv_search.dart @@ -23,14 +23,29 @@ class TvSearchCubit extends Cubit { return KeyEventResult.ignored; } + + setHasVideo(bool hasVideos) { + emit(state.copyWith(hasVideos: hasVideos)); + } + + setHasChannels(bool hasChannels) { + emit(state.copyWith(hasChannels: hasChannels)); + } + + setHasPlaylists(bool hasPlaylists) { + emit(state.copyWith(hasPlaylists: hasPlaylists)); + } } @CopyWith(constructor: "_") class TvSearchState { FocusNode resultFocus = FocusNode(); FocusNode searchFocus = FocusNode(); + ScrollController scrollController = ScrollController(); + + bool hasVideos = false, hasChannels = false, hasPlaylists = false; TvSearchState(); - TvSearchState._(this.resultFocus, this.searchFocus); + TvSearchState._(this.resultFocus, this.searchFocus, this.hasChannels, this.hasVideos, this.hasPlaylists); } diff --git a/lib/search/states/tv_search.g.dart b/lib/search/states/tv_search.g.dart index 21d9ce13..a8fba455 100644 --- a/lib/search/states/tv_search.g.dart +++ b/lib/search/states/tv_search.g.dart @@ -11,6 +11,12 @@ abstract class _$TvSearchStateCWProxy { TvSearchState searchFocus(FocusNode searchFocus); + TvSearchState hasChannels(bool hasChannels); + + TvSearchState hasVideos(bool hasVideos); + + TvSearchState hasPlaylists(bool hasPlaylists); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TvSearchState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -20,6 +26,9 @@ abstract class _$TvSearchStateCWProxy { TvSearchState call({ FocusNode? resultFocus, FocusNode? searchFocus, + bool? hasChannels, + bool? hasVideos, + bool? hasPlaylists, }); } @@ -37,6 +46,16 @@ class _$TvSearchStateCWProxyImpl implements _$TvSearchStateCWProxy { TvSearchState searchFocus(FocusNode searchFocus) => this(searchFocus: searchFocus); + @override + TvSearchState hasChannels(bool hasChannels) => this(hasChannels: hasChannels); + + @override + TvSearchState hasVideos(bool hasVideos) => this(hasVideos: hasVideos); + + @override + TvSearchState hasPlaylists(bool hasPlaylists) => + this(hasPlaylists: hasPlaylists); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TvSearchState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. @@ -48,6 +67,9 @@ class _$TvSearchStateCWProxyImpl implements _$TvSearchStateCWProxy { TvSearchState call({ Object? resultFocus = const $CopyWithPlaceholder(), Object? searchFocus = const $CopyWithPlaceholder(), + Object? hasChannels = const $CopyWithPlaceholder(), + Object? hasVideos = const $CopyWithPlaceholder(), + Object? hasPlaylists = const $CopyWithPlaceholder(), }) { return TvSearchState._( resultFocus == const $CopyWithPlaceholder() || resultFocus == null @@ -58,6 +80,18 @@ class _$TvSearchStateCWProxyImpl implements _$TvSearchStateCWProxy { ? _value.searchFocus // ignore: cast_nullable_to_non_nullable : searchFocus as FocusNode, + hasChannels == const $CopyWithPlaceholder() || hasChannels == null + ? _value.hasChannels + // ignore: cast_nullable_to_non_nullable + : hasChannels as bool, + hasVideos == const $CopyWithPlaceholder() || hasVideos == null + ? _value.hasVideos + // ignore: cast_nullable_to_non_nullable + : hasVideos as bool, + hasPlaylists == const $CopyWithPlaceholder() || hasPlaylists == null + ? _value.hasPlaylists + // ignore: cast_nullable_to_non_nullable + : hasPlaylists as bool, ); } } diff --git a/lib/search/views/screens/search.dart b/lib/search/views/screens/search.dart index d6078f3f..523aa082 100644 --- a/lib/search/views/screens/search.dart +++ b/lib/search/views/screens/search.dart @@ -1,17 +1,15 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/globals.dart'; -import 'package:invidious/myRouteObserver.dart'; import 'package:invidious/playlists/views/components/playlist_list.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/search/models/search_type.dart'; import 'package:invidious/videos/models/video_in_list.dart'; -import 'package:invidious/videos/views/components/video_in_list.dart'; import '../../../channels/models/channel.dart'; -import '../../../main.dart'; import '../../../playlists/models/playlist.dart'; -import '../../../playlists/states/playlist_list.dart'; import '../../../settings/states/settings.dart'; import '../../../utils.dart'; import '../../../utils/models/paginatedList.dart'; @@ -19,11 +17,16 @@ import '../../../utils/views/components/paginated_list_view.dart'; import '../../../videos/views/components/video_list.dart'; import '../../states/search.dart'; -class Search extends StatelessWidget { + +//Do not change, invidious doesn't allow any specific value, it's to make the paginated lists work as expected +const searchPageSize = 20; + +@RoutePage() +class SearchScreen extends StatelessWidget { final String? query; final bool? searchNow; - const Search({super.key, this.query, this.searchNow}); + const SearchScreen({super.key, this.query, this.searchNow}); @override Widget build(BuildContext context) { @@ -61,7 +64,13 @@ class Search extends StatelessWidget { onSubmitted: cubit.search, ), actions: [ - IconButton(onPressed: cubit.searchCleared, icon: const Icon(Icons.clear)), + IconButton( + onPressed: () { + if (cubit.searchCleared()) { + AutoRouter.of(context).pop(); + } + }, + icon: const Icon(Icons.clear)), ], ), body: SafeArea( @@ -76,7 +85,10 @@ class Search extends StatelessWidget { onTap: () => cubit.setSearchQuery(e), child: Padding( padding: const EdgeInsets.all(8.0), - child: Row(children: [const Icon(Icons.history), Padding(padding: const EdgeInsets.only(left: 8), child: Text(e))]), + child: Row(children: [ + const Icon(Icons.history), + Padding(padding: const EdgeInsets.only(left: 8), child: Text(e)) + ]), ), )) .toList() @@ -130,56 +142,59 @@ class Search extends StatelessWidget { child: FractionallySizedBox( widthFactor: 1, child: [ - _.videos.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(top: 8.0), - child: VideoList( - paginatedVideoList: SearchPaginatedList( - type: SearchType.video, query: _.queryController.value.text, items: _.videos, getFromResults: (res) => res.videos, sortBy: _.sortBy), - ), - ) - : Center(child: Text(locals.nVideos(0))), - _.channels.isNotEmpty - ? PaginatedListView( - paginatedList: SearchPaginatedList( - type: SearchType.channel, query: _.queryController.value.text, items: _.channels, getFromResults: (res) => res.channels, sortBy: _.sortBy), - startItems: _.channels, - itemBuilder: (e) => InkWell( - onTap: () { - navigatorKey.currentState?.pushNamed(PATH_CHANNEL, arguments: e.authorId); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 20), - child: Row( - children: [ - Expanded( - child: Text( - e.author, - style: TextStyle(color: colorScheme.primary), - )), - const Padding( - padding: EdgeInsets.only(right: 8.0), - child: Icon( - Icons.people, - size: 15, - ), - ), - Text(compactCurrency.format(e.subCount)), - ], + VideoList( + paginatedVideoList: PageBasedPaginatedList( + getItemsFunc: (page, maxResults) => service + .search(_.queryController.value.text, + page: page, sortBy: _.sortBy, type: SearchType.video) + .then((value) => value.videos), + maxResults: searchPageSize, + ), + ), + PaginatedListView( + paginatedList: PageBasedPaginatedList( + getItemsFunc: (page, maxResults) => service + .search(_.queryController.value.text, + page: page, sortBy: _.sortBy, type: SearchType.channel) + .then((value) => value.channels), + maxResults: searchPageSize, + ), + itemBuilder: (e) => InkWell( + onTap: () { + AutoRouter.of(context).push(ChannelRoute(channelId: e.authorId)); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 20), + child: Row( + children: [ + Expanded( + child: Text( + e.author, + style: TextStyle(color: colorScheme.primary), + )), + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: Icon( + Icons.people, + size: 15, + ), ), - ), - )) - : Center( - child: Text(locals.noChannels), - ), - _.playlists.isNotEmpty - ? FractionallySizedBox( + Text(compactCurrency.format(e.subCount)), + ], + ), + ), + )), + FractionallySizedBox( child: PlaylistList( - paginatedList: SearchPaginatedList( - type: SearchType.playlist, query: _.queryController.value.text, items: _.playlists, getFromResults: (res) => res.playlists, sortBy: _.sortBy), + paginatedList: PageBasedPaginatedList( + getItemsFunc: (page, maxResults) => service + .search(_.queryController.value.text, + page: page, sortBy: _.sortBy, type: SearchType.playlist) + .then((value) => value.playlists), + maxResults: searchPageSize, + ), canDeleteVideos: false), ) - : Center(child: Text(locals.noPlaylists)) ][_.selectedIndex], ), ), diff --git a/lib/search/views/tv/screens/search.dart b/lib/search/views/tv/screens/search.dart index 033fd920..fa8248c2 100644 --- a/lib/search/views/tv/screens/search.dart +++ b/lib/search/views/tv/screens/search.dart @@ -1,9 +1,10 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:invidious/channels/views/tv/screens/channel.dart'; import 'package:invidious/playlists/models/playlist.dart'; import 'package:invidious/playlists/views/components/playlist_in_list.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/search/models/search_type.dart'; import 'package:invidious/utils/models/paginatedList.dart'; import 'package:invidious/utils/views/components/placeholders.dart'; @@ -14,20 +15,26 @@ import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; import 'package:invidious/utils/views/tv/components/tv_text_field.dart'; import '../../../../channels/models/channel.dart'; +import '../../../../globals.dart'; import '../../../../settings/states/settings.dart'; import '../../../../videos/models/video_in_list.dart'; import '../../../states/search.dart'; import '../../../states/tv_search.dart'; +import '../../screens/search.dart'; -class TvSearch extends StatelessWidget { - const TvSearch({Key? key}) : super(key: key); +@RoutePage() +class TvSearchScreen extends StatelessWidget { + const TvSearchScreen({Key? key}) : super(key: key); Widget buildSuggestion(BuildContext context, SearchState _, bool isHistory, String suggestion) { ColorScheme colors = Theme.of(context).colorScheme; var searchCubit = context.read(); return TvButton( - onPressed: (context) => searchCubit.setSearchQuery(suggestion), + onPressed: (context) { + searchCubit.clearSearch(); + Future.delayed(const Duration(seconds: 1), () => searchCubit.setSearchQuery(suggestion)); + }, focusedColor: colors.secondaryContainer, unfocusedColor: Colors.transparent, child: isHistory @@ -111,71 +118,87 @@ class TvSearch extends StatelessWidget { child: ListView( shrinkWrap: true, children: search.queryController.value.text.isEmpty - ? searchCubit.getHistory().map((e) => buildSuggestion(context, search, true, e)).toList() - : search.suggestions.map((e) => buildSuggestion(context, search, false, e)).toList(), + ? searchCubit + .getHistory() + .map((e) => buildSuggestion(context, search, true, e)) + .toList() + : search.suggestions + .map((e) => buildSuggestion(context, search, false, e)) + .toList(), ), ), Expanded( child: SingleChildScrollView( + controller: tv.scrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: search.loading + children: search.showResults ? [ - const Center( - child: CircularProgressIndicator(), - ) - ] - : [ Visibility( - visible: search.videos.isNotEmpty ?? false, + visible: tv.hasVideos, child: Text( locals.videos, style: textTheme.titleLarge, )), + Focus( + focusNode: tv.resultFocus, + onFocusChange: (focused) { + if (focused) { + tv.scrollController.animateTo(0, + duration: animationDuration, curve: Curves.easeOutQuad); + } + }, + child: TvHorizontalVideoList( + paginatedVideoList: PageBasedPaginatedList( + getItemsFunc: (page, maxResults) => service + .search(search.queryController.value.text, + page: page, type: SearchType.video) + .then((value) { + if (page == 1) { + tvCubit.setHasVideo(value.videos.isNotEmpty); + } + return value.videos; + }), + maxResults: searchPageSize, + ))), Visibility( - visible: search.videos.isNotEmpty ?? false, - child: Focus( - focusNode: tv.resultFocus, - child: TvHorizontalVideoList( - paginatedVideoList: SearchPaginatedList( - getFromResults: (res) => res.videos, - sortBy: search.sortBy, - query: search.queryController.value.text, - type: SearchType.video, - items: search.videos))), - ), - Visibility( - visible: search.channels.isNotEmpty ?? false, + visible: tv.hasChannels, child: Text( locals.channels, style: textTheme.titleLarge, )), - Visibility( - visible: search.channels.isNotEmpty ?? false, - child: SizedBox( - height: 60, - child: TvHorizontalPaginatedListView( - getPlaceHolder: () => const TvChannelPlaceholder(), - paginatedList: SearchPaginatedList( - getFromResults: (res) => res.channels, - sortBy: search.sortBy, - query: search.queryController.value.text, - items: search.channels, - type: SearchType.channel), - startItems: search.channels, - itemBuilder: (e) => Padding( - padding: const EdgeInsets.all(8.0), - child: TvButton( - onPressed: (context) => openChannel(context, e), - borderRadius: 20, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(e.author), - ], - ), + SizedBox( + height: 60, + child: TvHorizontalPaginatedListView( + getPlaceHolder: () => const Padding( + padding: EdgeInsets.all(8.0), + child: TvChannelPlaceholder(), + ), + paginatedList: PageBasedPaginatedList( + getItemsFunc: (page, maxResults) => service + .search(search.queryController.value.text, + page: page, type: SearchType.channel) + .then((value) { + if (page == 1) { + tvCubit.setHasChannels(value.channels.isNotEmpty); + } + return value.channels; + }), + maxResults: searchPageSize, + ), + startItems: [], + itemBuilder: (e) => Padding( + padding: const EdgeInsets.all(8.0), + child: TvButton( + onPressed: (context) => openChannel(context, e), + borderRadius: 20, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(e.author), + ], ), ), ), @@ -183,32 +206,36 @@ class TvSearch extends StatelessWidget { ), ), Visibility( - visible: search.playlists.isNotEmpty ?? false, + visible: tv.hasPlaylists, child: Text( locals.playlists, style: textTheme.titleLarge, )), - Visibility( - visible: search.playlists.isNotEmpty ?? false, - child: TvHorizontalItemList( - getPlaceholder: () => const TvPlaylistPlaceHolder(), - paginatedList: SearchPaginatedList( - getFromResults: (res) => res.playlists, - sortBy: search.sortBy, - query: search.queryController.value.text, - items: search.playlists, - type: SearchType.playlist), - buildItem: (context, index, item) => Padding( - padding: const EdgeInsets.all(8.0), - child: PlaylistInList( - playlist: item, - canDeleteVideos: false, - isTv: true, - // cameFromSearch: true, - )), + TvHorizontalItemList( + getPlaceholder: () => const TvPlaylistPlaceHolder(), + paginatedList: PageBasedPaginatedList( + getItemsFunc: (page, maxResults) => service + .search(search.queryController.value.text, + page: page, type: SearchType.playlist) + .then((value) { + if (page == 1) { + tvCubit.setHasPlaylists(value.playlists.isNotEmpty); + } + return value.playlists; + }), + maxResults: searchPageSize, ), + buildItem: (context, index, item) => Padding( + padding: const EdgeInsets.all(8.0), + child: PlaylistInList( + playlist: item, + canDeleteVideos: false, + isTv: true, + // cameFromSearch: true, + )), ), - ], + ] + : [const Text('YOU')], ), )) ], @@ -229,8 +256,6 @@ class TvSearch extends StatelessWidget { } openChannel(BuildContext context, Channel c) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvChannelView(channelId: c.authorId), - )); + AutoRouter.of(context).push(TvChannelRoute(channelId: c.authorId)); } } diff --git a/lib/service.dart b/lib/service.dart index 580c8b00..d125c4dc 100644 --- a/lib/service.dart +++ b/lib/service.dart @@ -25,6 +25,7 @@ import 'channels/models/channel.dart'; import 'channels/models/channelPlaylists.dart'; import 'channels/models/channelVideos.dart'; import 'comments/models/video_comments.dart'; +import 'notifications/models/db/subscription_notifications.dart'; import 'search/models/search_suggestion.dart'; import 'settings/models/db/server.dart'; import 'settings/models/invidious_public_server.dart'; @@ -248,7 +249,8 @@ class Service { return results; } - Future getUserFeed({int? maxResults, int? page}) async { + Future getUserFeed({int? maxResults, int? page, bool saveLastSeen = true}) async { + // for background service to be able to use var currentlySelectedServer = db.getCurrentlySelectedServer(); Uri uri = buildUrl(urlGetUserFeed, query: {'max_results': maxResults?.toString(), 'page': page?.toString()}); @@ -258,6 +260,17 @@ class Service { var feed = UserFeed.fromJson(handleResponse(response)); feed.videos = (await VideoFilter.filterVideos(feed.videos)).cast(); feed.notifications = (await VideoFilter.filterVideos(feed.notifications)).cast(); + + // we only save the last video seen if we're on the first page otherwise it does not make sense + if (saveLastSeen && (page ?? 1) == 1) { + var videos = List.from(feed.notifications ?? [], growable: true); + videos.addAll(feed.videos ?? []); + if (videos.isNotEmpty) { + var toSave = SubscriptionNotification(videos.first.videoId, DateTime.now().millisecondsSinceEpoch); + db.setLastSubscriptionNotification(toSave); + } + } + return feed; } @@ -380,15 +393,23 @@ class Service { var channel = Channel.fromJson(handleResponse(response)); channel.latestVideos = (await VideoFilter.filterVideos(channel.latestVideos)).cast(); + + if (channel.latestVideos != null && channel.latestVideos!.isNotEmpty) { + db.setChannelNotificationLastViewedVideo(channel.authorUrl, channel.latestVideos![0].videoId); + } return channel; } - Future getChannelVideos(String channelId, String? continuation) async { + Future getChannelVideos(String channelId, String? continuation, {bool saveLastSeen = true}) async { Uri uri = buildUrl(urlGetChannelVideos, pathParams: {':id': channelId}, query: {'continuation': continuation}); final response = await http.get(uri, headers: {'Content-Type': 'application/json; charset=utf-16'}); var videosWithContinuation = VideosWithContinuation.fromJson(handleResponse(response)); videosWithContinuation.videos = (await VideoFilter.filterVideos(videosWithContinuation.videos)).cast(); + + if (saveLastSeen && videosWithContinuation.videos.isNotEmpty) { + db.setChannelNotificationLastViewedVideo(channelId, videosWithContinuation.videos.first.videoId); + } return videosWithContinuation; } @@ -574,7 +595,7 @@ class Service { return servers.where((s) => (s.api ?? false) && (s.stats?.openRegistrations ?? false)).toList(); } - Future getPublicPlaylists(String playlistId, {int? page}) async { + Future getPublicPlaylists(String playlistId, {int? page, bool saveLastSeen = true}) async { Uri uri = buildUrl(urlGetPublicPlaylist, pathParams: {':id': playlistId}, query: {'page': page?.toString()}); final response = await http.get(uri); @@ -583,6 +604,8 @@ class Service { playlist.videos = (await VideoFilter.filterVideos(playlist.videos)).cast(); playlist.removedByFilter = oldLength - playlist.videos.length; + if (saveLastSeen) db.setPlaylistNotificationLastViewedVideo(playlist.playlistId, playlist.videoCount); + return playlist; } diff --git a/lib/settings/models/db/video_filter.dart b/lib/settings/models/db/video_filter.dart index c4adf825..527fd9a4 100644 --- a/lib/settings/models/db/video_filter.dart +++ b/lib/settings/models/db/video_filter.dart @@ -93,7 +93,7 @@ class VideoFilter { var matches = filters.where((element) => element.filterVideo(v)).toList(); v.matchedFilters = matches; v.filtered = matches.isNotEmpty; - log.fine('Video ${v.title} filtered ? ${v.filtered}'); + // log.fine('Video ${v.title} filtered ? ${v.filtered}'); v.filterHide = v.filtered && matches.any((element) => element.hideFromFeed); return v; } @@ -120,12 +120,13 @@ class VideoFilter { String videoChannel = video.authorId?.replaceAll("/channel/", '') ?? ''; if (channelId != null && channelId != videoChannel) { - log.fine('Showing videos, no same channel $channelId (filter) - ${videoChannel} / ${video.author} (video)'); + // log.fine('Showing videos, no same channel $channelId (filter) - ${videoChannel} / ${video.author} (video)'); return false; } // Channel hide all if (channelId != null && filterAll == true && videoChannel == channelId) { - log.fine('Video filtered because hide all == $filterAll, video channel id: ${videoChannel}, channel id ${channelId}'); + log.fine( + 'Video filtered because hide all == $filterAll, video channel id: ${videoChannel}, channel id ${channelId}'); return !isTimeAllowed(); } @@ -165,13 +166,16 @@ class VideoFilter { List safeStartTime = (startTime.isEmpty ? defaultStartTime : startTime).split(":"); List safeEndTime = (endTime.isEmpty ? defaultEndTime : endTime).split(":"); - DateTime startDateTime = DateTime.now().copyWith(hour: int.parse(safeStartTime[0]), minute: int.parse(safeStartTime[1]), second: int.parse(safeStartTime[2])); - DateTime endDateTime = DateTime.now().copyWith(hour: int.parse(safeEndTime[0]), minute: int.parse(safeEndTime[1]), second: int.parse(safeEndTime[2])); + DateTime startDateTime = DateTime.now().copyWith( + hour: int.parse(safeStartTime[0]), minute: int.parse(safeStartTime[1]), second: int.parse(safeStartTime[2])); + DateTime endDateTime = DateTime.now().copyWith( + hour: int.parse(safeEndTime[0]), minute: int.parse(safeEndTime[1]), second: int.parse(safeEndTime[2])); // we only allow the video if current time is outside current time range bool isTimeAllowed = now.isBefore(startDateTime) || now.isAfter(endDateTime); - log.fine("Filter daysOfWeek ${daysOfWeek}, now day of week: ${now.weekday}, Filter from ${startDateTime} to ${endDateTime} current time ${now} "); + log.fine( + "Filter daysOfWeek ${daysOfWeek}, now day of week: ${now.weekday}, Filter from ${startDateTime} to ${endDateTime} current time ${now} "); return isTimeAllowed; } @@ -210,8 +214,11 @@ class VideoFilter { str = locals.videoFilterWholeChannel(hideFromFeed ? locals.videoFilterHideLabel : locals.videoFilterFilterLabel); } else if (type != null && operation != null) { log.fine("Filter type $hideFromFeed"); - str = locals.videoFilterDescriptionString(hideFromFeed ? locals.videoFilterHideLabel : locals.videoFilterFilterLabel, FilterType.localizedType(type!, locals).toLowerCase(), - FilterOperation.localizedLabel(operation!, locals).toLowerCase(), value ?? ''); + str = locals.videoFilterDescriptionString( + hideFromFeed ? locals.videoFilterHideLabel : locals.videoFilterFilterLabel, + FilterType.localizedType(type!, locals).toLowerCase(), + FilterOperation.localizedLabel(operation!, locals).toLowerCase(), + value ?? ''); } String daysOfWeek = localizedDaysOfWeek(locals); diff --git a/lib/settings/states/app_logs.dart b/lib/settings/states/app_logs.dart index cceb81c7..8f74b2e0 100644 --- a/lib/settings/states/app_logs.dart +++ b/lib/settings/states/app_logs.dart @@ -31,7 +31,8 @@ class AppLogsCubit extends Cubit { state.selected.sort(); String toClipboard = state.logs .where((element) => state.selected.contains(element.id)) - .map((e) => '[${e.level}] [${e.logger}] - ${e.time} - ${e.message} ${e.stacktrace != null ? '\n${e.stacktrace}' : ''}') + .map((e) => + '[${e.level}] [${e.logger}] - ${e.time} - ${e.message} ${e.stacktrace != null ? '\n${e.stacktrace}' : ''}') .toList() .reversed .join("\n"); diff --git a/lib/settings/states/channel_notifications.dart b/lib/settings/states/channel_notifications.dart new file mode 100644 index 00000000..a5e89b92 --- /dev/null +++ b/lib/settings/states/channel_notifications.dart @@ -0,0 +1,30 @@ +import 'package:bloc/bloc.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/notifications/models/db/channel_notifications.dart'; +import 'package:invidious/notifications/models/db/playlist_notifications.dart'; + +abstract class NotificationListCubit extends Cubit> { + NotificationListCubit(super.initialState); + + deleteNotification(T notif); +} + +class ChannelNotificationListCubit extends NotificationListCubit { + ChannelNotificationListCubit(super.initialState); + + @override + deleteNotification(ChannelNotification notif) { + db.deleteChannelNotification(notif); + emit(List.of(db.getAllChannelNotifications())); + } +} + +class PlaylistNotificationListCubit extends NotificationListCubit { + PlaylistNotificationListCubit(super.initialState); + + @override + deleteNotification(PlaylistNotification notif) { + db.deletePlaylistNotification(notif); + emit(List.of(db.getAllPlaylistNotifications())); + } +} diff --git a/lib/settings/states/server_list_settings.dart b/lib/settings/states/server_list_settings.dart index ecb260b2..1bfd01c2 100644 --- a/lib/settings/states/server_list_settings.dart +++ b/lib/settings/states/server_list_settings.dart @@ -37,7 +37,8 @@ class ServerListSettingsCubit extends Cubit { refreshServers() { var state = this.state.copyWith(); - var servers = state.publicServers.where((s) => state.dbServers.indexWhere((element) => element.url == s.url) == -1).toList(); + var servers = + state.publicServers.where((s) => state.dbServers.indexWhere((element) => element.url == s.url) == -1).toList(); state.dbServers = db.getServers(); state.publicServers = servers; @@ -65,7 +66,9 @@ class ServerListSettingsCubit extends Cubit { List pingedServers = await Future.wait(servers.map((e) async { try { var progressState = this.state.copyWith(); - e.ping = await service.pingServer(e.url).timeout(const Duration(seconds: pingTimeout), onTimeout: () => const Duration(seconds: pingTimeout)); + e.ping = await service + .pingServer(e.url) + .timeout(const Duration(seconds: pingTimeout), onTimeout: () => const Duration(seconds: pingTimeout)); progress++; progressState.publicServerProgress = progress / servers.length; emit(progressState); @@ -76,9 +79,11 @@ class ServerListSettingsCubit extends Cubit { } })); - List successfullyPingedServers = pingedServers.where((element) => element != null).map((e) => e!).toList(); + List successfullyPingedServers = + pingedServers.where((element) => element != null).map((e) => e!).toList(); - successfullyPingedServers.sort((a, b) => (a.ping ?? const Duration(seconds: pingTimeout)).compareTo(b.ping ?? const Duration(seconds: pingTimeout))); + successfullyPingedServers.sort((a, b) => + (a.ping ?? const Duration(seconds: pingTimeout)).compareTo(b.ping ?? const Duration(seconds: pingTimeout))); state = this.state.copyWith(); state.pinging = false; @@ -138,7 +143,8 @@ class ServerListSettingsCubit extends Cubit { @CopyWith(constructor: "_") class ServerListSettingsState { - ServerListSettingsState({required this.dbServers, required this.publicServers, this.publicServerProgress = 0, this.pinging = true}); + ServerListSettingsState( + {required this.dbServers, required this.publicServers, this.publicServerProgress = 0, this.pinging = true}); List dbServers; List publicServers; @@ -153,5 +159,6 @@ class ServerListSettingsState { addServerController.dispose(); } - ServerListSettingsState._(this.dbServers, this.publicServers, this.publicServerProgress, this.addServerController, this.pinging, this.publicServersError); + ServerListSettingsState._(this.dbServers, this.publicServers, this.publicServerProgress, this.addServerController, + this.pinging, this.publicServersError); } diff --git a/lib/settings/states/server_settings.dart b/lib/settings/states/server_settings.dart index b80b6914..793f9412 100644 --- a/lib/settings/states/server_settings.dart +++ b/lib/settings/states/server_settings.dart @@ -1,7 +1,6 @@ import 'package:bloc/bloc.dart'; import '../../app/states/app.dart'; -import '../../database.dart'; import '../../globals.dart'; import '../models/db/server.dart'; diff --git a/lib/settings/states/settings.dart b/lib/settings/states/settings.dart index 0e78d2c5..ae99a42a 100644 --- a/lib/settings/states/settings.dart +++ b/lib/settings/states/settings.dart @@ -1,10 +1,14 @@ +import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:bloc/bloc.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:easy_debounce/easy_debounce.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/app/states/app.dart'; +import 'package:invidious/foreground_service.dart'; import 'package:locale_names/locale_names.dart'; import 'package:logging/logging.dart'; +import 'package:optimize_battery/optimize_battery.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../../database.dart'; @@ -20,6 +24,11 @@ part 'settings.g.dart'; const String subtitleDefaultSize = '14'; const String searchHistoryDefaultLength = '12'; +enum EnableBackGroundNotificationResponse { + ok, + needBatteryOptimization; +} + var log = Logger('SettingsController'); class SettingsCubit extends Cubit { @@ -310,6 +319,54 @@ class SettingsCubit extends Cubit { state.navigationBarLabelBehavior = behavior; emit(state); } + + Future setBackgroundNotifications(bool b) async { + if (!b) { + var state = this.state.copyWith(); + state.backgroundNotifications = b; + backgroundService.invoke('stopService'); + emit(state); + } else { + AwesomeNotifications().isNotificationAllowed().then((isAllowed) { + if (!isAllowed) { + // This is just a basic example. For real apps, you must show some + // friendly dialog box before call the request method. + // This is very important to not harm the user experience + AwesomeNotifications().requestPermissionToSendNotifications(); + } + }); + + var ignoringBatterOptimization = await OptimizeBattery.isIgnoringBatteryOptimizations(); + if (!ignoringBatterOptimization) { + return EnableBackGroundNotificationResponse.needBatteryOptimization; + } else { + var state = this.state.copyWith(); + state.backgroundNotifications = b; + backgroundService.startService(); + emit(state); + return EnableBackGroundNotificationResponse.ok; + } + } + + return EnableBackGroundNotificationResponse.ok; + } + + setSubscriptionsNotifications(bool b) { + var state = this.state.copyWith(); + state.subscriptionsNotifications = b; + emit(state); + } + + setBackgroundCheckFrequency(int i) { + if (i > 0 && i <= 24) { + var state = this.state.copyWith(); + state.backgroundNotificationFrequency = i; + emit(state); + EasyDebounce.debounce('restarting-background-service', const Duration(seconds: 2), () { + backgroundService.invoke(restartTimerMethod); + }); + } + } } @CopyWith(constructor: "_") @@ -376,7 +433,9 @@ class SettingsState { Country get country => getCountryFromCode(_get(BROWSING_COUNTRY)?.value ?? 'US'); set country(Country c) { - String code = countryCodes.firstWhere((element) => element.name == c.name, orElse: () => country).code; + String code = countryCodes + .firstWhere((element) => element.name == c.name, orElse: () => country) + .code; _set(BROWSING_COUNTRY, code); } @@ -434,6 +493,18 @@ class SettingsState { set distractionFreeMode(bool b) => _set(DISTRACTION_FREE_MODE, b); + bool get backgroundNotifications => _get(BACKGROUND_NOTIFICATIONS)?.value == 'true'; + + set backgroundNotifications(bool b) => _set(BACKGROUND_NOTIFICATIONS, b); + + bool get subscriptionsNotifications => _get(SUBSCRIPTION_NOTIFICATIONS)?.value == 'true'; + + set subscriptionsNotifications(bool b) => _set(SUBSCRIPTION_NOTIFICATIONS, b); + + int get backgroundNotificationFrequency => int.parse(_get(BACKGROUND_CHECK_FREQUENCY)?.value ?? "1"); + + set backgroundNotificationFrequency(int i) => _set(BACKGROUND_CHECK_FREQUENCY, i); + void _set(String name, T value) { if (value == null) { db.deleteSetting(name); diff --git a/lib/settings/states/video_filter.dart b/lib/settings/states/video_filter.dart index 495880e7..5ec9fea6 100644 --- a/lib/settings/states/video_filter.dart +++ b/lib/settings/states/video_filter.dart @@ -2,7 +2,6 @@ import 'package:bloc/bloc.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:invidious/database.dart'; import 'package:invidious/globals.dart'; -import 'package:invidious/settings/models/db/settings.dart'; import 'package:logging/logging.dart'; import '../models/db/video_filter.dart'; diff --git a/lib/settings/states/video_filter_edit.dart b/lib/settings/states/video_filter_edit.dart index 1bd81886..7aa12664 100644 --- a/lib/settings/states/video_filter_edit.dart +++ b/lib/settings/states/video_filter_edit.dart @@ -2,7 +2,6 @@ import 'package:bloc/bloc.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:flutter/cupertino.dart'; import 'package:invidious/globals.dart'; -import 'package:invidious/main.dart'; import 'package:invidious/search/models/search_type.dart'; import 'package:logging/logging.dart'; @@ -88,7 +87,10 @@ class VideoFilterEditCubit extends Cubit { bool isFilterValid() { return (state.filter != null && state.filter?.channelId != null && (state.filter?.filterAll ?? false)) || - (state.filter != null && state.filter?.type != null && state.filter?.operation != null && (state.filter?.value ?? '').isNotEmpty); + (state.filter != null && + state.filter?.type != null && + state.filter?.operation != null && + (state.filter?.value ?? '').isNotEmpty); } void onSave() { @@ -97,7 +99,6 @@ class VideoFilterEditCubit extends Cubit { log.fine('hide all ? ${state.filter?.filterAll}'); db.saveFilter(state.filter!); // VideoFilterController.to()?.refreshFilters(); - navigatorKey.currentState?.pop(); } } @@ -201,9 +202,15 @@ class VideoFilterEditState { TextEditingController valueController; - VideoFilterEditState({this.filter, this.searchPage = 1, this.channel, List? channelResults, TextEditingController? valueController}) + VideoFilterEditState( + {this.filter, + this.searchPage = 1, + this.channel, + List? channelResults, + TextEditingController? valueController}) : channelResults = channelResults ?? [], valueController = valueController ?? TextEditingController(text: filter?.value ?? ''); - VideoFilterEditState._(this.filter, this.searchPage, this.channel, this.channelResults, this.valueController, this.showDateSettings); + VideoFilterEditState._( + this.filter, this.searchPage, this.channel, this.channelResults, this.valueController, this.showDateSettings); } diff --git a/lib/settings/views/components/app_customizer.dart b/lib/settings/views/components/app_customizer.dart index becf8b94..e0414db5 100644 --- a/lib/settings/views/components/app_customizer.dart +++ b/lib/settings/views/components/app_customizer.dart @@ -15,7 +15,8 @@ class AppCustomizer extends StatelessWidget { var colors = Theme.of(context).colorScheme; var textTheme = Theme.of(context).textTheme; return BlocProvider( - create: (context) => AppCustomizerCubit(context.read().state.appLayout, context.read()), + create: (context) => + AppCustomizerCubit(context.read().state.appLayout, context.read()), child: BlocBuilder>( builder: (context, state) { var settings = context.read(); @@ -38,7 +39,9 @@ class AppCustomizer extends StatelessWidget { key: ValueKey(source), children: [ IconButton( - onPressed: () => settings.selectOnOpen(index), icon: Icon(onStart == index ? Icons.home : Icons.home_outlined, color: onStart == index ? colors.primary : colors.secondary)), + onPressed: () => settings.selectOnOpen(index), + icon: Icon(onStart == index ? Icons.home : Icons.home_outlined, + color: onStart == index ? colors.primary : colors.secondary)), Checkbox( value: state.contains(source), onChanged: (value) => appLayout.updateSource(source, value ?? false), diff --git a/lib/settings/views/components/channel_notifications.dart b/lib/settings/views/components/channel_notifications.dart new file mode 100644 index 00000000..61ed8528 --- /dev/null +++ b/lib/settings/views/components/channel_notifications.dart @@ -0,0 +1,55 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/notifications/models/db/channel_notifications.dart'; +import 'package:invidious/router.dart'; +import 'package:invidious/settings/states/channel_notifications.dart'; +import 'package:invidious/utils.dart'; + +class ChannelNotificationList extends StatelessWidget { + const ChannelNotificationList({super.key}); + + deleteNotification(BuildContext context, ChannelNotification notif) { + var cubit = context.read(); + var locals = AppLocalizations.of(context)!; + okCancelDialog(context, locals.deleteChannelNotificationTitle, locals.deleteChannelNotificationContent, + () => cubit.deleteNotification(notif)); + } + + @override + Widget build(BuildContext context) { + var colors = Theme.of(context).colorScheme; + return BlocProvider( + create: (context) => ChannelNotificationListCubit(db.getAllChannelNotifications()), + child: BlocBuilder>(builder: (context, channels) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: ListView.builder( + itemCount: channels.length, + itemBuilder: (context, index) { + var notif = channels[index]; + return Container( + key: ValueKey(notif.id), + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + decoration: BoxDecoration( + color: index % 2 != 0 ? colors.secondaryContainer.withOpacity(0.5) : colors.background, + borderRadius: BorderRadius.circular(10)), + child: InkWell( + onTap: () => AutoRouter.of(context).push(ChannelRoute(channelId: notif.channelId)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(notif.channelName), + IconButton(onPressed: () => deleteNotification(context, notif), icon: const Icon(Icons.clear)) + ], + ), + ), + ); + }, + ), + ); + })); + } +} diff --git a/lib/settings/views/components/manager_server_inner.dart b/lib/settings/views/components/manager_server_inner.dart index b3e70b48..62c26313 100644 --- a/lib/settings/views/components/manager_server_inner.dart +++ b/lib/settings/views/components/manager_server_inner.dart @@ -1,12 +1,11 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/app/states/app.dart'; -import 'package:invidious/main.dart'; -import 'package:invidious/myRouteObserver.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/settings/states/server_list_settings.dart'; import 'package:invidious/settings/states/settings.dart'; -import 'package:invidious/settings/views/screens/manage_single_server.dart'; import 'package:settings_ui/settings_ui.dart'; import '../../../utils.dart'; @@ -14,7 +13,8 @@ import '../../models/db/server.dart'; import '../screens/settings.dart'; class ManagerServersView extends StatelessWidget { - const ManagerServersView({super.key}); + final bool fromWizard; + const ManagerServersView({super.key, bool this.fromWizard = false}); showPublicServerActions(BuildContext context, ServerListSettingsState controller, Server server) { var locals = AppLocalizations.of(context)!; @@ -121,14 +121,7 @@ class ManagerServersView extends StatelessWidget { openServer(BuildContext context, Server s) { var cubit = context.read(); - navigatorKey.currentState - ?.push(MaterialPageRoute( - settings: ROUTE_SETTINGS_MANAGE_ONE_SERVER, - builder: (context) => ManageSingleServer( - server: s, - ), - )) - .then((value) => cubit.refreshServers()); + AutoRouter.of(context).push(ManageSingleServerRoute(server: s)).then((value) => cubit.refreshServers()); } @override @@ -143,7 +136,8 @@ class ManagerServersView extends StatelessWidget { SettingsCubit settings = context.watch(); ServerListSettingsCubit cubit = context.read(); var app = context.read(); - var filteredPublicServers = _.publicServers.where((s) => _.dbServers.indexWhere((element) => element.url == s.url) == -1).toList(); + var filteredPublicServers = + _.publicServers.where((s) => _.dbServers.indexWhere((element) => element.url == s.url) == -1).toList(); return Stack( children: [ SettingsList( @@ -171,12 +165,15 @@ class ManagerServersView extends StatelessWidget { padding: const EdgeInsets.all(8.0), child: Icon( Icons.done, - color: s.url == app.state.server?.url ? colorScheme.primary : colorScheme.secondaryContainer, + color: s.url == app.state.server?.url + ? colorScheme.primary + : colorScheme.secondaryContainer, ), ), ), title: Text(s.url), - value: Text('${cubit.isLoggedInToServer(s.url) ? '${locals.loggedIn}, ' : ''} ${locals.tapToManage}'), + value: Text( + '${cubit.isLoggedInToServer(s.url) ? '${locals.loggedIn}, ' : ''} ${locals.tapToManage}'), onPressed: (context) => openServer(context, s), )) .toList() @@ -216,12 +213,21 @@ class ManagerServersView extends StatelessWidget { title: Row( children: [ Expanded(child: Text('${s.url} ')), - Text((s.ping != null && s.ping!.compareTo(const Duration(seconds: pingTimeout)) == -1) ? '${s.ping?.inMilliseconds}ms' : '>${pingTimeout}s', + Text( + (s.ping != null && + s.ping!.compareTo(const Duration(seconds: pingTimeout)) == -1) + ? '${s.ping?.inMilliseconds}ms' + : '>${pingTimeout}s', style: textTheme.labelLarge?.copyWith(color: colorScheme.secondary)) ], ), value: Wrap( - children: [Visibility(visible: s.flag != null && s.region != null, child: Text('${s.flag} - ${s.region} - ')), Text(locals.tapToAddServer)], + children: [ + Visibility( + visible: s.flag != null && s.region != null, + child: Text('${s.flag} - ${s.region} - ')), + Text(locals.tapToAddServer) + ], ), onPressed: (context) => showPublicServerActions(context, _, s), )) diff --git a/lib/settings/views/components/playlist_notifications.dart b/lib/settings/views/components/playlist_notifications.dart new file mode 100644 index 00000000..7eb1e04a --- /dev/null +++ b/lib/settings/views/components/playlist_notifications.dart @@ -0,0 +1,55 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/notifications/models/db/playlist_notifications.dart'; +import 'package:invidious/router.dart'; +import 'package:invidious/settings/states/channel_notifications.dart'; +import 'package:invidious/utils.dart'; + +class PlaylistNotificationList extends StatelessWidget { + const PlaylistNotificationList({super.key}); + + deleteNotification(BuildContext context, PlaylistNotification notif) { + var cubit = context.read(); + var locals = AppLocalizations.of(context)!; + okCancelDialog(context, locals.deletePlaylistNotificationTitle, locals.deletePlaylistNotificationContent, () => cubit.deleteNotification(notif)); + } + + openPlaylist(BuildContext context, String playlistId) { + service.getPublicPlaylists(playlistId).then((value) => { + if (context.mounted) {AutoRouter.of(context).push(PlaylistViewRoute(playlist: value, canDeleteVideos: false))} + }); + } + + @override + Widget build(BuildContext context) { + var colors = Theme.of(context).colorScheme; + return BlocProvider( + create: (context) => PlaylistNotificationListCubit(db.getAllPlaylistNotifications()), + child: BlocBuilder>(builder: (context, channels) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: ListView.builder( + itemCount: channels.length, + itemBuilder: (context, index) { + var notif = channels[index]; + return Container( + key: ValueKey(notif.id), + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + decoration: BoxDecoration(color: index % 2 != 0 ? colors.secondaryContainer.withOpacity(0.5) : colors.background, borderRadius: BorderRadius.circular(10)), + child: InkWell( + onTap: () => openPlaylist(context, notif.playlistId), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text(notif.playlistName), IconButton(onPressed: () => deleteNotification(context, notif), icon: const Icon(Icons.clear))], + ), + ), + ); + }, + ), + ); + })); + } +} diff --git a/lib/settings/views/components/video_filter_channel.dart b/lib/settings/views/components/video_filter_channel.dart index 0a052e0f..3e175dbd 100644 --- a/lib/settings/views/components/video_filter_channel.dart +++ b/lib/settings/views/components/video_filter_channel.dart @@ -1,14 +1,13 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_swipe_action_cell/core/cell.dart'; -import 'package:invidious/main.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/settings/states/video_filter.dart'; import 'package:invidious/settings/states/video_filter_channel.dart'; import 'package:invidious/settings/views/components/video_filter_item.dart'; -import 'package:invidious/settings/views/screens/video_filter_setup.dart'; -import '../../../myRouteObserver.dart'; import '../../../utils/models/image_object.dart'; import '../../../videos/views/components/video_thumbnail.dart'; import '../../models/db/video_filter.dart'; @@ -21,13 +20,8 @@ class VideoFilterChannel extends StatelessWidget { editFilter(BuildContext context, {required VideoFilter filter}) { var cubit = context.read(); - navigatorKey.currentState - ?.push(MaterialPageRoute( - settings: ROUTE_SETTINGS_VIDEO_FILTERS, - builder: (context) => VideoFilterSetup( - channelId: filter.channelId, - filter: filter, - ))) + AutoRouter.of(context) + .push(VideoFilterSetupRoute(channelId: filter.channelId, filter: filter)) .then((value) => cubit.refreshFilters()); } diff --git a/lib/settings/views/screens/app_logs.dart b/lib/settings/views/screens/app_logs.dart index fc27b451..eb52218e 100644 --- a/lib/settings/views/screens/app_logs.dart +++ b/lib/settings/views/screens/app_logs.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -7,8 +8,9 @@ import '../../../globals.dart'; import '../../../main.dart'; import '../../models/db/app_logs.dart'; -class AppLogs extends StatelessWidget { - const AppLogs({super.key}); +@RoutePage() +class AppLogsScreen extends StatelessWidget { + const AppLogsScreen({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/settings/views/screens/appearance.dart b/lib/settings/views/screens/appearance.dart new file mode 100644 index 00000000..29739863 --- /dev/null +++ b/lib/settings/views/screens/appearance.dart @@ -0,0 +1,118 @@ +import 'package:auto_route/annotations.dart'; +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/settings.dart'; +import 'package:invidious/settings/views/screens/settings.dart'; +import 'package:settings_ui/settings_ui.dart'; + +import '../../../utils/views/components/select_list_dialog.dart'; + +@RoutePage() +class AppearanceSettingsScreen extends StatelessWidget { + const AppearanceSettingsScreen({super.key}); + + selectTheme(BuildContext context, SettingsState _) { + var cubit = context.read(); + var locals = AppLocalizations.of(context)!; + showDialog( + context: context, + useRootNavigator: true, + useSafeArea: true, + builder: (ctx) => SizedBox( + height: 200, + child: SimpleDialog( + title: Text(locals.themeBrightness), + children: ThemeMode.values + .map((e) => RadioListTile( + title: Text(cubit.getThemeLabel(locals, e)), + value: e, + groupValue: _.themeMode, + onChanged: (value) { + Navigator.of(ctx).pop(); + cubit.setThemeMode(value); + })) + .toList()), + )); + } + + String getNavigationLabelText(BuildContext context, NavigationDestinationLabelBehavior behavior) { + var locals = AppLocalizations.of(context)!; + return switch (behavior) { + NavigationDestinationLabelBehavior.alwaysHide => locals.navigationBarLabelNeverShow, + NavigationDestinationLabelBehavior.alwaysShow => locals.navigationBarLabelAlwaysShowing, + NavigationDestinationLabelBehavior.onlyShowSelected => locals.navigationBarLabelShowOnSelect, + }; + } + + customizeNavigationLabel(BuildContext context) { + var locals = AppLocalizations.of(context)!; + var settings = context.read(); + var colors = Theme.of(context).colorScheme; + var textTheme = Theme.of(context).textTheme; + + 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); + } + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + var locals = AppLocalizations.of(context)!; + SettingsThemeData theme = settingsTheme(colorScheme); + + return Scaffold( + appBar: AppBar( + backgroundColor: colorScheme.background, + title: Text(locals.appearance), + elevation: 0, + scrolledUnderElevation: 0, + ), + body: SafeArea(child: BlocBuilder(builder: (context, _) { + var cubit = context.read(); + return DefaultTabController( + length: 2, + child: SettingsList( + lightTheme: theme, + darkTheme: theme, + sections: [ + SettingsSection( + tiles: [ + SettingsTile.switchTile( + initialValue: _.useDynamicTheme, + onToggle: cubit.toggleDynamicTheme, + title: Text(locals.useDynamicTheme), + description: Text(locals.useDynamicThemeDescription), + ), + SettingsTile( + title: Text(locals.themeBrightness), + value: Text(cubit.getThemeLabel(locals, _.themeMode)), + onPressed: (ctx) => selectTheme(ctx, _), + ), + SettingsTile.switchTile( + initialValue: _.blackBackground, + onToggle: cubit.toggleBlackBackground, + title: Text(locals.blackBackground), + description: Text(locals.blackBackgroundDescription), + ), + SettingsTile( + title: Text(locals.navigationBarStyle), + value: Text(getNavigationLabelText(context, _.navigationBarLabelBehavior)), + onPressed: (ctx) => customizeNavigationLabel(ctx), + ), + ], + ), + ], + ), + ); + })), + ); + } +} diff --git a/lib/settings/views/screens/browsing.dart b/lib/settings/views/screens/browsing.dart new file mode 100644 index 00000000..ac5354e6 --- /dev/null +++ b/lib/settings/views/screens/browsing.dart @@ -0,0 +1,154 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/router.dart'; +import 'package:invidious/settings/states/settings.dart'; +import 'package:invidious/settings/views/screens/settings.dart'; +import 'package:locale_names/locale_names.dart'; +import 'package:settings_ui/settings_ui.dart'; + +import '../../../utils/views/components/select_list_dialog.dart'; +import '../components/app_customizer.dart'; + +@RoutePage() +class BrowsingSettingsScreen extends StatelessWidget { + const BrowsingSettingsScreen({super.key}); + + customizeApp(BuildContext context) { + showDialog( + barrierDismissible: true, + context: context, + builder: (context) => const AlertDialog(content: SizedBox(width: 300, child: AppCustomizer()))); + } + + showSelectLanguage(BuildContext context, SettingsState controller) { + var localsList = AppLocalizations.supportedLocales; + 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); + } + + List getCategories(BuildContext context) { + var locals = AppLocalizations.of(context)!; + return [locals.popular, locals.trending, locals.subscriptions, locals.playlists, locals.history]; + } + + searchCountry(BuildContext context, SettingsState controller) { + var locals = AppLocalizations.of(context)!; + var cubit = context.read(); + 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); + } + + openVideoFilterSettings(BuildContext context) { + AutoRouter.of(context).push(const VideoFilterSettingsRoute()); + } + + openSearchHistorySettings(BuildContext context) { + AutoRouter.of(context).push(const SearchHistorySettingsRoute()); + } + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + var locals = AppLocalizations.of(context)!; + SettingsThemeData theme = settingsTheme(colorScheme); + + return Scaffold( + appBar: AppBar( + backgroundColor: colorScheme.background, + title: Text(locals.browsing), + elevation: 0, + scrolledUnderElevation: 0, + ), + body: SafeArea(child: BlocBuilder(builder: (context, _) { + var cubit = context.read(); + return DefaultTabController( + length: 2, + child: SettingsList( + lightTheme: theme, + darkTheme: theme, + sections: [ + SettingsSection( + tiles: [ + SettingsTile( + title: Text(locals.country), + value: Text(_.country.name), + onPressed: (ctx) => searchCountry(ctx, _), + ), + SettingsTile( + title: Text(locals.customizeAppLayout), + value: Text(_.appLayout.map((e) => e.getLabel(locals)).join(", ")), + onPressed: (ctx) => customizeApp(ctx), + ), + SettingsTile.switchTile( + title: Text(locals.distractionFreeMode), + description: Text(locals.distractionFreeModeDescription), + initialValue: _.distractionFreeMode, + onToggle: cubit.setDistractionFreeMode, + ), + SettingsTile( + title: Text(locals.appLanguage), + value: Text(cubit.getLocaleDisplayName() ?? locals.followSystem), + onPressed: (ctx) => showSelectLanguage(ctx, _), + ), + SettingsTile.switchTile( + title: const Text('Return YouTube Dislike'), + description: Text(locals.returnYoutubeDislikeDescription), + initialValue: _.useReturnYoutubeDislike, + onToggle: cubit.toggleReturnYoutubeDislike, + ), + SettingsTile.navigation( + title: Text(locals.searchHistory), + description: Text(locals.searchHistoryDescription), + onPressed: (context) => openSearchHistorySettings(context), + ), + SettingsTile.navigation( + title: Text(locals.videoFilters), + description: Text(locals.videoFiltersSettingTileDescriptions), + onPressed: openVideoFilterSettings, + ), + ], + ), + ], + ), + ); + })), + ); + } +} diff --git a/lib/settings/views/screens/manage_servers.dart b/lib/settings/views/screens/manage_servers.dart index d74f4022..78c2dd04 100644 --- a/lib/settings/views/screens/manage_servers.dart +++ b/lib/settings/views/screens/manage_servers.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -6,14 +7,15 @@ import 'package:invidious/settings/views/components/manager_server_inner.dart'; import '../../../app/states/app.dart'; -class ManageServers extends StatefulWidget { - const ManageServers({super.key}); +@RoutePage() +class ManageServersScreen extends StatefulWidget { + const ManageServersScreen({super.key}); @override ManageServerState createState() => ManageServerState(); } -class ManageServerState extends State { +class ManageServerState extends State { @override Widget build(BuildContext context) { var locals = AppLocalizations.of(context)!; @@ -28,6 +30,8 @@ class ManageServerState extends State { body: SafeArea( bottom: false, child: BlocProvider( - create: (BuildContext context) => ServerListSettingsCubit(ServerListSettingsState(dbServers: [], publicServers: []), context.read()), child: const ManagerServersView()))); + create: (BuildContext context) => ServerListSettingsCubit( + ServerListSettingsState(dbServers: [], publicServers: []), context.read()), + child: const ManagerServersView()))); } } diff --git a/lib/settings/views/screens/manage_single_server.dart b/lib/settings/views/screens/manage_single_server.dart index 2da9c93d..5fdd793a 100644 --- a/lib/settings/views/screens/manage_single_server.dart +++ b/lib/settings/views/screens/manage_single_server.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -9,10 +10,11 @@ import 'package:settings_ui/settings_ui.dart'; import '../../models/db/server.dart'; import 'settings.dart'; -class ManageSingleServer extends StatelessWidget { +@RoutePage() +class ManageSingleServerScreen extends StatelessWidget { final Server server; - const ManageSingleServer({Key? key, required this.server}) : super(key: key); + const ManageSingleServerScreen({Key? key, required this.server}) : super(key: key); void showLogInWithCookiesDialog(BuildContext context) async { var locals = AppLocalizations.of(context)!; @@ -28,7 +30,11 @@ class ManageSingleServer extends StatelessWidget { content: Column( mainAxisSize: MainAxisSize.min, children: [ - TextField(controller: userController, autocorrect: false, autofillHints: const [AutofillHints.username, AutofillHints.email], decoration: InputDecoration(label: Text(locals.username))), + TextField( + controller: userController, + autocorrect: false, + autofillHints: const [AutofillHints.username, AutofillHints.email], + decoration: InputDecoration(label: Text(locals.username))), TextField( obscureText: true, autocorrect: false, @@ -75,7 +81,8 @@ class ManageSingleServer extends StatelessWidget { child: BlocBuilder( builder: (context, server) { var cubit = context.read(); - bool isLoggedIn = (server.authToken != null && server.authToken!.isNotEmpty) || (server.sidCookie != null && server.sidCookie!.isNotEmpty); + bool isLoggedIn = (server.authToken != null && server.authToken!.isNotEmpty) || + (server.sidCookie != null && server.sidCookie!.isNotEmpty); return Scaffold( appBar: AppBar( backgroundColor: colorScheme.background, @@ -96,19 +103,24 @@ class ManageSingleServer extends StatelessWidget { ]), SettingsSection(title: Text(locals.authentication), tiles: [ SettingsTile( - leading: server.authToken?.isNotEmpty ?? false ? const Icon(Icons.check) : const Icon(Icons.token), + leading: + server.authToken?.isNotEmpty ?? false ? const Icon(Icons.check) : const Icon(Icons.token), enabled: !isLoggedIn, title: Text(locals.tokenLogin), - value: Text(server.authToken?.isNotEmpty ?? false ? locals.loggedIn : locals.tokenLoginDescription), + value: + Text(server.authToken?.isNotEmpty ?? false ? locals.loggedIn : locals.tokenLoginDescription), onPressed: (context) async { await cubit.logInWithToken(); }, ), SettingsTile( - leading: server.sidCookie?.isNotEmpty ?? false ? const Icon(Icons.check) : const Icon(Icons.cookie_outlined), + leading: server.sidCookie?.isNotEmpty ?? false + ? const Icon(Icons.check) + : const Icon(Icons.cookie_outlined), enabled: !isLoggedIn, title: Text(locals.cookieLogin), - value: Text(server.sidCookie?.isNotEmpty ?? false ? locals.loggedIn : locals.cookieLoginDescription), + value: + Text(server.sidCookie?.isNotEmpty ?? false ? locals.loggedIn : locals.cookieLoginDescription), onPressed: showLogInWithCookiesDialog, ), SettingsTile( diff --git a/lib/settings/views/screens/notifications.dart b/lib/settings/views/screens/notifications.dart new file mode 100644 index 00000000..7384e715 --- /dev/null +++ b/lib/settings/views/screens/notifications.dart @@ -0,0 +1,133 @@ +import 'package:auto_route/annotations.dart'; +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/settings.dart'; +import 'package:invidious/settings/views/components/channel_notifications.dart'; +import 'package:invidious/settings/views/components/playlist_notifications.dart'; +import 'package:invidious/settings/views/screens/settings.dart'; +import 'package:invidious/utils.dart'; +import 'package:settings_ui/settings_ui.dart'; + +@RoutePage() +class NotificationSettingsScreen extends StatelessWidget { + const NotificationSettingsScreen({super.key}); + + enableBackgroundService(BuildContext context, bool enable) async { + var settings = context.read(); + var result = await settings.setBackgroundNotifications(enable); + if (result == EnableBackGroundNotificationResponse.needBatteryOptimization && context.mounted) { + showBatteryOptimizationDialog(context); + } + } + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + var locals = AppLocalizations.of(context)!; + SettingsThemeData theme = settingsTheme(colorScheme); + + return Scaffold( + appBar: AppBar( + backgroundColor: colorScheme.background, + title: Text(locals.notifications), + elevation: 0, + scrolledUnderElevation: 0, + ), + body: SafeArea(child: BlocBuilder(builder: (context, state) { + var cubit = context.read(); + return DefaultTabController( + length: 2, + child: Column( + children: [ + SettingsList( + lightTheme: theme, + darkTheme: theme, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + sections: [ + SettingsSection( + tiles: [ + SettingsTile.switchTile( + initialValue: state.backgroundNotifications, + onToggle: (value) => enableBackgroundService(context, value), + title: Text(locals.notifications), + description: Text(locals.enableNotificationDescriptions), + ), + CustomSettingsTile( + child: Padding( + padding: const EdgeInsetsDirectional.only( + start: 24, + end: 24, + bottom: 19, + top: 19, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(locals.notificationFrequencySettingsTitle, + style: TextStyle( + color: theme.settingsTileTextColor, + fontSize: 18, + fontWeight: FontWeight.w400, + )), + Text(locals.notificationFrequencySettingsDescription, style: TextStyle(color: theme.tileDescriptionTextColor)), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Slider( + value: state.backgroundNotificationFrequency.toDouble(), + min: 1, + max: 24, + divisions: 23, + label: locals.notificationFrequencySliderLabel(state.backgroundNotificationFrequency.toString()), + onChanged: (value) => cubit.setBackgroundCheckFrequency(value.toInt()), + onChangeEnd: (value) => cubit.setBackgroundCheckFrequency(value.toInt()), + ), + ), + SizedBox( + width: 30, + child: Text( + locals.notificationFrequencySliderLabel(state.backgroundNotificationFrequency.toString()), + ), + ) + ], + ) + ], + ), + ), + ), + SettingsTile.switchTile( + enabled: state.backgroundNotifications, + initialValue: state.subscriptionsNotifications, + onToggle: cubit.setSubscriptionsNotifications, + title: Text(locals.subscriptionNotification), + description: Text(locals.subscriptionNotificationDescription), + ), + ], + ), + ], + ), + Expanded( + child: Column(children: [ + const SizedBox( + height: 8, + ), + Text(locals.otherNotifications), + TabBar(tabs: [Tab(icon: const Icon(Icons.people), text: locals.channels), Tab(icon: const Icon(Icons.playlist_play), text: locals.playlists)]), + const Expanded( + child: TabBarView( + children: [ChannelNotificationList(), PlaylistNotificationList()], + )) + ]), + ) + ], + ), + ); + })), + ); + } +} diff --git a/lib/settings/views/screens/search_history_settings.dart b/lib/settings/views/screens/search_history_settings.dart index 51b0e636..129d2956 100644 --- a/lib/settings/views/screens/search_history_settings.dart +++ b/lib/settings/views/screens/search_history_settings.dart @@ -1,15 +1,16 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:invidious/app/states/app.dart'; import 'package:invidious/globals.dart'; import 'package:invidious/settings/states/settings.dart'; import 'package:settings_ui/settings_ui.dart'; import 'settings.dart'; -class SearchHistorySettings extends StatelessWidget { - const SearchHistorySettings({Key? key}) : super(key: key); +@RoutePage() +class SearchHistorySettingsScreen extends StatelessWidget { + const SearchHistorySettingsScreen({Key? key}) : super(key: key); confirmClear(BuildContext context) { var locals = AppLocalizations.of(context)!; @@ -19,7 +20,8 @@ class SearchHistorySettings extends StatelessWidget { useSafeArea: true, builder: (ctx) => SizedBox( height: 200, - child: AlertDialog(title: Text(locals.clearSearchHistory), content: Text(locals.irreversibleAction), actions: [ + child: + AlertDialog(title: Text(locals.clearSearchHistory), content: Text(locals.irreversibleAction), actions: [ TextButton( child: Text(locals.cancel), onPressed: () { @@ -67,9 +69,14 @@ class SearchHistorySettings extends StatelessWidget { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - IconButton(onPressed: () => _.useSearchHistory ? cubit.changeSearchHistoryLimit(increase: false) : null, icon: const Icon(Icons.remove)), + IconButton( + onPressed: () => + _.useSearchHistory ? cubit.changeSearchHistoryLimit(increase: false) : null, + icon: const Icon(Icons.remove)), Text(_.searchHistoryLimit.toString()), - IconButton(onPressed: () => _.useSearchHistory ? cubit.changeSearchHistoryLimit(increase: true) : null, icon: const Icon(Icons.add)), + IconButton( + onPressed: () => _.useSearchHistory ? cubit.changeSearchHistoryLimit(increase: true) : null, + icon: const Icon(Icons.add)), ], ), ), diff --git a/lib/settings/views/screens/settings.dart b/lib/settings/views/screens/settings.dart index 6d980a10..b9d85933 100644 --- a/lib/settings/views/screens/settings.dart +++ b/lib/settings/views/screens/settings.dart @@ -1,21 +1,13 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/app/states/app.dart'; -import 'package:invidious/myRouteObserver.dart'; -import 'package:invidious/settings/views/components/app_customizer.dart'; -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/router.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:settings_ui/settings_ui.dart'; -import '../../../globals.dart'; import '../../states/settings.dart'; -import 'manage_servers.dart'; settingsTheme(ColorScheme colorScheme) => SettingsThemeData( settingsSectionBackground: colorScheme.background, @@ -26,53 +18,32 @@ settingsTheme(ColorScheme colorScheme) => SettingsThemeData( leadingIconsColor: colorScheme.secondary, tileHighlightColor: colorScheme.secondaryContainer); -class Settings extends StatelessWidget { - const Settings({super.key}); +@RoutePage() +class SettingsScreen extends StatelessWidget { + const SettingsScreen({super.key}); manageServers(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute(settings: ROUTE_SETTINGS_MANAGE_SERVERS, builder: (context) => const ManageServers())); + AutoRouter.of(context).push(const ManageServersRoute()); } - openSponsorBlockSettings(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute(settings: ROUTE_SETTINGS_SPONSOR_BLOCK, builder: (context) => const SponsorBlockSettings())); - } - - openVideoFilterSettings(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute(settings: ROUTE_SETTINGS_VIDEO_FILTERS, builder: (context) => const VideoFilterSettings())); + openAppLogs(BuildContext context) { + AutoRouter.of(context).push(const AppLogsRoute()); } - openSearchHistorySettings(BuildContext ctx) { - Navigator.of(ctx).push(MaterialPageRoute(settings: ROUTE_SETTINGS_SEARCH_HISTORY, builder: (context) => const SearchHistorySettings())); + openNotificationSettings(BuildContext context) { + AutoRouter.of(context).push(const NotificationSettingsRoute()); } - openAppLogs(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute(settings: ROUTE_SETTINGS_SEARCH_HISTORY, builder: (context) => const AppLogs())); + openBrowsingSettings(BuildContext context) { + AutoRouter.of(context).push(const BrowsingSettingsRoute()); } - searchCountry(BuildContext context, SettingsState controller) { - var locals = AppLocalizations.of(context)!; - var cubit = context.read(); - 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); + openVideoPlayerSettings(BuildContext context) { + AutoRouter.of(context).push(const VideoPlayerSettingsRoute()); } - String getNavigationLabelText(BuildContext context, NavigationDestinationLabelBehavior behavior) { - var locals = AppLocalizations.of(context)!; - return switch (behavior) { - NavigationDestinationLabelBehavior.alwaysHide => locals.navigationBarLabelNeverShow, - NavigationDestinationLabelBehavior.alwaysShow => locals.navigationBarLabelAlwaysShowing, - NavigationDestinationLabelBehavior.onlyShowSelected => locals.navigationBarLabelShowOnSelect, - }; + openAppearanceSettings(BuildContext context) { + AutoRouter.of(context).push(const AppearanceSettingsRoute()); } /* @@ -94,100 +65,6 @@ class Settings extends StatelessWidget { } */ - customizeApp(BuildContext context) { - showDialog(barrierDismissible: true, context: context, builder: (context) => const AlertDialog(content: SizedBox(width: 300, child: AppCustomizer()))); - } - - customizeNavigationLabel(BuildContext context) { - var locals = AppLocalizations.of(context)!; - var settings = context.read(); - var colors = Theme.of(context).colorScheme; - var textTheme = Theme.of(context).textTheme; - - 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) { - var localsList = AppLocalizations.supportedLocales; - 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, - selectedValue: selected?.nativeDisplayLanguageScript ?? locals.followSystem, - showSearchBox: false, - items: [locals.followSystem, ...localsStrings], - onChange: (String selected) { - if (selected == locals.followSystem) { - cubit.setLocale(localsList, localsStrings, null); - } else { - cubit.setLocale(localsList, localsStrings, selected); - } - }, - ); -*/ - } - - List getCategories(BuildContext context) { - var locals = AppLocalizations.of(context)!; - return [locals.popular, locals.trending, locals.subscriptions, locals.playlists, locals.history]; - } - - selectTheme(BuildContext context, SettingsState _) { - var cubit = context.read(); - var locals = AppLocalizations.of(context)!; - showDialog( - context: context, - useRootNavigator: true, - useSafeArea: true, - builder: (ctx) => SizedBox( - height: 200, - child: SimpleDialog( - title: Text(locals.themeBrightness), - children: ThemeMode.values - .map((e) => RadioListTile( - title: Text(cubit.getThemeLabel(locals, e)), - value: e, - groupValue: _.themeMode, - onChanged: (value) { - Navigator.of(ctx).pop(); - cubit.setThemeMode(value); - })) - .toList()), - )); - } - @override Widget build(BuildContext context) { ColorScheme colorScheme = Theme.of(context).colorScheme; @@ -211,144 +88,66 @@ class Settings extends StatelessWidget { lightTheme: theme, darkTheme: theme, sections: [ - SettingsSection( - title: Text(locals.browsing), - tiles: [ - SettingsTile( - title: Text(locals.country), - value: Text(_.country.name), - onPressed: (ctx) => searchCountry(ctx, _), - ), - SettingsTile( - title: Text(locals.customizeAppLayout), - value: Text(_.appLayout.map((e) => e.getLabel(locals)).join(", ")), - onPressed: (ctx) => customizeApp(ctx), - ), - SettingsTile.switchTile( - title: Text(locals.distractionFreeMode), - description: Text(locals.distractionFreeModeDescription), - initialValue: _.distractionFreeMode, - onToggle: cubit.setDistractionFreeMode, - ), - SettingsTile( - title: Text(locals.appLanguage), - value: Text(cubit.getLocaleDisplayName() ?? locals.followSystem), - onPressed: (ctx) => showSelectLanguage(ctx, _), - ), - SettingsTile.switchTile( - title: const Text('Return YouTube Dislike'), - description: Text(locals.returnYoutubeDislikeDescription), - initialValue: _.useReturnYoutubeDislike, - onToggle: cubit.toggleReturnYoutubeDislike, - ), - SettingsTile.navigation( - title: Text(locals.searchHistory), - description: Text(locals.searchHistoryDescription), - onPressed: (context) => openSearchHistorySettings(ctx), - ), - SettingsTile.navigation( - title: Text(locals.videoFilters), - description: Text(locals.videoFiltersSettingTileDescriptions), - onPressed: openVideoFilterSettings, - ), - ], - ), - SettingsSection(title: Text(locals.servers), tiles: [ + SettingsSection(tiles: [ + SettingsTile.navigation( + leading: const Icon(Icons.home_outlined), + title: Text(locals.browsing), + description: Text(List.of([ + locals.country, + locals.customizeAppLayout, + locals.distractionFreeMode, + locals.appLanguage, + "Return YouTube Dislike", + locals.searchHistory, + locals.videoFilters + ]).join(", "), maxLines: 3, overflow: TextOverflow.ellipsis,), + onPressed: openBrowsingSettings, + ), SettingsTile.navigation( + leading: const Icon(Icons.cloud_queue), title: Text(locals.manageServers), description: BlocBuilder( - buildWhen: (previous, current) => previous.server != current.server, builder: (context, app) => Text(app.server != null ? locals.currentServer(app.server!.url) : "")), + buildWhen: (previous, current) => previous.server != current.server, + builder: (context, app) => + Text(app.server != null ? locals.currentServer(app.server!.url) : "")), onPressed: manageServers, - ) - ]), - SettingsSection(title: Text(locals.videoPlayer), tiles: [ - SettingsTile.switchTile( - initialValue: _.useDash, - onToggle: cubit.toggleDash, - title: Text(locals.useDash), - description: Text(locals.useDashDescription), - ), - SettingsTile.switchTile( - initialValue: _.useProxy, - onToggle: cubit.toggleProxy, - title: Text(locals.useProxy), - description: Text(locals.useProxyDescription), - ), - SettingsTile.switchTile( - initialValue: _.autoplayVideoOnLoad, - onToggle: cubit.toggleAutoplayOnLoad, - title: Text(locals.autoplayVideoOnLoad), - description: Text(locals.autoplayVideoOnLoadDescription), - ), - SettingsTile( - title: Text(locals.subtitleFontSize), - description: Text(locals.subtitleFontSizeDescription), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton(onPressed: () => cubit.changeSubtitleSize(increase: false), icon: const Icon(Icons.remove)), - Text(_.subtitleSize.floor().toString()), - IconButton(onPressed: () => cubit.changeSubtitleSize(increase: true), icon: const Icon(Icons.add)), - ], - ), - ), - SettingsTile.switchTile( - initialValue: _.rememberSubtitles, - onToggle: cubit.toggleRememberSubtitles, - title: Text(locals.rememberSubtitleLanguage), - description: Text(locals.rememberSubtitleLanguageDescription), - ), - SettingsTile.switchTile( - initialValue: _.rememberPlayBackSpeed, - onToggle: cubit.toggleRememberPlaybackSpeed, - title: Text(locals.rememberPlaybackSpeed), - description: Text(locals.rememberPlaybackSpeedDescription), ), SettingsTile.navigation( - title: const Text('SponsorBlock'), - description: Text(locals.sponsorBlockDescription), - onPressed: openSponsorBlockSettings, + leading: const Icon(Icons.video_settings), + title: Text(locals.videoPlayer), + description: Text(List.of([ + locals.useDash, + locals.useProxy, + locals.autoplayVideoOnLoad, + locals.subtitleFontSize, + locals.rememberSubtitleLanguage, + locals.rememberPlaybackSpeed, + "SponsorBlock", + locals.lockFullScreenToLandscape, + locals.fillFullscreen + ]).join(", "), maxLines: 3, overflow: TextOverflow.ellipsis,), + onPressed: openVideoPlayerSettings, ), - SettingsTile.switchTile( - initialValue: _.forceLandscapeFullScreen, - onToggle: cubit.toggleForceLandscapeFullScreen, - title: Text(locals.lockFullScreenToLandscape), - description: Text(locals.lockFullScreenToLandscapeDescription), + SettingsTile.navigation( + leading: const Icon(Icons.notifications_outlined), + title: Text('${locals.notifications} (beta)'), + description: Text(locals.notificationsDescription), + onPressed: openNotificationSettings, ), - SettingsTile.switchTile( - initialValue: _.fillFullscreen, - onToggle: cubit.toggleFillFullscreen, - title: Text(locals.fillFullscreen), - description: Text(locals.fillFullscreenDescription), + SettingsTile.navigation( + leading: const Icon(Icons.palette_outlined), + title: Text(locals.appearance), + description: Text( + List.of([ + locals.useDynamicTheme, + locals.themeBrightness, + locals.blackBackground, + locals.navigationBarStyle + ]).join(", "), maxLines: 3, overflow: TextOverflow.ellipsis, + ), + onPressed: openAppearanceSettings, ), ]), - SettingsSection( - title: Text(locals.appearance), - tiles: [ - SettingsTile.switchTile( - initialValue: _.useDynamicTheme, - onToggle: cubit.toggleDynamicTheme, - title: Text(locals.useDynamicTheme), - description: Text(locals.useDynamicThemeDescription), - ), - SettingsTile( - title: Text(locals.themeBrightness), - value: Text(cubit.getThemeLabel(locals, _.themeMode)), - onPressed: (ctx) => selectTheme(ctx, _), - ), - SettingsTile.switchTile( - initialValue: _.blackBackground, - onToggle: cubit.toggleBlackBackground, - title: Text(locals.blackBackground), - description: Text(locals.blackBackgroundDescription), - ), - SettingsTile( - title: Text(locals.navigationBarStyle), - value: Text(getNavigationLabelText(context, _.navigationBarLabelBehavior)), - onPressed: (ctx) => customizeNavigationLabel(ctx), - ), - ], - ), SettingsSection(title: (Text(locals.about)), tiles: [ SettingsTile(title: const Center(child: SizedBox(height: 150, width: 150, child: AppIcon()))), SettingsTile( diff --git a/lib/settings/views/screens/sponsor_block_settings.dart b/lib/settings/views/screens/sponsor_block_settings.dart index 11757d93..fb9669a9 100644 --- a/lib/settings/views/screens/sponsor_block_settings.dart +++ b/lib/settings/views/screens/sponsor_block_settings.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -7,8 +8,9 @@ import 'package:settings_ui/settings_ui.dart'; import 'settings.dart'; -class SponsorBlockSettings extends StatelessWidget { - const SponsorBlockSettings({Key? key}) : super(key: key); +@RoutePage() +class SponsorBlockSettingsScreen extends StatelessWidget { + const SponsorBlockSettingsScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/settings/views/screens/video_filter.dart b/lib/settings/views/screens/video_filter.dart index 6ed48025..d89a9697 100644 --- a/lib/settings/views/screens/video_filter.dart +++ b/lib/settings/views/screens/video_filter.dart @@ -1,30 +1,24 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/extensions.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/settings/states/video_filter_channel.dart'; import 'package:invidious/settings/views/components/video_filter_channel.dart'; -import 'package:invidious/settings/views/screens/video_filter_setup.dart'; import 'package:uuid/uuid.dart'; -import '../../../main.dart'; -import '../../../myRouteObserver.dart'; import '../../models/db/video_filter.dart'; import '../../states/video_filter.dart'; -class VideoFilterSettings extends StatelessWidget { - const VideoFilterSettings({Key? key}) : super(key: key); +@RoutePage() +class VideoFilterSettingsScreen extends StatelessWidget { + const VideoFilterSettingsScreen({Key? key}) : super(key: key); createFilter(BuildContext context, {String? channelId}) { var cubit = context.read(); - navigatorKey.currentState - ?.push(MaterialPageRoute( - settings: ROUTE_SETTINGS_VIDEO_FILTERS, - builder: (context) => VideoFilterSetup( - channelId: channelId, - ))) - .then((value) => cubit.refreshFilters()); + AutoRouter.of(context).push(VideoFilterSetupRoute(channelId: channelId)).then((value) => cubit.refreshFilters()); } @override @@ -46,7 +40,10 @@ class VideoFilterSettings extends StatelessWidget { backgroundColor: colors.background, title: Text(locals.videoFilters), ), - floatingActionButton: FloatingActionButton(onPressed: () => createFilter(context), backgroundColor: colors.primaryContainer, child: const Icon(Icons.add)), + floatingActionButton: FloatingActionButton( + onPressed: () => createFilter(context), + backgroundColor: colors.primaryContainer, + child: const Icon(Icons.add)), body: SafeArea( bottom: false, child: Column( @@ -60,7 +57,8 @@ class VideoFilterSettings extends StatelessWidget { child: ListView.builder( itemCount: keys.length, itemBuilder: (context, index) { - return VideoFilterChannel(key: ValueKey(const Uuid().v4()), filters: mappedFilters[keys[index]] ?? []); + return VideoFilterChannel( + key: ValueKey(const Uuid().v4()), filters: mappedFilters[keys[index]] ?? []); }, ), ) diff --git a/lib/settings/views/screens/video_filter_setup.dart b/lib/settings/views/screens/video_filter_setup.dart index 6a78f9c8..2d9e47aa 100644 --- a/lib/settings/views/screens/video_filter_setup.dart +++ b/lib/settings/views/screens/video_filter_setup.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -10,11 +11,12 @@ import 'package:invidious/utils/views/components/select_list_dialog.dart'; import '../../../channels/models/channel.dart'; import '../../models/db/video_filter.dart'; -class VideoFilterSetup extends StatelessWidget { +@RoutePage() +class VideoFilterSetupScreen extends StatelessWidget { final String? channelId; final VideoFilter? filter; - const VideoFilterSetup({Key? key, this.channelId, this.filter}) : super(key: key); + const VideoFilterSetupScreen({Key? key, this.channelId, this.filter}) : super(key: key); List getFilterWidgets(BuildContext context) { var locals = AppLocalizations.of(context)!; @@ -28,7 +30,10 @@ class VideoFilterSetup extends StatelessWidget { Expanded(child: Text(locals.videoFilterType)), DropdownButton( value: _.filter?.type, - items: FilterType.values.map((e) => DropdownMenuItem(value: e, child: Text(FilterType.localizedType(e, locals)))).toList(), + items: FilterType.values + .map((e) => + DropdownMenuItem(value: e, child: Text(FilterType.localizedType(e, locals)))) + .toList(), onChanged: cubit.setType) ], ), @@ -39,7 +44,11 @@ class VideoFilterSetup extends StatelessWidget { Expanded(child: Text(locals.videoFilterOperation)), DropdownButton( value: _.filter?.operation, - items: cubit.getAvailableOperations().map((e) => DropdownMenuItem(value: e, child: Text(FilterOperation.localizedLabel(e, locals)))).toList(), + items: cubit + .getAvailableOperations() + .map((e) => DropdownMenuItem( + value: e, child: Text(FilterOperation.localizedLabel(e, locals)))) + .toList(), onChanged: cubit.setOperation) ], ), @@ -71,7 +80,10 @@ class VideoFilterSetup extends StatelessWidget { 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); + itemBuilder: (value, selected) => Text(value.author), + asyncSearch: (filter) => cubit.searchChannel(filter ?? ''), + onSelect: (value) => cubit.selectChannel(value), + title: locals.channel); } selectTime(BuildContext context, String initialTime, Function(String newTime) onNewTime) async { @@ -79,7 +91,8 @@ class VideoFilterSetup extends StatelessWidget { if (split.length == 3) { TimeOfDay? selectedTime = await showTimePicker(context: context, initialTime: timeStringToTimeOfDay(initialTime)); if (selectedTime != null) { - String newTime = '${selectedTime.hour.toString().padLeft(2, "0")}:${selectedTime.minute.toString().padLeft(2, "0")}:${split[2]}'; + String newTime = + '${selectedTime.hour.toString().padLeft(2, "0")}:${selectedTime.minute.toString().padLeft(2, "0")}:${split[2]}'; onNewTime(newTime); } } @@ -116,17 +129,23 @@ class VideoFilterSetup extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ if (_.channel == null) - FilledButton.tonalIcon(onPressed: () => searchChannel(context), icon: const Icon(Icons.personal_video), label: Text('${locals.channel} (${locals.optional})')), + 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)) + TextSpan( + text: _.channel?.author ?? '', + style: textTheme.bodyLarge?.copyWith(color: colors.primary)) ])), ), - if (_.channel != null) IconButton(onPressed: () => cubit.channelClear(), icon: Icon(Icons.clear)) + if (_.channel != null) + IconButton(onPressed: () => cubit.channelClear(), icon: Icon(Icons.clear)) ], ), ) @@ -159,7 +178,10 @@ class VideoFilterSetup extends StatelessWidget { , Visibility( visible: _.filter?.channelId != null, - child: SwitchListTile(title: Text(locals.videoFilterHideAllFromChannel), value: _.filter?.filterAll ?? false, onChanged: cubit.channelHideAll)), + child: SwitchListTile( + title: Text(locals.videoFilterHideAllFromChannel), + value: _.filter?.filterAll ?? false, + onChanged: cubit.channelHideAll)), ...getFilterWidgets(context), SwitchListTile( title: Text(locals.videoFilterDayOfWeek), @@ -189,12 +211,18 @@ class VideoFilterSetup extends StatelessWidget { width: 30, height: 30, alignment: Alignment.center, - decoration: BoxDecoration(shape: BoxShape.circle, color: isSelected ? colors.primaryContainer : colors.primaryContainer.withOpacity(0.4)), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected + ? colors.primaryContainer + : colors.primaryContainer.withOpacity(0.4)), duration: animationDuration, curve: Curves.easeInOutQuad, child: Text( day, - style: textTheme.bodySmall?.copyWith(fontWeight: FontWeight.bold, color: isSelected ? colors.onPrimaryContainer: colors.onBackground), + style: textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.bold, + color: isSelected ? colors.onPrimaryContainer : colors.onBackground), ), ), ); @@ -209,21 +237,24 @@ class VideoFilterSetup extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: FilledButton.tonal( - onPressed: () => selectTime(context, _.filter?.startTime ?? defaultStartTime, cubit.setStartTime), - child: Text(timeStringToTimeOfDay(_.filter?.startTime ?? defaultStartTime).format(context))), + onPressed: () => selectTime( + context, _.filter?.startTime ?? defaultStartTime, cubit.setStartTime), + child: Text(timeStringToTimeOfDay(_.filter?.startTime ?? defaultStartTime) + .format(context))), ), Text('${locals.to}:'), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: FilledButton.tonal( - onPressed: () => selectTime(context, _.filter?.endTime ?? defaultEndTime, cubit.setEndTime), - child: Text(timeStringToTimeOfDay(_.filter?.endTime ?? defaultEndTime).format(context))), + onPressed: () => + selectTime(context, _.filter?.endTime ?? defaultEndTime, cubit.setEndTime), + child: Text( + timeStringToTimeOfDay(_.filter?.endTime ?? defaultEndTime).format(context))), ), ], ) ], ), - ), secondChild: const SizedBox.shrink(), crossFadeState: cubit.showDateSettings ? CrossFadeState.showFirst : CrossFadeState.showSecond, @@ -231,7 +262,10 @@ class VideoFilterSetup extends StatelessWidget { sizeCurve: Curves.easeInOutQuad, firstCurve: Curves.easeInOutQuad, secondCurve: Curves.easeInOutQuad, - ).animate().slideY(duration: animationDuration, curve: Curves.easeInOutQuad).fadeIn(duration: animationDuration), + ) + .animate() + .slideY(duration: animationDuration, curve: Curves.easeInOutQuad) + .fadeIn(duration: animationDuration), SwitchListTile( title: Text(locals.videoFilterHide), subtitle: Text( @@ -251,7 +285,14 @@ class VideoFilterSetup extends StatelessWidget { )), Padding( padding: const EdgeInsets.all(8.0), - child: FilledButton(onPressed: cubit.isFilterValid() ? cubit.onSave : null, child: Text(locals.save)), + child: FilledButton( + onPressed: cubit.isFilterValid() + ? () { + cubit.onSave(); + AutoRouter.of(context).pop(); + } + : null, + child: Text(locals.save)), ) ], ), diff --git a/lib/settings/views/screens/video_player.dart b/lib/settings/views/screens/video_player.dart new file mode 100644 index 00000000..25acd66b --- /dev/null +++ b/lib/settings/views/screens/video_player.dart @@ -0,0 +1,109 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/router.dart'; +import 'package:invidious/settings/states/settings.dart'; +import 'package:invidious/settings/views/screens/settings.dart'; +import 'package:settings_ui/settings_ui.dart'; + + +@RoutePage() +class VideoPlayerSettingsScreen extends StatelessWidget { + const VideoPlayerSettingsScreen({super.key}); + + openSponsorBlockSettings(BuildContext context) { + AutoRouter.of(context).push(const SponsorBlockSettingsRoute()); + } + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + var locals = AppLocalizations.of(context)!; + SettingsThemeData theme = settingsTheme(colorScheme); + + return Scaffold( + appBar: AppBar( + backgroundColor: colorScheme.background, + title: Text(locals.videoPlayer), + elevation: 0, + scrolledUnderElevation: 0, + ), + body: SafeArea(child: BlocBuilder(builder: (context, _) { + var cubit = context.read(); + return DefaultTabController( + length: 2, + child: SettingsList( + lightTheme: theme, + darkTheme: theme, + sections: [ + SettingsSection(tiles: [ + SettingsTile.switchTile( + initialValue: _.useDash, + onToggle: cubit.toggleDash, + title: Text(locals.useDash), + description: Text(locals.useDashDescription), + ), + SettingsTile.switchTile( + initialValue: _.useProxy, + onToggle: cubit.toggleProxy, + title: Text(locals.useProxy), + description: Text(locals.useProxyDescription), + ), + SettingsTile.switchTile( + initialValue: _.autoplayVideoOnLoad, + onToggle: cubit.toggleAutoplayOnLoad, + title: Text(locals.autoplayVideoOnLoad), + description: Text(locals.autoplayVideoOnLoadDescription), + ), + SettingsTile( + title: Text(locals.subtitleFontSize), + description: Text(locals.subtitleFontSizeDescription), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => cubit.changeSubtitleSize(increase: false), icon: const Icon(Icons.remove)), + Text(_.subtitleSize.floor().toString()), + IconButton( + onPressed: () => cubit.changeSubtitleSize(increase: true), icon: const Icon(Icons.add)), + ], + ), + ), + SettingsTile.switchTile( + initialValue: _.rememberSubtitles, + onToggle: cubit.toggleRememberSubtitles, + title: Text(locals.rememberSubtitleLanguage), + description: Text(locals.rememberSubtitleLanguageDescription), + ), + SettingsTile.switchTile( + initialValue: _.rememberPlayBackSpeed, + onToggle: cubit.toggleRememberPlaybackSpeed, + title: Text(locals.rememberPlaybackSpeed), + description: Text(locals.rememberPlaybackSpeedDescription), + ), + SettingsTile.navigation( + title: const Text('SponsorBlock'), + description: Text(locals.sponsorBlockDescription), + onPressed: openSponsorBlockSettings, + ), + SettingsTile.switchTile( + initialValue: _.forceLandscapeFullScreen, + onToggle: cubit.toggleForceLandscapeFullScreen, + title: Text(locals.lockFullScreenToLandscape), + description: Text(locals.lockFullScreenToLandscapeDescription), + ), + SettingsTile.switchTile( + initialValue: _.fillFullscreen, + onToggle: cubit.toggleFillFullscreen, + title: Text(locals.fillFullscreen), + description: Text(locals.fillFullscreenDescription), + ), + ]), + ], + ), + ); + })), + ); + } +} diff --git a/lib/settings/views/tv/components/manage_server_inner.dart b/lib/settings/views/tv/components/manage_server_inner.dart index fdae3ead..37fb3fb3 100644 --- a/lib/settings/views/tv/components/manage_server_inner.dart +++ b/lib/settings/views/tv/components/manage_server_inner.dart @@ -1,6 +1,8 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/settings/states/settings.dart'; import 'package:invidious/settings/views/tv/screens/manage_single_server.dart'; import 'package:invidious/utils.dart'; @@ -15,12 +17,7 @@ class TvManageServersInner extends StatelessWidget { openServer(BuildContext context, Server s) { var cubit = context.read(); - Navigator.of(context) - .push(MaterialPageRoute( - builder: (context) => TvManageSingleServer( - server: s, - ), - )) + AutoRouter.of(context).push(TvManageSingleServerRoute(server: s)) .then((value) => cubit.refreshServers()); } @@ -98,7 +95,8 @@ class TvManageServersInner extends StatelessWidget { return BlocBuilder(builder: (context, _) { var cubit = context.read(); var settings = context.watch(); - var filteredPublicServers = _.publicServers.where((s) => _.dbServers.indexWhere((element) => element.url == s.url) == -1).toList(); + var filteredPublicServers = + _.publicServers.where((s) => _.dbServers.indexWhere((element) => element.url == s.url) == -1).toList(); return ListView(children: [ SettingsTile( title: locals.skipSslVerification, @@ -157,8 +155,10 @@ class TvManageServersInner extends StatelessWidget { : filteredPublicServers .map((s) => SettingsTile( key: Key(s.url), - title: '${s.url} - ${(s.ping != null && s.ping!.compareTo(const Duration(seconds: pingTimeout)) == -1) ? '${s.ping?.inMilliseconds}ms' : '>${pingTimeout}s'}', - description: '${(s.flag != null && s.region != null) ? '${s.flag} - ${s.region} - ' : ''} ${locals.tapToAddServer}', + title: + '${s.url} - ${(s.ping != null && s.ping!.compareTo(const Duration(seconds: pingTimeout)) == -1) ? '${s.ping?.inMilliseconds}ms' : '>${pingTimeout}s'}', + description: + '${(s.flag != null && s.region != null) ? '${s.flag} - ${s.region} - ' : ''} ${locals.tapToAddServer}', onSelected: (context) => cubit.upsertServer(s), )) .toList() diff --git a/lib/settings/views/tv/screens/manage_servers.dart b/lib/settings/views/tv/screens/manage_servers.dart index ab4c463d..97b54957 100644 --- a/lib/settings/views/tv/screens/manage_servers.dart +++ b/lib/settings/views/tv/screens/manage_servers.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:invidious/app/states/app.dart'; @@ -5,14 +6,16 @@ import 'package:invidious/settings/states/server_list_settings.dart'; import 'package:invidious/settings/views/tv/components/manage_server_inner.dart'; import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; -class TvSettingsManageServers extends StatelessWidget { - const TvSettingsManageServers({Key? key}) : super(key: key); +@RoutePage() +class TvSettingsManageServersScreen extends StatelessWidget { + const TvSettingsManageServersScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: BlocProvider( - create: (BuildContext context) => ServerListSettingsCubit(ServerListSettingsState(publicServers: [], dbServers: []), context.read()), + create: (BuildContext context) => ServerListSettingsCubit( + ServerListSettingsState(publicServers: [], dbServers: []), context.read()), child: const TvOverscan(child: TvManageServersInner())), ); } diff --git a/lib/settings/views/tv/screens/manage_single_server.dart b/lib/settings/views/tv/screens/manage_single_server.dart index 18c80c07..fbe9d4e3 100644 --- a/lib/settings/views/tv/screens/manage_single_server.dart +++ b/lib/settings/views/tv/screens/manage_single_server.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -11,10 +12,11 @@ import '../../../../utils/views/tv/components/tv_text_field.dart'; import '../../../models/db/server.dart'; import '../../../states/server_settings.dart'; -class TvManageSingleServer extends StatelessWidget { +@RoutePage() +class TvManageSingleServerScreen extends StatelessWidget { final Server server; - const TvManageSingleServer({Key? key, required this.server}) : super(key: key); + const TvManageSingleServerScreen({Key? key, required this.server}) : super(key: key); void showLogInWithCookiesDialog(BuildContext context) async { var locals = AppLocalizations.of(context)!; @@ -182,7 +184,8 @@ class TvManageSingleServer extends StatelessWidget { child: BlocBuilder(builder: (context, server) { var cubit = context.read(); AppLocalizations locals = AppLocalizations.of(context)!; - bool isLoggedIn = (server.authToken != null && server.authToken!.isNotEmpty) || (server.sidCookie != null && server.sidCookie!.isNotEmpty); + bool isLoggedIn = (server.authToken != null && server.authToken!.isNotEmpty) || + (server.sidCookie != null && server.sidCookie!.isNotEmpty); return ListView( children: [ diff --git a/lib/settings/views/tv/screens/search_history_settings.dart b/lib/settings/views/tv/screens/search_history_settings.dart index 13d46ed5..e1747d27 100644 --- a/lib/settings/views/tv/screens/search_history_settings.dart +++ b/lib/settings/views/tv/screens/search_history_settings.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -10,8 +11,9 @@ import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; import '../../../../utils.dart'; import '../../../../utils/views/tv/components/tv_button.dart'; -class TvSearchHistorySettings extends StatelessWidget { - const TvSearchHistorySettings({Key? key}) : super(key: key); +@RoutePage() +class TvSearchHistorySettingsScreen extends StatelessWidget { + const TvSearchHistorySettingsScreen({Key? key}) : super(key: key); void showClearHistoryDialog(BuildContext context) { var locals = AppLocalizations.of(context)!; diff --git a/lib/settings/views/tv/screens/settings.dart b/lib/settings/views/tv/screens/settings.dart index 5ca7babd..e32aeb0c 100644 --- a/lib/settings/views/tv/screens/settings.dart +++ b/lib/settings/views/tv/screens/settings.dart @@ -1,9 +1,12 @@ +import 'package:auto_route/annotations.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/app/states/app.dart'; import 'package:invidious/extensions.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/settings/views/tv/screens/manage_servers.dart'; import 'package:invidious/settings/views/tv/screens/search_history_settings.dart'; import 'package:invidious/settings/views/tv/screens/sponsor_block_settings.dart'; @@ -18,42 +21,22 @@ import '../../../states/settings.dart'; var log = Logger('TvSettings'); -class TVSettings extends StatelessWidget { - const TVSettings({Key? key}) : super(key: key); +@RoutePage() +class TVSettingsScreen extends StatelessWidget { + const TVSettingsScreen({Key? key}) : super(key: key); openSelectCountry(BuildContext context) { AppLocalizations locals = AppLocalizations.of(context)!; var cubit = context.read(); - - Navigator.of(context).push(MaterialPageRoute( - builder: (context) { - var countryNames = countryCodes.map((e) => e.name).toList(); - countryNames.sort(); - return TvSelectFromList( - title: locals.selectBrowsingCountry, - options: countryNames, - selected: cubit.state.country.name, - onSelect: cubit.selectCountry, - ); - }, - )); - } - -/* - openSelectOnStart(BuildContext context) { - AppLocalizations locals = AppLocalizations.of(context)!; - var cubit = context.read(); - var categories = getCategories(context); - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvSelectFromList( - title: locals.whenAppStartsShow, - options: categories, - selected: categories[cubit.state.onOpen], - onSelect: (selected) => cubit.selectOnOpen(selected, categories), - ), + var countryNames = countryCodes.map((e) => e.name).toList(); + countryNames.sort(); + AutoRouter.of(context).push(TvSelectFromListRoute( + title: locals.selectBrowsingCountry, + options: countryNames, + selected: cubit.state.country.name, + onSelect: cubit.selectCountry, )); } -*/ List getCategories(BuildContext context) { var locals = AppLocalizations.of(context)!; @@ -61,21 +44,15 @@ class TVSettings extends StatelessWidget { } openManageServers(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => const TvSettingsManageServers(), - )); + AutoRouter.of(context).push(const TvSettingsManageServersRoute()); } openSponsorBlockSettings(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => const TvSponsorBlockSettings(), - )); + AutoRouter.of(context).push(const TvSponsorBlockSettingsRoute()); } openSearchHistorySettings(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => const TvSearchHistorySettings(), - )); + AutoRouter.of(context).push(const TvSearchHistorySettingsRoute()); } showSelectLanguage(BuildContext context) { @@ -87,19 +64,17 @@ class TVSettings extends StatelessWidget { List? localeString = cubit.state.locale?.split('_'); Locale? selected = localeString != null ? Locale.fromSubtags(languageCode: localeString[0], scriptCode: localeString.length >= 2 ? localeString[1] : null) : null; - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvSelectFromList( - title: locals.appLanguage, - options: [locals.followSystem, ...localsStrings], - selected: selected?.nativeDisplayLanguageScript ?? locals.followSystem, - onSelect: (String selected) { - if (selected == locals.followSystem) { - cubit.setLocale(localsList, localsStrings, null); - } else { - cubit.setLocale(localsList, localsStrings, selected); - } - }, - ), + AutoRouter.of(context).push(TvSelectFromListRoute( + title: locals.appLanguage, + options: [locals.followSystem, ...localsStrings], + selected: selected?.nativeDisplayLanguageScript ?? locals.followSystem, + onSelect: (String selected) { + if (selected == locals.followSystem) { + cubit.setLocale(localsList, localsStrings, null); + } else { + cubit.setLocale(localsList, localsStrings, selected); + } + }, )); } @@ -107,17 +82,14 @@ class TVSettings extends StatelessWidget { var locals = AppLocalizations.of(context)!; ColorScheme colors = Theme.of(context).colorScheme; var cubit = context.read(); - - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvSelectFromList( - title: locals.themeBrightness, - options: ThemeMode.values.map((e) => cubit.getThemeLabel(locals, e)).toList(), - selected: cubit.getThemeLabel(locals, cubit.state.themeMode), - onSelect: (String selected) { - ThemeMode? theme = ThemeMode.values.firstWhereOrNull((element) => cubit.getThemeLabel(locals, element) == selected); - cubit.setThemeMode(theme); - }, - ), + AutoRouter.of(context).push(TvSelectFromListRoute( + title: locals.themeBrightness, + options: ThemeMode.values.map((e) => cubit.getThemeLabel(locals, e)).toList(), + selected: cubit.getThemeLabel(locals, cubit.state.themeMode), + onSelect: (String selected) { + ThemeMode? theme = ThemeMode.values.firstWhereOrNull((element) => cubit.getThemeLabel(locals, element) == selected); + cubit.setThemeMode(theme); + }, )); } diff --git a/lib/settings/views/tv/screens/sponsor_block_settings.dart b/lib/settings/views/tv/screens/sponsor_block_settings.dart index d1256e44..bb5ea237 100644 --- a/lib/settings/views/tv/screens/sponsor_block_settings.dart +++ b/lib/settings/views/tv/screens/sponsor_block_settings.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -7,8 +8,9 @@ import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; import '../../../../videos/models/sponsor_segment_types.dart'; -class TvSponsorBlockSettings extends StatelessWidget { - const TvSponsorBlockSettings({Key? key}) : super(key: key); +@RoutePage() +class TvSponsorBlockSettingsScreen extends StatelessWidget { + const TvSponsorBlockSettingsScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/subscription_management/view/components/subscribeButton.dart b/lib/subscription_management/view/components/subscribeButton.dart index 5fb9b07e..08a281f2 100644 --- a/lib/subscription_management/view/components/subscribeButton.dart +++ b/lib/subscription_management/view/components/subscribeButton.dart @@ -3,6 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/subscription_management/states/subscribe_button.dart'; +import '../../../notifications/views/components/bell_icon.dart'; + class SubscribeButton extends StatelessWidget { final String channelId; final String subCount; @@ -13,43 +15,52 @@ class SubscribeButton extends StatelessWidget { Widget build(BuildContext context) { var locals = AppLocalizations.of(context)!; - return BlocProvider( - create: (context) => SubscribeButtonCubit(SubscribeButtonState(channelId: channelId)), - child: BlocBuilder( - builder: (context, _) { - var cubit = context.read(); - return SizedBox( - height: 25, - child: FilledButton.tonal( - onPressed: _.isLoggedIn ? cubit.toggleSubscription : null, - child: Row( - children: _.isLoggedIn - ? [ - _.loading - ? const SizedBox( - width: 15, - height: 15, - child: CircularProgressIndicator( - strokeWidth: 1, - )) - : Icon(_.isSubscribed ? Icons.done : Icons.add), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text('${(_.isSubscribed ? locals.subscribed : locals.subscribe)} | $subCount'), - ), - ] - : [ - const Icon(Icons.people), - Padding( - padding: const EdgeInsets.only(left: 8.0), - // child: Text('${subCount.replaceAll("^0.00\$","no")} subscribers'), - child: Text(locals.nSubscribers(subCount.replaceAll(RegExp(r'^0.00$'), "no"))), - ), - ], - ), - )); - }, - ), + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + BlocProvider( + create: (context) => SubscribeButtonCubit(SubscribeButtonState(channelId: channelId)), + child: BlocBuilder( + builder: (context, _) { + var cubit = context.read(); + return SizedBox( + height: 25, + child: FilledButton.tonal( + onPressed: _.isLoggedIn ? cubit.toggleSubscription : null, + child: Row( + children: _.isLoggedIn + ? [ + _.loading + ? const SizedBox( + width: 15, + height: 15, + child: CircularProgressIndicator( + strokeWidth: 1, + )) + : Icon(_.isSubscribed ? Icons.done : Icons.add), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text('${(_.isSubscribed ? locals.subscribed : locals.subscribe)} | $subCount'), + ), + ] + : [ + const Icon(Icons.people), + Padding( + padding: const EdgeInsets.only(left: 8.0), + // child: Text('${subCount.replaceAll("^0.00\$","no")} subscribers'), + child: Text(locals.nSubscribers(subCount.replaceAll(RegExp(r'^0.00$'), "no"))), + ), + ], + ), + )); + }, + ), + ), + BellIcon( + itemId: channelId, + type: BellIconType.channel, + ) + ], ); } } diff --git a/lib/subscription_management/view/screens/manage_subscriptions.dart b/lib/subscription_management/view/screens/manage_subscriptions.dart index 0f881332..c04ee086 100644 --- a/lib/subscription_management/view/screens/manage_subscriptions.dart +++ b/lib/subscription_management/view/screens/manage_subscriptions.dart @@ -1,16 +1,18 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:invidious/main.dart'; -import 'package:invidious/myRouteObserver.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/subscription_management/models/subscription.dart'; import 'package:invidious/utils.dart'; +import 'package:invidious/utils/views/components/simple_list_item.dart'; import 'package:invidious/utils/views/components/top_loading.dart'; import '../../states/manage_subscriptions.dart'; -class ManageSubscriptions extends StatelessWidget { - const ManageSubscriptions({super.key}); +@RoutePage() +class ManageSubscriptionsScreen extends StatelessWidget { + const ManageSubscriptionsScreen({super.key}); @override Widget build(BuildContext context) { @@ -49,19 +51,24 @@ class ManageSubscriptions extends StatelessWidget { Subscription sub = _.subs[index]; return GestureDetector( - onTap: () => navigatorKey.currentState?.pushNamed(PATH_CHANNEL, arguments: sub.authorId).then((value) => cubit.refreshSubs()), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), - decoration: BoxDecoration(color: index % 2 != 0 ? colors.secondaryContainer.withOpacity(0.5) : colors.background, borderRadius: BorderRadius.circular(10)), + onTap: () => AutoRouter.of(context) + .push(ChannelRoute(channelId: sub.authorId)) + .then((value) => cubit.refreshSubs()), + child: SimpleListItem( + key: ValueKey(sub.authorId), + index: index, child: Row( - key: ValueKey(sub.authorId), mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(sub.author), IconButton.filledTonal( visualDensity: VisualDensity.compact, onPressed: () { - okCancelDialog(context, locals.unSubscribeQuestion, locals.youCanSubscribeAgainLater, () => cubit.unsubscribe(sub.authorId)); + okCancelDialog( + context, + locals.unSubscribeQuestion, + locals.youCanSubscribeAgainLater, + () => cubit.unsubscribe(sub.authorId)); }, icon: const Icon( Icons.clear, diff --git a/lib/subscription_management/view/tv/tv_subscribe_button.dart b/lib/subscription_management/view/tv/tv_subscribe_button.dart index ca36ede9..29713141 100644 --- a/lib/subscription_management/view/tv/tv_subscribe_button.dart +++ b/lib/subscription_management/view/tv/tv_subscribe_button.dart @@ -10,7 +10,9 @@ class TvSubscribeButton extends StatelessWidget { final bool? autoFocus; final Function(bool focus)? onFocusChanged; - const TvSubscribeButton({Key? key, required this.channelId, required this.subCount, this.autoFocus, this.onFocusChanged}) : super(key: key); + const TvSubscribeButton( + {Key? key, required this.channelId, required this.subCount, this.autoFocus, this.onFocusChanged}) + : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/utils.dart b/lib/utils.dart index 80bcda64..3b224762 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -11,6 +11,7 @@ import 'package:invidious/utils/views/tv/components/tv_button.dart'; import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; import 'package:invidious/videos/models/base_video.dart'; import 'package:logging/logging.dart'; +import 'package:optimize_battery/optimize_battery.dart'; import 'package:share_plus/share_plus.dart'; import 'utils/models/country.dart'; @@ -209,6 +210,14 @@ okCancelDialog(BuildContext context, String title, String message, Function() on ); } +showBatteryOptimizationDialog(BuildContext context) { + if (!context.mounted) return; + + var locals = AppLocalizations.of(context)!; + okCancelDialog(context, locals.askForDisableBatteryOptimizationTitle, locals.askForDisableBatteryOptimizationContent, + () => OptimizeBattery.openBatteryOptimizationSettings()); +} + showTvAlertdialog(BuildContext context, String title, List body) { var locals = AppLocalizations.of(context)!; showTvDialog(context: context, builder: (context) => body, actions: [ @@ -225,7 +234,11 @@ showTvAlertdialog(BuildContext context, String title, List body) { ]); } -showTvDialog({required BuildContext context, String? title, required List Function(BuildContext context) builder, required List actions}) { +showTvDialog( + {required BuildContext context, + String? title, + required List Function(BuildContext context) builder, + required List actions}) { var textTheme = Theme.of(context).textTheme; Navigator.of(context).push(MaterialPageRoute( @@ -257,7 +270,8 @@ showTvDialog({required BuildContext context, String? title, required List element.code == code, orElse: () => Country('US', 'United States of America')); + return countryCodes.firstWhere((element) => element.code == code, + orElse: () => Country('US', 'United States of America')); } KeyEventResult onTvSelect(KeyEvent event, BuildContext context, Function(BuildContext context) func) { diff --git a/lib/utils/models/paginatedList.dart b/lib/utils/models/paginatedList.dart index 68941710..803407a7 100644 --- a/lib/utils/models/paginatedList.dart +++ b/lib/utils/models/paginatedList.dart @@ -171,7 +171,12 @@ class SearchPaginatedList extends PaginatedList { List Function(SearchResults res) getFromResults; - SearchPaginatedList({required this.query, required this.items, required this.type, required this.getFromResults, required this.sortBy}); + SearchPaginatedList( + {required this.query, + required this.items, + required this.type, + required this.getFromResults, + required this.sortBy}); @override bool getHasMore() { @@ -203,7 +208,12 @@ class SearchPaginatedList extends PaginatedList { // Force refresh to fetch all videos as search endpoint only returns 2 videos for each playlist class PlaylistSearchPaginatedList extends SearchPaginatedList { - PlaylistSearchPaginatedList({required super.query, required super.items, required super.type, required super.getFromResults, required super.sortBy}); + PlaylistSearchPaginatedList( + {required super.query, + required super.items, + required super.type, + required super.getFromResults, + required super.sortBy}); @override bool getHasMore() { diff --git a/lib/utils/models/timestampLinkifier.dart b/lib/utils/models/timestampLinkifier.dart index a2b9cac1..01caf8cc 100644 --- a/lib/utils/models/timestampLinkifier.dart +++ b/lib/utils/models/timestampLinkifier.dart @@ -1,6 +1,7 @@ import 'package:flutter_linkify/flutter_linkify.dart'; -final _timeStampRegex = RegExp(r'^(.*?)(((([01])?[0-9]|2[0-3]):)?(([0-5])?[0-9](:[0-5][0-9])))', caseSensitive: false, dotAll: true); +final _timeStampRegex = + RegExp(r'^(.*?)(((([01])?[0-9]|2[0-3]):)?(([0-5])?[0-9](:[0-5][0-9])))', caseSensitive: false, dotAll: true); class TimestampLinkifier extends Linkifier { const TimestampLinkifier(); diff --git a/lib/utils/states/item_list.dart b/lib/utils/states/item_list.dart index 1b82b83d..b5a00a7e 100644 --- a/lib/utils/states/item_list.dart +++ b/lib/utils/states/item_list.dart @@ -65,9 +65,10 @@ class ItemListCubit extends Cubit> { try { state = this.state.copyWith(); state.items = await refreshFunction(); - ; state.loading = false; - emit(state); + if(!isClosed) { + emit(state); + } } catch (err) { state = this.state.copyWith(); state.items = []; @@ -77,7 +78,9 @@ class ItemListCubit extends Cubit> { } else { state.error = ItemListErrors.couldNotFetchItems; } - emit(state); + if(!isClosed) { + emit(state); + } rethrow; } } @@ -97,5 +100,5 @@ class ItemListState { ItemListState({required this.itemList}) {} - ItemListState._(this.itemList, this.items, this.loading, this.imageCache, this.scrollController, this.error); + ItemListState._(this.itemList, this.items, this.loading, this.imageCache, this.scrollController, this.error); } diff --git a/lib/utils/states/paginated_list_view.dart b/lib/utils/states/paginated_list_view.dart index 8cd131d1..831cb905 100644 --- a/lib/utils/states/paginated_list_view.dart +++ b/lib/utils/states/paginated_list_view.dart @@ -13,6 +13,7 @@ class PaginatedListCubit extends Cubit> { void onInit() { state.scrollController.addListener(getMore); + getItems(); } @override @@ -21,6 +22,13 @@ class PaginatedListCubit extends Cubit> { super.close(); } + getItems() async { + emit(state.copyWith(loading: true)); + var items = await state.paginatedList.getItems(); + + emit(state.copyWith(loading: false, items: items)); + } + getMore() { if (state.paginatedList.getHasMore()) { if (state.scrollController.hasClients) { diff --git a/lib/utils/states/select_list.dart b/lib/utils/states/select_list.dart index c9dc9491..18177178 100644 --- a/lib/utils/states/select_list.dart +++ b/lib/utils/states/select_list.dart @@ -1,13 +1,13 @@ 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 { + 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) { diff --git a/lib/utils/views/components/paginated_list_view.dart b/lib/utils/views/components/paginated_list_view.dart index 7ecdd199..5debf733 100644 --- a/lib/utils/views/components/paginated_list_view.dart +++ b/lib/utils/views/components/paginated_list_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:invidious/utils/views/components/top_loading.dart'; import '../../models/paginatedList.dart'; import '../../states/paginated_list_view.dart'; @@ -9,21 +10,28 @@ class PaginatedListView extends StatelessWidget { final List? startItems; final Widget Function(T item) itemBuilder; - const PaginatedListView({Key? key, required this.paginatedList, required this.itemBuilder, this.startItems}) : super(key: key); + const PaginatedListView({Key? key, required this.paginatedList, required this.itemBuilder, this.startItems}) + : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => PaginatedListCubit(PaginatedListViewController(paginatedList: this.paginatedList, startItems: this.startItems)), + create: (context) => PaginatedListCubit( + PaginatedListViewController(paginatedList: this.paginatedList, startItems: this.startItems)), child: BlocBuilder, PaginatedListViewController>( - builder: (context, _) => ListView.builder( - controller: _.scrollController, - itemCount: _.items.length, - itemBuilder: (BuildContext context, int index) { - T item = _.items[index]; - return itemBuilder(item); - }, - )), + builder: (context, _) => Stack( + children: [ + if(_.loading) const TopListLoading(), + ListView.builder( + controller: _.scrollController, + itemCount: _.items.length, + itemBuilder: (BuildContext context, int index) { + T item = _.items[index]; + return itemBuilder(item); + }, + ), + ], + )), ); } } diff --git a/lib/utils/views/components/placeholders.dart b/lib/utils/views/components/placeholders.dart index 7020e7fd..ebcfb37b 100644 --- a/lib/utils/views/components/placeholders.dart +++ b/lib/utils/views/components/placeholders.dart @@ -19,14 +19,18 @@ class AnimatedPlaceHolder extends StatelessWidget { var colors = Theme.of(context).colorScheme; return animate ? FadeIn( - child: - Animate(autoPlay: true, onComplete: (controller) => controller.repeat(reverse: true), effects: const [FadeEffect(begin: 0.4, end: 0.8, duration: Duration(seconds: 2))], child: child)) + child: Animate( + autoPlay: true, + onComplete: (controller) => controller.repeat(reverse: true), + effects: const [FadeEffect(begin: 0.4, end: 0.8, duration: Duration(seconds: 2))], + child: child)) : child; } } class TextPlaceHolder extends StatelessWidget { final bool small; + const TextPlaceHolder({super.key, this.small = false}); @override @@ -165,11 +169,13 @@ class CompactVideoPlaceHolder extends StatelessWidget { children: [ Container( height: 10, - decoration: BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(10)), + decoration: + BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(10)), ), Container( height: 10, - decoration: BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(10)), + decoration: + BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(10)), ) ], ), @@ -218,6 +224,7 @@ class VideoGridPlaceHolder extends StatelessWidget { class PlaylistPlaceHolder extends StatelessWidget { final bool small; + const PlaylistPlaceHolder({super.key, this.small = false}); @override @@ -343,7 +350,9 @@ class TvPlaylistPlaceHolder extends StatelessWidget { SizedBox( height: 4, ), - Padding(padding: EdgeInsets.only(bottom: 8.0), child: FractionallySizedBox(widthFactor: 0.4, child: TextPlaceHolder())), + Padding( + padding: EdgeInsets.only(bottom: 8.0), + child: FractionallySizedBox(widthFactor: 0.4, child: TextPlaceHolder())), ], ), ), @@ -360,7 +369,8 @@ class ParagraphPlaceHolder extends StatelessWidget { return Wrap( spacing: 4, runSpacing: 4, - children: repeatWidget(() => SizedBox(width: Random().nextInt(100) + 50, child: const TextPlaceHolder()), count: Random().nextInt(30) + 10), + children: repeatWidget(() => SizedBox(width: Random().nextInt(100) + 50, child: const TextPlaceHolder()), + count: Random().nextInt(30) + 10), ); } } @@ -420,7 +430,8 @@ class VideoPlaceHolder extends StatelessWidget { height: 4, ), Row( - children: repeatWidget(() => const SizedBox(height: 15, width: 50, child: TextPlaceHolder()), count: 3), + children: + repeatWidget(() => const SizedBox(height: 15, width: 50, child: TextPlaceHolder()), count: 3), ), Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -446,7 +457,8 @@ class VideoPlaceHolder extends StatelessWidget { child: Container( width: 120, height: 25, - decoration: BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(50)), + decoration: + BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(50)), ), ), ...repeatWidget(() => const Padding( @@ -462,7 +474,8 @@ class VideoPlaceHolder extends StatelessWidget { children: repeatWidget( () => Container( width: Random().nextInt(100) + 50, - decoration: BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(20)), + decoration: BoxDecoration( + color: colors.secondaryContainer, borderRadius: BorderRadius.circular(20)), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), child: Text(''), diff --git a/lib/utils/views/components/select_list_dialog.dart b/lib/utils/views/components/select_list_dialog.dart index cc796e52..c4af7f74 100644 --- a/lib/utils/views/components/select_list_dialog.dart +++ b/lib/utils/views/components/select_list_dialog.dart @@ -12,7 +12,15 @@ class SelectList extends StatelessWidget { 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}) + 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, diff --git a/lib/utils/views/components/simple_list_item.dart b/lib/utils/views/components/simple_list_item.dart new file mode 100644 index 00000000..82904148 --- /dev/null +++ b/lib/utils/views/components/simple_list_item.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class SimpleListItem extends StatelessWidget { + final Widget child; + final int index; + + const SimpleListItem({super.key, required this.child, required this.index}); + + @override + Widget build(BuildContext context) { + var colors = Theme.of(context).colorScheme; + return Container( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + decoration: BoxDecoration( + color: index % 2 != 0 ? colors.secondaryContainer.withOpacity(0.5) : colors.background, + borderRadius: BorderRadius.circular(10)), + child: child, + ); + } +} diff --git a/lib/utils/views/components/text_linkified.dart b/lib/utils/views/components/text_linkified.dart index f669f3f4..afe80428 100644 --- a/lib/utils/views/components/text_linkified.dart +++ b/lib/utils/views/components/text_linkified.dart @@ -1,12 +1,12 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:invidious/player/states/player.dart'; +import 'package:invidious/router.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../globals.dart'; -import '../../../main.dart'; import '../../../videos/models/base_video.dart'; -import '../../../videos/views/screens/video.dart'; import '../../models/timestampLinkifier.dart'; class TextLinkified extends StatelessWidget { @@ -22,29 +22,26 @@ class TextLinkified extends StatelessWidget { return SelectableLinkify( text: text, linkStyle: TextStyle(color: colorScheme.primary, decoration: TextDecoration.none), - onOpen: openLink, + onOpen: (link) => openLink(context, link), options: const LinkifyOptions(humanize: true, removeWww: true), linkifiers: const [UrlLinkifier(), TimestampLinkifier()], ); } - void openLink(LinkableElement link) { + void openLink(BuildContext context, LinkableElement link) { if (link is UrlElement) { var uri = Uri.parse(link.url); if (YOUTUBE_HOSTS.contains(uri.host)) { - if (uri.pathSegments.length == 1 && uri.pathSegments.contains("watch") && uri.queryParameters.containsKey('v')) { + if (uri.pathSegments.length == 1 && + uri.pathSegments.contains("watch") && + uri.queryParameters.containsKey('v')) { String videoId = uri.queryParameters['v']!; - navigatorKey.currentState?.push(MaterialPageRoute( - builder: (context) => VideoView( - videoId: videoId, - ))); + AutoRouter.of(context).push(VideoRoute(videoId: videoId)); } if (uri.host == 'youtu.be' && uri.pathSegments.length == 1) { String videoId = uri.pathSegments[0]; - navigatorKey.currentState?.push(MaterialPageRoute( - builder: (context) => VideoView( - videoId: videoId, - ))); + + AutoRouter.of(context).push(VideoRoute(videoId: videoId)); } } else { launchUrl(Uri.parse(link.url), mode: LaunchMode.externalApplication); @@ -53,8 +50,12 @@ class TextLinkified extends StatelessWidget { var split = link.url.split(':'); if (split.length >= 2 && split.length <= 3) { bool hours = split.length == 3; - Duration position = Duration(hours: hours ? int.parse(split[0]) : 0, minutes: int.parse(split[hours ? 1 : 0]), seconds: int.parse(split[hours ? 2 : 1])); - var videoPlaying = (player.state.isPlaying ?? false) && player.state.currentlyPlaying?.videoId == video?.videoId; + Duration position = Duration( + hours: hours ? int.parse(split[0]) : 0, + minutes: int.parse(split[hours ? 1 : 0]), + seconds: int.parse(split[hours ? 2 : 1])); + var videoPlaying = + (player.state.isPlaying ?? false) && player.state.currentlyPlaying?.videoId == video?.videoId; if (videoPlaying) { player.seek(position); } else if (video != null) { diff --git a/lib/utils/views/components/top_loading.dart b/lib/utils/views/components/top_loading.dart index 366c4058..4f5d2527 100644 --- a/lib/utils/views/components/top_loading.dart +++ b/lib/utils/views/components/top_loading.dart @@ -19,7 +19,10 @@ class TopListLoading extends StatelessWidget { Container( height: height, decoration: BoxDecoration( - gradient: LinearGradient(colors: [colorScheme.background, colorScheme.background.withOpacity(0), colorScheme.background], begin: Alignment.centerLeft, end: Alignment.bottomRight)), + gradient: LinearGradient( + colors: [colorScheme.background, colorScheme.background.withOpacity(0), colorScheme.background], + begin: Alignment.centerLeft, + end: Alignment.bottomRight)), ), ]), ); diff --git a/lib/utils/views/tv/components/select_from_list.dart b/lib/utils/views/tv/components/select_from_list.dart index aef19d9b..48773a20 100644 --- a/lib/utils/views/tv/components/select_from_list.dart +++ b/lib/utils/views/tv/components/select_from_list.dart @@ -1,14 +1,18 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:invidious/settings/views/tv/screens/settings.dart'; import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; -class TvSelectFromList extends StatelessWidget { +@RoutePage() +class TvSelectFromListScreen extends StatelessWidget { final List options; final String title; final String selected; final Function(String selected) onSelect; - const TvSelectFromList({Key? key, required this.options, required this.selected, required this.onSelect, required this.title}) : super(key: key); + const TvSelectFromListScreen( + {Key? key, required this.options, required this.selected, required this.onSelect, required this.title}) + : super(key: key); selectOption(BuildContext context, String s) { onSelect(s); @@ -26,7 +30,7 @@ class TvSelectFromList extends StatelessWidget { child: ListView( children: options .map((s) => SettingsTile( - leading: s == selected ? Icon(Icons.done) : null, + leading: s == selected ? const Icon(Icons.done) : null, title: s, onSelected: (context) => selectOption(context, s), )) diff --git a/lib/utils/views/tv/components/tv_button.dart b/lib/utils/views/tv/components/tv_button.dart index 64c5f23c..d9643e54 100644 --- a/lib/utils/views/tv/components/tv_button.dart +++ b/lib/utils/views/tv/components/tv_button.dart @@ -12,7 +12,17 @@ class TvButton extends StatelessWidget { final Function(bool focus)? onFocusChanged; final Widget Function(BuildContext context, bool hasFocus)? builder; - const TvButton({Key? key, this.child, this.onPressed, this.focusedColor, this.unfocusedColor, this.borderRadius, this.autofocus, this.onFocusChanged, this.builder}) : super(key: key); + const TvButton( + {Key? key, + this.child, + this.onPressed, + this.focusedColor, + this.unfocusedColor, + this.borderRadius, + this.autofocus, + this.onFocusChanged, + this.builder}) + : super(key: key); @override Widget build(BuildContext context) { @@ -31,7 +41,9 @@ class TvButton extends StatelessWidget { child: AnimatedContainer( duration: animationDuration, decoration: BoxDecoration( - color: hasFocus ? focusedColor ?? (brightness == Brightness.dark ? colors.primaryContainer : colors.primary) : unfocusedColor ?? colors.secondaryContainer, + color: hasFocus + ? focusedColor ?? (brightness == Brightness.dark ? colors.primaryContainer : colors.primary) + : unfocusedColor ?? colors.secondaryContainer, borderRadius: BorderRadius.circular(borderRadius ?? 2000), ), child: builder != null ? builder!(ctx, hasFocus) : child, diff --git a/lib/utils/views/tv/components/tv_expandable_text.dart b/lib/utils/views/tv/components/tv_expandable_text.dart index 96030273..9223b8b4 100644 --- a/lib/utils/views/tv/components/tv_expandable_text.dart +++ b/lib/utils/views/tv/components/tv_expandable_text.dart @@ -1,6 +1,7 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:invidious/globals.dart'; -import 'package:invidious/utils/views/tv/components/tv_plain_text.dart'; +import 'package:invidious/router.dart'; import '../../../../utils.dart'; @@ -12,9 +13,7 @@ class TvExpandableText extends StatelessWidget { const TvExpandableText({Key? key, required this.text, this.maxLines, this.fontSize}) : super(key: key); showText(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvPlainText(text: text), - )); + AutoRouter.of(context).push(TvPlainTextRoute(text: text)); } @override diff --git a/lib/utils/views/tv/components/tv_horizontal_item_list.dart b/lib/utils/views/tv/components/tv_horizontal_item_list.dart index 31ff5757..41685f7a 100644 --- a/lib/utils/views/tv/components/tv_horizontal_item_list.dart +++ b/lib/utils/views/tv/components/tv_horizontal_item_list.dart @@ -13,7 +13,9 @@ class TvHorizontalItemList extends StatelessWidget { final String? tags; final Widget Function(BuildContext context, int index, T item) buildItem; - const TvHorizontalItemList({Key? key, this.tags, required this.paginatedList, required this.buildItem, required this.getPlaceholder}) : super(key: key); + const TvHorizontalItemList( + {Key? key, this.tags, required this.paginatedList, required this.buildItem, required this.getPlaceholder}) + : super(key: key); @override Widget build(BuildContext context) { @@ -57,7 +59,14 @@ class TvHorizontalVideoList extends StatelessWidget { final int autoFocusedIndex; final void Function(VideoInList video, int index, bool focus)? onItemFocus; - const TvHorizontalVideoList({Key? key, this.tags, required this.paginatedVideoList, this.onSelect, this.autoFocusedIndex = 0, this.onItemFocus}) : super(key: key); + const TvHorizontalVideoList( + {Key? key, + this.tags, + required this.paginatedVideoList, + this.onSelect, + this.autoFocusedIndex = 0, + this.onItemFocus}) + : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/utils/views/tv/components/tv_horizontal_paginated_list.dart b/lib/utils/views/tv/components/tv_horizontal_paginated_list.dart index fa5a9529..3f24b23b 100644 --- a/lib/utils/views/tv/components/tv_horizontal_paginated_list.dart +++ b/lib/utils/views/tv/components/tv_horizontal_paginated_list.dart @@ -10,12 +10,15 @@ class TvHorizontalPaginatedListView extends StatelessWidget { final Widget Function(T item) itemBuilder; final Widget Function() getPlaceHolder; - const TvHorizontalPaginatedListView({Key? key, required this.paginatedList, required this.itemBuilder, this.startItems, required this.getPlaceHolder}) : super(key: key); + const TvHorizontalPaginatedListView( + {Key? key, required this.paginatedList, required this.itemBuilder, this.startItems, required this.getPlaceHolder}) + : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => PaginatedListCubit(PaginatedListViewController(paginatedList: this.paginatedList, startItems: this.startItems)), + create: (context) => PaginatedListCubit( + PaginatedListViewController(paginatedList: this.paginatedList, startItems: this.startItems)), child: BlocBuilder, PaginatedListViewController>( builder: (context, _) => Stack( children: [ @@ -28,7 +31,8 @@ class TvHorizontalPaginatedListView extends StatelessWidget { controller: _.scrollController, scrollDirection: Axis.horizontal, itemCount: _.items.length + (_.loading ? 10 : 0), - itemBuilder: (BuildContext context, int index) => index >= _.items.length ? getPlaceHolder() : itemBuilder(_.items[index]), + itemBuilder: (BuildContext context, int index) => + index >= _.items.length ? getPlaceHolder() : itemBuilder(_.items[index]), ), ], )), diff --git a/lib/utils/views/tv/components/tv_plain_text.dart b/lib/utils/views/tv/components/tv_plain_text.dart index 3ba2fcc8..391cb8cb 100644 --- a/lib/utils/views/tv/components/tv_plain_text.dart +++ b/lib/utils/views/tv/components/tv_plain_text.dart @@ -1,18 +1,21 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; -class TvPlainText extends StatefulWidget { + +@RoutePage() +class TvPlainTextScreen extends StatefulWidget { final String text; - const TvPlainText({Key? key, required this.text}) : super(key: key); + const TvPlainTextScreen({Key? key, required this.text}) : super(key: key); @override - State createState() => _TvPlainTextState(); + State createState() => _TvPlainTextScreenState(); } -class _TvPlainTextState extends State { - final ScrollController _scrollController = new ScrollController(); +class _TvPlainTextScreenState extends State { + final ScrollController _scrollController = ScrollController(); KeyEventResult scroll(FocusNode node, KeyEvent event) { if (event.logicalKey == LogicalKeyboardKey.arrowDown) { diff --git a/lib/utils/views/tv/components/tv_text_field.dart b/lib/utils/views/tv/components/tv_text_field.dart index 562df544..f07ca0ea 100644 --- a/lib/utils/views/tv/components/tv_text_field.dart +++ b/lib/utils/views/tv/components/tv_text_field.dart @@ -1,5 +1,8 @@ +import 'package:auto_route/annotations.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:invidious/globals.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/utils.dart'; import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; @@ -34,17 +37,16 @@ class TvTextField extends StatelessWidget { : super(key: key); openTextField(BuildContext context) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvTextFieldFiller( - controller: controller, - autocorrect: autocorrect, - autofocus: autofocus, - onSubmitted: onSubmitted, - textInputAction: textInputAction, - obscureText: obscureText, - autofillHints: autofillHints, - decoration: decoration, - ))); + AutoRouter.of(context).push(TvTextFieldRoute( + controller: controller, + autocorrect: autocorrect, + autofocus: autofocus, + onSubmitted: onSubmitted, + textInputAction: textInputAction, + obscureText: obscureText, + autofillHints: autofillHints, + decoration: decoration, + )); } @override @@ -90,7 +92,8 @@ class TvTextField extends StatelessWidget { } } -class TvTextFieldFiller extends StatelessWidget { +@RoutePage() +class TvTextFieldScreen extends StatelessWidget { final TextEditingController controller; final bool? autofocus; final bool? autocorrect; @@ -101,7 +104,7 @@ class TvTextFieldFiller extends StatelessWidget { final Iterable? autofillHints; final InputDecoration? decoration; - const TvTextFieldFiller( + const TvTextFieldScreen( {Key? key, required this.controller, this.autofocus, this.autocorrect, this.focusNode, this.onSubmitted, this.textInputAction, this.obscureText, this.autofillHints, this.decoration}) : super(key: key); diff --git a/lib/videos/models/adaptive_format.dart b/lib/videos/models/adaptive_format.dart index aea66ad0..d81ce810 100644 --- a/lib/videos/models/adaptive_format.dart +++ b/lib/videos/models/adaptive_format.dart @@ -18,7 +18,8 @@ class AdaptiveFormat { String? qualityLabel; String? resolution; - AdaptiveFormat(this.index, this.bitrate, this.init, this.url, this.itag, this.type, this.clen, this.lmt, this.projectionType, this.container, this.encoding, this.qualityLabel, this.resolution); + AdaptiveFormat(this.index, this.bitrate, this.init, this.url, this.itag, this.type, this.clen, this.lmt, + this.projectionType, this.container, this.encoding, this.qualityLabel, this.resolution); factory AdaptiveFormat.fromJson(Map json) => _$AdaptiveFormatFromJson(json); diff --git a/lib/videos/models/base_video.dart b/lib/videos/models/base_video.dart index 13d7c50b..a6e84456 100644 --- a/lib/videos/models/base_video.dart +++ b/lib/videos/models/base_video.dart @@ -1,4 +1,3 @@ -import 'package:flutter/services.dart'; import 'package:invidious/videos/models/video_in_list.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -29,7 +28,9 @@ class BaseVideo extends IdedVideo implements ShareLinks { @JsonKey(includeFromJson: false, includeToJson: false) bool filterHide = false; - BaseVideo(this.title, String videoId, this.lengthSeconds, this.author, this.authorId, this.authorUrl, this.videoThumbnails) : super(videoId); + BaseVideo( + this.title, String videoId, this.lengthSeconds, this.author, this.authorId, this.authorUrl, this.videoThumbnails) + : super(videoId); @override String getInvidiousLink(Server server, int? timestamp) { diff --git a/lib/videos/models/db/history_video_cache.dart b/lib/videos/models/db/history_video_cache.dart index dd829bb1..46af89bf 100644 --- a/lib/videos/models/db/history_video_cache.dart +++ b/lib/videos/models/db/history_video_cache.dart @@ -26,7 +26,8 @@ class HistoryVideoCache { var cachedVideo = db.getHistoryVideoByVideoId(e); if (cachedVideo == null) { var vid = await service.getVideo(e); - cachedVideo = HistoryVideoCache(vid.videoId, vid.title, vid.author, ImageObject.getWorstThumbnail(vid.videoThumbnails)?.url ?? ''); + cachedVideo = HistoryVideoCache( + vid.videoId, vid.title, vid.author, ImageObject.getWorstThumbnail(vid.videoThumbnails)?.url ?? ''); db.upsertHistoryVideo(cachedVideo); } return cachedVideo; diff --git a/lib/videos/models/format_stream.dart b/lib/videos/models/format_stream.dart index 5ccc327d..00f8ab25 100644 --- a/lib/videos/models/format_stream.dart +++ b/lib/videos/models/format_stream.dart @@ -14,7 +14,8 @@ class FormatStream { String resolution; String size; - FormatStream(this.url, this.itag, this.type, this.quality, this.container, this.encoding, this.qualityLabel, this.resolution, this.size); + FormatStream(this.url, this.itag, this.type, this.quality, this.container, this.encoding, this.qualityLabel, + this.resolution, this.size); factory FormatStream.fromJson(Map json) => _$FormatStreamFromJson(json); diff --git a/lib/videos/models/recommended_video.dart b/lib/videos/models/recommended_video.dart index 14fa9c69..ba33fb42 100644 --- a/lib/videos/models/recommended_video.dart +++ b/lib/videos/models/recommended_video.dart @@ -1,5 +1,4 @@ import 'package:invidious/videos/models/base_video.dart'; -import 'package:invidious/videos/models/video_in_list.dart'; import 'package:json_annotation/json_annotation.dart'; import '../../utils/models/image_object.dart'; @@ -10,7 +9,8 @@ part 'recommended_video.g.dart'; class RecommendedVideo extends BaseVideo { String viewCountText; - RecommendedVideo(String videoId, String title, List videoThumbnails, String? author, int lengthSeconds, this.viewCountText) + RecommendedVideo(String videoId, String title, List videoThumbnails, String? author, int lengthSeconds, + this.viewCountText) : super(title, videoId, lengthSeconds, author, null, null, videoThumbnails); factory RecommendedVideo.fromJson(Map json) => _$RecommendedVideoFromJson(json); diff --git a/lib/videos/models/video.dart b/lib/videos/models/video.dart index 802dd6a1..ae0ee980 100644 --- a/lib/videos/models/video.dart +++ b/lib/videos/models/video.dart @@ -94,6 +94,7 @@ class Video extends BaseVideo { } VideoInList toVideoInList() { - return VideoInList(title, videoId, lengthSeconds, viewCount, author, authorId, authorUrl, published, publishedText, videoThumbnails); + return VideoInList(title, videoId, lengthSeconds, viewCount, author, authorId, authorUrl, published, publishedText, + videoThumbnails); } } diff --git a/lib/videos/models/video_in_list.dart b/lib/videos/models/video_in_list.dart index e6632bd1..2c8ec542 100644 --- a/lib/videos/models/video_in_list.dart +++ b/lib/videos/models/video_in_list.dart @@ -14,8 +14,8 @@ class VideoInList extends BaseVideo { String? indexId; String? publishedText; - VideoInList( - String title, String videoId, int lengthSeconds, this.viewCount, String? author, String? authorId, String? authorUrl, this.published, this.publishedText, List videoThumbnails) + VideoInList(String title, String videoId, int lengthSeconds, this.viewCount, String? author, String? authorId, + String? authorUrl, this.published, this.publishedText, List videoThumbnails) : super(title, videoId, lengthSeconds, author, authorUrl, authorId, videoThumbnails); factory VideoInList.fromJson(Map json) => _$VideoInListFromJson(json); diff --git a/lib/videos/states/add_to_playlist.dart b/lib/videos/states/add_to_playlist.dart index df36b3ea..8d95d9d4 100644 --- a/lib/videos/states/add_to_playlist.dart +++ b/lib/videos/states/add_to_playlist.dart @@ -1,18 +1,21 @@ import 'package:bloc/bloc.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:invidious/videos/states/add_to_playlist_button.dart'; -import 'package:invidious/videos/states/video_like.dart'; +import 'package:invidious/extensions.dart'; +import 'package:invidious/videos/models/video_in_list.dart'; +import 'package:logging/logging.dart'; import '../../globals.dart'; import '../../playlists/models/playlist.dart'; part 'add_to_playlist.g.dart'; +const String likePlaylistName = '❤️'; + class AddToPlaylistCubit extends Cubit { - final AddToPlaylistButtonCubit? addToPlaylistButtonCubit; - final VideoLikeButtonCubit? videoLikeButtonCubit; - AddToPlaylistCubit(super.initialState, {this.videoLikeButtonCubit, this.addToPlaylistButtonCubit}) { + final log = Logger('AddToPlaylistcubit'); + + AddToPlaylistCubit(super.initialState) { onReady(); } @@ -24,12 +27,16 @@ class AddToPlaylistCubit extends Cubit { addToPlaylist(String playlistId) async { await service.addVideoToPlaylist(playlistId, state.videoId); - - addToPlaylistButtonCubit?.countPlaylistsForVideo(); - videoLikeButtonCubit?.checkVideoLikeStatus(); + onReady(); } Future onReady() async { + await getAllPlaylists(); + await countPlaylistsForVideo(); + await checkVideoLikeStatus(); + } + + getAllPlaylists() async { var state = this.state.copyWith(); if (state.isLoggedIn) { state.playlists = await service.getUserPlaylists(); @@ -37,6 +44,77 @@ class AddToPlaylistCubit extends Cubit { state.loading = false; emit(state); } + + Future getPlaylist() async { + List playlists = await service.getUserPlaylists(); + Playlist? pl = playlists.firstWhereOrNull((pl) => pl.title == likePlaylistName); + + return pl; + } + + checkVideoLikeStatus() async { + var state = this.state.copyWith(); + Playlist? p = await getPlaylist(); + VideoInList? video = p?.videos.firstWhereOrNull((element) => element.videoId == state.videoId); + + state.isVideoLiked = video != null; + + if (!isClosed) { + emit(state); + log.fine('video is currently liked ? $state.isVideoLiked'); + } + } + + Future createPlayList() async { + await service.createPlayList(likePlaylistName, "private"); + return getPlaylist(); + } + + countPlaylistsForVideo() async { + var state = this.state.copyWith(); + List lists = await service.getUserPlaylists(); + + state.playListCount = + lists.where((list) => list.videos.indexWhere((video) => video.videoId == state.videoId) >= 0).length; + log.fine('playlist count ${state.playListCount}'); + if (!isClosed) { + emit(state); + } + } + + Future toggleLike() async { + var state = this.state.copyWith(); + state.loading = true; + emit(state); + + state = this.state.copyWith(); + await checkVideoLikeStatus(); + Playlist? p = await getPlaylist(); + p ??= await createPlayList(); + + if (p != null && state.videoId != null) { + if (state.isVideoLiked) { + log.fine('Video is liked, unliking it'); + VideoInList? v = p.videos.firstWhereOrNull((element) => element.videoId == state.videoId!); + if (v?.indexId != null) { + await service.deleteUserPlaylistVideo(p.playlistId, v!.indexId!); + state.isVideoLiked = !state.isVideoLiked; + } + } else { + log.fine('Video is not liked yet, we add it to the like playlist'); + await service.addVideoToPlaylist(p.playlistId, state.videoId!); + state.isVideoLiked = !state.isVideoLiked; + } + } + state.loading = false; + emit(state); + onReady(); + } + + saveVideoToPlaylist(String selectedPlaylistId) async { + await service.addVideoToPlaylist(selectedPlaylistId, state.videoId); + await onReady(); + } } @CopyWith(constructor: "_") @@ -44,11 +122,12 @@ class AddToPlaylistController { List playlists = []; int playListCount = 0; String videoId; + bool isVideoLiked = false; bool loading = true; bool isLoggedIn = service.isLoggedIn(); AddToPlaylistController(this.videoId); - AddToPlaylistController._(this.playlists, this.playListCount, this.videoId, this.loading, this.isLoggedIn); + AddToPlaylistController._(this.playlists, this.playListCount, this.videoId, this.loading, this.isLoggedIn, this.isVideoLiked); } diff --git a/lib/videos/states/add_to_playlist.g.dart b/lib/videos/states/add_to_playlist.g.dart index 4ed7ea3f..0ce006dc 100644 --- a/lib/videos/states/add_to_playlist.g.dart +++ b/lib/videos/states/add_to_playlist.g.dart @@ -17,6 +17,8 @@ abstract class _$AddToPlaylistControllerCWProxy { AddToPlaylistController isLoggedIn(bool isLoggedIn); + AddToPlaylistController isVideoLiked(bool isVideoLiked); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `AddToPlaylistController(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -29,6 +31,7 @@ abstract class _$AddToPlaylistControllerCWProxy { String? videoId, bool? loading, bool? isLoggedIn, + bool? isVideoLiked, }); } @@ -57,6 +60,10 @@ class _$AddToPlaylistControllerCWProxyImpl AddToPlaylistController isLoggedIn(bool isLoggedIn) => this(isLoggedIn: isLoggedIn); + @override + AddToPlaylistController isVideoLiked(bool isVideoLiked) => + this(isVideoLiked: isVideoLiked); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `AddToPlaylistController(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. @@ -71,6 +78,7 @@ class _$AddToPlaylistControllerCWProxyImpl Object? videoId = const $CopyWithPlaceholder(), Object? loading = const $CopyWithPlaceholder(), Object? isLoggedIn = const $CopyWithPlaceholder(), + Object? isVideoLiked = const $CopyWithPlaceholder(), }) { return AddToPlaylistController._( playlists == const $CopyWithPlaceholder() || playlists == null @@ -93,6 +101,10 @@ class _$AddToPlaylistControllerCWProxyImpl ? _value.isLoggedIn // ignore: cast_nullable_to_non_nullable : isLoggedIn as bool, + isVideoLiked == const $CopyWithPlaceholder() || isVideoLiked == null + ? _value.isVideoLiked + // ignore: cast_nullable_to_non_nullable + : isVideoLiked as bool, ); } } diff --git a/lib/videos/states/add_to_playlist_button.dart b/lib/videos/states/add_to_playlist_button.dart deleted file mode 100644 index bf9d9765..00000000 --- a/lib/videos/states/add_to_playlist_button.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:logging/logging.dart'; - -import '../../globals.dart'; -import '../../playlists/models/playlist.dart'; - -part 'add_to_playlist_button.g.dart'; - -var log = Logger('AddToPlaylistButtonController'); - -class AddToPlaylistButtonCubit extends Cubit { - AddToPlaylistButtonCubit(super.initialState) { - onReady(); - } - - onReady() { - countPlaylistsForVideo(); - } - - countPlaylistsForVideo() async { - var state = this.state.copyWith(); - List lists = await service.getUserPlaylists(); - - state.playListCount = lists.where((list) => list.videos.indexWhere((video) => video.videoId == state.videoId) >= 0).length; - log.fine('playlist count $state.playListCount'); - emit(state); - } -} - -@CopyWith(constructor: "_") -class AddToPlaylistButtonState { - String? videoId; - bool isLoggedIn = service.isLoggedIn(); - - AddToPlaylistButtonState({this.videoId}); - - int playListCount = 0; - - AddToPlaylistButtonState._(this.videoId, this.isLoggedIn, this.playListCount); -} diff --git a/lib/videos/states/add_to_playlist_button.g.dart b/lib/videos/states/add_to_playlist_button.g.dart deleted file mode 100644 index 412407b6..00000000 --- a/lib/videos/states/add_to_playlist_button.g.dart +++ /dev/null @@ -1,82 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'add_to_playlist_button.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$AddToPlaylistButtonStateCWProxy { - AddToPlaylistButtonState videoId(String? videoId); - - AddToPlaylistButtonState isLoggedIn(bool isLoggedIn); - - AddToPlaylistButtonState playListCount(int playListCount); - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `AddToPlaylistButtonState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// AddToPlaylistButtonState(...).copyWith(id: 12, name: "My name") - /// ```` - AddToPlaylistButtonState call({ - String? videoId, - bool? isLoggedIn, - int? playListCount, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfAddToPlaylistButtonState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfAddToPlaylistButtonState.copyWith.fieldName(...)` -class _$AddToPlaylistButtonStateCWProxyImpl - implements _$AddToPlaylistButtonStateCWProxy { - const _$AddToPlaylistButtonStateCWProxyImpl(this._value); - - final AddToPlaylistButtonState _value; - - @override - AddToPlaylistButtonState videoId(String? videoId) => this(videoId: videoId); - - @override - AddToPlaylistButtonState isLoggedIn(bool isLoggedIn) => - this(isLoggedIn: isLoggedIn); - - @override - AddToPlaylistButtonState playListCount(int playListCount) => - this(playListCount: playListCount); - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `AddToPlaylistButtonState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// AddToPlaylistButtonState(...).copyWith(id: 12, name: "My name") - /// ```` - AddToPlaylistButtonState call({ - Object? videoId = const $CopyWithPlaceholder(), - Object? isLoggedIn = const $CopyWithPlaceholder(), - Object? playListCount = const $CopyWithPlaceholder(), - }) { - return AddToPlaylistButtonState._( - videoId == const $CopyWithPlaceholder() - ? _value.videoId - // ignore: cast_nullable_to_non_nullable - : videoId as String?, - isLoggedIn == const $CopyWithPlaceholder() || isLoggedIn == null - ? _value.isLoggedIn - // ignore: cast_nullable_to_non_nullable - : isLoggedIn as bool, - playListCount == const $CopyWithPlaceholder() || playListCount == null - ? _value.playListCount - // ignore: cast_nullable_to_non_nullable - : playListCount as int, - ); - } -} - -extension $AddToPlaylistButtonStateCopyWith on AddToPlaylistButtonState { - /// Returns a callable class that can be used as follows: `instanceOfAddToPlaylistButtonState.copyWith(...)` or like so:`instanceOfAddToPlaylistButtonState.copyWith.fieldName(...)`. - // ignore: library_private_types_in_public_api - _$AddToPlaylistButtonStateCWProxy get copyWith => - _$AddToPlaylistButtonStateCWProxyImpl(this); -} diff --git a/lib/videos/states/compact_video.dart b/lib/videos/states/compact_video.dart index 7a4cca4e..c909113b 100644 --- a/lib/videos/states/compact_video.dart +++ b/lib/videos/states/compact_video.dart @@ -28,10 +28,12 @@ class CompactVideoCubit extends Cubit { state.offlineVideoThumbnailPath = path; emit(state); } else { - EasyDebounce.debounce('${state.offlineVideo?.videoId}-compact-view-thumbnail', const Duration(seconds: 1), getThumbnail); + EasyDebounce.debounce( + '${state.offlineVideo?.videoId}-compact-view-thumbnail', const Duration(seconds: 1), getThumbnail); } } catch (e) { - EasyDebounce.debounce('${state.offlineVideo?.videoId}-compact-view-thumbnail', const Duration(seconds: 1), getThumbnail); + EasyDebounce.debounce( + '${state.offlineVideo?.videoId}-compact-view-thumbnail', const Duration(seconds: 1), getThumbnail); } } } diff --git a/lib/videos/states/history.dart b/lib/videos/states/history.dart index 62c7c083..028cc93d 100644 --- a/lib/videos/states/history.dart +++ b/lib/videos/states/history.dart @@ -1,7 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:invidious/globals.dart'; -import 'package:invidious/utils/models/image_object.dart'; import 'package:invidious/utils/states/item_list.dart'; import '../models/db/history_video_cache.dart'; diff --git a/lib/videos/states/tv_video.dart b/lib/videos/states/tv_video.dart index cc0b5c8b..257fa2b2 100644 --- a/lib/videos/states/tv_video.dart +++ b/lib/videos/states/tv_video.dart @@ -37,5 +37,6 @@ class TvVideoState { TvVideoState({ScrollController? scrollController}) : scrollController = scrollController ?? ScrollController(); - TvVideoState._(ScrollController? scrollController, this.showImage) : scrollController = scrollController ?? ScrollController(); + TvVideoState._(ScrollController? scrollController, this.showImage) + : scrollController = scrollController ?? ScrollController(); } diff --git a/lib/videos/states/video.dart b/lib/videos/states/video.dart index ce15eabb..899ed13e 100644 --- a/lib/videos/states/video.dart +++ b/lib/videos/states/video.dart @@ -163,6 +163,6 @@ class VideoState { } } - VideoState._(this.scrollController, this.video, this.dislikes, this.loadingVideo, this.selectedIndex, this.videoId, this.isLoggedIn, this.downloading, this.downloadProgress, this.downloadedVideo, - this.opacity, this.error); + VideoState._(this.scrollController, this.video, this.dislikes, this.loadingVideo, this.selectedIndex, this.videoId, + this.isLoggedIn, this.downloading, this.downloadProgress, this.downloadedVideo, this.opacity, this.error); } diff --git a/lib/videos/states/video_in_list.dart b/lib/videos/states/video_in_list.dart index 9e9d62ce..83dffbd5 100644 --- a/lib/videos/states/video_in_list.dart +++ b/lib/videos/states/video_in_list.dart @@ -45,7 +45,8 @@ class VideoInListState { BaseVideo? video; DownloadedVideo? offlineVideo; - VideoInListState({this.video, this.offlineVideo}) : assert(video == null || offlineVideo == null, 'cannot provide both video and offline video\n'); + VideoInListState({this.video, this.offlineVideo}) + : assert(video == null || offlineVideo == null, 'cannot provide both video and offline video\n'); VideoInListState._(this.progress, this.video, this.offlineVideo); } diff --git a/lib/videos/states/video_like.dart b/lib/videos/states/video_like.dart deleted file mode 100644 index 6ff9b82a..00000000 --- a/lib/videos/states/video_like.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:invidious/extensions.dart'; -import 'package:invidious/playlists/models/playlist.dart'; -import 'package:logging/logging.dart'; - -import '../../globals.dart'; -import '../models/video_in_list.dart'; -import 'add_to_playlist_button.dart'; - -part 'video_like.g.dart'; - -const String likePlaylistName = '❤️'; - -final log = Logger('VideoLikeButtonController'); - -class VideoLikeButtonCubit extends Cubit { - final AddToPlaylistButtonCubit addToPlaylistButtonCubit; - - //TODO: need add to playlistbutton cubit - VideoLikeButtonCubit(super.initialState, {required this.addToPlaylistButtonCubit}) { - onReady(); - } - - onReady() { - checkVideoLikeStatus(); - } - - Future getPlaylist() async { - List playlists = await service.getUserPlaylists(); - Playlist? pl = playlists.firstWhereOrNull((pl) => pl.title == likePlaylistName); - - return pl; - } - - checkVideoLikeStatus() async { - var state = this.state.copyWith(); - Playlist? p = await getPlaylist(); - VideoInList? video = p?.videos.firstWhereOrNull((element) => element.videoId == state.videoId); - - state.isVideoLiked = video != null; - - emit(state); - - log.fine('video is currently liked ? $state.isVideoLiked'); - } - - addVideoToPlaylist() {} - - Future createPlayList() async { - await service.createPlayList(likePlaylistName, "private"); - return getPlaylist(); - } - - toggleLike() async { - var state = this.state.copyWith(); - state.loading = true; - emit(state); - - state = this.state.copyWith(); - await checkVideoLikeStatus(); - Playlist? p = await getPlaylist(); - p ??= await createPlayList(); - - if (p != null && state.videoId != null) { - if (state.isVideoLiked) { - log.fine('Video is liked, unliking it'); - VideoInList? v = p.videos.firstWhereOrNull((element) => element.videoId == state.videoId!); - if (v?.indexId != null) { - await service.deleteUserPlaylistVideo(p.playlistId, v!.indexId!); - state.isVideoLiked = !state.isVideoLiked; - } - } else { - log.fine('Video is not liked yet, we add it to the like playlist'); - await service.addVideoToPlaylist(p.playlistId, state.videoId!); - state.isVideoLiked = !state.isVideoLiked; - } - } - state.loading = false; - emit(state); - - //TODO: Updat app playlistbutton cubit - addToPlaylistButtonCubit.countPlaylistsForVideo(); - } -} - -@CopyWith(constructor: "_") -class VideoLikeButtonController { - bool isLoggedIn = service.isLoggedIn(); - final String? videoId; - bool isVideoLiked = false; - bool loading = false; - - VideoLikeButtonController({this.videoId}); - - VideoLikeButtonController._(this.isLoggedIn, this.videoId, this.isVideoLiked, this.loading); -} diff --git a/lib/videos/states/video_like.g.dart b/lib/videos/states/video_like.g.dart deleted file mode 100644 index ae0304ba..00000000 --- a/lib/videos/states/video_like.g.dart +++ /dev/null @@ -1,93 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'video_like.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$VideoLikeButtonControllerCWProxy { - VideoLikeButtonController isLoggedIn(bool isLoggedIn); - - VideoLikeButtonController videoId(String? videoId); - - VideoLikeButtonController isVideoLiked(bool isVideoLiked); - - VideoLikeButtonController 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 `VideoLikeButtonController(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// VideoLikeButtonController(...).copyWith(id: 12, name: "My name") - /// ```` - VideoLikeButtonController call({ - bool? isLoggedIn, - String? videoId, - bool? isVideoLiked, - bool? loading, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfVideoLikeButtonController.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfVideoLikeButtonController.copyWith.fieldName(...)` -class _$VideoLikeButtonControllerCWProxyImpl - implements _$VideoLikeButtonControllerCWProxy { - const _$VideoLikeButtonControllerCWProxyImpl(this._value); - - final VideoLikeButtonController _value; - - @override - VideoLikeButtonController isLoggedIn(bool isLoggedIn) => - this(isLoggedIn: isLoggedIn); - - @override - VideoLikeButtonController videoId(String? videoId) => this(videoId: videoId); - - @override - VideoLikeButtonController isVideoLiked(bool isVideoLiked) => - this(isVideoLiked: isVideoLiked); - - @override - VideoLikeButtonController 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 `VideoLikeButtonController(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// VideoLikeButtonController(...).copyWith(id: 12, name: "My name") - /// ```` - VideoLikeButtonController call({ - Object? isLoggedIn = const $CopyWithPlaceholder(), - Object? videoId = const $CopyWithPlaceholder(), - Object? isVideoLiked = const $CopyWithPlaceholder(), - Object? loading = const $CopyWithPlaceholder(), - }) { - return VideoLikeButtonController._( - isLoggedIn == const $CopyWithPlaceholder() || isLoggedIn == null - ? _value.isLoggedIn - // ignore: cast_nullable_to_non_nullable - : isLoggedIn as bool, - videoId == const $CopyWithPlaceholder() - ? _value.videoId - // ignore: cast_nullable_to_non_nullable - : videoId as String?, - isVideoLiked == const $CopyWithPlaceholder() || isVideoLiked == null - ? _value.isVideoLiked - // ignore: cast_nullable_to_non_nullable - : isVideoLiked as bool, - loading == const $CopyWithPlaceholder() || loading == null - ? _value.loading - // ignore: cast_nullable_to_non_nullable - : loading as bool, - ); - } -} - -extension $VideoLikeButtonControllerCopyWith on VideoLikeButtonController { - /// Returns a callable class that can be used as follows: `instanceOfVideoLikeButtonController.copyWith(...)` or like so:`instanceOfVideoLikeButtonController.copyWith.fieldName(...)`. - // ignore: library_private_types_in_public_api - _$VideoLikeButtonControllerCWProxy get copyWith => - _$VideoLikeButtonControllerCWProxyImpl(this); -} diff --git a/lib/videos/views/components/add_to_playlist.dart b/lib/videos/views/components/add_to_playlist.dart deleted file mode 100644 index e86fdb44..00000000 --- a/lib/videos/views/components/add_to_playlist.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:invidious/globals.dart'; -import 'package:invidious/main.dart'; -import 'package:invidious/myRouteObserver.dart'; -import 'package:invidious/videos/states/add_to_playlist.dart'; -import 'package:invidious/videos/states/add_to_playlist_button.dart'; -import 'package:invidious/videos/states/video_like.dart'; -import 'package:logging/logging.dart'; - -import '../../../playlists/views/components/add_to_playlist_list.dart'; -import '../../../settings/views/screens/manage_single_server.dart'; - -final log = Logger('AddToPlaylistView'); - -class AddToPlaylist extends StatelessWidget { - final String videoId; - final VideoLikeButtonCubit? videoLikeButtonCubit; - final AddToPlaylistButtonCubit? addToPlaylistButtonCubit; - - const AddToPlaylist({super.key, required this.videoId, this.videoLikeButtonCubit, this.addToPlaylistButtonCubit}); - - static showAddToPlaylistDialog(BuildContext context, String videoId) { - VideoLikeButtonCubit? likeButtonCubit; - AddToPlaylistButtonCubit? playlistButtonCubit; - try { - likeButtonCubit = context.read(); - playlistButtonCubit = context.read(); - } catch (err) { - log.fine('We can\'t fine the providers but it\'s probably ok'); - } - showModalBottomSheet( - showDragHandle: true, - context: context, - builder: (BuildContext context) { - return AddToPlaylist( - videoId: videoId, - videoLikeButtonCubit: likeButtonCubit, - addToPlaylistButtonCubit: playlistButtonCubit, - ); - }); - } - - addToPlaylist(BuildContext context, String playlistId, AddToPlaylistCubit cubit) async { - var locals = AppLocalizations.of(context)!; - final scaffoldMessenger = ScaffoldMessenger.of(context); - try { - await cubit.addToPlaylist(playlistId); - scaffoldMessenger.showSnackBar(SnackBar( - content: Text(locals.videoAddedToPlaylist), - duration: const Duration(seconds: 3), - )); - } catch (err) { - scaffoldMessenger.showSnackBar(SnackBar( - content: Text(locals.errorAddingVideoToPlaylist), - duration: const Duration(seconds: 3), - )); - rethrow; - } - - if (context.mounted) { - navigatorKey.currentState?.pop(); - } - } - - newPlaylistAndAdd(BuildContext context, AddToPlaylistController controller, AddToPlaylistCubit cubit) { - showDialog( - context: context, - builder: (BuildContext context) => Dialog( - child: AddPlayListForm(afterAdd: (playlistId) => addToPlaylist(context, playlistId, cubit)), - )); - } - - openServerSettings(BuildContext context) { - navigatorKey.currentState?.push(MaterialPageRoute(settings: ROUTE_SETTINGS_MANAGE_ONE_SERVER, builder: (context) => ManageSingleServer(server: db.getCurrentlySelectedServer()))); - } - - @override - Widget build(BuildContext context) { - var locals = AppLocalizations.of(context)!; - - return BlocProvider( - create: (context) => AddToPlaylistCubit(AddToPlaylistController(videoId), videoLikeButtonCubit: videoLikeButtonCubit, addToPlaylistButtonCubit: addToPlaylistButtonCubit), - child: BlocBuilder( - builder: (context, _) { - var cubit = context.read(); - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Text(locals.selectPlaylist), - !_.isLoggedIn - ? Expanded( - child: Align( - alignment: Alignment.center, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(locals.notLoggedIn), - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: FilledButton(onPressed: () => openServerSettings(context), child: Text(locals.logIn)), - ) - ], - ))) - : const SizedBox.shrink(), - _.loading ? const Expanded(child: Align(alignment: Alignment.center, child: CircularProgressIndicator())) : const SizedBox.shrink(), - Expanded( - child: ListView( - children: _.playlists.map((p) { - bool inPlaylist = cubit.videoInPlaylist(p.playlistId); - return FilledButton.tonal( - onPressed: inPlaylist ? null : () => addToPlaylist(context, p.playlistId, cubit), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - width: 20, - child: inPlaylist - ? const Icon( - Icons.check, - size: 15, - ) - : const SizedBox.shrink()), - ), - Expanded(child: Text(p.title)), - ], - )); - }).toList(), - ), - ), - FilledButton.tonal( - onPressed: _.isLoggedIn ? () => newPlaylistAndAdd(context, _, cubit) : null, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [const Icon(Icons.add), Text(locals.createNewPlaylist)], - ), - ) - ]), - ); - }, - ), - ); - } -} diff --git a/lib/videos/views/components/add_to_playlist_button.dart b/lib/videos/views/components/add_to_playlist_button.dart index e1b6bf31..2996a76e 100644 --- a/lib/videos/views/components/add_to_playlist_button.dart +++ b/lib/videos/views/components/add_to_playlist_button.dart @@ -1,50 +1,94 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:invidious/videos/states/add_to_playlist_button.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/videos/states/add_to_playlist.dart'; -import 'add_to_playlist.dart'; +import 'add_to_playlist_dialog.dart'; -class VideoAddToPlaylistButton extends StatelessWidget { - String? videoId; +enum AddToPlayListButtonType { + appBar, + modalSheet; +} + +class AddToPlayListButton extends StatelessWidget { + final String videoId; + final AddToPlayListButtonType type; + final Function? afterAdd; - VideoAddToPlaylistButton({Key? key, this.videoId}) : super(key: key); + const AddToPlayListButton( + {super.key, required this.videoId, this.type = AddToPlayListButtonType.appBar, this.afterAdd}); + + showAddToPlaylistDialog(BuildContext context) { + var cubit = context.read(); + AddToPlaylistDialog.showAddToPlaylistDialog(context, playlists: cubit.state.playlists, videoId: videoId, + onAdd: (selectedPlaylistId) async { + await cubit.saveVideoToPlaylist(selectedPlaylistId); + if (afterAdd != null) { + afterAdd!(); + } + }); + } @override Widget build(BuildContext context) { - var textTheme = Theme.of(context).textTheme; var colors = Theme.of(context).colorScheme; - return BlocBuilder( - builder: (context, _) => Visibility( - visible: _.isLoggedIn, - child: Stack( - children: [ - IconButton( - style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)), - onPressed: () => AddToPlaylist.showAddToPlaylistDialog(context, _.videoId!), - icon: const Icon( - Icons.add, + var textTheme = Theme.of(context).textTheme; + var locals = AppLocalizations.of(context)!; + + return BlocProvider( + create: (BuildContext context) => AddToPlaylistCubit(AddToPlaylistController(videoId)), + child: BlocBuilder(builder: (context, _) { + var cubit = context.read(); + return switch (type) { + (AddToPlayListButtonType.modalSheet) => Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton.filledTonal( + onPressed: () => showAddToPlaylistDialog(context), icon: const Icon(Icons.playlist_add)), + Text(locals.addToPlaylist) + ], ), ), - _.playListCount > 0 - ? Positioned( - top: 1, - right: 1, - child: GestureDetector( - onTap: () => AddToPlaylist.showAddToPlaylistDialog(context, _.videoId!), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration(color: colors.secondaryContainer, shape: BoxShape.circle), - child: Text( - _.playListCount.toString(), - style: textTheme.labelSmall, - ), + (AddToPlayListButtonType.appBar) => Row( + children: [ + IconButton( + onPressed: cubit.toggleLike, + icon: _.isVideoLiked ? const Icon(Icons.favorite) : const Icon(Icons.favorite_border), + ), + Stack( + children: [ + IconButton( + style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)), + onPressed: () => showAddToPlaylistDialog(context), + icon: const Icon( + Icons.add, ), ), - ) - : const SizedBox.shrink() - ], - ), - ), + _.playListCount > 0 + ? Positioned( + top: 1, + right: 1, + child: GestureDetector( + onTap: () => showAddToPlaylistDialog(context), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration(color: colors.secondaryContainer, shape: BoxShape.circle), + child: Text( + _.playListCount.toString(), + style: textTheme.labelSmall, + ), + ), + ), + ) + : const SizedBox.shrink() + ], + ) + ], + ) + }; + }), ); } } diff --git a/lib/videos/views/components/add_to_playlist_dialog.dart b/lib/videos/views/components/add_to_playlist_dialog.dart new file mode 100644 index 00000000..36e19f2f --- /dev/null +++ b/lib/videos/views/components/add_to_playlist_dialog.dart @@ -0,0 +1,132 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/app/states/app.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/router.dart'; +import 'package:logging/logging.dart'; + +import '../../../playlists/models/playlist.dart'; +import '../../../playlists/views/components/add_to_playlist_list.dart'; + +final log = Logger('AddToPlaylistView'); + +class AddToPlaylistDialog extends StatelessWidget { + final String videoId; + final List playlists; + final Function(String selectedPlaylistId) onAdd; + + const AddToPlaylistDialog({super.key, required this.videoId, required this.playlists, required this.onAdd}); + + static showAddToPlaylistDialog(BuildContext context, + {required String videoId, + required List playlists, + required Function(String selectedPlaylistId) onAdd}) { + showModalBottomSheet( + showDragHandle: true, + isScrollControlled: true, + useSafeArea: true, + context: context, + builder: (BuildContext context) { + return AddToPlaylistDialog( + videoId: videoId, + playlists: playlists, + onAdd: onAdd, + ); + }); + } + + addToPlaylist(BuildContext context, String playlistId) async { + var locals = AppLocalizations.of(context)!; + final scaffoldMessenger = ScaffoldMessenger.of(context); + try { + onAdd(playlistId); + scaffoldMessenger.showSnackBar(SnackBar( + content: Text(locals.videoAddedToPlaylist), + duration: const Duration(seconds: 3), + )); + + if (context.mounted) { + Navigator.pop(context); + } + } catch (err) { + scaffoldMessenger.showSnackBar(SnackBar( + content: Text(locals.errorAddingVideoToPlaylist), + duration: const Duration(seconds: 3), + )); + rethrow; + } + + } + + newPlaylistAndAdd(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext ctx) => Dialog( + child: AddPlayListForm(afterAdd: (playlistId) => addToPlaylist(context, playlistId)), + )); + } + + openServerSettings(BuildContext context) { + AutoRouter.of(context).push(ManageSingleServerRoute(server: db.getCurrentlySelectedServer())); + } + + @override + Widget build(BuildContext context) { + var locals = AppLocalizations.of(context)!; + var app = context.read(); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Text(locals.selectPlaylist), + !app.isLoggedIn + ? Expanded( + child: Align( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(locals.notLoggedIn), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: FilledButton(onPressed: () => openServerSettings(context), child: Text(locals.logIn)), + ) + ], + ))) + : Expanded( + child: ListView( + children: playlists.map((p) { + bool inPlaylist = p.videos.any((element) => element.videoId == videoId); + return FilledButton.tonal( + onPressed: inPlaylist ? null : () => addToPlaylist(context, p.playlistId), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 20, + child: inPlaylist + ? const Icon( + Icons.check, + size: 15, + ) + : const SizedBox.shrink()), + ), + Expanded(child: Text(p.title)), + ], + )); + }).toList(), + ), + ), + FilledButton.tonal( + onPressed: app.isLoggedIn ? () => newPlaylistAndAdd(context) : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [const Icon(Icons.add), Text(locals.createNewPlaylist)], + ), + ) + ]), + ); + } +} diff --git a/lib/videos/views/components/add_to_queue_button.dart b/lib/videos/views/components/add_to_queue_button.dart index b523b8c9..8370cddd 100644 --- a/lib/videos/views/components/add_to_queue_button.dart +++ b/lib/videos/views/components/add_to_queue_button.dart @@ -12,7 +12,10 @@ class AddToQueueButton extends StatelessWidget { static bool canAddToQueue(BuildContext context, List videos) { var state = context.read().state; - return (state.videos.isNotEmpty) && (videos.length > 1 || (videos.length == 1 && (state.videos.indexWhere((element) => element.videoId == videos[0].videoId) ?? -1) < 0)); + return (state.videos.isNotEmpty) && + (videos.length > 1 || + (videos.length == 1 && + (state.videos.indexWhere((element) => element.videoId == videos[0].videoId) ?? -1) < 0)); } addToQueue(BuildContext context) { diff --git a/lib/videos/views/components/compact_video.dart b/lib/videos/views/components/compact_video.dart index 86c8a3b8..2097ea7b 100644 --- a/lib/videos/views/components/compact_video.dart +++ b/lib/videos/views/components/compact_video.dart @@ -53,7 +53,9 @@ class CompactVideo extends StatelessWidget { widthFactor: (highlighted ?? false) ? 1 : 0, curve: Curves.easeInOutQuad, child: AnimatedContainer( - decoration: BoxDecoration(color: (highlighted ?? false) ? colors.secondaryContainer : colors.background, borderRadius: BorderRadius.circular(10)), + decoration: BoxDecoration( + color: (highlighted ?? false) ? colors.secondaryContainer : colors.background, + borderRadius: BorderRadius.circular(10)), duration: animationDuration * 2, ), ), @@ -69,12 +71,17 @@ class CompactVideo extends StatelessWidget { ? AspectRatio( aspectRatio: 16 / 9, child: Container( - decoration: BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(10)), - child: Icon(Icons.visibility_off_outlined, color: colors.secondary.withOpacity(0.7), size: 15), + decoration: BoxDecoration( + color: colors.secondaryContainer, borderRadius: BorderRadius.circular(10)), + child: Icon(Icons.visibility_off_outlined, + color: colors.secondary.withOpacity(0.7), size: 15), ), ) : video != null - ? VideoThumbnailView(cacheKey: 'v-worst/${videoId}', videoId: videoId, thumbnailUrl: ImageObject.getWorstThumbnail(video?.videoThumbnails)?.url ?? '') + ? VideoThumbnailView( + cacheKey: 'v-worst/${videoId}', + videoId: videoId, + thumbnailUrl: ImageObject.getWorstThumbnail(video?.videoThumbnails)?.url ?? '') : offlineVideo != null ? OfflineVideoThumbnail( video: offlineVideo!, diff --git a/lib/videos/views/components/download_modal_sheet.dart b/lib/videos/views/components/download_modal_sheet.dart index d80d35cd..1131588b 100644 --- a/lib/videos/views/components/download_modal_sheet.dart +++ b/lib/videos/views/components/download_modal_sheet.dart @@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/main.dart'; import 'package:invidious/videos/states/download_modal_sheet.dart'; -import 'package:invidious/videos/views/components/video_in_list.dart'; import '../../../downloads/states/download_manager.dart'; import '../../models/base_video.dart'; @@ -21,7 +20,8 @@ class DownloadModalSheet extends StatelessWidget { const DownloadModalSheet({Key? key, required this.video, this.onDownloadStarted, this.onDownload}) : super(key: key); - static showVideoModalSheet(BuildContext context, BaseVideo video, {Function(bool isDownloadStarted)? onDownloadStarted, Function()? onDownload}) { + static showVideoModalSheet(BuildContext context, BaseVideo video, + {Function(bool isDownloadStarted)? onDownloadStarted, Function()? onDownload}) { showModalBottomSheet( enableDrag: true, showDragHandle: true, @@ -44,7 +44,8 @@ class DownloadModalSheet extends StatelessWidget { Navigator.of(context).pop(); var downloadController = context.read(); scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text(locals.videoDownloadStarted))); - bool canDownload = await downloadManager.addDownload(video.videoId, audioOnly: _.audioOnly, quality: _.quality) ?? false; + bool canDownload = + await downloadManager.addDownload(video.videoId, audioOnly: _.audioOnly, quality: _.quality) ?? false; if (!canDownload) { scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text(locals.videoAlreadyDownloaded))); } diff --git a/lib/videos/views/components/history.dart b/lib/videos/views/components/history.dart index ddca0185..34617d1e 100644 --- a/lib/videos/views/components/history.dart +++ b/lib/videos/views/components/history.dart @@ -22,7 +22,9 @@ class HistoryView extends StatelessWidget { var locals = AppLocalizations.of(context)!; return MultiBlocProvider( providers: [ - BlocProvider(create: (BuildContext context) => ItemListCubit(ItemListState(itemList: PageBasedPaginatedList(getItemsFunc: service.getUserHistory, maxResults: 20)))), + BlocProvider( + create: (BuildContext context) => ItemListCubit(ItemListState( + itemList: PageBasedPaginatedList(getItemsFunc: service.getUserHistory, maxResults: 20)))), BlocProvider( create: (context) => HistoryCubit(null, context.read>()), ) @@ -37,7 +39,10 @@ class HistoryView extends StatelessWidget { ? Center( child: Padding( padding: const EdgeInsets.all(8.0), - child: Text(switch (_.error) { ItemListErrors.invalidScope => locals.itemListErrorInvalidScope, _ => locals.itemlistErrorGeneric }), + child: Text(switch (_.error) { + ItemListErrors.invalidScope => locals.itemListErrorInvalidScope, + _ => locals.itemlistErrorGeneric + }), )) : !_.loading && _.items.isEmpty ? Center( @@ -84,7 +89,8 @@ class HistoryView extends StatelessWidget { right: 15, child: FloatingActionButton( onPressed: () { - okCancelDialog(context, locals.clearHistoryQuestion, locals.clearHistoryQuestionExplanation, () => historyCubit.clearHistory()); + okCancelDialog(context, locals.clearHistoryQuestion, locals.clearHistoryQuestionExplanation, + () => historyCubit.clearHistory()); }, child: const Icon(Icons.delete), )) diff --git a/lib/videos/views/components/historyVideo.dart b/lib/videos/views/components/historyVideo.dart index 54953640..576f9712 100644 --- a/lib/videos/views/components/historyVideo.dart +++ b/lib/videos/views/components/historyVideo.dart @@ -1,13 +1,9 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:invidious/globals.dart'; -import 'package:invidious/main.dart'; -import 'package:invidious/myRouteObserver.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/videos/views/components/compact_video.dart'; -import 'package:invidious/videos/views/components/history.dart'; -import 'package:invidious/videos/views/components/video_in_list.dart'; -import 'package:invidious/videos/views/components/video_list.dart'; -import 'package:invidious/videos/views/screens/video.dart'; import '../../../utils/views/components/placeholders.dart'; import '../../states/history.dart'; @@ -26,7 +22,7 @@ class HistoryVideoView extends StatelessWidget { firstChild: const CompactVideoPlaceHolder(), secondChild: _.cachedVid != null ? CompactVideo( - onTap: () => navigatorKey.currentState?.pushNamed(PATH_VIDEO, arguments: VideoRouteArguments(videoId: _.cachedVid!.videoId)), + onTap: () => AutoRouter.of(context).push(VideoRoute(videoId: _.cachedVid!.videoId)), video: _.cachedVid?.toBaseVideo(), ) : const CompactVideoPlaceHolder(), diff --git a/lib/videos/views/components/info.dart b/lib/videos/views/components/info.dart index a0ec7093..b736b882 100644 --- a/lib/videos/views/components/info.dart +++ b/lib/videos/views/components/info.dart @@ -1,15 +1,14 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:invidious/main.dart'; import 'package:invidious/player/states/player.dart'; -import 'package:invidious/search/views/screens/search.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/utils/views/components/text_linkified.dart'; import 'package:invidious/videos/models/video.dart'; import 'package:invidious/videos/views/components/video_metrics.dart'; import 'package:invidious/videos/views/components/video_thumbnail.dart'; -import '../../../myRouteObserver.dart'; import '../../../subscription_management/view/components/subscribeButton.dart'; import '../../../utils/models/image_object.dart'; @@ -20,16 +19,14 @@ class VideoInfo extends StatelessWidget { const VideoInfo({super.key, required this.video, this.dislikes}); openChannel(BuildContext context) { - navigatorKey.currentState?.pushNamed(PATH_CHANNEL, arguments: video.authorId); + AutoRouter.of(context).push(ChannelRoute(channelId: video.authorId!)); } showSearchWindow(BuildContext context, String query) { - navigatorKey.currentState?.push(MaterialPageRoute( - settings: ROUTE_CHANNEL, - builder: (context) => Search( - query: query, - searchNow: true, - ))); + AutoRouter.of(context).push(SearchRoute( + query: query, + searchNow: true, + )); } @override @@ -139,7 +136,8 @@ class VideoInfo extends StatelessWidget { .map((e) => InkWell( onTap: () => showSearchWindow(context, e), child: Container( - decoration: BoxDecoration(color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(20)), + decoration: BoxDecoration( + color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), child: Text(e), @@ -152,7 +150,8 @@ class VideoInfo extends StatelessWidget { InkWell( onTap: () => showSearchWindow(context, video.genre), child: Container( - decoration: BoxDecoration(color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(20)), + decoration: BoxDecoration( + color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), child: Text(video.genre), diff --git a/lib/videos/views/components/inner_view_tablet.dart b/lib/videos/views/components/inner_view_tablet.dart index a39a5df6..e9be91de 100644 --- a/lib/videos/views/components/inner_view_tablet.dart +++ b/lib/videos/views/components/inner_view_tablet.dart @@ -20,7 +20,8 @@ class VideoTabletInnerView extends StatelessWidget { final bool? playNow; final VideoState videoController; - const VideoTabletInnerView({super.key, required this.video, required this.selectedIndex, this.playNow, required this.videoController}); + const VideoTabletInnerView( + {super.key, required this.video, required this.selectedIndex, this.playNow, required this.videoController}); @override Widget build(BuildContext context) { @@ -66,7 +67,12 @@ class VideoTabletInnerView extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - SizedBox(height: 25, child: Checkbox(value: settings.state.playRecommendedNext, onChanged: cubit.togglePlayRecommendedNext, visualDensity: VisualDensity.compact)), + SizedBox( + height: 25, + child: Checkbox( + value: settings.state.playRecommendedNext, + onChanged: cubit.togglePlayRecommendedNext, + visualDensity: VisualDensity.compact)), InkWell( onTap: () => cubit.togglePlayRecommendedNext(!settings.state.playRecommendedNext), child: Text( @@ -104,7 +110,8 @@ class VideoTabletInnerView extends StatelessWidget { ), ), ), - if (!settings.state.distractionFreeMode) SizedBox(width: 350, child: SingleChildScrollView(child: RecommendedVideos(video: video))) + if (!settings.state.distractionFreeMode) + SizedBox(width: 350, child: SingleChildScrollView(child: RecommendedVideos(video: video))) ], ); } diff --git a/lib/videos/views/components/innter_view.dart b/lib/videos/views/components/innter_view.dart index b7eed603..ac6e14bc 100644 --- a/lib/videos/views/components/innter_view.dart +++ b/lib/videos/views/components/innter_view.dart @@ -19,7 +19,8 @@ class VideoInnerView extends StatelessWidget { bool? playNow; final VideoState videoController; - VideoInnerView({super.key, required this.video, required this.selectedIndex, this.playNow, required this.videoController}); + VideoInnerView( + {super.key, required this.video, required this.selectedIndex, this.playNow, required this.videoController}); @override Widget build(BuildContext context) { @@ -49,12 +50,17 @@ class VideoInnerView extends StatelessWidget { ], ), ), - if(!settings.state.distractionFreeMode)Builder( - builder: (context) { + if (!settings.state.distractionFreeMode) + Builder(builder: (context) { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - SizedBox(height: 25, child: Checkbox(value: settings.state.playRecommendedNext, onChanged: cubit.togglePlayRecommendedNext, visualDensity: VisualDensity.compact)), + SizedBox( + height: 25, + child: Checkbox( + value: settings.state.playRecommendedNext, + onChanged: cubit.togglePlayRecommendedNext, + visualDensity: VisualDensity.compact)), InkWell( onTap: () => cubit.togglePlayRecommendedNext(!settings.state.playRecommendedNext), child: Text( @@ -63,8 +69,7 @@ class VideoInnerView extends StatelessWidget { )) ], ); - } - ), + }), Expanded( child: Padding( padding: const EdgeInsets.only(top: 0), diff --git a/lib/videos/views/components/like_button.dart b/lib/videos/views/components/like_button.dart deleted file mode 100644 index 9944ae00..00000000 --- a/lib/videos/views/components/like_button.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:invidious/videos/states/video_like.dart'; - -class VideoLikeButton extends StatelessWidget { - final String? videoId; - final double? size; - final ButtonStyle? style; - final bool? global; - - const VideoLikeButton({Key? key, this.videoId, this.size, this.style, this.global}) : super(key: key); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, _) => Visibility( - visible: _.videoId != null && _.isLoggedIn, - child: IconButton( - style: style, - iconSize: size, - onPressed: context.read().toggleLike, - icon: _.isVideoLiked ? const Icon(Icons.favorite) : const Icon(Icons.favorite_border), - ), - ), - ); - } -} diff --git a/lib/videos/views/components/offline_video_thumbnail.dart b/lib/videos/views/components/offline_video_thumbnail.dart index 038cca76..6ab3d89e 100644 --- a/lib/videos/views/components/offline_video_thumbnail.dart +++ b/lib/videos/views/components/offline_video_thumbnail.dart @@ -40,7 +40,8 @@ class OfflineVideoThumbnail extends StatelessWidget { fit: BoxFit.contain, )) : Container( - decoration: BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(borderRadius)), + decoration: BoxDecoration( + color: colors.secondaryContainer, borderRadius: BorderRadius.circular(borderRadius)), ), ); }); diff --git a/lib/videos/views/components/play_button.dart b/lib/videos/views/components/play_button.dart index 93a6153c..4d7bb1d5 100644 --- a/lib/videos/views/components/play_button.dart +++ b/lib/videos/views/components/play_button.dart @@ -15,7 +15,8 @@ class PlayButton extends StatelessWidget { padding: const EdgeInsets.only(left: 100.0, top: 60), child: IconButton( onPressed: () => onPressed(true), - style: ButtonStyle(backgroundColor: MaterialStateColor.resolveWith((states) => colorScheme.primary.withOpacity(1))), + style: ButtonStyle( + backgroundColor: MaterialStateColor.resolveWith((states) => colorScheme.primary.withOpacity(1))), icon: const Icon( Icons.music_note, size: 35, @@ -25,7 +26,8 @@ class PlayButton extends StatelessWidget { ), IconButton( onPressed: () => onPressed(false), - style: ButtonStyle(backgroundColor: MaterialStateColor.resolveWith((states) => colorScheme.primaryContainer.withOpacity(1))), + style: ButtonStyle( + backgroundColor: MaterialStateColor.resolveWith((states) => colorScheme.primaryContainer.withOpacity(1))), icon: const Icon( Icons.play_arrow, size: 75, diff --git a/lib/videos/views/components/subscriptions.dart b/lib/videos/views/components/subscriptions.dart index d1be8dc7..46a68713 100644 --- a/lib/videos/views/components/subscriptions.dart +++ b/lib/videos/views/components/subscriptions.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:invidious/main.dart'; import 'package:invidious/utils/views/tv/components/tv_horizontal_item_list.dart'; -import 'package:invidious/videos/views/components/video_in_list.dart'; import '../../../utils/models/paginatedList.dart'; import '../../models/video_in_list.dart'; diff --git a/lib/videos/views/components/trending.dart b/lib/videos/views/components/trending.dart index d97e2e16..5459aa34 100644 --- a/lib/videos/views/components/trending.dart +++ b/lib/videos/views/components/trending.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:invidious/globals.dart'; import 'package:invidious/utils/views/tv/components/tv_horizontal_item_list.dart'; -import 'package:invidious/videos/views/components/video_in_list.dart'; import '../../../main.dart'; import '../../../utils/models/paginatedList.dart'; diff --git a/lib/videos/views/components/video_in_list.dart b/lib/videos/views/components/video_in_list.dart index 563fd622..a3e5b118 100644 --- a/lib/videos/views/components/video_in_list.dart +++ b/lib/videos/views/components/video_in_list.dart @@ -1,10 +1,10 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/globals.dart'; -import 'package:invidious/main.dart'; -import 'package:invidious/myRouteObserver.dart'; import 'package:invidious/player/states/player.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/videos/models/video_in_list.dart'; import 'package:invidious/videos/states/video_in_list.dart'; import 'package:invidious/videos/views/components/offline_video_thumbnail.dart'; @@ -16,7 +16,6 @@ import '../../../downloads/models/downloaded_video.dart'; import '../../../downloads/states/download_manager.dart'; import '../../../utils.dart'; import '../../../utils/models/image_object.dart'; -import '../screens/video.dart'; import 'video_metrics.dart'; final log = Logger('VideoInList'); @@ -35,7 +34,7 @@ class VideoListItem extends StatelessWidget { if (cubit.state.video!.filtered) { cubit.showVideoDetails(); } else { - navigatorKey.currentState?.push(MaterialPageRoute(settings: ROUTE_VIDEO, builder: (context) => VideoView(videoId: video!.videoId))); + AutoRouter.of(context).push(VideoRoute(videoId: video!.videoId)); } } else if (offlineVideo != null) { context.read().playOfflineVideos([offlineVideo!]); @@ -49,7 +48,8 @@ class VideoListItem extends StatelessWidget { var textTheme = Theme.of(context).textTheme; - TextStyle filterStyle = (textTheme.bodySmall ?? const TextStyle()).copyWith(color: colorScheme.secondary.withOpacity(0.7)); + TextStyle filterStyle = + (textTheme.bodySmall ?? const TextStyle()).copyWith(color: colorScheme.secondary.withOpacity(0.7)); var downloadManager = context.read(); String title = video?.title ?? offlineVideo?.title ?? ''; @@ -60,11 +60,16 @@ class VideoListItem extends StatelessWidget { create: (context) => VideoInListCubit(VideoInListState(video: video, offlineVideo: offlineVideo)), child: BlocBuilder( builder: (context, _) => BlocListener( - listenWhen: (previous, current) => _.video != null && current.currentlyPlaying?.videoId == video!.videoId && previous.position != current.position, + listenWhen: (previous, current) => + _.video != null && + current.currentlyPlaying?.videoId == video!.videoId && + previous.position != current.position, listener: (context, state) => context.read().updateProgress(), child: InkWell( onTap: () => openVideo(context), - onLongPress: _.video == null || _.video!.filtered ? null : () => VideoModalSheet.showVideoModalSheet(context, video!), + onLongPress: _.video == null || _.video!.filtered + ? null + : () => VideoModalSheet.showVideoModalSheet(context, video!), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -161,11 +166,15 @@ class VideoListItem extends StatelessWidget { child: Container( alignment: Alignment.center, height: 25, - decoration: BoxDecoration(color: Colors.black.withOpacity(0.75), borderRadius: BorderRadius.circular(5)), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.75), + borderRadius: BorderRadius.circular(5)), child: Padding( padding: const EdgeInsets.all(4.0), child: Text( - prettyDuration(Duration(seconds: video?.lengthSeconds ?? offlineVideo?.lengthSeconds ?? 0)), + prettyDuration(Duration( + seconds: + video?.lengthSeconds ?? offlineVideo?.lengthSeconds ?? 0)), style: textTheme.bodySmall?.copyWith(color: Colors.white), ), ), @@ -196,18 +205,21 @@ class VideoListItem extends StatelessWidget { textAlign: TextAlign.left, overflow: TextOverflow.ellipsis, maxLines: small ? 1 : 2, - style: (small ? textTheme.labelSmall : textTheme.bodyMedium)?.copyWith(color: colorScheme.primary, fontWeight: FontWeight.normal), + style: (small ? textTheme.labelSmall : textTheme.bodyMedium) + ?.copyWith(color: colorScheme.primary, fontWeight: FontWeight.normal), ), InkWell( onTap: () { - navigatorKey.currentState?.pushNamed(PATH_CHANNEL, arguments: video?.authorId ?? offlineVideo?.authorUrl ?? ''); + AutoRouter.of(context) + .push(ChannelRoute(channelId: video?.authorId ?? offlineVideo?.authorUrl ?? '')); }, child: Text( author, maxLines: 1, textAlign: TextAlign.left, overflow: TextOverflow.ellipsis, - style: (small ? textTheme.labelSmall : textTheme.bodyMedium)?.copyWith(color: colorScheme.secondary), + style: (small ? textTheme.labelSmall : textTheme.bodyMedium) + ?.copyWith(color: colorScheme.secondary), ), ), if (!small && video != null) @@ -222,7 +234,9 @@ class VideoListItem extends StatelessWidget { ), if (!small && video != null) InkWell( - onTap: (_.video?.filtered ?? true) ? null : () => VideoModalSheet.showVideoModalSheet(context, video!), + onTap: (_.video?.filtered ?? true) + ? null + : () => VideoModalSheet.showVideoModalSheet(context, video!), child: const Padding( padding: EdgeInsets.all(4), child: Icon(Icons.more_vert), diff --git a/lib/videos/views/components/video_list.dart b/lib/videos/views/components/video_list.dart index 467c87f5..92665ad3 100644 --- a/lib/videos/views/components/video_list.dart +++ b/lib/videos/views/components/video_list.dart @@ -23,7 +23,13 @@ class VideoList extends StatelessWidget { final Axis scrollDirection; final bool small; - const VideoList({super.key, required this.paginatedVideoList, this.tags, this.animateDownload = false, this.scrollDirection = Axis.vertical, this.small = false}); + const VideoList( + {super.key, + required this.paginatedVideoList, + this.tags, + this.animateDownload = false, + this.scrollDirection = Axis.vertical, + this.small = false}); /* @override @@ -71,7 +77,9 @@ class VideoList extends StatelessWidget { : Padding( padding: EdgeInsets.only(top: small ? 0.0 : 4.0), child: RefreshIndicator( - onRefresh: () async => !small && _.itemList.hasRefresh() ? await cubit.refreshItems() : Future.delayed(Duration.zero), + onRefresh: () async => !small && _.itemList.hasRefresh() + ? await cubit.refreshItems() + : Future.delayed(Duration.zero), child: GridView.count( crossAxisCount: gridCount, controller: _.scrollController, diff --git a/lib/videos/views/components/video_metrics.dart b/lib/videos/views/components/video_metrics.dart index bf6ce063..78ee5dd4 100644 --- a/lib/videos/views/components/video_metrics.dart +++ b/lib/videos/views/components/video_metrics.dart @@ -12,10 +12,24 @@ class VideoMetrics extends StatelessWidget { final int? lengthSeconds, viewCount, likeCount; final String? publishedText; - const VideoMetrics({super.key, this.video, this.dislikes, this.style, this.iconSize = 20, this.lengthSeconds, this.viewCount, this.likeCount, this.publishedText}) + const VideoMetrics( + {super.key, + this.video, + this.dislikes, + this.style, + this.iconSize = 20, + this.lengthSeconds, + this.viewCount, + this.likeCount, + this.publishedText}) : assert( - (video != null && lengthSeconds == null && viewCount == null && likeCount == null && publishedText == null) || - (video == null && (lengthSeconds != null || viewCount != null || likeCount != null || publishedText != null)), + (video != null && + lengthSeconds == null && + viewCount == null && + likeCount == null && + publishedText == null) || + (video == null && + (lengthSeconds != null || viewCount != null || likeCount != null || publishedText != null)), 'need either a video or given metrics'); Widget get separator => Padding( diff --git a/lib/videos/views/components/video_modal_sheet.dart b/lib/videos/views/components/video_modal_sheet.dart index 7e35a385..aa3bb666 100644 --- a/lib/videos/views/components/video_modal_sheet.dart +++ b/lib/videos/views/components/video_modal_sheet.dart @@ -3,11 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/player/states/player.dart'; import 'package:invidious/videos/models/base_video.dart'; +import 'package:invidious/videos/views/components/add_to_playlist_button.dart'; import 'package:invidious/videos/views/components/download_modal_sheet.dart'; -import 'package:invidious/videos/views/components/video_in_list.dart'; import '../../../main.dart'; -import 'add_to_playlist.dart'; import 'add_to_queue_button.dart'; class VideoModalSheet extends StatelessWidget { @@ -26,11 +25,6 @@ class VideoModalSheet extends StatelessWidget { }); } - void addToPlaylist(BuildContext context) { - Navigator.of(context).pop(); - AddToPlaylist.showAddToPlaylistDialog(context, video.videoId); - } - void playNext(BuildContext context) { var player = context.read(); var locals = AppLocalizations.of(context)!; @@ -72,19 +66,19 @@ class VideoModalSheet extends StatelessWidget { child: Wrap( alignment: WrapAlignment.center, children: [ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [IconButton.filledTonal(onPressed: () => addToPlaylist(context), icon: const Icon(Icons.playlist_add)), Text(locals.addToPlaylist)], - ), + AddToPlayListButton( + videoId: video.videoId, + type: AddToPlayListButtonType.modalSheet, + afterAdd: () => Navigator.pop(context), ), Padding( padding: const EdgeInsets.only(right: 16.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ - IconButton.filledTonal(onPressed: AddToQueueButton.canAddToQueue(context, [video]) ? () => addToQueue(context) : null, icon: const Icon(Icons.playlist_play)), + IconButton.filledTonal( + onPressed: AddToQueueButton.canAddToQueue(context, [video]) ? () => addToQueue(context) : null, + icon: const Icon(Icons.playlist_play)), Text(locals.addToQueueList) ], ), @@ -93,14 +87,20 @@ class VideoModalSheet extends StatelessWidget { padding: const EdgeInsets.only(right: 16.0), child: Column( mainAxisSize: MainAxisSize.min, - children: [IconButton.filledTonal(onPressed: () => playNext(context), icon: const Icon(Icons.play_arrow)), Text(locals.playNext)], + children: [ + IconButton.filledTonal(onPressed: () => playNext(context), icon: const Icon(Icons.play_arrow)), + Text(locals.playNext) + ], ), ), Padding( padding: const EdgeInsets.only(right: 16.0), child: Column( mainAxisSize: MainAxisSize.min, - children: [IconButton.filledTonal(onPressed: () => downloadVideo(context), icon: const Icon(Icons.download)), Text(locals.download)], + children: [ + IconButton.filledTonal(onPressed: () => downloadVideo(context), icon: const Icon(Icons.download)), + Text(locals.download) + ], ), ), ], diff --git a/lib/videos/views/components/video_thumbnail.dart b/lib/videos/views/components/video_thumbnail.dart index f954858d..1f81dbd1 100644 --- a/lib/videos/views/components/video_thumbnail.dart +++ b/lib/videos/views/components/video_thumbnail.dart @@ -9,7 +9,8 @@ class VideoThumbnailView extends StatelessWidget { final String? cacheKey; final BoxDecoration? decoration; - VideoThumbnailView({super.key, required this.videoId, required this.thumbnailUrl, this.child, this.cacheKey, this.decoration}); + VideoThumbnailView( + {super.key, required this.videoId, required this.thumbnailUrl, this.child, this.cacheKey, this.decoration}); @override Widget build(BuildContext context) { @@ -19,7 +20,9 @@ class VideoThumbnailView extends StatelessWidget { child: Thumbnail( id: cacheKey ?? 'v/$videoId', thumbnailUrl: thumbnailUrl, - decoration: decoration != null ? decoration! : BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(10)), + decoration: decoration != null + ? decoration! + : BoxDecoration(color: colors.secondaryContainer, borderRadius: BorderRadius.circular(10)), child: child), ); } @@ -34,7 +37,14 @@ class Thumbnail extends StatelessWidget { final String thumbnailUrl; final BoxDecoration decoration; - Thumbnail({super.key, required this.id, this.child, required this.thumbnailUrl, required this.decoration, this.width, this.height}); + Thumbnail( + {super.key, + required this.id, + this.child, + required this.thumbnailUrl, + required this.decoration, + this.width, + this.height}); @override Widget build(BuildContext context) { diff --git a/lib/videos/views/screens/subscriptions.dart b/lib/videos/views/screens/subscriptions.dart new file mode 100644 index 00000000..8648967b --- /dev/null +++ b/lib/videos/views/screens/subscriptions.dart @@ -0,0 +1,33 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/globals.dart'; +import 'package:invidious/router.dart'; +import 'package:invidious/videos/views/components/subscriptions.dart'; + +@RoutePage() +class SubscriptionScreen extends StatelessWidget { + const SubscriptionScreen({super.key}); + + @override + Widget build(BuildContext context) { + var locals = AppLocalizations.of(context)!; + var colors = Theme.of(context).colorScheme; + return Scaffold( + appBar: AppBar( + backgroundColor: colors.background, + title: Text(locals.subscriptions), + elevation: 0, + scrolledUnderElevation: 0, + actions: [ + IconButton( + onPressed: () => AutoRouter.of(context).push(const ManageSubscriptionsRoute()), + icon: const Icon(Icons.checklist)) + ], + ), + body: const SafeArea( + child: Padding(padding: EdgeInsets.symmetric(horizontal: innerHorizontalPadding), child: Subscriptions()), + ), + ); + } +} diff --git a/lib/videos/views/screens/video.dart b/lib/videos/views/screens/video.dart index 5d766a41..af7cad68 100644 --- a/lib/videos/views/screens/video.dart +++ b/lib/videos/views/screens/video.dart @@ -1,25 +1,21 @@ // import 'package:video_player/video_player.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_fadein/flutter_fadein.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/downloads/states/download_manager.dart'; import 'package:invidious/globals.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/settings/states/settings.dart'; import 'package:invidious/utils/views/components/placeholders.dart'; -import 'package:invidious/videos/states/add_to_playlist_button.dart'; import 'package:invidious/videos/states/video.dart'; -import 'package:invidious/videos/states/video_like.dart'; import 'package:invidious/videos/views/components/add_to_playlist_button.dart'; import 'package:invidious/videos/views/components/download_modal_sheet.dart'; import 'package:invidious/videos/views/components/inner_view_tablet.dart'; import 'package:invidious/videos/views/components/innter_view.dart'; -import 'package:invidious/videos/views/components/like_button.dart'; import 'package:invidious/videos/views/components/video_share_button.dart'; -import '../../../downloads/views/screens/download_manager.dart'; -import '../../../main.dart'; -import '../../../myRouteObserver.dart'; import '../../../player/states/player.dart'; import '../../../utils.dart'; @@ -30,11 +26,12 @@ class VideoRouteArguments { VideoRouteArguments({required this.videoId, this.playNow}); } -class VideoView extends StatelessWidget { +@RoutePage() +class VideoScreen extends StatelessWidget { final String videoId; final bool? playNow; - const VideoView({super.key, required this.videoId, this.playNow}); + const VideoScreen({super.key, required this.videoId, this.playNow}); void downloadVideo(BuildContext context, VideoState _) { var cubit = context.read(); @@ -54,7 +51,7 @@ class VideoView extends StatelessWidget { void openDownloadManager(BuildContext context) { var cubit = context.read(); - navigatorKey.currentState?.push(MaterialPageRoute(settings: ROUTE_DOWNLOAD_MANAGER, builder: (context) => const DownloadManager())).then((value) => cubit.getDownloadStatus()); + AutoRouter.of(context).push(const DownloadManagerRoute()).then((value) => cubit.getDownloadStatus()); } @override @@ -79,12 +76,9 @@ class VideoView extends StatelessWidget { var settings = context.read(); return MultiBlocProvider( providers: [ - BlocProvider(create: (BuildContext context) => VideoCubit(VideoState(videoId: videoId), downloadManager, player, settings)), - BlocProvider(create: (BuildContext context) => AddToPlaylistButtonCubit(AddToPlaylistButtonState(videoId: videoId))), BlocProvider( - create: (context) => VideoLikeButtonCubit(VideoLikeButtonController(videoId: videoId), addToPlaylistButtonCubit: context.read()), - ), - // BlocProvider(create: (context) => AddToPlaylistCubit(AddToPlaylistController(videoId), videoLikeButtonCubit: context.read(), addToPlaylistButtonCubit: context.read()),) + create: (BuildContext context) => + VideoCubit(VideoState(videoId: videoId), downloadManager, player, settings)), ], child: BlocBuilder( builder: (context, _) { @@ -128,8 +122,12 @@ class VideoView extends StatelessWidget { : Stack( children: [ IconButton( - onPressed: _.isDownloaded || _.downloadFailed ? () => openDownloadManager(context) : () => downloadVideo(context, _), - icon: _.isDownloaded && !_.downloadFailed ? const Icon(Icons.download_done) : const Icon(Icons.download)), + onPressed: _.isDownloaded || _.downloadFailed + ? () => openDownloadManager(context) + : () => downloadVideo(context, _), + icon: _.isDownloaded && !_.downloadFailed + ? const Icon(Icons.download_done) + : const Icon(Icons.download)), Positioned( right: 5, top: 5, @@ -146,8 +144,11 @@ class VideoView extends StatelessWidget { visible: _.video != null, child: VideoShareButton(video: _.video!), ), +/* VideoLikeButton(videoId: _.video?.videoId), VideoAddToPlaylistButton(videoId: _.video?.videoId), +*/ + AddToPlayListButton(videoId: _.videoId) ], scrolledUnderElevation: 0, ), @@ -189,7 +190,9 @@ class VideoView extends StatelessWidget { videoController: _, ) : _.loadingVideo - ? Container(constraints: BoxConstraints(maxWidth: tabletMaxVideoWidth), child: const VideoPlaceHolder()) + ? Container( + constraints: BoxConstraints(maxWidth: tabletMaxVideoWidth), + child: const VideoPlaceHolder()) : VideoTabletInnerView( video: _.video!, playNow: playNow, diff --git a/lib/videos/views/tv/components/video_item.dart b/lib/videos/views/tv/components/video_item.dart index 176d3c3f..994ede01 100644 --- a/lib/videos/views/tv/components/video_item.dart +++ b/lib/videos/views/tv/components/video_item.dart @@ -1,5 +1,7 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/utils.dart'; import 'package:invidious/videos/views/tv/screens/video.dart'; @@ -14,14 +16,16 @@ class TvVideoItem extends StatelessWidget { final void Function(bool focus)? onFocusChange; final Function(BuildContext context, VideoInList video)? onSelect; - const TvVideoItem({Key? key, required this.video, required this.autoFocus, this.onSelect, this.onFocusChange}) : super(key: key); + const TvVideoItem({Key? key, required this.video, required this.autoFocus, this.onSelect, this.onFocusChange}) + : super(key: key); openVideo(BuildContext context, VideoInList e, FocusNode node, KeyEvent event) { - if (event is KeyUpEvent && (event.logicalKey == LogicalKeyboardKey.enter || event.logicalKey == LogicalKeyboardKey.select)) { + if (event is KeyUpEvent && + (event.logicalKey == LogicalKeyboardKey.enter || event.logicalKey == LogicalKeyboardKey.select)) { if (onSelect != null) { onSelect!(context, e); } else { - Navigator.of(context).push(MaterialPageRoute(builder: (ctx) => TvVideoView(videoId: e.videoId))); + AutoRouter.of(context).push(TvVideoRoute(videoId: e.videoId)); } return KeyEventResult.handled; } @@ -69,7 +73,8 @@ class TvVideoItem extends StatelessWidget { child: Container( alignment: Alignment.bottomRight, child: Container( - decoration: BoxDecoration(color: Colors.black.withOpacity(0.5), borderRadius: BorderRadius.circular(5)), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), borderRadius: BorderRadius.circular(5)), child: Padding( padding: const EdgeInsets.all(3.0), child: Text( diff --git a/lib/videos/views/tv/screens/video.dart b/lib/videos/views/tv/screens/video.dart index c3b7da25..2558ce5b 100644 --- a/lib/videos/views/tv/screens/video.dart +++ b/lib/videos/views/tv/screens/video.dart @@ -1,5 +1,7 @@ import 'dart:ui'; +import 'package:auto_route/annotations.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -8,6 +10,7 @@ import 'package:invidious/channels/views/tv/screens/channel.dart'; import 'package:invidious/downloads/states/download_manager.dart'; import 'package:invidious/globals.dart'; import 'package:invidious/player/views/tv/screens/tvPlayerView.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/settings/states/settings.dart'; import 'package:invidious/subscription_management/view/tv/tv_subscribe_button.dart'; import 'package:invidious/utils/models/paginatedList.dart'; @@ -25,19 +28,18 @@ import '../../../states/tv_video.dart'; import '../../../states/video.dart'; import '../../components/video_thumbnail.dart'; -class TvVideoView extends StatelessWidget { +@RoutePage() +class TvVideoScreen extends StatelessWidget { final String videoId; - const TvVideoView({Key? key, required this.videoId}) : super(key: key); + const TvVideoScreen({Key? key, required this.videoId}) : super(key: key); playVideo(BuildContext context, Video video) { - Navigator.of(context).push(MaterialPageRoute(builder: (ctx) => TvPlayerView(videos: [video, ...video.recommendedVideos]))); + AutoRouter.of(context).push(TvPlayerRoute(videos: [video, ...video.recommendedVideos])); } showChannel(BuildContext context, String channelId) { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => TvChannelView(channelId: channelId), - )); + AutoRouter.of(context).push(TvChannelRoute(channelId: channelId)); } @override diff --git a/lib/videos/views/tv/screens/video_grid_view.dart b/lib/videos/views/tv/screens/video_grid_view.dart index 367f8477..fb406c0a 100644 --- a/lib/videos/views/tv/screens/video_grid_view.dart +++ b/lib/videos/views/tv/screens/video_grid_view.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:invidious/utils/models/paginatedList.dart'; @@ -8,12 +9,13 @@ import 'package:invidious/videos/views/tv/components/video_item.dart'; import '../../../../utils/states/item_list.dart'; import '../../../models/video_in_list.dart'; -class TvGridView extends StatelessWidget { +@RoutePage() +class TvGridScreen extends StatelessWidget { final PaginatedList paginatedVideoList; final String? tags; final String title; - const TvGridView({Key? key, required this.paginatedVideoList, this.tags, required this.title}) : super(key: key); + const TvGridScreen({Key? key, required this.paginatedVideoList, this.tags, required this.title}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/welcome_wizard/views/screens/welcome_wizard.dart b/lib/welcome_wizard/views/screens/welcome_wizard.dart index f72ee63f..dfc4ec18 100644 --- a/lib/welcome_wizard/views/screens/welcome_wizard.dart +++ b/lib/welcome_wizard/views/screens/welcome_wizard.dart @@ -1,8 +1,9 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/app/states/app.dart'; -import 'package:invidious/main.dart'; +import 'package:invidious/router.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'; @@ -10,8 +11,9 @@ import 'package:invidious/welcome_wizard/states/welcome_wizard.dart'; import '../../../settings/models/db/server.dart'; -class WelcomeWizard extends StatelessWidget { - const WelcomeWizard({super.key}); +@RoutePage() +class WelcomeWizardScreen extends StatelessWidget { + const WelcomeWizardScreen({super.key}); @override Widget build(BuildContext context) { @@ -23,7 +25,8 @@ class WelcomeWizard extends StatelessWidget { providers: [ BlocProvider(create: (context) => WelcomeWizardCubit(null)), BlocProvider( - create: (context) => ServerListSettingsCubit(ServerListSettingsState(publicServers: [], dbServers: []), context.read()), + create: (context) => ServerListSettingsCubit( + ServerListSettingsState(publicServers: [], dbServers: []), context.read()), ) ], child: BlocListener( @@ -53,16 +56,14 @@ class WelcomeWizard extends StatelessWidget { padding: const EdgeInsets.all(8.0), child: Text(locals.wizardIntro), ), - const Expanded(child: ManagerServersView()), + const Expanded(child: ManagerServersView(fromWizard : true)), Padding( padding: const EdgeInsets.all(8.0), child: FilledButton.tonal( onPressed: server != null ? () { - navigatorKey.currentState - ?.pushReplacement(MaterialPageRoute( - builder: (context) => const Home(), - )) + AutoRouter.of(context) + .replace(const MainRoute()) .then((value) => cubit.getSelectedServer()); } : null, diff --git a/lib/welcome_wizard/views/tv/screens/welcome_wizard.dart b/lib/welcome_wizard/views/tv/components/welcome_wizard.dart similarity index 90% rename from lib/welcome_wizard/views/tv/screens/welcome_wizard.dart rename to lib/welcome_wizard/views/tv/components/welcome_wizard.dart index bf176b4d..e1210764 100644 --- a/lib/welcome_wizard/views/tv/screens/welcome_wizard.dart +++ b/lib/welcome_wizard/views/tv/components/welcome_wizard.dart @@ -1,7 +1,9 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:invidious/app/states/app.dart'; +import 'package:invidious/router.dart'; import 'package:invidious/settings/views/tv/components/manage_server_inner.dart'; import 'package:invidious/utils/views/tv/components/tv_button.dart'; import 'package:invidious/utils/views/tv/components/tv_overscan.dart'; @@ -11,8 +13,10 @@ import '../../../../app/views/tv/screens/tv_home.dart'; import '../../../../settings/models/db/server.dart'; import '../../../../settings/states/server_list_settings.dart'; -class TvWelcomeWizard extends StatelessWidget { - const TvWelcomeWizard({Key? key}) : super(key: key); + +@RoutePage() +class TvWelcomeWizardScreen extends StatelessWidget { + const TvWelcomeWizardScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -47,9 +51,7 @@ class TvWelcomeWizard extends StatelessWidget { unfocusedColor: server == null ? colors.background : null, onPressed: server != null ? (context) { - Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (context) => const TvHome(), - )); + AutoRouter.of(context).replace(const TvHomeRoute()); } : null, child: Padding( diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index fafd4649..e2d2bba4 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) awesome_notifications_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "AwesomeNotificationsPlugin"); + awesome_notifications_plugin_register_with_registrar(awesome_notifications_registrar); g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2b7931c3..81f0fff8 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + awesome_notifications dynamic_color objectbox_flutter_libs url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b4b8e6f5..f2d62612 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import audio_service import audio_session +import awesome_notifications import device_info_plus import dynamic_color import flutter_web_auth @@ -22,6 +23,7 @@ import wakelock_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + AwesomeNotificationsPlugin.register(with: registry.registrar(forPlugin: "AwesomeNotificationsPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FlutterWebAuthPlugin.register(with: registry.registrar(forPlugin: "FlutterWebAuthPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 2867f963..dd7a0fc2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.16" + auto_route: + dependency: "direct main" + description: + name: auto_route + sha256: "72f21e8b6cbbe25f02ea69183e024996530bf495cc1b077a49e0ec6726f0c271" + url: "https://pub.dev" + source: hosted + version: "7.8.3" + auto_route_generator: + dependency: "direct dev" + description: + name: auto_route_generator + sha256: e7aa9ab44b77cd31a4619d94db645ab5736e543fd0b4c6058c281249e479dfb8 + url: "https://pub.dev" + source: hosted + version: "7.3.1" + awesome_notifications: + dependency: "direct main" + description: + name: awesome_notifications + sha256: "6ba98d73553c8a54e7b77f8dd8b95ce9d32a7839ef182b28cf2ad54ec28b1821" + url: "https://pub.dev" + source: hosted + version: "0.7.5-dev.3" back_button_interceptor: dependency: "direct main" description: @@ -431,6 +455,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0+1" + flutter_background_service: + dependency: "direct main" + description: + name: flutter_background_service + sha256: "5ec79841c3e9f3bd1885b06c5d7502d6df415cb1665e6717792cc0e51716619f" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + flutter_background_service_android: + dependency: transitive + description: + name: flutter_background_service_android + sha256: a295c7604782b3723fa356679e5b14c5e0fb694d77a7299af135364fa851ee1a + url: "https://pub.dev" + source: hosted + version: "6.0.1" + flutter_background_service_ios: + dependency: transitive + description: + name: flutter_background_service_ios + sha256: ab73657535876e16abc89e40f924df3e92ad3dee83f64d187081417e824709ed + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_background_service_platform_interface: + dependency: transitive + description: + name: flutter_background_service_platform_interface + sha256: cd5720ff5b051d551a4734fae16683aace779bd0425e8d3f15d84a0cdcc2d8d9 + url: "https://pub.dev" + source: hosted + version: "5.0.0" flutter_bloc: dependency: "direct main" description: @@ -801,6 +857,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + optimize_battery: + dependency: "direct main" + description: + name: optimize_battery + sha256: "4f0f974addbe54d3a705c1da5bf3a4bdae39502b1b2d2a17f9da1e558a34a4f8" + url: "https://pub.dev" + source: hosted + version: "0.0.4" package_config: dependency: transitive description: @@ -1457,4 +1521,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.10.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9da23300..d1273169 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.15.4+4028 +version: 1.16.0+4029 environment: sdk: '>=3.0.0 <4.0.0' @@ -80,6 +80,10 @@ dependencies: copy_with_extension: 5.0.4 pretty_bytes: 6.1.0 simple_pip_mode: ^0.8.0 + flutter_background_service: 5.0.1 + optimize_battery: 0.0.4 + awesome_notifications: ^0.7.5-dev.3 + auto_route: ^7.8.3 dependency_overrides: # wakelock_windows: # git: # see https://github.com/creativecreatorormaybenot/wakelock/pull/203 for updates @@ -108,6 +112,7 @@ dev_dependencies: test: any copy_with_extension_gen: 5.0.4 flutter_launcher_icons: ^0.13.1 + auto_route_generator: ^7.3.1 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is diff --git a/test/widget_test.dart b/test/widget_test.dart index 30757dce..49cdb4ba 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -7,13 +7,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:invidious/home/views/screens/home.dart'; import 'package:invidious/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const Home()); + await tester.pumpWidget(const HomeScreen()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5881b9d1..3d052b2a 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AwesomeNotificationsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AwesomeNotificationsPluginCApi")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); ObjectboxFlutterLibsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 5dc49f95..dd0cb955 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + awesome_notifications dynamic_color objectbox_flutter_libs share_plus