mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-01-11 22:40:23 +00:00
fix: trakt issues
Some checks are pending
Build and Deploy / build_windows (push) Waiting to run
Build and Deploy / build_android (push) Waiting to run
Build and Deploy / build_android_tv (push) Waiting to run
Build and Deploy / build_ipa (push) Waiting to run
Build and Deploy / build_linux (push) Waiting to run
Build and Deploy / build_macos (push) Waiting to run
Some checks are pending
Build and Deploy / build_windows (push) Waiting to run
Build and Deploy / build_android (push) Waiting to run
Build and Deploy / build_android_tv (push) Waiting to run
Build and Deploy / build_ipa (push) Waiting to run
Build and Deploy / build_linux (push) Waiting to run
Build and Deploy / build_macos (push) Waiting to run
This commit is contained in:
parent
f9e4c0387a
commit
f129eb7360
13 changed files with 709 additions and 456 deletions
|
|
@ -105,8 +105,6 @@ abstract class BaseConnectionService {
|
|||
|
||||
Future<void> getStreams(
|
||||
LibraryItem id, {
|
||||
String? season,
|
||||
String? episode,
|
||||
OnStreamCallback? callback,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -300,11 +300,7 @@ class StremioConnectionService extends BaseConnectionService {
|
|||
|
||||
return (item as Meta).copyWith(
|
||||
progress: (res as Meta).progress,
|
||||
nextSeason: res.nextSeason,
|
||||
nextEpisode: res.nextEpisode,
|
||||
nextEpisodeTitle: res.nextEpisodeTitle,
|
||||
externalIds: res.externalIds,
|
||||
episodeExternalIds: res.episodeExternalIds,
|
||||
selectedVideoIndex: res.selectedVideoIndex,
|
||||
);
|
||||
}).catchError((err, stack) {
|
||||
_logger.severe('Error fetching item: ${res.id}', err, stack);
|
||||
|
|
@ -422,8 +418,6 @@ class StremioConnectionService extends BaseConnectionService {
|
|||
@override
|
||||
Future<void> getStreams(
|
||||
LibraryItem id, {
|
||||
String? season,
|
||||
String? episode,
|
||||
OnStreamCallback? callback,
|
||||
}) async {
|
||||
_logger.fine('Fetching streams for item: ${id.id}');
|
||||
|
|
@ -491,8 +485,6 @@ class StremioConnectionService extends BaseConnectionService {
|
|||
(item) => videoStreamToStreamList(
|
||||
item,
|
||||
meta,
|
||||
season,
|
||||
episode,
|
||||
addonManifest,
|
||||
),
|
||||
)
|
||||
|
|
@ -558,8 +550,6 @@ class StremioConnectionService extends BaseConnectionService {
|
|||
StreamList? videoStreamToStreamList(
|
||||
VideoStream item,
|
||||
Meta meta,
|
||||
String? season,
|
||||
String? episode,
|
||||
StremioManifest addonManifest,
|
||||
) {
|
||||
String streamTitle = (item.name != null
|
||||
|
|
@ -597,8 +587,6 @@ class StremioConnectionService extends BaseConnectionService {
|
|||
infoHash: item.infoHash!,
|
||||
id: meta.id,
|
||||
fileName: "$title.mp4",
|
||||
season: season,
|
||||
episode: episode,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
|
|
@ -279,7 +278,7 @@ class Meta extends LibraryItem {
|
|||
@JsonKey(name: "id")
|
||||
final String id;
|
||||
@JsonKey(name: "videos")
|
||||
final List<Video>? videos;
|
||||
List<Video>? videos;
|
||||
@JsonKey(name: "genres")
|
||||
final List<String>? genres;
|
||||
@JsonKey(name: "releaseInfo")
|
||||
|
|
@ -299,25 +298,14 @@ class Meta extends LibraryItem {
|
|||
@JsonKey(name: "dvdRelease")
|
||||
final DateTime? dvdRelease;
|
||||
|
||||
final int? traktProgressId;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final double? progress;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final int? nextSeason;
|
||||
final int? selectedVideoIndex;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final int? nextEpisode;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final String? nextEpisodeTitle;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final int? traktId;
|
||||
|
||||
final dynamic externalIds;
|
||||
final dynamic episodeExternalIds;
|
||||
|
||||
bool forceRegularMode;
|
||||
bool? forceRegular = false;
|
||||
|
||||
String get imdbRating {
|
||||
return (imdbRating_ ?? "").toString();
|
||||
|
|
@ -332,15 +320,9 @@ class Meta extends LibraryItem {
|
|||
return null;
|
||||
}
|
||||
|
||||
return videos?.firstWhereOrNull(
|
||||
(episode) {
|
||||
if (episode.tvdbId != null && episodeExternalIds != null) {
|
||||
return episode.tvdbId == episodeExternalIds['tvdb'];
|
||||
}
|
||||
if (selectedVideoIndex != null) return videos![selectedVideoIndex!];
|
||||
|
||||
return nextEpisode == episode.episode && nextSeason == episode.season;
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
Meta({
|
||||
|
|
@ -349,22 +331,18 @@ class Meta extends LibraryItem {
|
|||
this.popularities,
|
||||
required this.type,
|
||||
this.cast,
|
||||
this.traktId,
|
||||
this.forceRegular,
|
||||
this.country,
|
||||
this.forceRegularMode = false,
|
||||
this.externalIds,
|
||||
this.description,
|
||||
this.selectedVideoIndex,
|
||||
this.genre,
|
||||
this.imdbRating_,
|
||||
this.poster,
|
||||
this.nextEpisode,
|
||||
this.nextSeason,
|
||||
this.released,
|
||||
this.slug,
|
||||
this.year,
|
||||
this.status,
|
||||
this.tvdbId,
|
||||
this.nextEpisodeTitle,
|
||||
this.director,
|
||||
this.writer,
|
||||
this.background,
|
||||
|
|
@ -386,7 +364,7 @@ class Meta extends LibraryItem {
|
|||
this.language,
|
||||
this.dvdRelease,
|
||||
this.progress,
|
||||
this.episodeExternalIds,
|
||||
this.traktProgressId,
|
||||
}) : super(id: id);
|
||||
|
||||
Meta copyWith({
|
||||
|
|
@ -407,6 +385,7 @@ class Meta extends LibraryItem {
|
|||
dynamic tvdbId,
|
||||
List<dynamic>? director,
|
||||
List<String>? writer,
|
||||
final dynamic traktInfo,
|
||||
String? background,
|
||||
String? logo,
|
||||
dynamic externalIds,
|
||||
|
|
@ -419,6 +398,7 @@ class Meta extends LibraryItem {
|
|||
String? id,
|
||||
List<Video>? videos,
|
||||
List<String>? genres,
|
||||
int? selectedVideoIndex,
|
||||
String? releaseInfo,
|
||||
List<TrailerStream>? trailerStreams,
|
||||
List<Link>? links,
|
||||
|
|
@ -427,10 +407,9 @@ class Meta extends LibraryItem {
|
|||
List<CreditsCrew>? creditsCrew,
|
||||
String? language,
|
||||
DateTime? dvdRelease,
|
||||
int? nextSeason,
|
||||
int? nextEpisode,
|
||||
String? nextEpisodeTitle,
|
||||
double? progress,
|
||||
bool? forceRegular,
|
||||
int? traktProgressId,
|
||||
}) =>
|
||||
Meta(
|
||||
imdbId: imdbId ?? this.imdbId,
|
||||
|
|
@ -439,16 +418,18 @@ class Meta extends LibraryItem {
|
|||
type: type ?? this.type,
|
||||
cast: cast ?? this.cast,
|
||||
country: country ?? this.country,
|
||||
selectedVideoIndex: selectedVideoIndex ?? this.selectedVideoIndex,
|
||||
description: description ?? this.description,
|
||||
genre: genre ?? this.genre,
|
||||
imdbRating_: imdbRating ?? imdbRating_.toString(),
|
||||
poster: poster ?? this.poster,
|
||||
released: released ?? this.released,
|
||||
traktProgressId: traktProgressId ?? this.traktProgressId,
|
||||
slug: slug ?? this.slug,
|
||||
year: year ?? this.year,
|
||||
forceRegular: forceRegular ?? this.forceRegular,
|
||||
status: status ?? this.status,
|
||||
tvdbId: tvdbId ?? this.tvdbId,
|
||||
externalIds: externalIds ?? this.externalIds,
|
||||
director: director ?? this.director,
|
||||
writer: writer ?? this.writer,
|
||||
background: background ?? this.background,
|
||||
|
|
@ -469,11 +450,7 @@ class Meta extends LibraryItem {
|
|||
creditsCrew: creditsCrew ?? this.creditsCrew,
|
||||
language: language ?? this.language,
|
||||
dvdRelease: dvdRelease ?? this.dvdRelease,
|
||||
nextEpisode: nextEpisode ?? this.nextEpisode,
|
||||
nextEpisodeTitle: nextEpisodeTitle ?? this.nextEpisodeTitle,
|
||||
nextSeason: nextSeason ?? this.nextSeason,
|
||||
progress: progress ?? this.progress,
|
||||
episodeExternalIds: episodeExternalIds ?? this.episodeExternalIds,
|
||||
);
|
||||
|
||||
factory Meta.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -679,7 +656,7 @@ class Trailer {
|
|||
@JsonSerializable()
|
||||
class Video {
|
||||
@JsonKey(name: "name")
|
||||
final String? name;
|
||||
String? name;
|
||||
@JsonKey(name: "season")
|
||||
final int season;
|
||||
@JsonKey(name: "number")
|
||||
|
|
@ -687,7 +664,7 @@ class Video {
|
|||
@JsonKey(name: "firstAired")
|
||||
final DateTime? firstAired;
|
||||
@JsonKey(name: "tvdb_id")
|
||||
final int? tvdbId;
|
||||
int? tvdbId;
|
||||
@JsonKey(name: "overview")
|
||||
final String? overview;
|
||||
@JsonKey(name: "thumbnail")
|
||||
|
|
@ -704,6 +681,8 @@ class Video {
|
|||
final String? title;
|
||||
@JsonKey(name: "moviedb_id")
|
||||
final int? moviedbId;
|
||||
double? progress;
|
||||
dynamic ids;
|
||||
|
||||
Video({
|
||||
this.name,
|
||||
|
|
@ -712,13 +691,15 @@ class Video {
|
|||
this.firstAired,
|
||||
this.tvdbId,
|
||||
this.overview,
|
||||
required this.thumbnail,
|
||||
this.thumbnail,
|
||||
required this.id,
|
||||
required this.released,
|
||||
this.released,
|
||||
this.episode,
|
||||
this.description,
|
||||
this.title,
|
||||
this.moviedbId,
|
||||
this.progress,
|
||||
this.ids,
|
||||
});
|
||||
|
||||
Video copyWith({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:cached_query_flutter/cached_query_flutter.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:madari_client/engine/engine.dart';
|
||||
|
|
@ -310,7 +311,30 @@ class __RenderLibraryListState extends State<_RenderLibraryList> {
|
|||
}
|
||||
}
|
||||
|
||||
class RenderListItems extends StatelessWidget {
|
||||
typedef OnContextTap = void Function(
|
||||
String actionId,
|
||||
LibraryItem item,
|
||||
);
|
||||
|
||||
class ContextMenuItem {
|
||||
final String id;
|
||||
final String title;
|
||||
final bool isDefaultAction;
|
||||
final bool isDestructiveAction;
|
||||
final IconData? icon;
|
||||
final OnContextTap? onCallback;
|
||||
|
||||
ContextMenuItem({
|
||||
required this.title,
|
||||
this.isDefaultAction = false,
|
||||
this.isDestructiveAction = false,
|
||||
this.icon,
|
||||
required this.id,
|
||||
this.onCallback,
|
||||
});
|
||||
}
|
||||
|
||||
class RenderListItems extends StatefulWidget {
|
||||
final ScrollController? controller;
|
||||
final ScrollController? itemScrollController;
|
||||
final bool isGrid;
|
||||
|
|
@ -323,6 +347,8 @@ class RenderListItems extends StatelessWidget {
|
|||
final bool isWide;
|
||||
final bool isLoadingMore;
|
||||
final VoidCallback? loadMore;
|
||||
final List<ContextMenuItem> contextMenuItems;
|
||||
final OnContextTap? onContextMenu;
|
||||
|
||||
const RenderListItems({
|
||||
super.key,
|
||||
|
|
@ -338,23 +364,30 @@ class RenderListItems extends StatelessWidget {
|
|||
this.isWide = false,
|
||||
this.isLoadingMore = false,
|
||||
this.loadMore,
|
||||
this.contextMenuItems = const [],
|
||||
this.onContextMenu,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RenderListItems> createState() => _RenderListItemsState();
|
||||
}
|
||||
|
||||
class _RenderListItemsState extends State<RenderListItems> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final listHeight = getListHeight(context);
|
||||
final itemWidth = getItemWidth(
|
||||
context,
|
||||
isWide: isWide,
|
||||
isWide: widget.isWide,
|
||||
);
|
||||
|
||||
return CustomScrollView(
|
||||
controller: controller,
|
||||
physics: isGrid
|
||||
controller: widget.controller,
|
||||
physics: widget.isGrid
|
||||
? const AlwaysScrollableScrollPhysics()
|
||||
: const NeverScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
if (hasError)
|
||||
if (widget.hasError)
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: listHeight,
|
||||
|
|
@ -370,7 +403,7 @@ class RenderListItems extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Something went wrong while loading the library \n$error",
|
||||
"Something went wrong while loading the library \n${widget.error}",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
|
|
@ -379,7 +412,7 @@ class RenderListItems extends StatelessWidget {
|
|||
),
|
||||
TextButton.icon(
|
||||
label: const Text("Retry"),
|
||||
onPressed: onRefresh,
|
||||
onPressed: widget.onRefresh,
|
||||
icon: const Icon(
|
||||
Icons.refresh,
|
||||
),
|
||||
|
|
@ -390,7 +423,7 @@ class RenderListItems extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (isGrid) ...[
|
||||
if (widget.isGrid) ...[
|
||||
SliverGrid.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: getGridResponsiveColumnCount(context),
|
||||
|
|
@ -398,17 +431,17 @@ class RenderListItems extends StatelessWidget {
|
|||
crossAxisSpacing: getGridResponsiveSpacing(context),
|
||||
childAspectRatio: 2 / 3,
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemCount: widget.items.length,
|
||||
itemBuilder: (ctx, index) {
|
||||
final item = items[index];
|
||||
final item = widget.items[index];
|
||||
|
||||
return service.renderCard(
|
||||
return widget.service.renderCard(
|
||||
item,
|
||||
"${index}_$heroPrefix",
|
||||
"${index}_${widget.heroPrefix}",
|
||||
);
|
||||
},
|
||||
),
|
||||
if (isLoadingMore)
|
||||
if (widget.isLoadingMore)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
|
|
@ -439,35 +472,71 @@ class RenderListItems extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
] else ...[
|
||||
if (!isLoadingMore)
|
||||
if (!widget.isLoadingMore)
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: listHeight,
|
||||
child: ListView.builder(
|
||||
controller: itemScrollController,
|
||||
itemBuilder: (ctx, index) {
|
||||
final item = items[index];
|
||||
child: CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
child: SizedBox(
|
||||
height: listHeight,
|
||||
child: ListView.builder(
|
||||
controller: widget.itemScrollController,
|
||||
itemBuilder: (ctx, index) {
|
||||
final item = widget.items[index];
|
||||
|
||||
return SizedBox(
|
||||
width: itemWidth,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(),
|
||||
child: service.renderCard(
|
||||
item,
|
||||
"${index}_${heroPrefix}",
|
||||
if (widget.contextMenuItems.isEmpty) {
|
||||
return SizedBox(
|
||||
width: itemWidth,
|
||||
child: Container(
|
||||
child: widget.service.renderCard(
|
||||
item,
|
||||
"${index}_${widget.heroPrefix}",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return CupertinoContextMenu(
|
||||
enableHapticFeedback: true,
|
||||
actions: widget.contextMenuItems.map((menu) {
|
||||
return CupertinoContextMenuAction(
|
||||
isDefaultAction: menu.isDefaultAction,
|
||||
isDestructiveAction: menu.isDestructiveAction,
|
||||
trailingIcon: menu.icon,
|
||||
onPressed: () {
|
||||
if (widget.onContextMenu != null) {
|
||||
widget.onContextMenu!(
|
||||
menu.id,
|
||||
item,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(menu.title),
|
||||
);
|
||||
}).toList(),
|
||||
child: SizedBox(
|
||||
width: itemWidth,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: listHeight,
|
||||
),
|
||||
child: widget.service.renderCard(
|
||||
item,
|
||||
"${index}_${widget.heroPrefix}",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: items.length,
|
||||
);
|
||||
},
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: widget.items.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isLoadingMore)
|
||||
if (widget.isLoadingMore)
|
||||
SliverToBoxAdapter(
|
||||
child: SpinnerCards(
|
||||
isWide: isWide,
|
||||
isWide: widget.isWide,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ const kIsWeb = bool.fromEnvironment('dart.library.js_util');
|
|||
class RenderStreamList extends StatefulWidget {
|
||||
final BaseConnectionService service;
|
||||
final LibraryItem id;
|
||||
final String? episode;
|
||||
final String? season;
|
||||
final bool shouldPop;
|
||||
final double? progress;
|
||||
|
||||
|
|
@ -27,8 +25,6 @@ class RenderStreamList extends StatefulWidget {
|
|||
super.key,
|
||||
required this.service,
|
||||
required this.id,
|
||||
this.season,
|
||||
this.episode,
|
||||
this.progress,
|
||||
required this.shouldPop,
|
||||
});
|
||||
|
|
@ -173,8 +169,6 @@ class _RenderStreamListState extends State<RenderStreamList> {
|
|||
|
||||
await widget.service.getStreams(
|
||||
widget.id,
|
||||
episode: widget.episode,
|
||||
season: widget.season,
|
||||
callback: (items, error) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
|
@ -303,25 +297,7 @@ class _RenderStreamListState extends State<RenderStreamList> {
|
|||
}
|
||||
}
|
||||
|
||||
int? season;
|
||||
int? episode;
|
||||
|
||||
if (widget.season != null) {
|
||||
season = int.parse(widget.season!);
|
||||
} else if ((widget.id as Meta).nextSeason != null) {
|
||||
season = (widget.id as Meta).nextSeason!;
|
||||
}
|
||||
|
||||
if (widget.episode != null) {
|
||||
episode = int.parse(widget.episode!);
|
||||
} else if ((widget.id as Meta).nextEpisode != null) {
|
||||
episode = (widget.id as Meta).nextEpisode!;
|
||||
}
|
||||
|
||||
final meta = (widget.id as Meta).copyWith(
|
||||
nextSeason: season,
|
||||
nextEpisode: episode,
|
||||
);
|
||||
final meta = (widget.id as Meta).copyWith();
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
|
|
@ -329,7 +305,6 @@ class _RenderStreamListState extends State<RenderStreamList> {
|
|||
source: item.source,
|
||||
service: widget.service,
|
||||
meta: meta,
|
||||
season: meta.nextSeason?.toString(),
|
||||
progress: widget.progress,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ class _StremioCardState extends State<StremioCard> {
|
|||
},
|
||||
);
|
||||
},
|
||||
child: (meta.nextSeason == null || meta.progress != null)
|
||||
child: ((meta.currentVideo == null || meta.progress != null) ||
|
||||
(meta.forceRegular == true))
|
||||
? _buildRegular(context, meta)
|
||||
: _buildWideCard(context, meta),
|
||||
),
|
||||
|
|
@ -144,14 +145,15 @@ class _StremioCardState extends State<StremioCard> {
|
|||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
"S${meta.currentVideo?.season ?? meta.nextSeason} E${meta.currentVideo?.episode ?? meta.nextEpisode}",
|
||||
"S${meta.currentVideo?.season} E${meta.currentVideo?.episode}",
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${meta.nextEpisodeTitle}".trim(),
|
||||
"${meta.currentVideo?.name ?? meta.currentVideo?.title}"
|
||||
.trim(),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
],
|
||||
|
|
@ -245,15 +247,8 @@ class _StremioCardState extends State<StremioCard> {
|
|||
String? getBackgroundImage(Meta meta) {
|
||||
String? backgroundImage;
|
||||
|
||||
if (meta.nextEpisode != null &&
|
||||
meta.nextSeason != null &&
|
||||
meta.videos != null) {
|
||||
for (final video in meta.videos!) {
|
||||
if (video.season == meta.nextSeason &&
|
||||
video.episode == meta.nextEpisode) {
|
||||
return video.thumbnail ?? meta.poster;
|
||||
}
|
||||
}
|
||||
if (meta.currentVideo != null) {
|
||||
return meta.currentVideo?.thumbnail ?? meta.poster;
|
||||
}
|
||||
|
||||
if (meta.poster != null) {
|
||||
|
|
@ -374,7 +369,7 @@ class _StremioCardState extends State<StremioCard> {
|
|||
minHeight: 5,
|
||||
),
|
||||
),
|
||||
if (meta.nextEpisode != null && meta.nextSeason != null)
|
||||
if (meta.currentVideo != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
|
|
@ -407,7 +402,7 @@ class _StremioCardState extends State<StremioCard> {
|
|||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
"S${meta.currentVideo?.season ?? meta.nextSeason} E${meta.currentVideo?.episode ?? meta.nextEpisode}",
|
||||
"S${meta.currentVideo?.season} E${meta.currentVideo?.episode}",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
|
|
|
|||
|
|
@ -279,7 +279,9 @@ class _StremioItemViewerState extends State<StremioItemViewer> {
|
|||
widget.original?.type == "series" &&
|
||||
widget.original?.videos?.isNotEmpty == true)
|
||||
StremioItemSeasonSelector(
|
||||
meta: (item as Meta),
|
||||
meta: (item as Meta).copyWith(
|
||||
selectedVideoIndex: widget.meta?.selectedVideoIndex,
|
||||
),
|
||||
service: widget.service,
|
||||
),
|
||||
SliverPadding(
|
||||
|
|
@ -294,7 +296,6 @@ class _StremioItemViewerState extends State<StremioItemViewer> {
|
|||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
// Description
|
||||
Text(
|
||||
'Description',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:madari_client/features/connection/types/stremio.dart';
|
|||
import 'package:madari_client/features/connections/service/base_connection_service.dart';
|
||||
import 'package:madari_client/features/connections/widget/base/render_stream_list.dart';
|
||||
import 'package:madari_client/features/trakt/service/trakt.service.dart';
|
||||
import 'package:madari_client/utils/common.dart';
|
||||
|
||||
import '../../../doc_viewer/types/doc_source.dart';
|
||||
import '../../../watch_history/service/base_watch_history.dart';
|
||||
|
|
@ -37,15 +38,15 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
late final Map<int, List<Video>> seasonMap;
|
||||
final zeeeWatchHistory = ZeeeWatchHistoryStatic.service;
|
||||
|
||||
late Meta meta = widget.meta;
|
||||
|
||||
final Map<String, double> _progress = {};
|
||||
final Map<int, Map<int, double>> _traktProgress = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
seasonMap = _organizeEpisodes();
|
||||
selectedSeason = widget.season;
|
||||
|
||||
if (seasonMap.keys.isEmpty) {
|
||||
return;
|
||||
|
|
@ -54,36 +55,42 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
_tabController = TabController(
|
||||
length: seasonMap.keys.length,
|
||||
vsync: this,
|
||||
initialIndex: selectedSeason != null
|
||||
? selectedSeason! - 1
|
||||
: (seasonMap.keys.first == 0 ? 1 : 0),
|
||||
initialIndex: getSelectedSeason(),
|
||||
);
|
||||
|
||||
_tabController?.addListener(() {
|
||||
// This is for rendering the component again for the selection of another tab
|
||||
_tabController!.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
getWatchHistory();
|
||||
}
|
||||
|
||||
int getSelectedSeason() {
|
||||
return widget.meta.currentVideo?.season ??
|
||||
widget.meta.videos?.lastWhereOrNull((item) {
|
||||
return item.progress != null;
|
||||
})?.season ??
|
||||
widget.season ??
|
||||
0;
|
||||
}
|
||||
|
||||
getWatchHistory() async {
|
||||
final traktService = TraktService.instance;
|
||||
|
||||
try {
|
||||
if (TraktService.isEnabled()) {
|
||||
final result = await traktService!.getProgress(widget.meta);
|
||||
final result = await traktService!.getProgress(
|
||||
widget.meta,
|
||||
bypassCache: false,
|
||||
);
|
||||
|
||||
for (final item in result) {
|
||||
if (!_traktProgress.containsKey(item.season)) {
|
||||
_traktProgress.addAll(<int, Map<int, double>>{
|
||||
item.season!: {},
|
||||
});
|
||||
}
|
||||
_traktProgress[item.season!] = _traktProgress[item.season] ?? {};
|
||||
_traktProgress[item.season]![item.episode!] = item.progress;
|
||||
}
|
||||
setState(() {
|
||||
meta = result;
|
||||
});
|
||||
|
||||
setState(() {});
|
||||
final index = getSelectedSeason();
|
||||
_tabController?.animateTo(index);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -107,7 +114,9 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
_progress[item.id] = item.progress.toDouble();
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
final index = getSelectedSeason();
|
||||
|
||||
_tabController?.animateTo(index);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -117,13 +126,12 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
}
|
||||
|
||||
Map<int, List<Video>> _organizeEpisodes() {
|
||||
final episodes = widget.meta.videos ?? [];
|
||||
final episodes = meta.videos ?? [];
|
||||
return groupBy(episodes, (Video video) => video.season);
|
||||
}
|
||||
|
||||
void openEpisode({
|
||||
required int currentSeason,
|
||||
required Video episode,
|
||||
required int index,
|
||||
}) async {
|
||||
if (widget.service == null) {
|
||||
return;
|
||||
|
|
@ -131,26 +139,19 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
final onClose = showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final meta = widget.meta.copyWith(
|
||||
nextSeason: currentSeason,
|
||||
nextEpisode: episode.episode,
|
||||
);
|
||||
final meta = this.meta.copyWith(
|
||||
selectedVideoIndex: index,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Streams for S$currentSeason E${episode.episode}"),
|
||||
title: Text(
|
||||
"Streams for S${meta.currentVideo?.season} E${meta.currentVideo?.episode}",
|
||||
),
|
||||
),
|
||||
body: RenderStreamList(
|
||||
service: widget.service!,
|
||||
id: episode.tvdbId != null
|
||||
? meta.copyWith(
|
||||
episodeExternalIds: {
|
||||
"tvdb": episode.tvdbId,
|
||||
},
|
||||
)
|
||||
: meta,
|
||||
season: currentSeason.toString(),
|
||||
episode: episode.number?.toString(),
|
||||
id: meta,
|
||||
shouldPop: widget.shouldPop,
|
||||
),
|
||||
);
|
||||
|
|
@ -160,7 +161,7 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
if (widget.shouldPop) {
|
||||
final val = await onClose;
|
||||
|
||||
if (val is MediaURLSource && context.mounted) {
|
||||
if (val is MediaURLSource && context.mounted && mounted) {
|
||||
Navigator.pop(
|
||||
context,
|
||||
val,
|
||||
|
|
@ -225,11 +226,7 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
widget.meta.videos!.length,
|
||||
);
|
||||
|
||||
openEpisode(
|
||||
currentSeason:
|
||||
widget.meta.videos![randomIndex].season,
|
||||
episode: widget.meta.videos![randomIndex],
|
||||
);
|
||||
openEpisode(index: randomIndex);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -271,19 +268,24 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
final episodes = seasonMap[currentSeason]!;
|
||||
final episode = episodes[index];
|
||||
|
||||
final progress = _traktProgress[episode.season]
|
||||
?[episode.episode] ==
|
||||
null
|
||||
? (_progress[episode.id] ?? 0) / 100
|
||||
: (_traktProgress[episode.season]![episode.episode]! / 100);
|
||||
final videoIndex = meta.videos?.indexOf(episode);
|
||||
|
||||
final progress = ((!TraktService.isEnabled()
|
||||
? (_progress[episode.id] ?? 0) / 100
|
||||
: videoIndex != -1
|
||||
? (meta.videos![videoIndex!].progress)
|
||||
: 0.toDouble()) ??
|
||||
0) /
|
||||
100;
|
||||
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () async {
|
||||
openEpisode(
|
||||
currentSeason: currentSeason,
|
||||
episode: episode,
|
||||
);
|
||||
if (videoIndex != null) {
|
||||
openEpisode(
|
||||
index: videoIndex,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import '../../../utils/load_language.dart';
|
|||
import '../../connections/types/stremio/stremio_base.types.dart' as types;
|
||||
import '../../connections/widget/stremio/stremio_season_selector.dart';
|
||||
import '../../trakt/service/trakt.service.dart';
|
||||
import '../../trakt/types/common.dart';
|
||||
import '../../watch_history/service/zeee_watch_history.dart';
|
||||
import '../types/doc_source.dart';
|
||||
import 'video_viewer/video_viewer_ui.dart';
|
||||
|
|
@ -55,7 +54,9 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
return duration > 0 ? (position / duration * 100) : 0;
|
||||
}
|
||||
|
||||
Future<List<TraktProgress>>? traktProgress;
|
||||
bool timeLoaded = false;
|
||||
|
||||
Future<types.Meta>? traktProgress;
|
||||
|
||||
Future<void> saveWatchHistory() async {
|
||||
final duration = player.state.duration.inSeconds;
|
||||
|
|
@ -65,10 +66,17 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
return;
|
||||
}
|
||||
|
||||
final position = player.state.position.inSeconds;
|
||||
final progress = duration > 0 ? (position / duration * 100).round() : 0;
|
||||
if (gotFromTraktDuration == false) {
|
||||
_logger.info(
|
||||
"did not start the scrobbing because initially time is not retrieved from the api",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress == 0) {
|
||||
final position = player.state.position.inSeconds;
|
||||
final progress = duration > 0 ? (position / duration * 100) : 0;
|
||||
|
||||
if (progress < 0.01) {
|
||||
_logger.info('No progress to save.');
|
||||
return;
|
||||
}
|
||||
|
|
@ -99,7 +107,7 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
await zeeeWatchHistory!.saveWatchHistory(
|
||||
history: WatchHistory(
|
||||
id: _source.id,
|
||||
progress: progress,
|
||||
progress: progress.round(),
|
||||
duration: duration.toDouble(),
|
||||
episode: _source.episode,
|
||||
season: _source.season,
|
||||
|
|
@ -116,44 +124,50 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
|
||||
late DocSource _source;
|
||||
|
||||
bool canCallOnce = false;
|
||||
bool gotFromTraktDuration = false;
|
||||
|
||||
int? traktId;
|
||||
|
||||
Future<void> setDurationFromTrakt() async {
|
||||
if (player.state.duration.inSeconds < 2) {
|
||||
return;
|
||||
try {
|
||||
if (player.state.duration.inSeconds < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gotFromTraktDuration) {
|
||||
return;
|
||||
}
|
||||
|
||||
gotFromTraktDuration = true;
|
||||
|
||||
if (!TraktService.isEnabled() || traktProgress == null) {
|
||||
player.play();
|
||||
return;
|
||||
}
|
||||
|
||||
final progress = await traktProgress;
|
||||
|
||||
if (widget.meta is! types.Meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
final meta = (progress ?? widget.meta) as types.Meta;
|
||||
|
||||
final duration = Duration(
|
||||
seconds: calculateSecondsFromProgress(
|
||||
player.state.duration.inSeconds.toDouble(),
|
||||
meta.currentVideo?.progress ?? meta.progress ?? 0,
|
||||
),
|
||||
);
|
||||
|
||||
if (duration.inSeconds > 10) {
|
||||
await player.seek(duration);
|
||||
}
|
||||
|
||||
await player.play();
|
||||
} catch (e) {
|
||||
await player.play();
|
||||
}
|
||||
|
||||
if (canCallOnce) {
|
||||
return;
|
||||
}
|
||||
|
||||
canCallOnce = true;
|
||||
|
||||
if (!TraktService.isEnabled() || traktProgress == null) {
|
||||
player.play();
|
||||
return;
|
||||
}
|
||||
|
||||
final progress = await traktProgress;
|
||||
|
||||
if ((progress ?? []).isEmpty) {
|
||||
player.play();
|
||||
return;
|
||||
}
|
||||
|
||||
traktId = progress!.first.traktId;
|
||||
|
||||
final duration = Duration(
|
||||
seconds: calculateSecondsFromProgress(
|
||||
player.state.duration.inSeconds.toDouble(),
|
||||
progress.first.progress,
|
||||
),
|
||||
);
|
||||
|
||||
await player.seek(duration);
|
||||
await player.play();
|
||||
}
|
||||
|
||||
List<StreamSubscription> listener = [];
|
||||
|
|
@ -190,9 +204,7 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
});
|
||||
|
||||
_streamListen = player.stream.playing.listen((playing) {
|
||||
if (playing) {
|
||||
saveWatchHistory();
|
||||
}
|
||||
saveWatchHistory();
|
||||
});
|
||||
|
||||
if (widget.meta is types.Meta && TraktService.isEnabled()) {
|
||||
|
|
@ -250,7 +262,6 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
}
|
||||
}
|
||||
|
||||
late StreamSubscription<bool> _streamComplete;
|
||||
late StreamSubscription<bool> _streamListen;
|
||||
late StreamSubscription<dynamic> _duration;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:madari_client/features/connections/service/base_connection_service.dart';
|
||||
import 'package:madari_client/features/trakt/service/trakt.service.dart';
|
||||
import 'package:madari_client/utils/common.dart';
|
||||
|
||||
import '../../connections/types/stremio/stremio_base.types.dart';
|
||||
import '../../connections/widget/base/render_library_list.dart';
|
||||
import '../../settings/screen/trakt_integration_screen.dart';
|
||||
|
||||
|
|
@ -155,6 +158,49 @@ class TraktContainerState extends State<TraktContainer> {
|
|||
}
|
||||
}
|
||||
|
||||
late final Map<String, List<ContextMenuItem>> actions = {
|
||||
"continue_watching": [
|
||||
ContextMenuItem(
|
||||
id: "remove",
|
||||
icon: CupertinoIcons.clear,
|
||||
title: 'Remove',
|
||||
isDestructiveAction: true,
|
||||
onCallback: (action, key) async {
|
||||
if (key is! Meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.traktProgressId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await TraktService.instance!.removeFromContinueWatching(
|
||||
key.traktProgressId!.toString(),
|
||||
);
|
||||
|
||||
if (context.mounted && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Removed successfully"),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
"watchlist": [
|
||||
ContextMenuItem(
|
||||
id: "remove",
|
||||
icon: CupertinoIcons.clear,
|
||||
title: 'Remove',
|
||||
isDestructiveAction: true,
|
||||
onCallback: (action, key) {
|
||||
TraktService.instance!.removeFromWatchlist(key as Meta);
|
||||
},
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
Future<void> refresh() async {
|
||||
try {
|
||||
_logger.info('Refreshing data for ${widget.loadId}');
|
||||
|
|
@ -209,6 +255,16 @@ class TraktContainerState extends State<TraktContainer> {
|
|||
},
|
||||
items: _cachedItems ?? [],
|
||||
error: _error,
|
||||
contextMenuItems:
|
||||
actions.containsKey(widget.loadId)
|
||||
? actions[widget.loadId]!
|
||||
: [],
|
||||
onContextMenu: (action, items) {
|
||||
actions[widget.loadId]!
|
||||
.firstWhereOrNull((item) {
|
||||
return item.id == action;
|
||||
})?.onCallback!(action, items);
|
||||
},
|
||||
isLoadingMore: _isLoading,
|
||||
hasError: _error != null,
|
||||
heroPrefix: "trakt_up_next${widget.loadId}",
|
||||
|
|
@ -276,9 +332,20 @@ class TraktContainerState extends State<TraktContainer> {
|
|||
SizedBox(
|
||||
height: getListHeight(context),
|
||||
child: RenderListItems(
|
||||
isWide: widget.loadId == "up_next_series",
|
||||
isWide: widget.loadId == "up_next_series" ||
|
||||
widget.loadId == "upcoming_schedule",
|
||||
items: _cachedItems ?? [],
|
||||
error: _error,
|
||||
contextMenuItems: actions.containsKey(widget.loadId)
|
||||
? actions[widget.loadId]!
|
||||
: [],
|
||||
onContextMenu: (action, items) async {
|
||||
actions[widget.loadId]!.firstWhereOrNull((item) {
|
||||
return item.id == action;
|
||||
})?.onCallback!(action, items);
|
||||
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
itemScrollController: _scrollController,
|
||||
hasError: _error != null,
|
||||
heroPrefix: "trakt_up_next${widget.loadId}",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:madari_client/utils/common.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
|
|
@ -14,7 +15,6 @@ import '../../../engine/engine.dart';
|
|||
import '../../connections/service/base_connection_service.dart';
|
||||
import '../../connections/types/stremio/stremio_base.types.dart';
|
||||
import '../../settings/types/connection.dart';
|
||||
import '../types/common.dart';
|
||||
|
||||
class TraktService {
|
||||
static final Logger _logger = Logger('TraktService');
|
||||
|
|
@ -41,6 +41,92 @@ class TraktService {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> removeFromContinueWatching(String id) async {
|
||||
if (!isEnabled()) {
|
||||
_logger.info('Trakt integration is not enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_logger.info(
|
||||
'Removing item from history (continue watching): $id',
|
||||
);
|
||||
|
||||
final response = await http.delete(
|
||||
Uri.parse('$_baseUrl/sync/playback/$id'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode != 204) {
|
||||
_logger.severe(
|
||||
'Failed to remove item from history: ${response.statusCode} $id',
|
||||
);
|
||||
throw Exception('Failed to remove item from history');
|
||||
}
|
||||
|
||||
_cache.remove('$_baseUrl/sync/watched/shows');
|
||||
_cache.remove('$_baseUrl/sync/playback');
|
||||
|
||||
refetchKey.add(["continue_watching", "up_next_series"]);
|
||||
|
||||
_logger.info(
|
||||
'Successfully removed item from history (continue watching)',
|
||||
);
|
||||
} catch (e, stack) {
|
||||
_logger.severe('Error removing item from history: $e', stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeFromWatchlist(Meta meta) async {
|
||||
if (!isEnabled()) {
|
||||
_logger.info('Trakt integration is not enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_logger.info('Removing item from watchlist: ${meta.id}');
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/sync/watchlist/remove'),
|
||||
headers: headers,
|
||||
body: json.encode({
|
||||
if (meta.type == "movie")
|
||||
'movies': [
|
||||
{
|
||||
'ids': {
|
||||
'imdb': meta.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
if (meta.type == "shows")
|
||||
'shows': [
|
||||
{
|
||||
'ids': {
|
||||
'imdb': meta.id,
|
||||
},
|
||||
}
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
_logger.severe(
|
||||
'Failed to remove item from watchlist: ${response.statusCode}');
|
||||
throw Exception('Failed to remove item from watchlist');
|
||||
}
|
||||
|
||||
_cache.remove('$_baseUrl/sync/watchlist');
|
||||
|
||||
refetchKey.add(["watchlist"]);
|
||||
|
||||
_logger.info('Successfully removed item from watchlist');
|
||||
} catch (e, stack) {
|
||||
_logger.severe('Error removing item from watchlist: $e', stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
_logger.info('Clearing cache');
|
||||
_cache.clear();
|
||||
|
|
@ -165,41 +251,37 @@ class TraktService {
|
|||
},
|
||||
};
|
||||
} else {
|
||||
final Map<String, dynamic> episodeExternalIds =
|
||||
meta.episodeExternalIds ?? {};
|
||||
|
||||
final isEmpty = episodeExternalIds.keys.isEmpty;
|
||||
|
||||
if (!isEmpty) {
|
||||
return {
|
||||
"episode": {
|
||||
"ids": meta.episodeExternalIds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (meta.currentVideo?.id != null) {
|
||||
if (meta.currentVideo?.tvdbId != null) {
|
||||
return {
|
||||
"episode": {
|
||||
"ids": {
|
||||
"imdb": meta.currentVideo?.id,
|
||||
"tvdb": meta.currentVideo?.tvdbId!,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (meta.currentVideo?.season != null &&
|
||||
meta.currentVideo?.episode != null) {
|
||||
return {
|
||||
"episode": {
|
||||
"season": meta.currentVideo!.season,
|
||||
"episode": meta.currentVideo!.episode,
|
||||
},
|
||||
"show": {
|
||||
"ids": {
|
||||
"imdb": meta.imdbId ?? meta.id,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
"show": {
|
||||
"title": meta.name,
|
||||
"year": meta.year,
|
||||
"ids": {
|
||||
"imdb": meta.imdbId ?? meta.id,
|
||||
}
|
||||
},
|
||||
"episode": {
|
||||
"season": meta.currentVideo?.season ?? meta.nextSeason,
|
||||
"number": meta.currentVideo?.number ?? meta.nextEpisode,
|
||||
},
|
||||
"ids": {
|
||||
"imdb": meta.currentVideo?.id ?? meta.id,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -218,21 +300,14 @@ class TraktService {
|
|||
|
||||
try {
|
||||
_logger.info('Fetching up next series');
|
||||
final List<dynamic> watchedShows =
|
||||
await _makeRequest('$_baseUrl/sync/watched/shows');
|
||||
final List<dynamic> watchedShows = await _makeRequest(
|
||||
'$_baseUrl/sync/watched/shows',
|
||||
);
|
||||
|
||||
final startIndex = (page - 1) * itemsPerPage;
|
||||
final endIndex = startIndex + itemsPerPage;
|
||||
|
||||
final items = watchedShows.where((show) {
|
||||
try {
|
||||
show['show']['ids']['trakt'];
|
||||
show['show']['ids']['imdb'];
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
final items = watchedShows.toList();
|
||||
|
||||
if (startIndex >= items.length) {
|
||||
yield [];
|
||||
|
|
@ -260,23 +335,17 @@ class TraktService {
|
|||
Meta(
|
||||
type: "series",
|
||||
id: imdb,
|
||||
externalIds: show['show']['ids'],
|
||||
episodeExternalIds: nextEpisode['ids'],
|
||||
),
|
||||
);
|
||||
|
||||
item as Meta;
|
||||
|
||||
return item.copyWith(
|
||||
nextEpisode: nextEpisode['number'],
|
||||
nextSeason: nextEpisode['season'],
|
||||
nextEpisodeTitle: nextEpisode['title'],
|
||||
externalIds: show['show']['ids'],
|
||||
episodeExternalIds: nextEpisode['ids'],
|
||||
);
|
||||
return patchMetaObjectForShow(item as Meta, nextEpisode);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe('Error fetching progress for show $showId: $e');
|
||||
} catch (e, stack) {
|
||||
_logger.severe(
|
||||
'Error fetching progress for show $showId: $e',
|
||||
e,
|
||||
stack,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -295,8 +364,89 @@ class TraktService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<LibraryItem>> getContinueWatching(
|
||||
{int page = 1, int itemsPerPage = 5}) async {
|
||||
Meta patchMetaObjectForShow(
|
||||
Meta meta,
|
||||
dynamic obj, {
|
||||
double? progress,
|
||||
}) {
|
||||
if (meta.videos?.isEmpty == true) {
|
||||
meta.videos = [];
|
||||
meta.videos?.add(
|
||||
Video(
|
||||
season: obj['season'],
|
||||
number: obj['number'],
|
||||
thumbnail: meta.poster,
|
||||
id: _traktIdsToMetaId(obj['ids']),
|
||||
),
|
||||
);
|
||||
return meta;
|
||||
}
|
||||
|
||||
final videoIndexByTvDB = meta.videos?.firstWhereOrNull((item) {
|
||||
return item.tvdbId == obj['ids']['tvdb'] && item.tvdbId != null;
|
||||
});
|
||||
|
||||
final videoBySeasonOrEpisode = meta.videos?.firstWhereOrNull((item) {
|
||||
return item.season == obj['season'] && item.episode == obj['number'];
|
||||
});
|
||||
|
||||
final video = videoIndexByTvDB ?? videoBySeasonOrEpisode;
|
||||
|
||||
if (video == null) {
|
||||
final id = _traktIdsToMetaId(obj['ids']);
|
||||
|
||||
meta.videos = meta.videos ?? [];
|
||||
|
||||
meta.videos?.add(
|
||||
Video(
|
||||
name: obj['title'],
|
||||
season: obj['season'],
|
||||
number: obj['number'],
|
||||
thumbnail: meta.poster,
|
||||
id: id,
|
||||
episode: obj['number'],
|
||||
),
|
||||
);
|
||||
|
||||
final videosIndex = meta.videos?.length ?? 1;
|
||||
|
||||
return meta.copyWith(
|
||||
selectedVideoIndex: videosIndex - 1,
|
||||
);
|
||||
}
|
||||
|
||||
final index = meta.videos?.indexOf(video);
|
||||
|
||||
meta.videos![index!].name = obj['title'];
|
||||
meta.videos![index].tvdbId =
|
||||
meta.videos![index].tvdbId ?? obj['ids']['tvdb'];
|
||||
meta.videos![index].ids = obj['ids'];
|
||||
|
||||
return meta.copyWith(
|
||||
selectedVideoIndex: index,
|
||||
);
|
||||
}
|
||||
|
||||
String _traktIdsToMetaId(dynamic ids) {
|
||||
String id;
|
||||
|
||||
if (ids['imdb'] != null) {
|
||||
id = ids['imdb'];
|
||||
} else if (ids['tmdb'] != null) {
|
||||
id = "tmdb:${ids['tmdb']}";
|
||||
} else if (ids['trakt']) {
|
||||
id = "trakt:${ids['trakt']}";
|
||||
} else {
|
||||
id = "na";
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
Future<List<LibraryItem>> getContinueWatching({
|
||||
int page = 1,
|
||||
int itemsPerPage = 5,
|
||||
}) async {
|
||||
await initStremioService();
|
||||
|
||||
if (!isEnabled()) {
|
||||
|
|
@ -306,61 +456,65 @@ class TraktService {
|
|||
|
||||
try {
|
||||
_logger.info('Fetching continue watching');
|
||||
final continueWatching = await _makeRequest('$_baseUrl/sync/playback');
|
||||
|
||||
final Map<String, double> progress = {};
|
||||
|
||||
final metaList = continueWatching
|
||||
.map((movie) {
|
||||
try {
|
||||
if (movie['type'] == 'episode') {
|
||||
progress[movie['show']['ids']['imdb']] = movie['progress'];
|
||||
|
||||
return Meta(
|
||||
type: "series",
|
||||
id: movie['show']['ids']['imdb'],
|
||||
progress: movie['progress'],
|
||||
nextSeason: movie['episode']['season'],
|
||||
nextEpisode: movie['episode']['number'],
|
||||
nextEpisodeTitle: movie['episode']['title'],
|
||||
externalIds: movie['show']['ids'],
|
||||
episodeExternalIds: movie['episode']['ids'],
|
||||
);
|
||||
}
|
||||
|
||||
final imdb = movie['movie']['ids']['imdb'];
|
||||
progress[imdb] = movie['progress'];
|
||||
|
||||
return Meta(
|
||||
type: "movie",
|
||||
id: imdb,
|
||||
progress: movie['progress'],
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.warning('Error mapping movie: $e');
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereType<Meta>()
|
||||
.toList();
|
||||
final List<dynamic> continueWatching =
|
||||
await _makeRequest('$_baseUrl/sync/playback');
|
||||
|
||||
final startIndex = (page - 1) * itemsPerPage;
|
||||
final endIndex = startIndex + itemsPerPage;
|
||||
|
||||
if (startIndex >= metaList.length) {
|
||||
if (startIndex >= continueWatching.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final result = await stremioService!.getBulkItem(
|
||||
metaList
|
||||
.sublist(
|
||||
startIndex,
|
||||
endIndex > metaList.length ? metaList.length : endIndex,
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
final metaList = (await Future.wait(continueWatching
|
||||
.sublist(
|
||||
startIndex,
|
||||
endIndex,
|
||||
)
|
||||
.map((movie) async {
|
||||
try {
|
||||
if (movie['type'] == 'episode') {
|
||||
final meta = Meta(
|
||||
type: "series",
|
||||
id: _traktIdsToMetaId(
|
||||
movie['show']['ids'],
|
||||
),
|
||||
);
|
||||
|
||||
return result;
|
||||
return patchMetaObjectForShow(
|
||||
(await stremioService!.getItemById(meta) as Meta),
|
||||
movie['episode'],
|
||||
).copyWith(
|
||||
forceRegular: true,
|
||||
progress: movie['progress'],
|
||||
traktProgressId: movie['id'],
|
||||
);
|
||||
}
|
||||
|
||||
final movieId = _traktIdsToMetaId(movie['movie']['ids']);
|
||||
|
||||
final meta = Meta(
|
||||
type: "movie",
|
||||
id: movieId,
|
||||
);
|
||||
|
||||
return ((await stremioService!.getItemById(meta)) as Meta).copyWith(
|
||||
progress: movie['progress'],
|
||||
traktProgressId: movie['id'],
|
||||
);
|
||||
} catch (e, stack) {
|
||||
_logger.warning(
|
||||
'Error mapping movie: $e',
|
||||
e,
|
||||
stack,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
})))
|
||||
.whereType<Meta>()
|
||||
.toList();
|
||||
|
||||
return metaList;
|
||||
} catch (e, stack) {
|
||||
_logger.severe('Error fetching continue watching: $e', stack);
|
||||
return [];
|
||||
|
|
@ -384,36 +538,46 @@ class TraktService {
|
|||
'$_baseUrl/calendars/my/shows/${DateFormat('yyyy-MM-dd').format(DateTime.now())}/7',
|
||||
);
|
||||
|
||||
final result = await stremioService!.getBulkItem(
|
||||
scheduleShows
|
||||
.map((show) {
|
||||
try {
|
||||
final imdb = show['show']['ids']['imdb'];
|
||||
return Meta(
|
||||
type: "series",
|
||||
id: imdb,
|
||||
externalIds: show['show']['ids'],
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.warning('Error mapping show: $e');
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereType<Meta>()
|
||||
.toList(),
|
||||
);
|
||||
|
||||
final startIndex = (page - 1) * itemsPerPage;
|
||||
final endIndex = startIndex + itemsPerPage;
|
||||
|
||||
if (startIndex >= result.length) {
|
||||
if (startIndex >= scheduleShows.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.sublist(
|
||||
final result = (await Future.wait(scheduleShows
|
||||
.sublist(
|
||||
startIndex,
|
||||
endIndex > result.length ? result.length : endIndex,
|
||||
);
|
||||
endIndex > scheduleShows.length ? scheduleShows.length : endIndex,
|
||||
)
|
||||
.map((show) async {
|
||||
try {
|
||||
final imdb = _traktIdsToMetaId(
|
||||
show['show']['ids'],
|
||||
);
|
||||
|
||||
final result = Meta(
|
||||
type: "series",
|
||||
id: imdb,
|
||||
);
|
||||
|
||||
final item = await stremioService!.getItemById(result);
|
||||
|
||||
return patchMetaObjectForShow(
|
||||
(item ?? result) as Meta,
|
||||
show['episode'],
|
||||
).copyWith(
|
||||
progress: null,
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.warning('Error mapping show: $e');
|
||||
return null;
|
||||
}
|
||||
})))
|
||||
.whereType<Meta>()
|
||||
.toList();
|
||||
|
||||
return result;
|
||||
} catch (e, stack) {
|
||||
_logger.severe('Error fetching upcoming schedule: $e', stack);
|
||||
return [];
|
||||
|
|
@ -431,15 +595,29 @@ class TraktService {
|
|||
|
||||
try {
|
||||
_logger.info('Fetching watchlist');
|
||||
final watchlistItems = await _makeRequest('$_baseUrl/sync/watchlist');
|
||||
final List<dynamic> watchlistItems =
|
||||
await _makeRequest('$_baseUrl/sync/watchlist');
|
||||
_logger.info('Got watchlist');
|
||||
|
||||
final startIndex = (page - 1) * itemsPerPage;
|
||||
final endIndex = startIndex + itemsPerPage;
|
||||
|
||||
if (startIndex >= watchlistItems.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final result = await stremioService!.getBulkItem(
|
||||
watchlistItems
|
||||
.sublist(
|
||||
startIndex,
|
||||
endIndex > watchlistItems.length
|
||||
? watchlistItems.length
|
||||
: endIndex,
|
||||
)
|
||||
.map((item) {
|
||||
try {
|
||||
final type = item['type'];
|
||||
final imdb = item[type]['ids']['imdb'];
|
||||
final imdb = _traktIdsToMetaId(item[type]['ids']);
|
||||
|
||||
if (type == "show") {
|
||||
return Meta(
|
||||
|
|
@ -452,8 +630,8 @@ class TraktService {
|
|||
type: type,
|
||||
id: imdb,
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.warning('Error mapping watchlist item: $e');
|
||||
} catch (e, stack) {
|
||||
_logger.warning('Error mapping watchlist item: $e', e, stack);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
|
@ -461,25 +639,17 @@ class TraktService {
|
|||
.toList(),
|
||||
);
|
||||
|
||||
final startIndex = (page - 1) * itemsPerPage;
|
||||
final endIndex = startIndex + itemsPerPage;
|
||||
|
||||
if (startIndex >= result.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.sublist(
|
||||
startIndex,
|
||||
endIndex > result.length ? result.length : endIndex,
|
||||
);
|
||||
return result;
|
||||
} catch (e, stack) {
|
||||
_logger.severe('Error fetching watchlist: $e', stack);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<LibraryItem>> getShowRecommendations(
|
||||
{int page = 1, int itemsPerPage = 5}) async {
|
||||
Future<List<LibraryItem>> getShowRecommendations({
|
||||
int page = 1,
|
||||
int itemsPerPage = 5,
|
||||
}) async {
|
||||
await initStremioService();
|
||||
|
||||
if (!isEnabled()) {
|
||||
|
|
@ -489,17 +659,27 @@ class TraktService {
|
|||
|
||||
try {
|
||||
_logger.info('Fetching show recommendations');
|
||||
final recommendedShows =
|
||||
await _makeRequest('$_baseUrl/recommendations/shows');
|
||||
final List<dynamic> recommendedShows = await _makeRequest(
|
||||
'$_baseUrl/recommendations/shows',
|
||||
);
|
||||
|
||||
final startIndex = (page - 1) * itemsPerPage;
|
||||
final endIndex = startIndex + itemsPerPage;
|
||||
|
||||
if (startIndex >= recommendedShows.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final result = (await stremioService!.getBulkItem(
|
||||
recommendedShows
|
||||
.sublist(
|
||||
startIndex,
|
||||
endIndex > recommendedShows.length
|
||||
? recommendedShows.length
|
||||
: endIndex,
|
||||
)
|
||||
.map((show) {
|
||||
final imdb = show['ids']?['imdb'];
|
||||
|
||||
if (imdb == null) {
|
||||
return null;
|
||||
}
|
||||
final imdb = _traktIdsToMetaId(show['ids']);
|
||||
|
||||
return Meta(
|
||||
type: "series",
|
||||
|
|
@ -510,18 +690,7 @@ class TraktService {
|
|||
.toList(),
|
||||
));
|
||||
|
||||
// Pagination logic
|
||||
final startIndex = (page - 1) * itemsPerPage;
|
||||
final endIndex = startIndex + itemsPerPage;
|
||||
|
||||
if (startIndex >= result.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.sublist(
|
||||
startIndex,
|
||||
endIndex > result.length ? result.length : endIndex,
|
||||
);
|
||||
return result;
|
||||
} catch (e, stack) {
|
||||
_logger.severe('Error fetching show recommendations: $e', stack);
|
||||
return [];
|
||||
|
|
@ -541,15 +710,28 @@ class TraktService {
|
|||
|
||||
try {
|
||||
_logger.info('Fetching movie recommendations');
|
||||
final recommendedMovies = await _makeRequest(
|
||||
final List<dynamic> recommendedMovies = await _makeRequest(
|
||||
'$_baseUrl/recommendations/movies',
|
||||
);
|
||||
|
||||
final startIndex = (page - 1) * itemsPerPage;
|
||||
final endIndex = startIndex + itemsPerPage;
|
||||
|
||||
if (startIndex >= recommendedMovies.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final result = await stremioService!.getBulkItem(
|
||||
recommendedMovies
|
||||
.sublist(
|
||||
startIndex,
|
||||
endIndex > recommendedMovies.length
|
||||
? recommendedMovies.length
|
||||
: endIndex,
|
||||
)
|
||||
.map((movie) {
|
||||
try {
|
||||
final imdb = movie['ids']['imdb'];
|
||||
final imdb = _traktIdsToMetaId(movie['ids']);
|
||||
return Meta(
|
||||
type: "movie",
|
||||
id: imdb,
|
||||
|
|
@ -563,18 +745,7 @@ class TraktService {
|
|||
.toList(),
|
||||
);
|
||||
|
||||
// Pagination logic
|
||||
final startIndex = (page - 1) * itemsPerPage;
|
||||
final endIndex = startIndex + itemsPerPage;
|
||||
|
||||
if (startIndex >= result.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.sublist(
|
||||
startIndex,
|
||||
endIndex > result.length ? result.length : endIndex,
|
||||
);
|
||||
return result;
|
||||
} catch (e, stack) {
|
||||
_logger.severe('Error fetching movie recommendations: $e', stack);
|
||||
return [];
|
||||
|
|
@ -794,58 +965,49 @@ class TraktService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<TraktProgress>> getProgress(Meta meta) async {
|
||||
Future<Meta> getProgress(
|
||||
Meta meta, {
|
||||
bool bypassCache = true,
|
||||
}) async {
|
||||
if (!isEnabled()) {
|
||||
_logger.info('Trakt integration is not enabled');
|
||||
return [];
|
||||
return meta;
|
||||
}
|
||||
|
||||
try {
|
||||
if (meta.type == "series") {
|
||||
final body = await _makeRequest(
|
||||
final List<dynamic> body = await _makeRequest(
|
||||
"$_baseUrl/sync/playback/episodes",
|
||||
bypassCache: true,
|
||||
bypassCache: bypassCache,
|
||||
);
|
||||
|
||||
final List<TraktProgress> result = [];
|
||||
|
||||
for (final item in body) {
|
||||
if (item["type"] != "episode") {
|
||||
final isCurrentShow =
|
||||
item["show"]?["ids"]?["imdb"] == (meta.imdbId ?? meta.id);
|
||||
|
||||
if (isCurrentShow == false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final isShow =
|
||||
item["show"]?["ids"]?["imdb"] == (meta.imdbId ?? meta.id);
|
||||
|
||||
final currentEpisode = item["episode"]["number"];
|
||||
final currentSeason = item["episode"]["season"];
|
||||
|
||||
if (isShow && meta.nextEpisode != null && meta.nextSeason != null) {
|
||||
if (meta.nextSeason == currentSeason &&
|
||||
meta.nextEpisode == currentEpisode) {
|
||||
result.add(
|
||||
TraktProgress(
|
||||
id: meta.id,
|
||||
progress: item["progress"]!,
|
||||
episode: currentEpisode,
|
||||
season: currentSeason,
|
||||
traktId: item["show"]["ids"]["trakt"],
|
||||
),
|
||||
);
|
||||
final result = meta.videos?.firstWhereOrNull((video) {
|
||||
if (video.tvdbId != null &&
|
||||
item['episode']['ids']['tvdb'] != null) {
|
||||
return video.tvdbId == item['episode']['ids']['tvdb'];
|
||||
}
|
||||
} else if (isShow) {
|
||||
result.add(
|
||||
TraktProgress(
|
||||
id: meta.id,
|
||||
progress: item["progress"]!,
|
||||
episode: currentEpisode,
|
||||
season: currentSeason,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return video.season == item['season'] &&
|
||||
video.number == item['number'];
|
||||
});
|
||||
|
||||
if (result == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final videoIndex = meta.videos!.indexOf(result);
|
||||
|
||||
meta.videos![videoIndex].progress = item['progress'];
|
||||
}
|
||||
return meta;
|
||||
} else {
|
||||
final body = await _makeRequest(
|
||||
"$_baseUrl/sync/playback/movies",
|
||||
|
|
@ -858,20 +1020,17 @@ class TraktService {
|
|||
}
|
||||
|
||||
if (item["movie"]["ids"]["imdb"] == (meta.imdbId ?? meta.id)) {
|
||||
return [
|
||||
TraktProgress(
|
||||
id: item["movie"]["ids"]["imdb"],
|
||||
progress: item["progress"],
|
||||
),
|
||||
];
|
||||
return meta.copyWith(
|
||||
progress: item["progress"],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe('Error fetching progress: $e');
|
||||
return [];
|
||||
return meta;
|
||||
}
|
||||
|
||||
return [];
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class _StremioItemPageState extends State<StremioItemPage> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (widget.meta?.progress != null || widget.meta?.nextEpisode != null) {
|
||||
if (widget.meta?.currentVideo != null) {
|
||||
openVideo();
|
||||
}
|
||||
}
|
||||
|
|
@ -78,11 +78,6 @@ class _StremioItemPageState extends State<StremioItemPage> {
|
|||
);
|
||||
|
||||
if (mounted) {
|
||||
final videoEpisode =
|
||||
widget.meta?.currentVideo?.episode ?? widget.meta?.nextEpisode;
|
||||
final videoSeason =
|
||||
widget.meta?.currentVideo?.season ?? widget.meta?.nextSeason;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
|
|
@ -95,7 +90,7 @@ class _StremioItemPageState extends State<StremioItemPage> {
|
|||
icon: const Icon(Icons.close),
|
||||
),
|
||||
title: Text(
|
||||
"Streams ${videoSeason != null ? "S$videoSeason" : ""} ${videoEpisode != null ? " E$videoEpisode" : ""}"
|
||||
"Streams S${widget.meta?.currentVideo?.season ?? 0} E${widget.meta?.currentVideo?.episode ?? 0}"
|
||||
.trim(),
|
||||
),
|
||||
),
|
||||
|
|
@ -108,8 +103,6 @@ class _StremioItemPageState extends State<StremioItemPage> {
|
|||
: null,
|
||||
service: widget.service!,
|
||||
id: widget.meta as LibraryItem,
|
||||
season: videoSeason?.toString(),
|
||||
episode: videoEpisode?.toString(),
|
||||
shouldPop: false,
|
||||
),
|
||||
),
|
||||
|
|
@ -161,7 +154,8 @@ class _StremioItemPageState extends State<StremioItemPage> {
|
|||
|
||||
return StremioItemViewer(
|
||||
hero: widget.hero,
|
||||
meta: meta ?? widget.meta,
|
||||
meta: (meta ?? widget.meta)
|
||||
?.copyWith(selectedVideoIndex: widget.meta?.selectedVideoIndex),
|
||||
original: meta,
|
||||
progress: widget.meta?.progress != null ? widget.meta!.progress : 0,
|
||||
service: state.data == null
|
||||
|
|
|
|||
|
|
@ -8,3 +8,16 @@ extension FirstWhereOrNullExtension<T> on Iterable<T> {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extension LastWhereOrNullExtension<T> on Iterable<T> {
|
||||
T? lastWhereOrNull(bool Function(T) test) {
|
||||
T? elementItem;
|
||||
|
||||
for (var element in this) {
|
||||
if (test(element)) {
|
||||
elementItem = element;
|
||||
}
|
||||
}
|
||||
return elementItem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue