fix: small changes
Some checks failed
Build and Deploy / build_windows (push) Has been cancelled
Build and Deploy / build_android (push) Has been cancelled
Build and Deploy / build_ipa (push) Has been cancelled
Build and Deploy / build_linux (push) Has been cancelled
Build and Deploy / build_macos (push) Has been cancelled

This commit is contained in:
omkar 2025-02-06 23:08:55 +05:30
parent 87a9f0bf76
commit 46571852de
24 changed files with 568 additions and 324 deletions

View file

@ -9,7 +9,7 @@ schema:
dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_schema_v1.json dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_schema_v1.json
build_web: 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: build_mac:
flutter build macos --target lib/main.dart --release --dart-define=BUILD_ID=$(BUILD_ID) flutter build macos --target lib/main.dart --release --dart-define=BUILD_ID=$(BUILD_ID)

View file

@ -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();
}
}

View file

@ -35,6 +35,10 @@ class AppDatabase extends _$AppDatabase {
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
name: 'app_db', name: 'app_db',
web: DriftWebOptions(
sqlite3Wasm: Uri.parse("wasm"),
driftWorker: Uri.parse("worker"),
),
); );
} }

View file

@ -1,181 +1,231 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../common/utils/refresh_auth.dart'; import '../service/zeku_service.dart';
import '../../pocketbase/service/pocketbase.service.dart';
class TraktContainer extends StatefulWidget { class ServicesGrid extends StatefulWidget {
const TraktContainer({super.key}); const ServicesGrid({super.key});
@override @override
State<TraktContainer> createState() => _TraktContainerState(); State<ServicesGrid> createState() => _ServicesGridState();
} }
class _TraktContainerState extends State<TraktContainer> { class _ServicesGridState extends State<ServicesGrid> {
final pb = AppPocketBaseService.instance.pb; final _zekuService = ZekuService();
bool isLoggedIn = false; late Future<List<ZekuServiceItem>> _servicesFuture;
bool isLoading = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
checkIsLoggedIn(); _servicesFuture = _zekuService.getServices();
}
void checkIsLoggedIn() {
final traktToken = pb.authStore.record!.getStringValue("trakt_token");
setState(() {
isLoggedIn = traktToken != "";
});
}
Future<void> 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<void> 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);
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final screenWidth = MediaQuery.of(context).size.width;
final isDesktopOrTV = screenWidth > 1024; return FutureBuilder<List<ZekuServiceItem>>(
final isTablet = screenWidth > 600 && screenWidth <= 1024; future: _servicesFuture,
final horizontalPadding = isDesktopOrTV builder: (context, snapshot) {
? screenWidth * 0.2 if (snapshot.connectionState == ConnectionState.waiting) {
: isTablet return Center(
? 48.0 child: CircularProgressIndicator(
: 24.0; color: theme.colorScheme.primary,
return Padding(
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding,
vertical: 24,
), ),
child: ConstrainedBox( );
constraints: const BoxConstraints(maxWidth: 600), }
if (snapshot.hasError) {
return Center(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Icon(
isLoggedIn ? 'Connected to Trakt' : 'Connect with Trakt', Icons.error_outline,
style: theme.textTheme.displaySmall?.copyWith( color: theme.colorScheme.error,
fontWeight: FontWeight.bold, size: 48,
color: colorScheme.onSurface, ),
const SizedBox(height: 16),
Text(
'Failed to load services',
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.error,
), ),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
isLoggedIn '${snapshot.error}',
? 'Your Trakt account is connected' style: theme.textTheme.titleMedium?.copyWith(
: 'Sign in to track your movies and shows', color: theme.colorScheme.error,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.7),
), ),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 32), const SizedBox(height: 8),
SizedBox( ElevatedButton(
height: 50, onPressed: () {
child: FilledButton( setState(() {
onPressed: isLoading _servicesFuture = _zekuService.getServices();
? null });
: (isLoggedIn ? removeAccount : loginWithTrakt), },
style: FilledButton.styleFrom( child: const Text('Retry'),
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); final services = snapshot.data ?? [];
}
return null; if (services.isEmpty) {
}), return Center(
child: Text(
'No services available',
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onSurface,
), ),
child: isLoading ),
? const SizedBox( );
height: 20, }
width: 20,
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<ServiceCard> createState() => _ServiceCardState();
}
class _ServiceCardState extends State<ServiceCard> {
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( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
valueColor: color: theme.colorScheme.primary,
AlwaysStoppedAnimation<Color>(Colors.white),
), ),
) ),
: Text( errorWidget: (context, url, error) => Container(
isLoggedIn color: theme.colorScheme.surfaceContainerHighest,
? 'Disconnect Account' child: Icon(
: 'Connect with Trakt', Icons.image_not_supported,
style: const TextStyle( color: theme.colorScheme.onSurfaceVariant,
fontSize: 16, size: 24,
fontWeight: FontWeight.w600,
), ),
), ),
), ),
), ),
),
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"),
),
], ],
), ),
), ),

