mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-03-11 17:15:39 +00:00
fix: change video
This commit is contained in:
parent
f129eb7360
commit
481491d172
10 changed files with 830 additions and 455 deletions
|
|
@ -460,6 +460,13 @@ class Meta extends LibraryItem {
|
|||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$MetaToJson(this);
|
||||
|
||||
String toString() {
|
||||
if (currentVideo != null) {
|
||||
return "$name ${currentVideo!.name} S${currentVideo!.season} E${currentVideo!.episode}";
|
||||
}
|
||||
return name ?? "No name";
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ class StremioCard extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _StremioCardState extends State<StremioCard> {
|
||||
bool hasErrorWhileLoading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final meta = widget.item as Meta;
|
||||
|
|
@ -59,189 +57,8 @@ class _StremioCardState extends State<StremioCard> {
|
|||
);
|
||||
}
|
||||
|
||||
bool get isInFuture {
|
||||
final video = (widget.item as Meta).currentVideo;
|
||||
return video != null &&
|
||||
video.firstAired != null &&
|
||||
video.firstAired!.isAfter(DateTime.now());
|
||||
}
|
||||
|
||||
_buildWideCard(BuildContext context, Meta meta) {
|
||||
if (meta.background == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final video = meta.currentVideo;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: CachedNetworkImageProvider(
|
||||
"https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent(
|
||||
hasErrorWhileLoading
|
||||
? meta.background!
|
||||
: (meta.currentVideo?.thumbnail ?? meta.background!),
|
||||
)}@webp",
|
||||
errorListener: (error) {
|
||||
setState(() {
|
||||
hasErrorWhileLoading = true;
|
||||
});
|
||||
},
|
||||
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (isInFuture)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black,
|
||||
Colors.black54,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black,
|
||||
Colors.transparent,
|
||||
],
|
||||
begin: Alignment.bottomLeft,
|
||||
end: Alignment.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${meta.name}",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
"S${meta.currentVideo?.season} E${meta.currentVideo?.episode}",
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${meta.currentVideo?.name ?? meta.currentVideo?.title}"
|
||||
.trim(),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isInFuture)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
getRelativeDate(video!.firstAired!),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
if (isInFuture)
|
||||
const Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.calendar_month,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
child: Center(
|
||||
child: IconButton.filled(
|
||||
onPressed: null,
|
||||
icon: Icon(
|
||||
Icons.play_arrow,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
meta.imdbRating != ""
|
||||
? Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.star,
|
||||
color: Colors.amber,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
meta.imdbRating,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
);
|
||||
return WideCardStremio(meta: meta);
|
||||
}
|
||||
|
||||
String? getBackgroundImage(Meta meta) {
|
||||
|
|
@ -419,6 +236,210 @@ class _StremioCardState extends State<StremioCard> {
|
|||
}
|
||||
}
|
||||
|
||||
class WideCardStremio extends StatefulWidget {
|
||||
final Meta meta;
|
||||
final Video? video;
|
||||
|
||||
const WideCardStremio({
|
||||
super.key,
|
||||
required this.meta,
|
||||
this.video,
|
||||
});
|
||||
|
||||
@override
|
||||
State<WideCardStremio> createState() => _WideCardStremioState();
|
||||
}
|
||||
|
||||
class _WideCardStremioState extends State<WideCardStremio> {
|
||||
bool hasErrorWhileLoading = false;
|
||||
|
||||
bool get isInFuture {
|
||||
final video = widget.video ?? widget.meta.currentVideo;
|
||||
return video != null &&
|
||||
video.firstAired != null &&
|
||||
video.firstAired!.isAfter(DateTime.now());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.meta.background == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final video = widget.video ?? widget.meta.currentVideo;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: CachedNetworkImageProvider(
|
||||
"https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent(
|
||||
hasErrorWhileLoading
|
||||
? widget.meta.background!
|
||||
: (widget.meta.currentVideo?.thumbnail ??
|
||||
widget.meta.background!),
|
||||
)}@webp",
|
||||
errorListener: (error) {
|
||||
setState(() {
|
||||
hasErrorWhileLoading = true;
|
||||
});
|
||||
},
|
||||
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (isInFuture)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black,
|
||||
Colors.black54,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black,
|
||||
Colors.transparent,
|
||||
],
|
||||
begin: Alignment.bottomLeft,
|
||||
end: Alignment.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${widget.meta.name}",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
"S${video?.season} E${video?.episode}",
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${video?.name ?? video?.title}".trim(),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isInFuture)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
getRelativeDate(video!.firstAired!),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
if (isInFuture)
|
||||
const Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.calendar_month,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
child: Center(
|
||||
child: IconButton.filled(
|
||||
onPressed: null,
|
||||
icon: Icon(
|
||||
Icons.play_arrow,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
widget.meta.imdbRating != "" && widget.video == null
|
||||
? Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.star,
|
||||
color: Colors.amber,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.meta.imdbRating,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String getRelativeDate(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -12,7 +11,6 @@ import 'package:media_kit_video/media_kit_video.dart';
|
|||
|
||||
import '../../../utils/load_language.dart';
|
||||
import '../../connections/types/stremio/stremio_base.types.dart' as types;
|
||||
import '../../connections/widget/stremio/stremio_season_selector.dart';
|
||||
import '../../trakt/service/trakt.service.dart';
|
||||
import '../../watch_history/service/zeee_watch_history.dart';
|
||||
import '../types/doc_source.dart';
|
||||
|
|
@ -39,6 +37,8 @@ class VideoViewer extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _VideoViewerState extends State<VideoViewer> {
|
||||
late LibraryItem? meta = widget.meta;
|
||||
|
||||
final zeeeWatchHistory = ZeeeWatchHistoryStatic.service;
|
||||
Timer? _timer;
|
||||
late final Player player = Player(
|
||||
|
|
@ -81,18 +81,18 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (widget.meta is types.Meta && TraktService.instance != null) {
|
||||
if (meta is types.Meta && TraktService.instance != null) {
|
||||
try {
|
||||
if (player.state.playing) {
|
||||
_logger.info('Starting scrobbling...');
|
||||
await TraktService.instance!.startScrobbling(
|
||||
meta: widget.meta as types.Meta,
|
||||
meta: meta as types.Meta,
|
||||
progress: currentProgressInPercentage,
|
||||
);
|
||||
} else {
|
||||
_logger.info('Stopping scrobbling...');
|
||||
await TraktService.instance!.stopScrobbling(
|
||||
meta: widget.meta as types.Meta,
|
||||
meta: meta as types.Meta,
|
||||
progress: currentProgressInPercentage,
|
||||
);
|
||||
}
|
||||
|
|
@ -147,11 +147,11 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
|
||||
final progress = await traktProgress;
|
||||
|
||||
if (widget.meta is! types.Meta) {
|
||||
if (this.meta is! types.Meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
final meta = (progress ?? widget.meta) as types.Meta;
|
||||
final meta = (progress ?? this.meta) as types.Meta;
|
||||
|
||||
final duration = Duration(
|
||||
seconds: calculateSecondsFromProgress(
|
||||
|
|
@ -174,6 +174,54 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
|
||||
PlaybackConfig config = getPlaybackConfig();
|
||||
|
||||
Future setupVideoThings() async {
|
||||
_duration = player.stream.duration.listen((item) async {
|
||||
if (item.inSeconds != 0) {
|
||||
await saveWatchHistory();
|
||||
}
|
||||
});
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 30), (timer) {
|
||||
saveWatchHistory();
|
||||
});
|
||||
|
||||
_streamListen = player.stream.playing.listen((playing) {
|
||||
saveWatchHistory();
|
||||
});
|
||||
|
||||
return loadFile();
|
||||
}
|
||||
|
||||
destroyVideoThing() async {
|
||||
timeLoaded = false;
|
||||
gotFromTraktDuration = false;
|
||||
traktProgress = null;
|
||||
|
||||
for (final item in listener) {
|
||||
item.cancel();
|
||||
}
|
||||
_timer?.cancel();
|
||||
_streamListen?.cancel();
|
||||
_duration?.cancel();
|
||||
|
||||
if (meta is types.Meta && player.state.duration.inSeconds > 30) {
|
||||
await TraktService.instance!.stopScrobbling(
|
||||
meta: meta as types.Meta,
|
||||
progress: currentProgressInPercentage,
|
||||
shouldClearCache: true,
|
||||
traktId: traktId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GlobalKey videoKey = GlobalKey();
|
||||
|
||||
generateNewKey() {
|
||||
videoKey = GlobalKey();
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -184,58 +232,50 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
overlays: [],
|
||||
);
|
||||
|
||||
_duration = player.stream.duration.listen((item) async {
|
||||
if (item.inSeconds != 0) {
|
||||
await setDurationFromTrakt();
|
||||
await saveWatchHistory();
|
||||
}
|
||||
});
|
||||
|
||||
loadFile();
|
||||
|
||||
if (player.platform is NativePlayer && !kIsWeb) {
|
||||
Future.microtask(() async {
|
||||
await (player.platform as dynamic).setProperty('network-timeout', '60');
|
||||
});
|
||||
}
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 30), (timer) {
|
||||
saveWatchHistory();
|
||||
});
|
||||
|
||||
_streamListen = player.stream.playing.listen((playing) {
|
||||
saveWatchHistory();
|
||||
});
|
||||
|
||||
if (widget.meta is types.Meta && TraktService.isEnabled()) {
|
||||
traktProgress = TraktService.instance!.getProgress(
|
||||
widget.meta as types.Meta,
|
||||
);
|
||||
}
|
||||
onVideoChange(
|
||||
_source,
|
||||
widget.meta!,
|
||||
);
|
||||
}
|
||||
|
||||
loadFile() async {
|
||||
Future<void> loadFile() async {
|
||||
Duration duration = const Duration(seconds: 0);
|
||||
|
||||
if (meta is types.Meta && TraktService.isEnabled()) {
|
||||
_logger.info("Playing video ${(meta as types.Meta).selectedVideoIndex}");
|
||||
|
||||
traktProgress = TraktService.instance!.getProgress(
|
||||
meta as types.Meta,
|
||||
);
|
||||
} else {
|
||||
final item = await zeeeWatchHistory!.getItemWatchHistory(
|
||||
ids: [
|
||||
WatchHistoryGetRequest(
|
||||
id: _source.id,
|
||||
season: _source.season,
|
||||
episode: _source.episode,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
duration = Duration(
|
||||
seconds: item.isEmpty
|
||||
? 0
|
||||
: calculateSecondsFromProgress(
|
||||
item.first.duration,
|
||||
item.first.progress.toDouble(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_logger.info('Loading file for source: ${_source.id}');
|
||||
|
||||
final item = await zeeeWatchHistory!.getItemWatchHistory(
|
||||
ids: [
|
||||
WatchHistoryGetRequest(
|
||||
id: _source.id,
|
||||
season: _source.season,
|
||||
episode: _source.episode,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final duration = Duration(
|
||||
seconds: item.isEmpty
|
||||
? 0
|
||||
: calculateSecondsFromProgress(
|
||||
item.first.duration,
|
||||
item.first.progress.toDouble(),
|
||||
),
|
||||
);
|
||||
|
||||
switch (_source.runtimeType) {
|
||||
case const (FileSource):
|
||||
if (kIsWeb) {
|
||||
|
|
@ -262,41 +302,8 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
}
|
||||
}
|
||||
|
||||
late StreamSubscription<bool> _streamListen;
|
||||
late StreamSubscription<dynamic> _duration;
|
||||
|
||||
onLibrarySelect() async {
|
||||
_logger.info('Library selection triggered.');
|
||||
|
||||
controller.player.pause();
|
||||
|
||||
final result = await showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Seasons"),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
StremioItemSeasonSelector(
|
||||
service: widget.service,
|
||||
meta: widget.meta as types.Meta,
|
||||
shouldPop: true,
|
||||
season: int.tryParse(widget.currentSeason!),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result is MediaURLSource) {
|
||||
_source = result;
|
||||
|
||||
loadFile();
|
||||
}
|
||||
}
|
||||
late StreamSubscription<bool>? _streamListen;
|
||||
late StreamSubscription<dynamic>? _duration;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
|
@ -308,43 +315,42 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
for (final item in listener) {
|
||||
item.cancel();
|
||||
}
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
overlays: [],
|
||||
);
|
||||
_timer?.cancel();
|
||||
_streamListen.cancel();
|
||||
_duration.cancel();
|
||||
|
||||
if (widget.meta is types.Meta && player.state.duration.inSeconds > 30) {
|
||||
TraktService.instance!.stopScrobbling(
|
||||
meta: widget.meta as types.Meta,
|
||||
progress: currentProgressInPercentage,
|
||||
shouldClearCache: true,
|
||||
traktId: traktId,
|
||||
);
|
||||
}
|
||||
|
||||
destroyVideoThing();
|
||||
player.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
onVideoChange(DocSource source, LibraryItem item) async {
|
||||
_source = source;
|
||||
meta = item;
|
||||
|
||||
setState(() {});
|
||||
await destroyVideoThing();
|
||||
setState(() {});
|
||||
traktProgress = null;
|
||||
await setupVideoThings();
|
||||
await setDurationFromTrakt();
|
||||
setState(() {});
|
||||
generateNewKey();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: VideoViewerUi(
|
||||
key: videoKey,
|
||||
controller: controller,
|
||||
player: player,
|
||||
config: config,
|
||||
source: _source,
|
||||
onLibrarySelect: onLibrarySelect,
|
||||
title: _source.title,
|
||||
onLibrarySelect: () {},
|
||||
service: widget.service,
|
||||
meta: widget.meta,
|
||||
meta: meta,
|
||||
onSourceChange: (source, meta) => onVideoChange(source, meta),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@ import 'dart:io';
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:madari_client/features/connections/service/base_connection_service.dart';
|
||||
import 'package:madari_client/features/doc_viewer/container/video_viewer/season_source.dart';
|
||||
import 'package:madari_client/features/doc_viewer/container/video_viewer/torrent_stat.dart';
|
||||
import 'package:madari_client/features/doc_viewer/types/doc_source.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../../connections/types/stremio/stremio_base.types.dart';
|
||||
|
||||
MaterialDesktopVideoControlsThemeData getDesktopControls(
|
||||
BuildContext context, {
|
||||
required DocSource source,
|
||||
|
|
@ -16,6 +20,8 @@ MaterialDesktopVideoControlsThemeData getDesktopControls(
|
|||
Widget? library,
|
||||
required Function() onSubtitleSelect,
|
||||
required Function() onAudioSelect,
|
||||
LibraryItem? meta,
|
||||
required Function(int index) onVideoChange,
|
||||
}) {
|
||||
return MaterialDesktopVideoControlsThemeData(
|
||||
toggleFullscreenOnDoublePress: true,
|
||||
|
|
@ -34,14 +40,25 @@ MaterialDesktopVideoControlsThemeData getDesktopControls(
|
|||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width - 120,
|
||||
child: Text(
|
||||
source.title.endsWith(".mp4")
|
||||
? source.title.substring(0, source.title.length - 4)
|
||||
: source.title,
|
||||
(meta is Meta && meta.currentVideo != null)
|
||||
? "${meta.name ?? ""} S${meta.currentVideo?.season} E${meta.currentVideo?.episode}"
|
||||
: source.title.endsWith(".mp4")
|
||||
? source.title.substring(0, source.title.length - 4)
|
||||
: source.title,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (meta is Meta)
|
||||
if (meta.type == "series")
|
||||
SeasonSource(
|
||||
meta: meta,
|
||||
isMobile: false,
|
||||
player: player,
|
||||
onVideoChange: onVideoChange,
|
||||
),
|
||||
],
|
||||
bufferingIndicatorBuilder: source is TorrentSource
|
||||
? (ctx) {
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:madari_client/features/doc_viewer/container/video_viewer/torrent_stat.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
import '../../types/doc_source.dart';
|
||||
|
||||
MaterialVideoControlsThemeData getMobileVideoPlayer(
|
||||
BuildContext context, {
|
||||
required DocSource source,
|
||||
required Player player,
|
||||
required VoidCallback onSubtitleClick,
|
||||
required VoidCallback onAudioClick,
|
||||
required VoidCallback toggleScale,
|
||||
required VoidCallback onLibrarySelect,
|
||||
}) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
return MaterialVideoControlsThemeData(
|
||||
topButtonBar: [
|
||||
MaterialCustomButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_back,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
source.title.endsWith(".mp4")
|
||||
? source.title.substring(0, source.title.length - 4)
|
||||
: source.title,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
bufferingIndicatorBuilder: (source is TorrentSource)
|
||||
? (ctx) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: TorrentStats(
|
||||
torrentHash: (source).infoHash,
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
brightnessGesture: true,
|
||||
seekGesture: true,
|
||||
seekOnDoubleTap: true,
|
||||
gesturesEnabledWhileControlsVisible: true,
|
||||
shiftSubtitlesOnControlsVisibilityChange: true,
|
||||
seekBarMargin: const EdgeInsets.only(bottom: 54),
|
||||
speedUpOnLongPress: true,
|
||||
speedUpFactor: 2,
|
||||
volumeGesture: true,
|
||||
bottomButtonBar: [
|
||||
const MaterialPlayOrPauseButton(),
|
||||
const MaterialPositionIndicator(),
|
||||
const Spacer(),
|
||||
MaterialCustomButton(
|
||||
onPressed: () {
|
||||
final speeds = [
|
||||
0.5,
|
||||
0.75,
|
||||
1.0,
|
||||
1.25,
|
||||
1.5,
|
||||
1.75,
|
||||
2.0,
|
||||
2.25,
|
||||
2.5,
|
||||
3.0,
|
||||
3.25,
|
||||
3.5,
|
||||
3.75,
|
||||
4.0,
|
||||
4.25,
|
||||
4.5,
|
||||
4.75,
|
||||
5.0
|
||||
];
|
||||
showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (ctx) => Card(
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).size.height * 0.4,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dialogBackgroundColor,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Select Playback Speed',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: speeds.length,
|
||||
itemBuilder: (context, index) {
|
||||
final speed = speeds[index];
|
||||
return ListTile(
|
||||
title: Text('${speed}x'),
|
||||
selected: player.state.rate == speed,
|
||||
onTap: () {
|
||||
player.setRate(speed);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.speed),
|
||||
),
|
||||
MaterialCustomButton(
|
||||
onPressed: () {
|
||||
onSubtitleClick();
|
||||
},
|
||||
icon: const Icon(Icons.subtitles),
|
||||
),
|
||||
MaterialCustomButton(
|
||||
onPressed: () {
|
||||
onAudioClick();
|
||||
},
|
||||
icon: const Icon(Icons.audio_file),
|
||||
),
|
||||
MaterialCustomButton(
|
||||
onPressed: () {
|
||||
toggleScale();
|
||||
},
|
||||
icon: const Icon(Icons.fit_screen_outlined),
|
||||
),
|
||||
],
|
||||
topButtonBarMargin: EdgeInsets.only(
|
||||
top: mediaQuery.padding.top,
|
||||
),
|
||||
bottomButtonBarMargin: EdgeInsets.only(
|
||||
bottom: mediaQuery.viewInsets.bottom,
|
||||
left: 4.0,
|
||||
right: 4.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
import '../../../connections/types/stremio/stremio_base.types.dart';
|
||||
|
||||
class SeasonSource extends StatelessWidget {
|
||||
final Meta meta;
|
||||
final bool isMobile;
|
||||
final Player player;
|
||||
final Function(int index) onVideoChange;
|
||||
|
||||
const SeasonSource({
|
||||
super.key,
|
||||
required this.meta,
|
||||
required this.isMobile,
|
||||
required this.player,
|
||||
required this.onVideoChange,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialCustomButton(
|
||||
onPressed: () => onSelectMobile(context),
|
||||
icon: const Icon(Icons.list_alt),
|
||||
);
|
||||
}
|
||||
|
||||
onSelectDesktop(BuildContext context) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return VideoSelectView(
|
||||
meta: meta,
|
||||
onVideoChange: onVideoChange,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
onSelectMobile(BuildContext context) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return VideoSelectView(
|
||||
meta: meta,
|
||||
onVideoChange: onVideoChange,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoSelectView extends StatefulWidget {
|
||||
final Meta meta;
|
||||
final Function(int index) onVideoChange;
|
||||
|
||||
const VideoSelectView({
|
||||
super.key,
|
||||
required this.meta,
|
||||
required this.onVideoChange,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VideoSelectView> createState() => _VideoSelectViewState();
|
||||
}
|
||||
|
||||
class _VideoSelectViewState extends State<VideoSelectView> {
|
||||
final ScrollController controller = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (widget.meta.selectedVideoIndex != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
const itemWidth = 240.0 + 16.0;
|
||||
final offset = widget.meta.selectedVideoIndex! * itemWidth;
|
||||
|
||||
controller.jumpTo(offset);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onVerticalDragEnd: (details) {
|
||||
if (details.primaryVelocity! > 0) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.black38,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
title: const Text("Episodes"),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 150,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
final video = widget.meta.videos![index];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
widget.onVideoChange(index);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.fill,
|
||||
image: CachedNetworkImageProvider(
|
||||
video.thumbnail ??
|
||||
widget.meta.poster ??
|
||||
widget.meta.background ??
|
||||
""),
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 240,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black,
|
||||
Colors.black54,
|
||||
Colors.black38,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"S${video.season} E${video.episode}",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge,
|
||||
),
|
||||
Text(
|
||||
video.name ?? video.title ?? "",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.meta.selectedVideoIndex == index)
|
||||
Positioned(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black,
|
||||
Colors.black54,
|
||||
Colors.black38,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Text("Playing"),
|
||||
Icon(Icons.play_arrow),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: (widget.meta.videos ?? []).length,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:madari_client/features/connections/service/base_connection_service.dart';
|
||||
import 'package:madari_client/features/doc_viewer/container/video_viewer/season_source.dart';
|
||||
import 'package:madari_client/features/doc_viewer/container/video_viewer/torrent_stat.dart';
|
||||
import 'package:madari_client/features/doc_viewer/types/doc_source.dart';
|
||||
import 'package:madari_client/utils/load_language.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
import 'mobile_video_player.dart';
|
||||
import '../../../connections/types/stremio/stremio_base.types.dart' as types;
|
||||
|
||||
class VideoViewerMobile extends StatefulWidget {
|
||||
final VoidCallback onSubtitleSelect;
|
||||
|
|
@ -16,6 +20,8 @@ class VideoViewerMobile extends StatefulWidget {
|
|||
final VoidCallback onAudioSelect;
|
||||
final PlaybackConfig config;
|
||||
final GlobalKey<VideoState> videoKey;
|
||||
final LibraryItem? meta;
|
||||
final Future<void> Function(int index) onVideoChange;
|
||||
|
||||
const VideoViewerMobile({
|
||||
super.key,
|
||||
|
|
@ -27,6 +33,8 @@ class VideoViewerMobile extends StatefulWidget {
|
|||
required this.onAudioSelect,
|
||||
required this.config,
|
||||
required this.videoKey,
|
||||
required this.meta,
|
||||
required this.onVideoChange,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -39,7 +47,7 @@ class _VideoViewerMobileState extends State<VideoViewerMobile> {
|
|||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
final mobile = getMobileVideoPlayer(
|
||||
final mobile = _getMobileControls(
|
||||
context,
|
||||
onLibrarySelect: widget.onLibrarySelect,
|
||||
player: widget.player,
|
||||
|
|
@ -52,6 +60,7 @@ class _VideoViewerMobileState extends State<VideoViewerMobile> {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
String subtitleStyleName = widget.config.subtitleStyle ?? 'Normal';
|
||||
String subtitleStyleColor = widget.config.subtitleColor ?? 'white';
|
||||
double subtitleSize = widget.config.subtitleSize;
|
||||
|
|
@ -100,4 +109,161 @@ class _VideoViewerMobileState extends State<VideoViewerMobile> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
_getMobileControls(
|
||||
BuildContext context, {
|
||||
required DocSource source,
|
||||
required Player player,
|
||||
required VoidCallback onSubtitleClick,
|
||||
required VoidCallback onAudioClick,
|
||||
required VoidCallback toggleScale,
|
||||
required VoidCallback onLibrarySelect,
|
||||
}) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final meta = widget.meta;
|
||||
|
||||
return MaterialVideoControlsThemeData(
|
||||
topButtonBar: [
|
||||
MaterialCustomButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_back,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
meta.toString(),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const Spacer(),
|
||||
if (meta is types.Meta)
|
||||
if (meta.type == "series")
|
||||
SeasonSource(
|
||||
meta: meta,
|
||||
isMobile: true,
|
||||
player: player,
|
||||
onVideoChange: (index) async {
|
||||
await widget.onVideoChange(index);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
bufferingIndicatorBuilder: (source is TorrentSource)
|
||||
? (ctx) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: TorrentStats(
|
||||
torrentHash: (source).infoHash,
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
brightnessGesture: true,
|
||||
seekGesture: true,
|
||||
seekOnDoubleTap: true,
|
||||
gesturesEnabledWhileControlsVisible: true,
|
||||
shiftSubtitlesOnControlsVisibilityChange: true,
|
||||
seekBarMargin: const EdgeInsets.only(bottom: 54),
|
||||
speedUpOnLongPress: true,
|
||||
speedUpFactor: 2,
|
||||
volumeGesture: true,
|
||||
bottomButtonBar: [
|
||||
const MaterialPlayOrPauseButton(),
|
||||
const MaterialPositionIndicator(),
|
||||
const Spacer(),
|
||||
MaterialCustomButton(
|
||||
onPressed: () {
|
||||
final speeds = [
|
||||
0.5,
|
||||
0.75,
|
||||
1.0,
|
||||
1.25,
|
||||
1.5,
|
||||
1.75,
|
||||
2.0,
|
||||
2.25,
|
||||
2.5,
|
||||
3.0,
|
||||
3.25,
|
||||
3.5,
|
||||
3.75,
|
||||
4.0,
|
||||
4.25,
|
||||
4.5,
|
||||
4.75,
|
||||
5.0
|
||||
];
|
||||
showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (ctx) => Card(
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).size.height * 0.4,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dialogBackgroundColor,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Select Playback Speed',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: speeds.length,
|
||||
itemBuilder: (context, index) {
|
||||
final speed = speeds[index];
|
||||
return ListTile(
|
||||
title: Text('${speed}x'),
|
||||
selected: player.state.rate == speed,
|
||||
onTap: () {
|
||||
player.setRate(speed);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.speed),
|
||||
),
|
||||
MaterialCustomButton(
|
||||
onPressed: () {
|
||||
onSubtitleClick();
|
||||
},
|
||||
icon: const Icon(Icons.subtitles),
|
||||
),
|
||||
MaterialCustomButton(
|
||||
onPressed: () {
|
||||
onAudioClick();
|
||||
},
|
||||
icon: const Icon(Icons.audio_file),
|
||||
),
|
||||
MaterialCustomButton(
|
||||
onPressed: () {
|
||||
toggleScale();
|
||||
},
|
||||
icon: const Icon(Icons.fit_screen_outlined),
|
||||
),
|
||||
],
|
||||
topButtonBarMargin: EdgeInsets.only(
|
||||
top: mediaQuery.padding.top,
|
||||
),
|
||||
bottomButtonBarMargin: EdgeInsets.only(
|
||||
bottom: mediaQuery.viewInsets.bottom,
|
||||
left: 4.0,
|
||||
right: 4.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import 'package:media_kit/media_kit.dart';
|
|||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
import '../../../../utils/tv_detector.dart';
|
||||
import '../../../connections/types/stremio/stremio_base.types.dart' as types;
|
||||
import '../../../connections/widget/base/render_stream_list.dart';
|
||||
import 'desktop_video_player.dart';
|
||||
|
||||
|
|
@ -24,9 +25,12 @@ class VideoViewerUi extends StatefulWidget {
|
|||
final PlaybackConfig config;
|
||||
final DocSource source;
|
||||
final VoidCallback onLibrarySelect;
|
||||
final String title;
|
||||
final BaseConnectionService? service;
|
||||
final LibraryItem? meta;
|
||||
final Function(
|
||||
DocSource source,
|
||||
LibraryItem item,
|
||||
) onSourceChange;
|
||||
|
||||
const VideoViewerUi({
|
||||
super.key,
|
||||
|
|
@ -35,9 +39,9 @@ class VideoViewerUi extends StatefulWidget {
|
|||
required this.config,
|
||||
required this.source,
|
||||
required this.onLibrarySelect,
|
||||
required this.title,
|
||||
required this.service,
|
||||
this.meta,
|
||||
required this.onSourceChange,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -131,6 +135,13 @@ class _VideoViewerUiState extends State<VideoViewerUi> {
|
|||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
print(widget.meta.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
|
@ -171,6 +182,41 @@ class _VideoViewerUiState extends State<VideoViewerUi> {
|
|||
onAudioSelect: onAudioSelect,
|
||||
config: widget.config,
|
||||
videoKey: key,
|
||||
meta: widget.meta,
|
||||
onVideoChange: (index) async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
widget.player.pause();
|
||||
|
||||
final result = await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height,
|
||||
),
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: RenderStreamList(
|
||||
service: widget.service!,
|
||||
id: (widget.meta as types.Meta).copyWith(
|
||||
selectedVideoIndex: index,
|
||||
),
|
||||
shouldPop: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
widget.onSourceChange(
|
||||
result,
|
||||
(widget.meta as types.Meta).copyWith(
|
||||
selectedVideoIndex: index,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
default:
|
||||
return _buildDesktop(context);
|
||||
|
|
@ -184,6 +230,41 @@ class _VideoViewerUiState extends State<VideoViewerUi> {
|
|||
source: widget.source,
|
||||
onAudioSelect: onAudioSelect,
|
||||
onSubtitleSelect: onSubtitleSelect,
|
||||
meta: widget.meta,
|
||||
onVideoChange: (index) async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
widget.player.pause();
|
||||
|
||||
final result = await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height,
|
||||
),
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: RenderStreamList(
|
||||
service: widget.service!,
|
||||
id: (widget.meta as types.Meta).copyWith(
|
||||
selectedVideoIndex: index,
|
||||
),
|
||||
shouldPop: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
widget.onSourceChange(
|
||||
result,
|
||||
(widget.meta as types.Meta).copyWith(
|
||||
selectedVideoIndex: index,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return MaterialDesktopVideoControlsTheme(
|
||||
|
|
|
|||
|
|
@ -246,8 +246,10 @@ class _StremioStreamSelectorState extends State<StremioStreamSelector> {
|
|||
url: url,
|
||||
title: widget.item.name!,
|
||||
id: widget.item.id,
|
||||
season: widget.season,
|
||||
episode: widget.episode,
|
||||
season: widget.item.currentVideo?.season.toString() ??
|
||||
widget.season,
|
||||
episode: widget.item.currentVideo?.episode.toString() ??
|
||||
widget.episode,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -258,8 +260,10 @@ class _StremioStreamSelectorState extends State<StremioStreamSelector> {
|
|||
infoHash: item.infoHash!,
|
||||
fileName:
|
||||
"${item.behaviorHints?["filename"] as String}.mp4",
|
||||
season: widget.season,
|
||||
episode: widget.episode,
|
||||
season: widget.item.currentVideo?.season.toString() ??
|
||||
widget.season,
|
||||
episode: widget.item.currentVideo?.episode.toString() ??
|
||||
widget.episode,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -271,8 +275,10 @@ class _StremioStreamSelectorState extends State<StremioStreamSelector> {
|
|||
url: item.url!,
|
||||
id: widget.item.id,
|
||||
fileName: "${_getFileName(item)}.mp4",
|
||||
season: widget.season,
|
||||
episode: widget.episode,
|
||||
season: widget.item.currentVideo?.season.toString() ??
|
||||
widget.season,
|
||||
episode: widget.item.currentVideo?.episode.toString() ??
|
||||
widget.episode,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -459,6 +459,9 @@ class TraktService {
|
|||
final List<dynamic> continueWatching =
|
||||
await _makeRequest('$_baseUrl/sync/playback');
|
||||
|
||||
continueWatching.sort((v2, v1) => DateTime.parse(v1["paused_at"])
|
||||
.compareTo(DateTime.parse(v2["paused_at"])));
|
||||
|
||||
final startIndex = (page - 1) * itemsPerPage;
|
||||
final endIndex = startIndex + itemsPerPage;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue