fix: change video

This commit is contained in:
omkar 2025-01-14 17:02:03 +05:30
parent f129eb7360
commit 481491d172
10 changed files with 830 additions and 455 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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