diff --git a/lib/database/quries/watch_history_queries.dart b/lib/database/quries/watch_history_queries.dart index a6ff6a7..ba44d1e 100644 --- a/lib/database/quries/watch_history_queries.dart +++ b/lib/database/quries/watch_history_queries.dart @@ -35,4 +35,8 @@ class WatchHistoryQueries extends DatabaseAccessor return (select(watchHistoryTable)..where((t) => t.id.equals(id))) .getSingleOrNull(); } + + Future clearWatchHistory() async { + await delete(watchHistoryTable).go(); + } } diff --git a/lib/engine/engine.dart b/lib/engine/engine.dart index 0f40860..31a70d9 100644 --- a/lib/engine/engine.dart +++ b/lib/engine/engine.dart @@ -35,8 +35,7 @@ class AppEngine { AppEngine(AuthStore authStore) { pb = PocketBase( - // 'https://zeee.fly.dev' ?? - (kDebugMode ? 'http://100.64.0.1:8090' : 'https://zeee.fly.dev'), + (kDebugMode ? 'http://100.64.0.1:8090' : 'https://api.madari.media'), authStore: authStore, ); _databaseProvider = DatabaseProvider(); diff --git a/lib/features/connections/service/base_connection_service.dart b/lib/features/connections/service/base_connection_service.dart index 9a59ad7..823eb8d 100644 --- a/lib/features/connections/service/base_connection_service.dart +++ b/lib/features/connections/service/base_connection_service.dart @@ -90,11 +90,12 @@ abstract class BaseConnectionService { Future getItemById(LibraryItem id); - Stream> getStreams( + Future getStreams( LibraryRecord library, LibraryItem id, { String? season, String? episode, + OnStreamCallback? callback, }); BaseConnectionService({ @@ -106,11 +107,23 @@ class StreamList { final String title; final String? description; final DocSource source; + final StreamSource? streamSource; StreamList({ required this.title, this.description, required this.source, + this.streamSource, + }); +} + +class StreamSource { + final String title; + final String id; + + StreamSource({ + required this.title, + required this.id, }); } diff --git a/lib/features/connections/service/stremio_connection_service.dart b/lib/features/connections/service/stremio_connection_service.dart index 8325f34..467012f 100644 --- a/lib/features/connections/service/stremio_connection_service.dart +++ b/lib/features/connections/service/stremio_connection_service.dart @@ -17,6 +17,8 @@ part 'stremio_connection_service.g.dart'; final Map manifestCache = {}; +typedef OnStreamCallback = void Function(List? items, Error?); + class StremioConnectionService extends BaseConnectionService { final StremioConfig config; @@ -224,47 +226,43 @@ class StremioConnectionService extends BaseConnectionService { } @override - Stream> getStreams( + Future getStreams( LibraryRecord library, LibraryItem id, { String? season, String? episode, - }) async* { + OnStreamCallback? callback, + }) async { final List streams = []; final meta = id as Meta; - for (final addon in config.addons) { - final addonManifest = await _getManifest(addon); - - for (final _resource in (addonManifest.resources ?? [])) { - final resource = _resource as ResourceObject; - - if (resource.name != "stream") { - continue; - } - - final idPrefixes = resource.idPrefixes ?? addonManifest.idPrefixes; - final types = resource.types ?? addonManifest.types; + final List> promises = []; - if (types == null || !types.contains(meta.type)) { - continue; - } + for (final addon in config.addons) { + final future = Future.delayed(const Duration(seconds: 0), () async { + final addonManifest = await _getManifest(addon); - final hasIdPrefix = (idPrefixes ?? []).where( - (item) => meta.id.startsWith(item), - ); + for (final resource_ in (addonManifest.resources ?? [])) { + final resource = resource_ as ResourceObject; - if (hasIdPrefix.isEmpty) { - continue; - } + if (!doesAddonSupportStream(resource, addonManifest, meta)) { + continue; + } - try { final url = "${_getAddonBaseURL(addon)}/stream/${meta.type}/${Uri.encodeComponent(id.id)}.json"; final result = await http.get(Uri.parse(url), headers: {}); if (result.statusCode == 404) { + if (callback != null) { + callback( + null, + ArgumentError( + "Invalid status code for the addon ${addonManifest.name} with id ${addonManifest.id}", + ), + ); + } continue; } @@ -272,72 +270,126 @@ class StremioConnectionService extends BaseConnectionService { streams.addAll( body.streams - .map((item) { - String streamTitle = item.title ?? item.name ?? "No title"; - - try { - streamTitle = utf8.decode( - (item.title ?? item.name ?? "No Title").runes.toList(), - ); - } catch (e) {} - - final streamDescription = item.description != null - ? utf8.decode( - (item.description!).runes.toList(), - ) - : null; - - String title = meta.name ?? item.title ?? "No title"; - - if (season != null) title += " S$season"; - if (episode != null) title += " E$episode"; - - DocSource? source; - - if (item.url != null) { - source = MediaURLSource( - title: title, - url: item.url!, - id: meta.id, - ); - } - - if (item.infoHash != null) { - source = TorrentSource( - title: title, - infoHash: item.infoHash!, - id: meta.id, - fileName: "$title.mp4", - season: season, - episode: episode, - ); - } - - if (source == null) { - return null; - } - - return StreamList( - title: streamTitle, - description: streamDescription, - source: source, - ); - }) + .map( + (item) => videoStreamToStreamList( + item, meta, season, episode, addonManifest), + ) .whereType() .toList(), ); - } catch (e) { - continue; + + if (callback != null) { + callback(streams, null); + } } + }).catchError((error) { + if (callback != null) callback(null, error); + }); - if (streams.isNotEmpty) yield streams; - } + promises.add(future); } - yield streams; + await Future.wait(promises); return; } + + bool doesAddonSupportStream( + ResourceObject resource, + StremioManifest addonManifest, + Meta meta, + ) { + if (resource.name != "stream") { + return false; + } + + final idPrefixes = resource.idPrefixes ?? addonManifest.idPrefixes; + final types = resource.types ?? addonManifest.types; + + if (types == null || !types.contains(meta.type)) { + return false; + } + + final hasIdPrefix = (idPrefixes ?? []).where( + (item) => meta.id.startsWith(item), + ); + + if (hasIdPrefix.isEmpty) { + return false; + } + + return true; + } + + StreamList? videoStreamToStreamList( + VideoStream item, + Meta meta, + String? season, + String? episode, + StremioManifest addonManifest, + ) { + String streamTitle = + (item.name != null ? "${item.name} ${item.title}" : item.title) ?? + "No title"; + + try { + streamTitle = utf8.decode(streamTitle.runes.toList()); + } catch (e) {} + + final streamDescription = item.description != null + ? utf8.decode( + (item.description!).runes.toList(), + ) + : null; + + String title = meta.name ?? item.title ?? "No title"; + + if (season != null) title += " S$season"; + if (episode != null) title += " E$episode"; + + DocSource? source; + + if (item.url != null) { + source = MediaURLSource( + title: title, + url: item.url!, + id: meta.id, + ); + } + + if (item.infoHash != null) { + source = TorrentSource( + title: title, + infoHash: item.infoHash!, + id: meta.id, + fileName: "$title.mp4", + season: season, + episode: episode, + ); + } + + if (source == null) { + return null; + } + + String addonName = addonManifest.name; + + try { + addonName = utf8.decode( + (addonName).runes.toList(), + ); + } catch (e) {} + + return StreamList( + title: streamTitle, + description: streamDescription, + source: source, + streamSource: StreamSource( + title: addonName, + id: addonManifest.id, + ), + ); + } } @JsonSerializable() diff --git a/lib/features/connections/types/stremio/stremio_base.types.dart b/lib/features/connections/types/stremio/stremio_base.types.dart index bc0842c..a2467f4 100644 --- a/lib/features/connections/types/stremio/stremio_base.types.dart +++ b/lib/features/connections/types/stremio/stremio_base.types.dart @@ -243,7 +243,7 @@ class Meta extends LibraryItem { @JsonKey(name: "genre") final List? genre; @JsonKey(name: "imdbRating") - final String? imdbRating; + final dynamic imdbRating_; @JsonKey(name: "poster") String? poster; @JsonKey(name: "released") @@ -281,7 +281,7 @@ class Meta extends LibraryItem { @JsonKey(name: "genres") final List? genres; @JsonKey(name: "releaseInfo") - final String? releaseInfo; + final dynamic releaseInfo_; @JsonKey(name: "trailerStreams") final List? trailerStreams; @JsonKey(name: "links") @@ -297,6 +297,14 @@ class Meta extends LibraryItem { @JsonKey(name: "dvdRelease") final DateTime? dvdRelease; + String get imdbRating { + return (imdbRating_ ?? "").toString(); + } + + String get releaseInfo { + return (releaseInfo_).toString(); + } + Meta({ this.imdbId, this.name, @@ -306,7 +314,7 @@ class Meta extends LibraryItem { this.country, this.description, this.genre, - this.imdbRating, + this.imdbRating_, this.poster, this.released, this.slug, @@ -325,7 +333,7 @@ class Meta extends LibraryItem { required this.id, this.videos, this.genres, - this.releaseInfo, + this.releaseInfo_, this.trailerStreams, this.links, this.behaviorHints, @@ -381,7 +389,7 @@ class Meta extends LibraryItem { country: country ?? this.country, description: description ?? this.description, genre: genre ?? this.genre, - imdbRating: imdbRating ?? this.imdbRating, + imdbRating_: imdbRating ?? imdbRating_.toString(), poster: poster ?? this.poster, released: released ?? this.released, slug: slug ?? this.slug, @@ -400,7 +408,7 @@ class Meta extends LibraryItem { id: id ?? this.id, videos: videos ?? this.videos, genres: genres ?? this.genres, - releaseInfo: releaseInfo ?? this.releaseInfo, + releaseInfo_: releaseInfo ?? this.releaseInfo, trailerStreams: trailerStreams ?? this.trailerStreams, links: links ?? this.links, behaviorHints: behaviorHints ?? this.behaviorHints, diff --git a/lib/features/connections/widget/base/render_stream_list.dart b/lib/features/connections/widget/base/render_stream_list.dart index d39b288..e41d14f 100644 --- a/lib/features/connections/widget/base/render_stream_list.dart +++ b/lib/features/connections/widget/base/render_stream_list.dart @@ -161,108 +161,160 @@ class _RenderStreamListState extends State { ); } + bool hasError = false; + bool isLoading = true; + List? _list; + + final List errors = []; + + final Map _sources = {}; + Future getLibrary() async { final library = await BaseConnectionService.getLibraries(); - setState(() { - _stream = widget.service.getStreams( - library.data.firstWhere((i) => i.id == widget.library), - widget.id, - episode: widget.episode, - season: widget.season, - ); - }); + final result = await widget.service.getStreams( + library.data.firstWhere((i) => i.id == widget.library), + widget.id, + episode: widget.episode, + season: widget.season, + callback: (items, error) { + if (mounted) { + setState(() { + isLoading = false; + _list = items; + + _list?.forEach((item) { + if (item.streamSource != null) { + _sources[item.streamSource!.id] = item.streamSource!; + } + }); + }); + } + }, + ); + + if (mounted) { + setState(() { + isLoading = false; + _list = _list ?? []; + }); + } } + String? selectedAddonFilter; + @override Widget build(BuildContext context) { - if (_stream == null) { + if (isLoading || _list == null) { return const Center( child: CircularProgressIndicator(), ); } - return StreamBuilder( - stream: _stream, - builder: (BuildContext context, snapshot) { - if (snapshot.hasError && (snapshot.data?.isEmpty ?? true) == true) { - print(snapshot.error); - print(snapshot.stackTrace); - return Text("Error: ${snapshot.error}"); - } + if (hasError) { + return const Text("Something went wrong"); + } - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), - ); - } + if ((_list ?? []).isEmpty) { + return Center( + child: Text( + "No stream found", + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + } - if (snapshot.data?.isEmpty == true && - snapshot.connectionState == ConnectionState.done) { - return Center( - child: Text( - "No stream found", - style: Theme.of(context).textTheme.bodyLarge, + final filteredList = (_list ?? []).where((item) { + if (item.streamSource == null || selectedAddonFilter == null) { + return true; + } + + return item.streamSource!.id == selectedAddonFilter; + }).toList(); + + return ListView.builder( + itemBuilder: (context, index) { + if (index == 0) { + return SizedBox( + height: 42, + width: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + right: 12.0, + ), + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (final value in _sources.values) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ChoiceChip( + selected: value.id == selectedAddonFilter, + label: Text(value.title), + onSelected: (i) { + setState(() { + selectedAddonFilter = i ? value.id : null; + }); + }, + ), + ), + ], + ), ), ); } - return ListView.builder( - itemBuilder: (context, index) { - final item = snapshot.data![index]; - - return ListTile( - title: Text(item.title), - subtitle: - item.description == null ? null : Text(item.description!), - trailing: (item.source is MediaURLSource) - ? _buildDownloadButton( - context, - (item.source as MediaURLSource).url, - item.title, - ) - : null, - onTap: () { - if (widget.shouldPop) { - Navigator.of(context).pop(item.source); - - return; - } - - PlaybackConfig config = getPlaybackConfig(); - - if (config.externalPlayer) { - if (!kIsWeb) { - if (item.source is URLSource || - item.source is TorrentSource) { - if (config.externalPlayer && Platform.isAndroid) { - openVideoUrlInExternalPlayerAndroid( - videoUrl: (item.source as URLSource).url, - playerPackage: config.currentPlayerPackage, - ); - return; - } - } + final item = filteredList[index - 1]; + + return ListTile( + title: Text(item.title), + subtitle: item.description == null ? null : Text(item.description!), + trailing: (item.source is MediaURLSource) + ? _buildDownloadButton( + context, + (item.source as MediaURLSource).url, + item.title, + ) + : null, + onTap: () { + if (widget.shouldPop) { + Navigator.of(context).pop(item.source); + + return; + } + + PlaybackConfig config = getPlaybackConfig(); + + if (config.externalPlayer) { + if (!kIsWeb) { + if (item.source is URLSource || item.source is TorrentSource) { + if (config.externalPlayer && Platform.isAndroid) { + openVideoUrlInExternalPlayerAndroid( + videoUrl: (item.source as URLSource).url, + playerPackage: config.currentPlayerPackage, + ); + return; } } - - Navigator.of(context).push( - MaterialPageRoute( - builder: (ctx) => DocViewer( - source: item.source, - service: widget.service, - library: widget.library, - meta: widget.id, - season: widget.season, - ), - ), - ); - }, + } + } + + Navigator.of(context).push( + MaterialPageRoute( + builder: (ctx) => DocViewer( + source: item.source, + service: widget.service, + library: widget.library, + meta: widget.id, + season: widget.season, + ), + ), ); }, - itemCount: snapshot.data!.length, ); }, + itemCount: filteredList.length + 1, ); } } diff --git a/lib/features/connections/widget/stremio/stremio_item_viewer.dart b/lib/features/connections/widget/stremio/stremio_item_viewer.dart index a8f497e..2d4ce90 100644 --- a/lib/features/connections/widget/stremio/stremio_item_viewer.dart +++ b/lib/features/connections/widget/stremio/stremio_item_viewer.dart @@ -96,252 +96,259 @@ class _StremioItemViewerState extends State { } return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - expandedHeight: isWideScreen ? 600 : 500, - pinned: true, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(40), - child: Container( - width: double.infinity, - color: Colors.black, - padding: EdgeInsets.symmetric( - horizontal: - isWideScreen ? (screenWidth - contentWidth) / 2 : 16, - vertical: 16, - ), - child: Row( - children: [ - Expanded( - child: Text( - item!.name!, - style: Theme.of(context).textTheme.titleLarge, - maxLines: 1, - overflow: TextOverflow.ellipsis, + body: SafeArea( + child: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: isWideScreen ? 600 : 500, + pinned: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(40), + child: Container( + width: double.infinity, + color: Colors.black, + padding: EdgeInsets.symmetric( + horizontal: + isWideScreen ? (screenWidth - contentWidth) / 2 : 16, + vertical: 16, + ), + child: Row( + children: [ + Expanded( + child: Text( + item!.name!, + style: Theme.of(context).textTheme.titleLarge, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - ), - const SizedBox(width: 16), - ElevatedButton.icon( - icon: _isLoading - ? Container( - margin: const EdgeInsets.only(right: 6), - child: const SizedBox( - width: 12, - height: 12, - child: CircularProgressIndicator(), + const SizedBox(width: 16), + ElevatedButton.icon( + icon: _isLoading + ? Container( + margin: const EdgeInsets.only(right: 6), + child: const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator(), + ), + ) + : const Icon( + Icons.play_arrow_rounded, + size: 24, + color: Colors.black87, ), - ) - : const Icon( - Icons.play_arrow_rounded, - size: 24, - color: Colors.black87, - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - ), - onPressed: () { - if (item!.type == "series" && _isLoading) { - return; - } + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + ), + onPressed: () { + if (item!.type == "series" && _isLoading) { + return; + } - _onPlayPressed(context); - }, - label: Text( - "Play", - style: Theme.of(context) - .primaryTextTheme - .bodyMedium - ?.copyWith( - color: Colors.black87, - ), + _onPlayPressed(context); + }, + label: Text( + "Play", + style: Theme.of(context) + .primaryTextTheme + .bodyMedium + ?.copyWith( + color: Colors.black87, + ), + ), ), - ), - ], + ], + ), ), ), - ), - flexibleSpace: FlexibleSpaceBar( - background: Stack( - fit: StackFit.expand, - children: [ - if (item!.background != null) - Image.network( - item!.background!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - if (item!.poster == null) { - return Container(); - } - return Image.network(item!.poster!, fit: BoxFit.cover); - }, - ), - DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.transparent, - Colors.black.withOpacity(0.8), - ], + flexibleSpace: FlexibleSpaceBar( + background: Stack( + fit: StackFit.expand, + children: [ + if (item!.background != null) + Image.network( + item!.background!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + if (item!.poster == null) { + return Container(); + } + return Image.network(item!.poster!, + fit: BoxFit.cover); + }, ), - ), - ), - Positioned( - bottom: 86, - left: 16, - right: 16, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: isWideScreen - ? (screenWidth - contentWidth) / 2 - : 16, - vertical: 16, + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.8), + ], + ), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Hero( - tag: "${widget.hero}", - child: Container( - width: 150, - height: 225, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - image: item!.poster == null - ? null - : DecorationImage( - image: NetworkImage(item!.poster!), - fit: BoxFit.cover, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - spreadRadius: 2, - blurRadius: 8, + ), + Positioned( + bottom: 86, + left: 16, + right: 16, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: isWideScreen + ? (screenWidth - contentWidth) / 2 + : 16, + vertical: 16, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Hero( + tag: "${widget.hero}", + child: Container( + width: 150, + height: 225, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + image: item!.poster == null + ? null + : DecorationImage( + image: NetworkImage(item!.poster!), + fit: BoxFit.cover, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + spreadRadius: 2, + blurRadius: 8, + ), + ], + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (item!.year != null) + Chip( + label: Text(item!.year!), + backgroundColor: Colors.white24, + labelStyle: const TextStyle( + color: Colors.white), + ), + const SizedBox(width: 8), + if (item!.imdbRating != null) + Row( + children: [ + const Icon( + Icons.star, + color: Colors.amber, + size: 20, + ), + const SizedBox(width: 4), + Text( + item!.imdbRating!, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: Colors.white), + ), + ], + ), + ], ), ], ), ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (item!.year != null) - Chip( - label: Text(item!.year!), - backgroundColor: Colors.white24, - labelStyle: const TextStyle( - color: Colors.white), - ), - const SizedBox(width: 8), - if (item!.imdbRating != null) - Row( - children: [ - const Icon( - Icons.star, - color: Colors.amber, - size: 20, - ), - const SizedBox(width: 4), - Text( - item!.imdbRating!, - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Colors.white), - ), - ], - ), - ], - ), - ], - ), - ), - ], + ], + ), ), ), - ), - ], + ], + ), ), ), - ), - if (widget.original != null && - widget.original?.type == "series" && - widget.original?.videos?.isNotEmpty == true) - StremioItemSeasonSelector( - meta: item!, - library: widget.library, - service: widget.service, - ), - SliverPadding( - padding: EdgeInsets.symmetric( - horizontal: isWideScreen ? (screenWidth - contentWidth) / 2 : 16, - vertical: 16, - ), - sliver: SliverList( - delegate: SliverChildListDelegate([ - if (widget.original != null) - const SizedBox( - height: 12, - ), - // Description - Text( - 'Description', - style: Theme.of(context).textTheme.titleLarge, - ), - if (item!.description != null) const SizedBox(height: 8), - if (item!.description != null) + if (widget.original != null && + widget.original?.type == "series" && + widget.original?.videos?.isNotEmpty == true) + StremioItemSeasonSelector( + meta: item!, + library: widget.library, + service: widget.service, + ), + SliverPadding( + padding: EdgeInsets.symmetric( + horizontal: + isWideScreen ? (screenWidth - contentWidth) / 2 : 16, + vertical: 16, + ), + sliver: SliverList( + delegate: SliverChildListDelegate([ + if (widget.original != null) + const SizedBox( + height: 12, + ), + // Description Text( - item!.description!, - style: Theme.of(context).textTheme.bodyMedium, + 'Description', + style: Theme.of(context).textTheme.titleLarge, ), - const SizedBox(height: 16), + if (item!.description != null) const SizedBox(height: 8), + if (item!.description != null) + Text( + item!.description!, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 16), - // Additional Details - _buildDetailSection(context, 'Additional Information', [ - if (item!.genre != null) - _buildDetailRow('Genres', item!.genre!.join(', ')), - if (item!.country != null) - _buildDetailRow('Country', item!.country!), - if (item!.runtime != null) - _buildDetailRow('Runtime', item!.runtime!), - if (item!.language != null) - _buildDetailRow('Language', item!.language!), - ]), + // Additional Details + _buildDetailSection(context, 'Additional Information', [ + if (item!.genre != null) + _buildDetailRow('Genres', item!.genre!.join(', ')), + if (item!.country != null) + _buildDetailRow('Country', item!.country!), + if (item!.runtime != null) + _buildDetailRow('Runtime', item!.runtime!), + if (item!.language != null) + _buildDetailRow('Language', item!.language!), + ]), - // Cast - if (item!.creditsCast != null && item!.creditsCast!.isNotEmpty) - _buildCastSection(context, item!.creditsCast!), + // Cast + if (item!.creditsCast != null && + item!.creditsCast!.isNotEmpty) + _buildCastSection(context, item!.creditsCast!), - // Cast - if (item!.creditsCrew != null && item!.creditsCrew!.isNotEmpty) - _buildCastSection( - context, - title: "Crew", - item!.creditsCrew!.map((item) { - return CreditsCast( - character: item.department, - name: item.name, - profilePath: item.profilePath, - id: item.id, - ); - }).toList(), - ), + // Cast + if (item!.creditsCrew != null && + item!.creditsCrew!.isNotEmpty) + _buildCastSection( + context, + title: "Crew", + item!.creditsCrew!.map((item) { + return CreditsCast( + character: item.department, + name: item.name, + profilePath: item.profilePath, + id: item.id, + ); + }).toList(), + ), - // Trailers - if (item!.trailerStreams != null && - item!.trailerStreams!.isNotEmpty) - _buildTrailersSection(context, item!.trailerStreams!), - ]), + // Trailers + if (item!.trailerStreams != null && + item!.trailerStreams!.isNotEmpty) + _buildTrailersSection(context, item!.trailerStreams!), + ]), + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/features/doc_viewer/container/video_viewer.dart b/lib/features/doc_viewer/container/video_viewer.dart index 41acf78..c8ab3c6 100644 --- a/lib/features/doc_viewer/container/video_viewer.dart +++ b/lib/features/doc_viewer/container/video_viewer.dart @@ -104,12 +104,20 @@ class _VideoViewerState extends State { } } - for (final item in tracks.subtitle) { - if (defaultSubtitle == item.id || - defaultSubtitle == item.language || - defaultSubtitle == item.title) { - controller.player.setSubtitleTrack(item); - break; + if (config.disableSubtitle) { + for (final item in tracks.subtitle) { + if (item.id == "no" || item.language == "no" || item.title == "no") { + controller.player.setSubtitleTrack(item); + } + } + } else { + for (final item in tracks.subtitle) { + if (defaultSubtitle == item.id || + defaultSubtitle == item.language || + defaultSubtitle == item.title) { + controller.player.setSubtitleTrack(item); + break; + } } } } diff --git a/lib/features/settings/screen/playback_settings_screen.dart b/lib/features/settings/screen/playback_settings_screen.dart index 5b14f6b..d29d50e 100644 --- a/lib/features/settings/screen/playback_settings_screen.dart +++ b/lib/features/settings/screen/playback_settings_screen.dart @@ -25,6 +25,7 @@ class _PlaybackSettingsScreenState extends State { String _defaultSubtitleTrack = 'eng'; bool _enableExternalPlayer = true; String? _defaultPlayerId; + bool _disabledSubtitle = false; Map _availableLanguages = {}; @@ -62,6 +63,7 @@ class _PlaybackSettingsScreenState extends State { playbackConfig.externalPlayerId?.containsKey(currentPlatform) == true ? playbackConfig.externalPlayerId![currentPlatform] : null; + _disabledSubtitle = playbackConfig.disableSubtitle; } @override @@ -72,8 +74,10 @@ class _PlaybackSettingsScreenState extends State { void _debouncedSave() { _saveDebouncer?.cancel(); - _saveDebouncer = - Timer(const Duration(milliseconds: 500), _savePlaybackSettings); + _saveDebouncer = Timer( + const Duration(milliseconds: 500), + _savePlaybackSettings, + ); } Future _savePlaybackSettings() async { @@ -98,6 +102,7 @@ class _PlaybackSettingsScreenState extends State { 'defaultSubtitleTrack': _defaultSubtitleTrack, 'externalPlayer': _enableExternalPlayer, 'externalPlayerId': extranalId, + 'disableSubtitle': _disabledSubtitle, }, }; @@ -182,6 +187,7 @@ class _PlaybackSettingsScreenState extends State { ], ), ), + const Divider(), ListTile( title: const Text('Default Audio Track'), trailing: DropdownButton( @@ -195,19 +201,29 @@ class _PlaybackSettingsScreenState extends State { }, ), ), - ListTile( - title: const Text('Default Subtitle Track'), - trailing: DropdownButton( - value: _defaultSubtitleTrack, - items: dropdown, - onChanged: (value) { - if (value != null) { - setState(() => _defaultSubtitleTrack = value); - _debouncedSave(); - } - }, - ), + SwitchListTile( + title: const Text('Disable Subtitle'), + value: _disabledSubtitle, + onChanged: (value) { + setState(() => _disabledSubtitle = value); + _debouncedSave(); + }, ), + if (!_disabledSubtitle) + ListTile( + title: const Text('Default Subtitle Track'), + trailing: DropdownButton( + value: _defaultSubtitleTrack, + items: dropdown, + onChanged: (value) { + if (value != null) { + setState(() => _defaultSubtitleTrack = value); + _debouncedSave(); + } + }, + ), + ), + const Divider(), if (!isWeb) SwitchListTile( title: const Text('External Player'), diff --git a/lib/features/watch_history/service/zeee_watch_history.dart b/lib/features/watch_history/service/zeee_watch_history.dart index 7614893..7aeaae1 100644 --- a/lib/features/watch_history/service/zeee_watch_history.dart +++ b/lib/features/watch_history/service/zeee_watch_history.dart @@ -30,11 +30,13 @@ class ZeeeWatchHistory extends BaseWatchHistory { Timer? _syncTimer; static const _lastSyncTimeKey = 'watch_history_last_sync_time'; final _prefs = SharedPreferences.getInstance(); + final db = AppEngine.engine.database; late final StreamSubscription _listener; Future clear() async { (await _prefs).remove(_lastSyncTimeKey); + await db.watchHistoryQueries.clearWatchHistory(); } ZeeeWatchHistory() { diff --git a/lib/utils/load_language.dart b/lib/utils/load_language.dart index f41d9bc..e850655 100644 --- a/lib/utils/load_language.dart +++ b/lib/utils/load_language.dart @@ -46,6 +46,8 @@ class PlaybackConfig { final String defaultAudioTrack; @JsonKey(defaultValue: "eng") final String defaultSubtitleTrack; + @JsonKey(defaultValue: false) + final bool disableSubtitle; @JsonKey(defaultValue: false) final bool externalPlayer; @@ -57,6 +59,7 @@ class PlaybackConfig { required this.defaultAudioTrack, required this.defaultSubtitleTrack, required this.externalPlayer, + required this.disableSubtitle, this.externalPlayerId, }); diff --git a/pubspec.yaml b/pubspec.yaml index 37f0e6f..66a4ed6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: madari_client description: "Madari Media Manager" publish_to: 'none' -version: 1.0.1+3 +version: 1.0.2+4 environment: sdk: ^3.5.3