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