View file

@ -14,40 +14,8 @@ class ExternalAccount extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: const Text("External Accounts"), title: const Text("External Accounts"),
), ),
body: SettingWrapper( body: const SettingWrapper(
child: ListView( child: ServicesGrid(),
children: [
_buildSection(
"Trakt",
[
const TraktContainer(),
],
),
],
),
),
);
}
Widget _buildSection(String title, List<Widget> 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,
],
),
), ),
); );
} }

View file

@ -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<String, dynamic> json) {
return _$ZekuServiceItemFromJson(json);
}
Map<String, dynamic> 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<ZekuServiceItem> _services = [];
Future<List<ZekuServiceItem>> 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<ZekuServiceItem> returnValue = [];
for (final item in bodyParsed["data"]) {
returnValue.add(ZekuServiceItem.fromJson(item));
}
return returnValue;
} catch (e) {
throw Exception('Failed to fetch services: $e');
}
}
Future<bool> 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;
}
}

View file

@ -21,6 +21,19 @@ Future startupApp() async {
await AppTheme().ensureInitialized(); await AppTheme().ensureInitialized();
await SelectedProfileService.instance.initialize(); 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) { if (UniversalPlatform.isDesktop) {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
} }
@ -39,6 +52,9 @@ Future startupApp() async {
config: QueryConfigFlutter( config: QueryConfigFlutter(
refetchDuration: const Duration(minutes: 60), refetchDuration: const Duration(minutes: 60),
cacheDuration: const Duration(minutes: 60), cacheDuration: const Duration(minutes: 60),
refetchOnResume: false,
refetchOnConnection: false,
refetchOnResumeMinBackgroundDuration: const Duration(days: 30),
), ),
); );
} catch (e) { } catch (e) {

View file

@ -132,7 +132,7 @@ class StremioManifestCatalog {
String type; String type;
String id; String id;
String? name; String? name;
@JsonKey(name: "itemCount", defaultValue: 50) @JsonKey(name: "itemCount")
final int itemCount; final int itemCount;
final List<StremioManifestCatalogExtra>? extra; final List<StremioManifestCatalogExtra>? extra;
@JsonKey(name: "extraRequired") @JsonKey(name: "extraRequired")

View file

@ -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';

View file

@ -24,7 +24,7 @@ class _AudioTrackSelectorState extends State<AudioTrackSelector> {
} }
if (trakt.id == "no") { if (trakt.id == "no") {
return "No subtitles"; return "No audio";
} }
final result = trakt.language ?? trakt.id; final result = trakt.language ?? trakt.id;

View file

@ -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}");
}

View file

@ -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) {}

View file

@ -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) {}

View file

@ -2,11 +2,15 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.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/options/settings_sheet.dart';
import 'package:madari_client/features/video_player/container/state/video_settings.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:media_kit_video/media_kit_video.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart';
import 'package:universal_platform/universal_platform.dart'; import 'package:universal_platform/universal_platform.dart';
import '../../streamio_addons/models/stremio_base_types.dart' as types; import '../../streamio_addons/models/stremio_base_types.dart' as types;
import '../widgets/video_selector.dart';
import 'options/always_on_top.dart'; import 'options/always_on_top.dart';
import 'options/audio_track_selector.dart'; import 'options/audio_track_selector.dart';
import 'options/scale_option.dart'; import 'options/scale_option.dart';
@ -15,11 +19,15 @@ import 'options/subtitle_selector.dart';
class VideoDesktop extends StatefulWidget { class VideoDesktop extends StatefulWidget {
final VideoController controller; final VideoController controller;
final types.Meta? meta; final types.Meta? meta;
final BehaviorSubject<int> updateSubject;
final OnVideoChangeCallback onVideoChange;
const VideoDesktop({ const VideoDesktop({
super.key, super.key,
required this.controller, required this.controller,
required this.meta, required this.meta,
required this.updateSubject,
required this.onVideoChange,
}); });
@override @override
@ -34,11 +42,6 @@ class _VideoDesktopState extends State<VideoDesktop> {
super.initState(); super.initState();
} }
void _toggleLock(BuildContext context) {
final settings = context.read<VideoSettingsProvider>();
settings.toggleLock();
}
Future<void> _showPopupMenu({ Future<void> _showPopupMenu({
required BuildContext context, required BuildContext context,
required String title, required String title,
@ -49,7 +52,7 @@ class _VideoDesktopState extends State<VideoDesktop> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(title), title: Text(title),
content: SizedBox( content: SizedBox(
width: 400, // Fixed width for desktop width: 400,
child: child, child: child,
), ),
backgroundColor: Colors.black87, backgroundColor: Colors.black87,
@ -130,14 +133,9 @@ class _VideoDesktopState extends State<VideoDesktop> {
), ),
if (widget.meta?.currentVideo != null) if (widget.meta?.currentVideo != null)
Expanded( Expanded(
child: Text( child: VideoTitle(
"${widget.meta?.name} - ${widget.meta?.currentVideo?.name ?? widget.meta?.currentVideo?.title} - S${widget.meta!.currentVideo?.season} E${widget.meta?.currentVideo?.episode}", meta: widget.meta!,
style: const TextStyle( updateSubject: widget.updateSubject,
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
), ),
), ),
if (widget.meta?.currentVideo == null) if (widget.meta?.currentVideo == null)
@ -152,6 +150,13 @@ class _VideoDesktopState extends State<VideoDesktop> {
overflow: TextOverflow.ellipsis, 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, seekBarThumbColor: Theme.of(context).primaryColorLight,
seekBarColor: Theme.of(context).primaryColor, seekBarColor: Theme.of(context).primaryColor,
@ -159,6 +164,12 @@ class _VideoDesktopState extends State<VideoDesktop> {
bottomButtonBar: [ bottomButtonBar: [
const MaterialPlayOrPauseButton(), const MaterialPlayOrPauseButton(),
const MaterialDesktopVolumeButton(), 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 MaterialSkipNextButton(),
const SizedBox(width: 12), const SizedBox(width: 12),
const MaterialPositionIndicator(), const MaterialPositionIndicator(),

View file

@ -158,7 +158,6 @@ class _VideoMobileState extends State<VideoMobile> {
Expanded( Expanded(
child: VideoTitle( child: VideoTitle(
meta: widget.meta!, meta: widget.meta!,
index: widget.index,
updateSubject: widget.updateSubject, updateSubject: widget.updateSubject,
), ),
), ),
@ -255,13 +254,11 @@ class _VideoMobileState extends State<VideoMobile> {
class VideoTitle extends StatefulWidget { class VideoTitle extends StatefulWidget {
final types.Meta meta; final types.Meta meta;
final int index;
final BehaviorSubject<int> updateSubject; final BehaviorSubject<int> updateSubject;
const VideoTitle({ const VideoTitle({
super.key, super.key,
required this.meta, required this.meta,
required this.index,
required this.updateSubject, required this.updateSubject,
}); });
@ -270,7 +267,7 @@ class VideoTitle extends StatefulWidget {
} }
class _VideoTitleState extends State<VideoTitle> { class _VideoTitleState extends State<VideoTitle> {
late int index = widget.index; late int index = widget.updateSubject.value;
late StreamSubscription<int> _updateStatus; late StreamSubscription<int> _updateStatus;

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:madari_client/features/settings/model/playback_settings_model.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/state/video_settings.dart';
import 'package:madari_client/features/video_player/container/video_desktop.dart'; import 'package:madari_client/features/video_player/container/video_desktop.dart';
import 'package:madari_client/features/video_player/container/video_mobile.dart'; import 'package:madari_client/features/video_player/container/video_mobile.dart';
@ -100,8 +101,9 @@ class _VideoPlayState extends State<VideoPlay> {
final platform = player.platform; final platform = player.platform;
if (platform is NativePlayer) { if (platform is NativePlayer) {
_debouncer.run(() { _debouncer.run(() {
platform.setProperty('sub-delay', "${-_settings.subtitleDelay}"); if (!UniversalPlatform.isWeb) {
platform.setProperty('audio-delay', "${-_settings.audioDelay}"); setDelay(platform, _settings);
}
}); });
} }
} }
@ -147,6 +149,8 @@ class _VideoPlayState extends State<VideoPlay> {
return VideoDesktop( return VideoDesktop(
controller: controller, controller: controller,
meta: widget.meta, meta: widget.meta,
onVideoChange: widget.onVideoChange,
updateSubject: widget.updateSubject,
); );
} }
} }

View file

@ -0,0 +1,43 @@
import 'package:json_annotation/json_annotation.dart';
part 'zeku_player.g.dart';
class ZekuSyncer {
ZekuSyncer();
Future<WatchPoint> getPlayingTimestamp(PlayingTimestampRequest input) async {
return WatchPoint(
duration: const Duration(
seconds: 10,
),
request: input,
);
}
Future<List<WatchPoint>> 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,
});
}

View file

@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:universal_platform/universal_platform.dart';
final _logger = Logger('StreamioShimmer'); final _logger = Logger('StreamioShimmer');
@ -62,7 +63,10 @@ class StreamioShimmer extends StatelessWidget {
image: image != null image: image != null
? DecorationImage( ? DecorationImage(
image: CachedNetworkImageProvider( image: CachedNetworkImageProvider(
image!), UniversalPlatform.isWeb
? "https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent(image!)}@webp"
: image!,
),
fit: BoxFit.cover, fit: BoxFit.cover,
) )
: null, : null,

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:madari_client/features/streamio_addons/models/stremio_base_types.dart'; import 'package:madari_client/features/streamio_addons/models/stremio_base_types.dart';
import 'package:madari_client/utils/array-extension.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 '../../../../library/container/add_to_list_button.dart';
import '../../../../streamio_addons/service/stremio_addon_service.dart'; import '../../../../streamio_addons/service/stremio_addon_service.dart';
@ -64,7 +65,10 @@ Future<String?> openVideoStream(
class StreamioBackground extends StatelessWidget { class StreamioBackground extends StatelessWidget {
final String? imageUrl; final String? imageUrl;
const StreamioBackground({super.key, this.imageUrl}); const StreamioBackground({
super.key,
this.imageUrl,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -75,7 +79,9 @@ class StreamioBackground extends StatelessWidget {
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
CachedNetworkImage( CachedNetworkImage(
imageUrl: imageUrl!, imageUrl: UniversalPlatform.isWeb
? "https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent(imageUrl!)}@webp"
: imageUrl!,
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (context, url, error) { errorWidget: (context, url, error) {
_logger.warning('Error loading background image', error); _logger.warning('Error loading background image', error);
@ -469,7 +475,9 @@ class _StreamioEpisodeListState extends State<StreamioEpisodeList> {
Hero( Hero(
tag: 'episode_thumb_${episode.id}', tag: 'episode_thumb_${episode.id}',
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: episode.thumbnail!, imageUrl: UniversalPlatform.isWeb
? "https://proxy-image.syncws.com/insecure/plain/${episode.thumbnail!}@webp"
: episode.thumbnail!,
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (context, url, error) { errorWidget: (context, url, error) {
_logger.warning( _logger.warning(

View file

@ -1,5 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../../../streamio_addons/models/stremio_base_types.dart'; import '../../../../streamio_addons/models/stremio_base_types.dart';
import 'cast_info.dart'; import 'cast_info.dart';
@ -59,6 +60,8 @@ class StreamioCastSection extends StatelessWidget {
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: actor.profilePath!.startsWith("/") imageUrl: actor.profilePath!.startsWith("/")
? "https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent("https://image.tmdb.org/t/p/original/${actor.profilePath}")}@webp" ? "https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent("https://image.tmdb.org/t/p/original/${actor.profilePath}")}@webp"
: UniversalPlatform.isWeb
? "https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent(actor.profilePath!)}@webp"
: actor.profilePath!, : actor.profilePath!,
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (context, url, error) { errorWidget: (context, url, error) {

View file

@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../../streamio_addons/models/stremio_base_types.dart'; import '../../../../streamio_addons/models/stremio_base_types.dart';
@ -68,7 +69,9 @@ class StreamioTrailerSection extends StatelessWidget {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage( 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, fit: BoxFit.cover,
errorWidget: (context, url, error) { errorWidget: (context, url, error) {
_logger.warning( _logger.warning(

View file

@ -63,6 +63,24 @@ class _CatalogFeaturedState extends State<CatalogFeatured> {
_loadImage(); _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
@ -178,7 +196,6 @@ class _CatalogFeaturedState extends State<CatalogFeatured> {
), ),
), ),
), ),
// Dark overlay gradient
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
@ -194,10 +211,10 @@ class _CatalogFeaturedState extends State<CatalogFeatured> {
), ),
), ),
), ),
// Dim overlay for consistent text readability
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withOpacity(0.2), color:
Colors.black.withValues(alpha: 0.2),
), ),
), ),
Positioned.fill( Positioned.fill(
@ -286,6 +303,66 @@ class _CatalogFeaturedState extends State<CatalogFeatured> {
), ),
), ),
), ),
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,
),
),
),
),
),
], ],
), ),
), ),

View file

@ -4,7 +4,6 @@ import 'dart:math';
import 'package:cached_query_flutter/cached_query_flutter.dart'; import 'package:cached_query_flutter/cached_query_flutter.dart';
import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flex_color_picker/flex_color_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:madari_client/features/streamio_addons/extension/query_extension.dart'; import 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
@ -46,7 +45,6 @@ class CatalogGrid extends StatefulWidget {
class _CatalogGridState extends State<CatalogGrid> implements Refreshable { class _CatalogGridState extends State<CatalogGrid> implements Refreshable {
late InfiniteQuery<List<Meta>, int> _query; late InfiniteQuery<List<Meta>, int> _query;
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
late FocusNode _gridFocusNode;
final service = StremioAddonService.instance; final service = StremioAddonService.instance;
static const int pageSize = 20; static const int pageSize = 20;
int _focusedIndex = 0; int _focusedIndex = 0;
@ -66,10 +64,6 @@ class _CatalogGridState extends State<CatalogGrid> implements Refreshable {
return InfiniteQuery( return InfiniteQuery(
key: (id ?? this.id) + state.search.trim(), key: (id ?? this.id) + state.search.trim(),
config: QueryConfig(
cacheDuration: const Duration(days: 30),
refetchDuration: const Duration(hours: 8),
),
getNextArg: (state) { getNextArg: (state) {
final lastPage = state.lastPage; final lastPage = state.lastPage;
if (lastPage == null) return 1; if (lastPage == null) return 1;
@ -123,45 +117,6 @@ class _CatalogGridState extends State<CatalogGrid> 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 @override
void initState() { void initState() {
super.initState(); super.initState();
@ -170,31 +125,11 @@ class _CatalogGridState extends State<CatalogGrid> implements Refreshable {
_refresh = HomeLayoutService.instance.refreshWidgets.listen((value) { _refresh = HomeLayoutService.instance.refreshWidgets.listen((value) {
_query.refetch(); _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 @override
void dispose() { void dispose() {
_scrollController.dispose(); _scrollController.dispose();
_gridFocusNode.dispose();
_refresh.cancel(); _refresh.cancel();
super.dispose(); super.dispose();
} }
@ -398,9 +333,7 @@ class _CatalogGridState extends State<CatalogGrid> implements Refreshable {
); );
} }
return Focus( return ListView.builder(
focusNode: _gridFocusNode,
child: ListView.builder(
controller: _scrollController, controller: _scrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: itemCount, itemCount: itemCount,
@ -422,7 +355,6 @@ class _CatalogGridState extends State<CatalogGrid> implements Refreshable {
index == _focusedIndex && _isFocused, index == _focusedIndex && _isFocused,
); );
}, },
),
); );
} }

View file

@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:madari_client/app/app_web.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'app/app.dart';
import 'features/common/utils/startup_app.dart'; import 'features/common/utils/startup_app.dart';
import 'features/logger/service/logger.service.dart'; import 'features/logger/service/logger.service.dart';
import 'features/theme/theme/app_theme.dart'; import 'features/theme/theme/app_theme.dart';
import 'features/widgetter/state/widget_state_provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -15,7 +16,10 @@ void main() async {
runApp( runApp(
ChangeNotifierProvider.value( ChangeNotifierProvider.value(
value: AppTheme().themeProvider, value: AppTheme().themeProvider,
child: const AppWeb(), child: ChangeNotifierProvider(
create: (context) => StateProvider(),
child: const AppDefault(),
),
), ),
); );
} }