diff --git a/lib/features/connections/widget/stremio/stremio_season_selector.dart b/lib/features/connections/widget/stremio/stremio_season_selector.dart index dd7c46b..d2ffd21 100644 --- a/lib/features/connections/widget/stremio/stremio_season_selector.dart +++ b/lib/features/connections/widget/stremio/stremio_season_selector.dart @@ -34,13 +34,13 @@ class StremioItemSeasonSelector extends StatefulWidget { class _StremioItemSeasonSelectorState extends State with SingleTickerProviderStateMixin { int? selectedSeason; - late TabController? _tabController; late final Map> seasonMap; final zeeeWatchHistory = ZeeeWatchHistoryStatic.service; late Meta meta = widget.meta; final Map _progress = {}; + Map> watchedEpisodesBySeason = {}; @override void initState() { @@ -51,26 +51,64 @@ class _StremioItemSeasonSelectorState extends State if (seasonMap.keys.isEmpty) { return; } + if (seasonMap.isNotEmpty) { + final seasons = seasonMap.keys.toList()..sort(); + int initialSeason = getSelectedSeason(); - final index = getSelectedSeason(); - - _tabController = TabController( - length: seasonMap.keys.length, - vsync: this, - initialIndex: index.clamp( - 0, - seasonMap.keys.isNotEmpty ? seasonMap.keys.length - 1 : 0, - ), - ); - - // This is for rendering the component again for the selection of another tab - _tabController!.addListener(() { - setState(() {}); - }); + if (seasons.contains(initialSeason)) { + // Check if initialSeason is in seasons + selectedSeason = initialSeason; + } else if (seasons.isNotEmpty) { + selectedSeason = seasons.first; // Or any other default if not found + } + } getWatchHistory(); + getWatchedHistory(); } +getWatchedHistory() async { + final traktService = TraktService.instance; + try { + final result = + await traktService!.getWatchedShowsWithEpisodes(widget.meta); + watchedEpisodesBySeason.clear(); + for (final show in result) { + if (show.episodes != null) { + for (final episode in show.episodes!) { + if (!watchedEpisodesBySeason.containsKey(episode.season)) { + watchedEpisodesBySeason[episode.season] = {}; + } + watchedEpisodesBySeason[episode.season]!.add(episode.episode); + } + } else { + //print("No episodes found for ${show.title}"); + } + } + + setState(() {}); + return; + } catch (e, stack) { + print("Error fetching Trakt data: $e"); + print("Stack Trace: $stack"); + } +} +bool isEpisodeWatched(int season, int episode) { + return watchedEpisodesBySeason.containsKey(season) && + watchedEpisodesBySeason[season]!.contains(episode); +} + +bool isSeasonWatched(int season) { + if (!watchedEpisodesBySeason.containsKey(season)) { + return false; // No episodes watched in this season + } + if (seasonMap.containsKey(season)) { + return watchedEpisodesBySeason[season]!.length == + seasonMap[season]!.length; + } + return false; +} + int getSelectedSeason() { return widget.meta.currentVideo?.season ?? widget.meta.videos?.lastWhereOrNull((item) { @@ -94,8 +132,6 @@ class _StremioItemSeasonSelectorState extends State meta = result; }); - final index = getSelectedSeason(); - _tabController?.animateTo(index); return; } @@ -119,14 +155,10 @@ class _StremioItemSeasonSelectorState extends State _progress[item.id] = item.progress.toDouble(); } - final index = getSelectedSeason(); - - _tabController?.animateTo(index); } @override void dispose() { - _tabController?.dispose(); super.dispose(); } @@ -190,22 +222,15 @@ class _StremioItemSeasonSelectorState extends State final isWideScreen = screenWidth > 900; final contentWidth = isWideScreen ? 900.0 : screenWidth; - if (_tabController == null) { + if (seasonMap.keys.isEmpty) { return const SliverMainAxisGroup( slivers: [ SliverToBoxAdapter( - child: Column( - children: [ - SizedBox( - height: 0, - ) - ], - ), + child: Center(child: Text("No seasons available")), ), ], ); } - return SliverMainAxisGroup( slivers: [ SliverPadding( @@ -215,6 +240,7 @@ class _StremioItemSeasonSelectorState extends State sliver: SliverToBoxAdapter( child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox( height: 12, @@ -236,25 +262,47 @@ class _StremioItemSeasonSelectorState extends State ), ), ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: colorScheme.surface.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: TabBar( - tabAlignment: TabAlignment.start, - dividerColor: Colors.transparent, - controller: _tabController, - isScrollable: true, - splashBorderRadius: BorderRadius.circular(8), - padding: const EdgeInsets.all(4), - tabs: seasons.map((season) { - return Tab( - text: season == 0 ? "Specials" : 'Season $season', - height: 40, - ); - }).toList(), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 120), + child: DropdownButtonFormField( + isExpanded: true, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 2, vertical: 8), + filled: true, + fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.3), + ), + + value: selectedSeason, + onChanged: (newValue) { + setState(() { + selectedSeason = newValue!; + }); + }, + items: seasons.map((season) { + final isWatched = isSeasonWatched(season); + return DropdownMenuItem( + value: season, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text(season == 0 ? "Specials" : 'Season $season'), + if (isWatched) ...[ + const SizedBox(width: 4), + Icon( + Icons.check_circle, + color: Theme.of(context).colorScheme.primary, + size: 16, + + ) + ] + ], + ), + ); + }).toList(), + ), ), ), const SizedBox(height: 16), @@ -268,19 +316,23 @@ class _StremioItemSeasonSelectorState extends State ), sliver: SliverList( delegate: SliverChildBuilderDelegate( - (context, index) { - final currentSeason = seasons[_tabController!.index]; + (context, index) { + final currentSeason = selectedSeason; + if (currentSeason == null || + !seasonMap.containsKey(currentSeason)) { + return const Center(child: Text("Select a season")); + } final episodes = seasonMap[currentSeason]!; final episode = episodes[index]; final videoIndex = meta.videos?.indexOf(episode); final progress = ((!TraktService.isEnabled() - ? (_progress[episode.id] ?? 0) / 100 - : videoIndex != -1 - ? (meta.videos![videoIndex!].progress) - : 0.toDouble()) ?? - 0) / + ? (_progress[episode.id] ?? 0) / 100 + : videoIndex != -1 + ? (meta.videos![videoIndex!].progress) + : 0.toDouble()) ?? + 0) / 100; return InkWell( @@ -307,37 +359,37 @@ class _StremioItemSeasonSelectorState extends State children: [ Container( child: episode.thumbnail != null && - episode.thumbnail!.isNotEmpty + episode.thumbnail!.isNotEmpty ? Image.network( - episode.thumbnail!, - width: 140, - height: 90, - fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) { - return Container( - width: 140, - height: 90, - color: colorScheme - .surfaceContainerHighest, - child: Icon( - Icons.movie, - color: - colorScheme.onSurfaceVariant, - ), - ); - }, - ) - : Container( - width: 140, - height: 90, + episode.thumbnail!, + width: 140, + height: 90, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) { + return Container( + width: 140, + height: 90, + color: colorScheme + .surfaceContainerHighest, + child: Icon( + Icons.movie, color: - colorScheme.surfaceContainerHighest, - child: Icon( - Icons.movie, - color: colorScheme.onSurfaceVariant, - ), + colorScheme.onSurfaceVariant, ), + ); + }, + ) + : Container( + width: 140, + height: 90, + color: + colorScheme.surfaceContainerHighest, + child: Icon( + Icons.movie, + color: colorScheme.onSurfaceVariant, + ), + ), ), Positioned( top: 0, @@ -359,7 +411,8 @@ class _StremioItemSeasonSelectorState extends State ], ), ), - if (progress > .9) + if (isEpisodeWatched( + currentSeason, episode.episode!)) Positioned( bottom: 0, right: 0, @@ -368,7 +421,7 @@ class _StremioItemSeasonSelectorState extends State child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: Colors.teal, + color: Colors.grey.shade900, ), child: Padding( padding: const EdgeInsets.only( @@ -378,14 +431,15 @@ class _StremioItemSeasonSelectorState extends State top: 2.0, ), child: Center( - child: Text( - "Watched", - style: Theme.of(context) + child: Icon( + Icons.done_all, + size: Theme.of(context) .textTheme - .bodySmall - ?.copyWith( - color: Colors.black, - ), + .bodyLarge! + .fontSize, + color: Theme.of(context) + .colorScheme + .primary, // Use primary color from theme ), ), ), @@ -448,8 +502,10 @@ class _StremioItemSeasonSelectorState extends State ), ); }, - childCount: - seasonMap[seasons[_tabController!.index]]?.length ?? 0, + childCount: selectedSeason != null && + seasonMap.containsKey(selectedSeason!) + ? seasonMap[selectedSeason!]!.length + : 0, ), ), ), diff --git a/lib/features/trakt/service/trakt.service.dart b/lib/features/trakt/service/trakt.service.dart index 25b49b5..0c3b637 100644 --- a/lib/features/trakt/service/trakt.service.dart +++ b/lib/features/trakt/service/trakt.service.dart @@ -15,6 +15,7 @@ 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'); @@ -1078,4 +1079,81 @@ class TraktService { return meta; } + Future> getWatchedShowsWithEpisodes(Meta meta) async { + if (!isEnabled()) { + _logger.info('Trakt integration is not enabled'); + return []; + } + if (meta.type == "series" ) { + final watchedShows = await getWatchedShows(); + for (final show in watchedShows) { + if(show.ids.imdb==meta.imdbId) { + // await Future.delayed(const Duration(seconds: 5)); + show.episodes = await _getWatchedEpisodes(show.ids.trakt); + } + } + return watchedShows; + } + return []; + } + Future> getWatchedShows() async { + if (!isEnabled()) { + _logger.info('Trakt integration is not enabled'); + return []; + } + try { + final body = await _makeRequest( + "$_baseUrl/sync/watched/shows/", + bypassCache: true, + ); + final List result = []; + for (final item in body) { + try { + result.add( + TraktShowWatched( + title: item["show"]["title"], + seasons: item["seasons"], + ids: TraktIds.fromJson(item["show"]["ids"]), + lastWatchedAt: item["last_watched_at"] != null ? DateTime.parse(item["last_watched_at"]) : null, + plays: item["plays"], + ), + ); + + } catch (e, stack) { + _logger.warning('Error parsing watched show: $e\n$stack item: $item'); + } + } + return result; + } catch (e, stack) { + _logger.severe('Error fetching watched shows: $e\n$stack'); + return []; + } + } + Future> _getWatchedEpisodes(int? traktId) async { + if (traktId == null) return []; + int page = 1; + const int limit = 1000; + try { + final body = await _makeRequest( + "$_baseUrl/sync/history/shows/$traktId?page=$page&limit=$limit", + bypassCache: true, + ); + final List episodes = []; + for (final item in body) { + if (item['episode'] != null ) { + episodes.add( + TraktEpisodeWatched( + season: item['episode']['season'], + episode: item['episode']['number'], + watchedAt: DateTime.parse(item['watched_at']), + ), + ); + } + } + return episodes; + } catch (e, stack) { + _logger.severe('Error fetching watched episodes: $e\n$stack'); + return []; + } + } } diff --git a/lib/features/trakt/types/common.dart b/lib/features/trakt/types/common.dart index 492fffd..06d30e7 100644 --- a/lib/features/trakt/types/common.dart +++ b/lib/features/trakt/types/common.dart @@ -13,3 +13,54 @@ class TraktProgress { this.traktId, }); } + +class TraktShowWatched { + final String title; + final List seasons; + final TraktIds ids; + final DateTime? lastWatchedAt; + final int plays; + List? episodes; // Add episodes list + + TraktShowWatched({ + required this.title, + required this.seasons, + required this.ids, + this.lastWatchedAt, + required this.plays, + this.episodes, + }); +} + +class TraktIds { + final int? trakt; + final String? slug; + final String? imdb; + final int? tmdb; + + TraktIds({ + this.trakt, + this.slug, + this.imdb, + this.tmdb, + }); + + factory TraktIds.fromJson(Map json) => TraktIds( + trakt: json['trakt'], + slug: json['slug'], + imdb: json['imdb'], + tmdb: json['tmdb'], + ); +} + +class TraktEpisodeWatched { + final int season; + final int episode; + final DateTime watchedAt; + + TraktEpisodeWatched({ + required this.season, + required this.episode, + required this.watchedAt, + }); +} \ No newline at end of file