mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-04-20 05:52:04 +00:00
Project import generated by Copybara.
GitOrigin-RevId: 478aa0586cb2eea16867f7ca78cdf7913e5d3795
This commit is contained in:
parent
c38cd8400a
commit
e8e1c5e046
12 changed files with 576 additions and 412 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue