From 46571852de34bf2fc679e0b9074a6339a24badd6 Mon Sep 17 00:00:00 2001 From: omkar Date: Thu, 6 Feb 2025 23:08:55 +0530 Subject: [PATCH] fix: small changes --- Makefile | 2 +- lib/app/app_web.dart | 12 - lib/data/db.dart | 4 + .../accounts/container/trakt.container.dart | 364 ++++++++++-------- .../accounts/pages/external_account.dart | 36 +- .../accounts/service/zeku_service.dart | 114 ++++++ lib/features/common/utils/startup_app.dart | 16 + .../models/stremio_base_types.dart | 2 +- .../video_player/container/native.dart | 3 + .../options/audio_track_selector.dart | 2 +- .../container/set_delay_native.dart | 7 + .../container/set_delay_stub.dart | 4 + .../video_player/container/set_delay_web.dart | 4 + .../video_player/container/video_desktop.dart | 39 +- .../video_player/container/video_mobile.dart | 5 +- .../video_player/container/video_play.dart | 8 +- .../video_player/service/zeku_player.dart | 43 +++ .../plugins/stremio/containers/shimmer.dart | 6 +- .../containers/streamio_background.dart | 14 +- .../containers/streamio_cast_section.dart | 5 +- .../containers/streamio_trailer_section.dart | 5 +- .../stremio/widgets/catalog_featured.dart | 83 +++- .../plugins/stremio/widgets/catalog_grid.dart | 106 +---- lib/main_web.dart | 8 +- 24 files changed, 568 insertions(+), 324 deletions(-) delete mode 100644 lib/app/app_web.dart create mode 100644 lib/features/accounts/service/zeku_service.dart create mode 100644 lib/features/video_player/container/native.dart create mode 100644 lib/features/video_player/container/set_delay_native.dart create mode 100644 lib/features/video_player/container/set_delay_stub.dart create mode 100644 lib/features/video_player/container/set_delay_web.dart create mode 100644 lib/features/video_player/service/zeku_player.dart diff --git a/Makefile b/Makefile index 4f9d342..e202840 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ schema: dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_schema_v1.json build_web: - flutter build web --target lib/main_web.dart --release --pwa-strategy none --wasm --dart-define=BUILD_ID=$(BUILD_ID) + flutter build web --target lib/main_web.dart --release --pwa-strategy none --web-renderer html --dart-define=BUILD_ID=$(BUILD_ID) build_mac: flutter build macos --target lib/main.dart --release --dart-define=BUILD_ID=$(BUILD_ID) diff --git a/lib/app/app_web.dart b/lib/app/app_web.dart deleted file mode 100644 index bef1964..0000000 --- a/lib/app/app_web.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppWeb extends StatelessWidget { - const AppWeb({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return const MaterialApp(); - } -} diff --git a/lib/data/db.dart b/lib/data/db.dart index 5f9fb17..0306352 100644 --- a/lib/data/db.dart +++ b/lib/data/db.dart @@ -35,6 +35,10 @@ class AppDatabase extends _$AppDatabase { static QueryExecutor _openConnection() { return driftDatabase( name: 'app_db', + web: DriftWebOptions( + sqlite3Wasm: Uri.parse("wasm"), + driftWorker: Uri.parse("worker"), + ), ); } diff --git a/lib/features/accounts/container/trakt.container.dart b/lib/features/accounts/container/trakt.container.dart index 2ef61a8..8b07194 100644 --- a/lib/features/accounts/container/trakt.container.dart +++ b/lib/features/accounts/container/trakt.container.dart @@ -1,181 +1,231 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; -import '../../common/utils/refresh_auth.dart'; -import '../../pocketbase/service/pocketbase.service.dart'; +import '../service/zeku_service.dart'; -class TraktContainer extends StatefulWidget { - const TraktContainer({super.key}); +class ServicesGrid extends StatefulWidget { + const ServicesGrid({super.key}); @override - State createState() => _TraktContainerState(); + State createState() => _ServicesGridState(); } -class _TraktContainerState extends State { - final pb = AppPocketBaseService.instance.pb; - bool isLoggedIn = false; - bool isLoading = false; +class _ServicesGridState extends State { + final _zekuService = ZekuService(); + late Future> _servicesFuture; @override void initState() { super.initState(); - checkIsLoggedIn(); - } - - void checkIsLoggedIn() { - final traktToken = pb.authStore.record!.getStringValue("trakt_token"); - setState(() { - isLoggedIn = traktToken != ""; - }); - } - - Future removeAccount() async { - setState(() => isLoading = true); - try { - final record = pb.authStore.record!; - record.set("trakt_token", ""); - - await pb.collection('users').update( - record.id, - body: record.toJson(), - ); - - await refreshAuth(); - setState(() { - isLoggedIn = false; - }); - } catch (e) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(e.toString()), - backgroundColor: Theme.of(context).colorScheme.error, - ), - ); - } finally { - if (mounted) setState(() => isLoading = false); - } - } - - Future loginWithTrakt() async { - setState(() => isLoading = true); - try { - await pb.collection("users").authWithOAuth2( - "oidc", - (url) async { - final newUrl = Uri.parse( - url.toString().replaceFirst( - "scope=openid&", - "", - ), - ); - await launchUrl(newUrl); - }, - scopes: ["openid"], - ); - - await refreshAuth(); - checkIsLoggedIn(); - } catch (e) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(e.toString()), - backgroundColor: Theme.of(context).colorScheme.error, - ), - ); - } finally { - if (mounted) setState(() => isLoading = false); - } + _servicesFuture = _zekuService.getServices(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final screenWidth = MediaQuery.of(context).size.width; - final isDesktopOrTV = screenWidth > 1024; - final isTablet = screenWidth > 600 && screenWidth <= 1024; - final horizontalPadding = isDesktopOrTV - ? screenWidth * 0.2 - : isTablet - ? 48.0 - : 24.0; + return FutureBuilder>( + future: _servicesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center( + child: CircularProgressIndicator( + color: theme.colorScheme.primary, + ), + ); + } - return Padding( - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding, - vertical: 24, - ), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 600), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - isLoggedIn ? 'Connected to Trakt' : 'Connect with Trakt', - style: theme.textTheme.displaySmall?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - isLoggedIn - ? 'Your Trakt account is connected' - : 'Sign in to track your movies and shows', - style: theme.textTheme.bodyLarge?.copyWith( - color: colorScheme.onSurface.withValues(alpha: 0.7), - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - SizedBox( - height: 50, - child: FilledButton( - onPressed: isLoading - ? null - : (isLoggedIn ? removeAccount : loginWithTrakt), - style: FilledButton.styleFrom( - backgroundColor: - isLoggedIn ? colorScheme.error : colorScheme.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ).copyWith( - overlayColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.hovered)) { - return colorScheme.onPrimary.withValues(alpha: 0.08); - } - if (states.contains(WidgetState.pressed)) { - return colorScheme.onPrimary.withValues(alpha: 0.12); - } - return null; - }), + if (snapshot.hasError) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + color: theme.colorScheme.error, + size: 48, ), - child: isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: - AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text( - isLoggedIn - ? 'Disconnect Account' - : 'Connect with Trakt', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), + const SizedBox(height: 16), + Text( + 'Failed to load services', + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.error, + ), + ), + const SizedBox(height: 8), + Text( + '${snapshot.error}', + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.error, + ), + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () { + setState(() { + _servicesFuture = _zekuService.getServices(); + }); + }, + child: const Text('Retry'), + ), + ], + ), + ); + } + + final services = snapshot.data ?? []; + + if (services.isEmpty) { + return Center( + child: Text( + 'No services available', + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onSurface, ), ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(0), + itemCount: services.length, + itemBuilder: (context, index) { + final service = services[index]; + return ServiceCard( + service: service, + onRefresh: () { + setState(() { + _servicesFuture = _zekuService.getServices(); + }); + }, + ); + }, + ); + }, + ); + } +} + +class ServiceCard extends StatefulWidget { + final ZekuServiceItem service; + final VoidCallback onRefresh; + + const ServiceCard({ + super.key, + required this.service, + required this.onRefresh, + }); + + @override + State createState() => _ServiceCardState(); +} + +class _ServiceCardState extends State { + authenticate() async { + await ZekuService.instance.authenticate(); + + showAdaptiveDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text("Authenticated?"), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text("Cancel"), + ), + TextButton( + child: const Text("Refresh"), + onPressed: () { + widget.onRefresh(); + Navigator.pop(context); + }, + ), + ], + ); + }, + ); + } + + disconnect(String service) async { + await ZekuService.instance.removeSession(service); + widget.onRefresh(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: SizedBox( + width: 60, + height: 60, + child: CachedNetworkImage( + imageUrl: widget.service.logo, + fit: BoxFit.cover, + placeholder: (context, url) => Center( + child: CircularProgressIndicator( + strokeWidth: 2, + color: theme.colorScheme.primary, + ), + ), + errorWidget: (context, url, error) => Container( + color: theme.colorScheme.surfaceContainerHighest, + child: Icon( + Icons.image_not_supported, + color: theme.colorScheme.onSurfaceVariant, + size: 24, + ), + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.service.name, + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + widget.service.website, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + TextButton( + onPressed: () { + if (widget.service.enabled) { + disconnect(widget.service.name); + } else { + authenticate(); + } + }, + child: widget.service.enabled + ? const Text("Disconnect") + : const Text("Authenticate"), + ), ], ), ), diff --git a/lib/features/accounts/pages/external_account.dart b/lib/features/accounts/pages/external_account.dart index 7f60e00..5205684 100644 --- a/lib/features/accounts/pages/external_account.dart +++ b/lib/features/accounts/pages/external_account.dart @@ -14,40 +14,8 @@ class ExternalAccount extends StatelessWidget { appBar: AppBar( title: const Text("External Accounts"), ), - body: SettingWrapper( - child: ListView( - children: [ - _buildSection( - "Trakt", - [ - const TraktContainer(), - ], - ), - ], - ), - ), - ); - } - - Widget _buildSection(String title, List children) { - return Card( - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 0), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - ...children, - ], - ), + body: const SettingWrapper( + child: ServicesGrid(), ), ); } diff --git a/lib/features/accounts/service/zeku_service.dart b/lib/features/accounts/service/zeku_service.dart new file mode 100644 index 0000000..64bf4f2 --- /dev/null +++ b/lib/features/accounts/service/zeku_service.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:json_annotation/json_annotation.dart'; +import 'package:madari_client/features/pocketbase/service/pocketbase.service.dart'; +import 'package:madari_client/features/settings/service/selected_profile.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +part 'zeku_service.g.dart'; + +@JsonSerializable() +class ZekuServiceItem { + final String name; + final String logo; + final String website; + final bool enabled; + + ZekuServiceItem({ + required this.name, + required this.logo, + required this.website, + required this.enabled, + }); + + factory ZekuServiceItem.fromJson(Map json) { + return _$ZekuServiceItemFromJson(json); + } + + Map toJson() => _$ZekuServiceItemToJson(this); +} + +class ZekuService { + static final ZekuService _instance = ZekuService._internal(); + final pocketbase = AppPocketBaseService.instance.pb; + final String endpoint = + kDebugMode ? 'http://100.64.0.1:3001' : 'https://zeku.madari.media'; + + authenticate() async { + final result = await http.get( + Uri.parse( + "$endpoint/${SelectedProfileService.instance.selectedProfileId}/session", + ), + headers: { + "Authorization": + "Bearer ${AppPocketBaseService.instance.pb.authStore.token}", + }, + ); + + final res = jsonDecode(result.body); + + final id = res["data"]["id"]; + + await launchUrlString( + "$endpoint/$id/trakt/auth", + ); + } + + disconnect() async {} + + factory ZekuService() { + return _instance; + } + + ZekuService._internal(); + + static ZekuService get instance => _instance; + + final List _services = []; + + Future> getServices() async { + try { + final result = await http.get( + Uri.parse( + "$endpoint/${SelectedProfileService.instance.selectedProfileId}/services", + ), + headers: { + "Authorization": + "Bearer ${AppPocketBaseService.instance.pb.authStore.token}", + }, + ); + + final bodyParsed = jsonDecode(result.body); + + final List returnValue = []; + + for (final item in bodyParsed["data"]) { + returnValue.add(ZekuServiceItem.fromJson(item)); + } + + return returnValue; + } catch (e) { + throw Exception('Failed to fetch services: $e'); + } + } + + Future removeSession(String service) async { + final result = await http.get( + Uri.parse( + "$endpoint/${SelectedProfileService.instance.selectedProfileId}/${service.toLowerCase()}/revoke", + ), + headers: { + "Authorization": + "Bearer ${AppPocketBaseService.instance.pb.authStore.token}", + }, + ); + + if (result.statusCode != 200) { + return false; + } + + return true; + } +} diff --git a/lib/features/common/utils/startup_app.dart b/lib/features/common/utils/startup_app.dart index f5fd822..9e153fd 100644 --- a/lib/features/common/utils/startup_app.dart +++ b/lib/features/common/utils/startup_app.dart @@ -21,6 +21,19 @@ Future startupApp() async { await AppTheme().ensureInitialized(); await SelectedProfileService.instance.initialize(); + final pb = AppPocketBaseService.instance.pb; + final userCollection = pb.collection("users"); + + if (pb.authStore.isValid) { + try { + final user = await userCollection.authRefresh(); + + pb.authStore.save(user.token, user.record); + } catch (e) { + pb.authStore.clear(); + } + } + if (UniversalPlatform.isDesktop) { await windowManager.ensureInitialized(); } @@ -39,6 +52,9 @@ Future startupApp() async { config: QueryConfigFlutter( refetchDuration: const Duration(minutes: 60), cacheDuration: const Duration(minutes: 60), + refetchOnResume: false, + refetchOnConnection: false, + refetchOnResumeMinBackgroundDuration: const Duration(days: 30), ), ); } catch (e) { diff --git a/lib/features/streamio_addons/models/stremio_base_types.dart b/lib/features/streamio_addons/models/stremio_base_types.dart index 53026bd..b44e3ae 100644 --- a/lib/features/streamio_addons/models/stremio_base_types.dart +++ b/lib/features/streamio_addons/models/stremio_base_types.dart @@ -132,7 +132,7 @@ class StremioManifestCatalog { String type; String id; String? name; - @JsonKey(name: "itemCount", defaultValue: 50) + @JsonKey(name: "itemCount") final int itemCount; final List? extra; @JsonKey(name: "extraRequired") diff --git a/lib/features/video_player/container/native.dart b/lib/features/video_player/container/native.dart new file mode 100644 index 0000000..89aa5bf --- /dev/null +++ b/lib/features/video_player/container/native.dart @@ -0,0 +1,3 @@ +export 'set_delay_stub.dart' + if (dart.library.html) 'set_delay_web.dart' + if (dart.library.io) 'set_delay_native.dart'; diff --git a/lib/features/video_player/container/options/audio_track_selector.dart b/lib/features/video_player/container/options/audio_track_selector.dart index 69c697c..98f5873 100644 --- a/lib/features/video_player/container/options/audio_track_selector.dart +++ b/lib/features/video_player/container/options/audio_track_selector.dart @@ -24,7 +24,7 @@ class _AudioTrackSelectorState extends State { } if (trakt.id == "no") { - return "No subtitles"; + return "No audio"; } final result = trakt.language ?? trakt.id; diff --git a/lib/features/video_player/container/set_delay_native.dart b/lib/features/video_player/container/set_delay_native.dart new file mode 100644 index 0000000..6cee5a5 --- /dev/null +++ b/lib/features/video_player/container/set_delay_native.dart @@ -0,0 +1,7 @@ +import 'package:madari_client/features/video_player/container/state/video_settings.dart'; +import 'package:media_kit/media_kit.dart'; + +void setDelay(NativePlayer platform, VideoSettingsProvider settings) { + platform.setProperty('sub-delay', "${-settings.subtitleDelay}"); + platform.setProperty('audio-delay', "${-settings.audioDelay}"); +} diff --git a/lib/features/video_player/container/set_delay_stub.dart b/lib/features/video_player/container/set_delay_stub.dart new file mode 100644 index 0000000..95e124e --- /dev/null +++ b/lib/features/video_player/container/set_delay_stub.dart @@ -0,0 +1,4 @@ +import 'package:madari_client/features/video_player/container/state/video_settings.dart'; +import 'package:media_kit/media_kit.dart'; + +void setDelay(NativePlayer platform, VideoSettingsProvider settings) {} diff --git a/lib/features/video_player/container/set_delay_web.dart b/lib/features/video_player/container/set_delay_web.dart new file mode 100644 index 0000000..95e124e --- /dev/null +++ b/lib/features/video_player/container/set_delay_web.dart @@ -0,0 +1,4 @@ +import 'package:madari_client/features/video_player/container/state/video_settings.dart'; +import 'package:media_kit/media_kit.dart'; + +void setDelay(NativePlayer platform, VideoSettingsProvider settings) {} diff --git a/lib/features/video_player/container/video_desktop.dart b/lib/features/video_player/container/video_desktop.dart index 44a589c..feca52a 100644 --- a/lib/features/video_player/container/video_desktop.dart +++ b/lib/features/video_player/container/video_desktop.dart @@ -2,11 +2,15 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:madari_client/features/video_player/container/options/settings_sheet.dart'; import 'package:madari_client/features/video_player/container/state/video_settings.dart'; +import 'package:madari_client/features/video_player/container/video_mobile.dart'; +import 'package:madari_client/features/video_player/container/video_play.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:provider/provider.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:universal_platform/universal_platform.dart'; import '../../streamio_addons/models/stremio_base_types.dart' as types; +import '../widgets/video_selector.dart'; import 'options/always_on_top.dart'; import 'options/audio_track_selector.dart'; import 'options/scale_option.dart'; @@ -15,11 +19,15 @@ import 'options/subtitle_selector.dart'; class VideoDesktop extends StatefulWidget { final VideoController controller; final types.Meta? meta; + final BehaviorSubject updateSubject; + final OnVideoChangeCallback onVideoChange; const VideoDesktop({ super.key, required this.controller, required this.meta, + required this.updateSubject, + required this.onVideoChange, }); @override @@ -34,11 +42,6 @@ class _VideoDesktopState extends State { super.initState(); } - void _toggleLock(BuildContext context) { - final settings = context.read(); - settings.toggleLock(); - } - Future _showPopupMenu({ required BuildContext context, required String title, @@ -49,7 +52,7 @@ class _VideoDesktopState extends State { builder: (context) => AlertDialog( title: Text(title), content: SizedBox( - width: 400, // Fixed width for desktop + width: 400, child: child, ), backgroundColor: Colors.black87, @@ -130,14 +133,9 @@ class _VideoDesktopState extends State { ), if (widget.meta?.currentVideo != null) Expanded( - child: Text( - "${widget.meta?.name} - ${widget.meta?.currentVideo?.name ?? widget.meta?.currentVideo?.title} - S${widget.meta!.currentVideo?.season} E${widget.meta?.currentVideo?.episode}", - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - overflow: TextOverflow.ellipsis, + child: VideoTitle( + meta: widget.meta!, + updateSubject: widget.updateSubject, ), ), if (widget.meta?.currentVideo == null) @@ -152,6 +150,13 @@ class _VideoDesktopState extends State { overflow: TextOverflow.ellipsis, ), ), + if (widget.meta is types.Meta && widget.meta?.type == "series") + SeasonSource( + onVideoChange: widget.onVideoChange, + meta: widget.meta!, + isMobile: true, + updateSubject: widget.updateSubject, + ), ], seekBarThumbColor: Theme.of(context).primaryColorLight, seekBarColor: Theme.of(context).primaryColor, @@ -159,6 +164,12 @@ class _VideoDesktopState extends State { bottomButtonBar: [ const MaterialPlayOrPauseButton(), const MaterialDesktopVolumeButton(), + if (widget.meta is types.Meta && widget.meta?.type == "series") + NextVideo( + updateSubject: widget.updateSubject, + onVideoChange: widget.onVideoChange, + meta: widget.meta!, + ), const MaterialSkipNextButton(), const SizedBox(width: 12), const MaterialPositionIndicator(), diff --git a/lib/features/video_player/container/video_mobile.dart b/lib/features/video_player/container/video_mobile.dart index a6068d4..92ace12 100644 --- a/lib/features/video_player/container/video_mobile.dart +++ b/lib/features/video_player/container/video_mobile.dart @@ -158,7 +158,6 @@ class _VideoMobileState extends State { Expanded( child: VideoTitle( meta: widget.meta!, - index: widget.index, updateSubject: widget.updateSubject, ), ), @@ -255,13 +254,11 @@ class _VideoMobileState extends State { class VideoTitle extends StatefulWidget { final types.Meta meta; - final int index; final BehaviorSubject updateSubject; const VideoTitle({ super.key, required this.meta, - required this.index, required this.updateSubject, }); @@ -270,7 +267,7 @@ class VideoTitle extends StatefulWidget { } class _VideoTitleState extends State { - late int index = widget.index; + late int index = widget.updateSubject.value; late StreamSubscription _updateStatus; diff --git a/lib/features/video_player/container/video_play.dart b/lib/features/video_player/container/video_play.dart index 78c01f2..7b1068f 100644 --- a/lib/features/video_player/container/video_play.dart +++ b/lib/features/video_player/container/video_play.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:madari_client/features/settings/model/playback_settings_model.dart'; +import 'package:madari_client/features/video_player/container/native.dart'; import 'package:madari_client/features/video_player/container/state/video_settings.dart'; import 'package:madari_client/features/video_player/container/video_desktop.dart'; import 'package:madari_client/features/video_player/container/video_mobile.dart'; @@ -100,8 +101,9 @@ class _VideoPlayState extends State { final platform = player.platform; if (platform is NativePlayer) { _debouncer.run(() { - platform.setProperty('sub-delay', "${-_settings.subtitleDelay}"); - platform.setProperty('audio-delay', "${-_settings.audioDelay}"); + if (!UniversalPlatform.isWeb) { + setDelay(platform, _settings); + } }); } } @@ -147,6 +149,8 @@ class _VideoPlayState extends State { return VideoDesktop( controller: controller, meta: widget.meta, + onVideoChange: widget.onVideoChange, + updateSubject: widget.updateSubject, ); } } diff --git a/lib/features/video_player/service/zeku_player.dart b/lib/features/video_player/service/zeku_player.dart new file mode 100644 index 0000000..d094c8d --- /dev/null +++ b/lib/features/video_player/service/zeku_player.dart @@ -0,0 +1,43 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'zeku_player.g.dart'; + +class ZekuSyncer { + ZekuSyncer(); + + Future getPlayingTimestamp(PlayingTimestampRequest input) async { + return WatchPoint( + duration: const Duration( + seconds: 10, + ), + request: input, + ); + } + + Future> getPlaying(PlayingTimestampRequest input) async { + return []; + } +} + +class WatchPoint { + final Duration duration; + final PlayingTimestampRequest request; + + WatchPoint({ + required this.duration, + required this.request, + }); +} + +@JsonSerializable() +class PlayingTimestampRequest { + final String id; + final int? episode; + final int? season; + + PlayingTimestampRequest({ + required this.id, + required this.episode, + required this.season, + }); +} diff --git a/lib/features/widgetter/plugins/stremio/containers/shimmer.dart b/lib/features/widgetter/plugins/stremio/containers/shimmer.dart index 139d809..4a7a486 100644 --- a/lib/features/widgetter/plugins/stremio/containers/shimmer.dart +++ b/lib/features/widgetter/plugins/stremio/containers/shimmer.dart @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:shimmer/shimmer.dart'; +import 'package:universal_platform/universal_platform.dart'; final _logger = Logger('StreamioShimmer'); @@ -62,7 +63,10 @@ class StreamioShimmer extends StatelessWidget { image: image != null ? DecorationImage( image: CachedNetworkImageProvider( - image!), + UniversalPlatform.isWeb + ? "https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent(image!)}@webp" + : image!, + ), fit: BoxFit.cover, ) : null, diff --git a/lib/features/widgetter/plugins/stremio/containers/streamio_background.dart b/lib/features/widgetter/plugins/stremio/containers/streamio_background.dart index 392158f..40a7d8a 100644 --- a/lib/features/widgetter/plugins/stremio/containers/streamio_background.dart +++ b/lib/features/widgetter/plugins/stremio/containers/streamio_background.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:madari_client/features/streamio_addons/models/stremio_base_types.dart'; import 'package:madari_client/utils/array-extension.dart'; +import 'package:universal_platform/universal_platform.dart'; import '../../../../library/container/add_to_list_button.dart'; import '../../../../streamio_addons/service/stremio_addon_service.dart'; @@ -64,7 +65,10 @@ Future openVideoStream( class StreamioBackground extends StatelessWidget { final String? imageUrl; - const StreamioBackground({super.key, this.imageUrl}); + const StreamioBackground({ + super.key, + this.imageUrl, + }); @override Widget build(BuildContext context) { @@ -75,7 +79,9 @@ class StreamioBackground extends StatelessWidget { fit: StackFit.expand, children: [ CachedNetworkImage( - imageUrl: imageUrl!, + imageUrl: UniversalPlatform.isWeb + ? "https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent(imageUrl!)}@webp" + : imageUrl!, fit: BoxFit.cover, errorWidget: (context, url, error) { _logger.warning('Error loading background image', error); @@ -469,7 +475,9 @@ class _StreamioEpisodeListState extends State { Hero( tag: 'episode_thumb_${episode.id}', child: CachedNetworkImage( - imageUrl: episode.thumbnail!, + imageUrl: UniversalPlatform.isWeb + ? "https://proxy-image.syncws.com/insecure/plain/${episode.thumbnail!}@webp" + : episode.thumbnail!, fit: BoxFit.cover, errorWidget: (context, url, error) { _logger.warning( diff --git a/lib/features/widgetter/plugins/stremio/containers/streamio_cast_section.dart b/lib/features/widgetter/plugins/stremio/containers/streamio_cast_section.dart index 14ca233..1bc8c3d 100644 --- a/lib/features/widgetter/plugins/stremio/containers/streamio_cast_section.dart +++ b/lib/features/widgetter/plugins/stremio/containers/streamio_cast_section.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:universal_platform/universal_platform.dart'; import '../../../../streamio_addons/models/stremio_base_types.dart'; import 'cast_info.dart'; @@ -59,7 +60,9 @@ class StreamioCastSection extends StatelessWidget { child: CachedNetworkImage( imageUrl: actor.profilePath!.startsWith("/") ? "https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent("https://image.tmdb.org/t/p/original/${actor.profilePath}")}@webp" - : actor.profilePath!, + : UniversalPlatform.isWeb + ? "https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent(actor.profilePath!)}@webp" + : actor.profilePath!, fit: BoxFit.cover, errorWidget: (context, url, error) { return const CircleAvatar( diff --git a/lib/features/widgetter/plugins/stremio/containers/streamio_trailer_section.dart b/lib/features/widgetter/plugins/stremio/containers/streamio_trailer_section.dart index b6fd9a3..10e165f 100644 --- a/lib/features/widgetter/plugins/stremio/containers/streamio_trailer_section.dart +++ b/lib/features/widgetter/plugins/stremio/containers/streamio_trailer_section.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:universal_platform/universal_platform.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../../streamio_addons/models/stremio_base_types.dart'; @@ -68,7 +69,9 @@ class StreamioTrailerSection extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(8), child: CachedNetworkImage( - imageUrl: getYoutubeThumbnail(trailer.ytId), + imageUrl: UniversalPlatform.isWeb + ? "https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent(getYoutubeThumbnail(trailer.ytId))}@webp" + : getYoutubeThumbnail(trailer.ytId), fit: BoxFit.cover, errorWidget: (context, url, error) { _logger.warning( diff --git a/lib/features/widgetter/plugins/stremio/widgets/catalog_featured.dart b/lib/features/widgetter/plugins/stremio/widgets/catalog_featured.dart index 73cee14..d36cb18 100644 --- a/lib/features/widgetter/plugins/stremio/widgets/catalog_featured.dart +++ b/lib/features/widgetter/plugins/stremio/widgets/catalog_featured.dart @@ -63,6 +63,24 @@ class _CatalogFeaturedState extends State { _loadImage(); } + void _nextPage() { + if (_selectedIndex < widget.meta.length - 1) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + void _previousPage() { + if (_selectedIndex > 0) { + _pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; @@ -178,7 +196,6 @@ class _CatalogFeaturedState extends State { ), ), ), - // Dark overlay gradient Container( decoration: BoxDecoration( gradient: LinearGradient( @@ -194,10 +211,10 @@ class _CatalogFeaturedState extends State { ), ), ), - // Dim overlay for consistent text readability Container( decoration: BoxDecoration( - color: Colors.black.withOpacity(0.2), + color: + Colors.black.withValues(alpha: 0.2), ), ), Positioned.fill( @@ -286,6 +303,66 @@ class _CatalogFeaturedState extends State { ), ), ), + if (!isMobile) + Positioned( + left: 16, + top: 0, + bottom: 0, + child: Center( + child: AnimatedOpacity( + duration: + const Duration(milliseconds: 200), + opacity: + _selectedIndex > 0 ? 1.0 : 0.3, + child: Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: + BorderRadius.circular(30), + ), + child: IconButton( + icon: const Icon( + Icons.chevron_left_rounded, + size: 36, + color: Colors.white, + ), + onPressed: _previousPage, + ), + ), + ), + ), + ), + if (!isMobile) + Positioned( + right: 16, + top: 0, + bottom: 0, + child: Center( + child: AnimatedOpacity( + duration: + const Duration(milliseconds: 200), + opacity: _selectedIndex < + widget.meta.length - 1 + ? 1.0 + : 0.3, + child: Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: + BorderRadius.circular(30), + ), + child: IconButton( + icon: const Icon( + Icons.chevron_right_rounded, + size: 36, + color: Colors.white, + ), + onPressed: _nextPage, + ), + ), + ), + ), + ), ], ), ), diff --git a/lib/features/widgetter/plugins/stremio/widgets/catalog_grid.dart b/lib/features/widgetter/plugins/stremio/widgets/catalog_grid.dart index 151a6b3..01217d5 100644 --- a/lib/features/widgetter/plugins/stremio/widgets/catalog_grid.dart +++ b/lib/features/widgetter/plugins/stremio/widgets/catalog_grid.dart @@ -4,7 +4,6 @@ import 'dart:math'; import 'package:cached_query_flutter/cached_query_flutter.dart'; import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:madari_client/features/streamio_addons/extension/query_extension.dart'; @@ -46,7 +45,6 @@ class CatalogGrid extends StatefulWidget { class _CatalogGridState extends State implements Refreshable { late InfiniteQuery, int> _query; final ScrollController _scrollController = ScrollController(); - late FocusNode _gridFocusNode; final service = StremioAddonService.instance; static const int pageSize = 20; int _focusedIndex = 0; @@ -66,10 +64,6 @@ class _CatalogGridState extends State implements Refreshable { return InfiniteQuery( key: (id ?? this.id) + state.search.trim(), - config: QueryConfig( - cacheDuration: const Duration(days: 30), - refetchDuration: const Duration(hours: 8), - ), getNextArg: (state) { final lastPage = state.lastPage; if (lastPage == null) return 1; @@ -123,45 +117,6 @@ class _CatalogGridState extends State implements Refreshable { ); } - KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { - if (!_isFocused) return KeyEventResult.ignored; - - if (event is KeyDownEvent) { - if (event.logicalKey == LogicalKeyboardKey.arrowRight || - event.logicalKey == LogicalKeyboardKey.arrowLeft) { - final allItems = - _query.state.data?.expand((page) => page).toList() ?? []; - final itemCount = - allItems.take(15).length + (allItems.isNotEmpty ? 1 : 0); - - if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - setState(() { - _focusedIndex = (_focusedIndex + 1).clamp(0, itemCount - 1); - }); - } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - setState(() { - _focusedIndex = (_focusedIndex - 1).clamp(0, itemCount - 1); - }); - } - - _scrollToFocusedItem(); - return KeyEventResult.handled; - } - } - return KeyEventResult.ignored; - } - - void _scrollToFocusedItem() { - final cardSize = StremioCardSize.getSize(context); - final offset = _focusedIndex * (cardSize.width + 8.0); - - _scrollController.animateTo( - offset, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - @override void initState() { super.initState(); @@ -170,31 +125,11 @@ class _CatalogGridState extends State implements Refreshable { _refresh = HomeLayoutService.instance.refreshWidgets.listen((value) { _query.refetch(); }); - - _gridFocusNode = FocusNode( - debugLabel: 'CatalogGrid-$id', - onKeyEvent: _handleKeyEvent, - ); - - _gridFocusNode.addListener(() { - if (mounted) { - Scrollable.ensureVisible( - context, - alignment: 0.3, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - setState(() { - _isFocused = _gridFocusNode.hasFocus; - }); - } - }); } @override void dispose() { _scrollController.dispose(); - _gridFocusNode.dispose(); _refresh.cancel(); super.dispose(); } @@ -398,31 +333,28 @@ class _CatalogGridState extends State implements Refreshable { ); } - return Focus( - focusNode: _gridFocusNode, - child: ListView.builder( - controller: _scrollController, - scrollDirection: Axis.horizontal, - itemCount: itemCount, - padding: const EdgeInsets.symmetric(horizontal: 8), - itemBuilder: (context, index) { - if (index == allItems.take(15).length) { - return _buildShowMoreCard( - context, - cardSize, - allItems, - title: title, - ); - } - - return _buildItemCard( + return ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + itemCount: itemCount, + padding: const EdgeInsets.symmetric(horizontal: 8), + itemBuilder: (context, index) { + if (index == allItems.take(15).length) { + return _buildShowMoreCard( context, - allItems[index], cardSize, - index == _focusedIndex && _isFocused, + allItems, + title: title, ); - }, - ), + } + + return _buildItemCard( + context, + allItems[index], + cardSize, + index == _focusedIndex && _isFocused, + ); + }, ); } diff --git a/lib/main_web.dart b/lib/main_web.dart index 9925ded..0c9fba3 100644 --- a/lib/main_web.dart +++ b/lib/main_web.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:madari_client/app/app_web.dart'; import 'package:provider/provider.dart'; +import 'app/app.dart'; import 'features/common/utils/startup_app.dart'; import 'features/logger/service/logger.service.dart'; import 'features/theme/theme/app_theme.dart'; +import 'features/widgetter/state/widget_state_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -15,7 +16,10 @@ void main() async { runApp( ChangeNotifierProvider.value( value: AppTheme().themeProvider, - child: const AppWeb(), + child: ChangeNotifierProvider( + create: (context) => StateProvider(), + child: const AppDefault(), + ), ), ); }