Project import generated by Copybara.

GitOrigin-RevId: 478aa0586cb2eea16867f7ca78cdf7913e5d3795
This commit is contained in:
Madari Developers 2025-01-05 13:46:39 +00:00
parent c38cd8400a
commit e8e1c5e046
12 changed files with 576 additions and 412 deletions

View file

@ -35,4 +35,8 @@ class WatchHistoryQueries extends DatabaseAccessor<AppDatabase>
return (select(watchHistoryTable)..where((t) => t.id.equals(id)))
.getSingleOrNull();
}
Future<void> clearWatchHistory() async {
await delete(watchHistoryTable).go();
}
}

View file

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

View file

@ -90,11 +90,12 @@ abstract class BaseConnectionService {
Future<LibraryItem?> getItemById(LibraryItem id);
Stream<List<StreamList>> getStreams(
Future<void> 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,
});
}

View file

@ -17,6 +17,8 @@ part 'stremio_connection_service.g.dart';
final Map<String, String> manifestCache = {};
typedef OnStreamCallback = void Function(List<StreamList>? items, Error?);
class StremioConnectionService extends BaseConnectionService {
final StremioConfig config;
@ -224,47 +226,43 @@ class StremioConnectionService extends BaseConnectionService {
}
@override
Stream<List<StreamList>> getStreams(
Future<void> getStreams(
LibraryRecord library,
LibraryItem id, {
String? season,
String? episode,
}) async* {
OnStreamCallback? callback,
}) async {
final List<StreamList> streams = [];
final meta = id as Meta;
final List<Future<void>> promises = [];
for (final addon in config.addons) {
final addonManifest = await _getManifest(addon);
final future = Future.delayed(const Duration(seconds: 0), () async {
final addonManifest = await _getManifest(addon);
for (final _resource in (addonManifest.resources ?? [])) {
final resource = _resource as ResourceObject;
for (final resource_ in (addonManifest.resources ?? [])) {
final resource = resource_ as ResourceObject;
if (resource.name != "stream") {
continue;
}
if (!doesAddonSupportStream(resource, addonManifest, meta)) {
continue;
}
final idPrefixes = resource.idPrefixes ?? addonManifest.idPrefixes;
final types = resource.types ?? addonManifest.types;
if (types == null || !types.contains(meta.type)) {
continue;
}
final hasIdPrefix = (idPrefixes ?? []).where(
(item) => meta.id.startsWith(item),
);
if (hasIdPrefix.isEmpty) {
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<StreamList>()
.toList(),
);
} catch (e) {
continue;
}
if (streams.isNotEmpty) yield streams;
}
if (callback != null) {
callback(streams, null);
}
}
}).catchError((error) {
if (callback != null) callback(null, error);
});
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()

View file

@ -243,7 +243,7 @@ class Meta extends LibraryItem {
@JsonKey(name: "genre")
final List<String>? 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<String>? genres;
@JsonKey(name: "releaseInfo")
final String? releaseInfo;
final dynamic releaseInfo_;
@JsonKey(name: "trailerStreams")
final List<TrailerStream>? 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,

View file

@ -161,108 +161,160 @@ class _RenderStreamListState extends State<RenderStreamList> {
);
}
bool hasError = false;
bool isLoading = true;
List<StreamList>? _list;
final List<Error> errors = [];
final Map<String, StreamSource> _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];
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 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;
}
return;
}
PlaybackConfig config = getPlaybackConfig();
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;
}
}
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,
);
}
}

View file

@ -96,252 +96,259 @@ class _StremioItemViewerState extends State<StremioItemViewer> {
}
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,
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 Icon(
Icons.play_arrow_rounded,
size: 24,
color: Colors.black87,
),
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,
),
),
),
],
),
),
child: Row(
),
flexibleSpace: FlexibleSpaceBar(
background: Stack(
fit: StackFit.expand,
children: [
Expanded(
child: Text(
item!.name!,
style: Theme.of(context).textTheme.titleLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
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),
],
),
),
),
const SizedBox(width: 16),
ElevatedButton.icon(
icon: _isLoading
? Container(
margin: const EdgeInsets.only(right: 6),
child: const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(),
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 Icon(
Icons.play_arrow_rounded,
size: 24,
color: Colors.black87,
),
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,
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),
),
],
),
],
),
],
),
),
],
),
),
),
],
),
),
),
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),
],
),
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,
),
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),
),
],
),
],
),
],
),
),
],
),
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!),
]),
// 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(),
),
// Trailers
if (item!.trailerStreams != null &&
item!.trailerStreams!.isNotEmpty)
_buildTrailersSection(context, item!.trailerStreams!),
]),
),
),
),
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)
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!),
]),
// 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(),
),
// Trailers
if (item!.trailerStreams != null &&
item!.trailerStreams!.isNotEmpty)
_buildTrailersSection(context, item!.trailerStreams!),
]),
),
),
],
],
),
),
);
}

View file

@ -104,12 +104,20 @@ class _VideoViewerState extends State<VideoViewer> {
}
}
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;
}
}
}
}

View file

@ -25,6 +25,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
String _defaultSubtitleTrack = 'eng';
bool _enableExternalPlayer = true;
String? _defaultPlayerId;
bool _disabledSubtitle = false;
Map<String, String> _availableLanguages = {};
@ -62,6 +63,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
playbackConfig.externalPlayerId?.containsKey(currentPlatform) == true
? playbackConfig.externalPlayerId![currentPlatform]
: null;
_disabledSubtitle = playbackConfig.disableSubtitle;
}
@override
@ -72,8 +74,10 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
void _debouncedSave() {
_saveDebouncer?.cancel();
_saveDebouncer =
Timer(const Duration(milliseconds: 500), _savePlaybackSettings);
_saveDebouncer = Timer(
const Duration(milliseconds: 500),
_savePlaybackSettings,
);
}
Future<void> _savePlaybackSettings() async {
@ -98,6 +102,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
'defaultSubtitleTrack': _defaultSubtitleTrack,
'externalPlayer': _enableExternalPlayer,
'externalPlayerId': extranalId,
'disableSubtitle': _disabledSubtitle,
},
};
@ -182,6 +187,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
],
),
),
const Divider(),
ListTile(
title: const Text('Default Audio Track'),
trailing: DropdownButton<String>(
@ -195,19 +201,29 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
},
),
),
ListTile(
title: const Text('Default Subtitle Track'),
trailing: DropdownButton<String>(
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<String>(
value: _defaultSubtitleTrack,
items: dropdown,
onChanged: (value) {
if (value != null) {
setState(() => _defaultSubtitleTrack = value);
_debouncedSave();
}
},
),
),
const Divider(),
if (!isWeb)
SwitchListTile(
title: const Text('External Player'),

View file

@ -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<AuthStoreEvent> _listener;
Future clear() async {
(await _prefs).remove(_lastSyncTimeKey);
await db.watchHistoryQueries.clearWatchHistory();
}
ZeeeWatchHistory() {

View file

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

View file

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