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