fix: set default language
Some checks failed
Build and Deploy / build_windows (push) Has been cancelled
Build and Deploy / build_android (push) Has been cancelled
Build and Deploy / build_ipa (push) Has been cancelled
Build and Deploy / build_linux (push) Has been cancelled
Build and Deploy / build_macos (push) Has been cancelled

This commit is contained in:
omkar 2025-02-02 15:11:31 +05:30
parent 309be2be2c
commit 87a9f0bf76
13 changed files with 897 additions and 152 deletions

View file

@ -185,6 +185,7 @@ GoRouter createRouterDesktop() {
stream: state.pathParameters["stream"]!,
selectedIndex: state.uri.queryParameters["index"],
meta: state.extra is Map ? (state.extra as Map)["meta"] : null,
bingGroup: state.uri.queryParameters["binge-group"],
),
),
GoRoute(

View file

@ -82,7 +82,7 @@ class _PlaybackSettingsPageState extends State<PlaybackSettingsPage> {
Widget _buildSubtitlePreview(PlaybackSettings settings) {
return Container(
height: 120,
height: 100,
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(8),
@ -97,7 +97,7 @@ class _PlaybackSettingsPageState extends State<PlaybackSettingsPage> {
textAlign: TextAlign.center,
style: TextStyle(
color: settings.subtitleColor,
fontSize: settings.fontSize,
fontSize: 14 * settings.fontSize,
shadows: [
Shadow(
color: Colors.black.withOpacity(0.8),
@ -320,17 +320,19 @@ class _PlaybackSettingsPageState extends State<PlaybackSettingsPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Font Size'),
const Text('Font Scale'),
Row(
children: [
const Text('11'),
const Text('0.1'),
Expanded(
child: Slider(
value: settings.fontSize,
min: 11,
max: 60,
divisions: 49,
label: settings.fontSize.round().toString(),
value: settings.fontSize.clamp(0.1, 2.5),
min: 0.1,
max: 2.5,
divisions: 50,
label: settings.fontSize
.toStringAsFixed(2)
.toString(),
onChanged: (value) {
setState(() {
settings.fontSize = value;
@ -340,7 +342,7 @@ class _PlaybackSettingsPageState extends State<PlaybackSettingsPage> {
},
),
),
const Text('60'),
const Text('2.5'),
],
),
],
@ -381,12 +383,12 @@ class _PlaybackSettingsPageState extends State<PlaybackSettingsPage> {
Text("${settings.bufferSize} MB"),
Row(
children: [
const Text('32 MB'),
const Text('12 MB'),
Expanded(
child: Slider(
value: settings.bufferSize.toDouble(),
min: 32,
max: 2024,
min: 12,
max: 5120,
label: settings.bufferSize.round().toString(),
onChanged: (value) {
setState(() {
@ -397,7 +399,7 @@ class _PlaybackSettingsPageState extends State<PlaybackSettingsPage> {
},
),
),
const Text('2024 MB'),
const Text('${5120} MB'),
],
),
],

View file

@ -93,21 +93,43 @@ class SettingsPage extends StatelessWidget {
),
],
),
const _SettingsCategory(
_SettingsCategory(
title: 'System',
items: [
_SettingsItem(
const _SettingsItem(
title: 'Debug',
icon: Icons.bug_report,
path: '/settings/debug',
description: 'Debug options and logs',
),
_SettingsItem(
const _SettingsItem(
title: 'Offline Ratings',
icon: Icons.offline_bolt,
path: '/settings/offline-ratings',
description: 'Configure offline ratings',
),
_SettingsItem(
title: 'About US',
icon: Icons.perm_identity,
description: 'About US',
onClick: () {
showAboutDialog(
context: context,
applicationIcon: const Image(
width: 28,
image: AssetImage("assets/icon/icon_mini.png"),
),
children: [
const Text("Powered by TMDB"),
const Image(
image: NetworkImage(
"https://upload.wikimedia.org/wikipedia/commons/6/6e/Tmdb-312x276-logo.png",
),
),
],
);
},
),
],
),
];

View file

@ -10,120 +10,170 @@ class SubtitleStylesheet extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<VideoSettingsProvider>(
builder: (context, settings, _) {
return StatefulBuilder(
builder: (context, setState) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Background Color',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
return SingleChildScrollView(
child: Consumer<VideoSettingsProvider>(
builder: (context, settings, _) {
return StatefulBuilder(
builder: (context, setState) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Text Color',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 8),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
Colors.black,
Colors.grey[900]!,
Colors.grey[800]!,
Colors.grey[700]!,
Colors.blue[900]!,
Colors.brown[900]!,
].map((color) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: InkWell(
onTap: () {
settings.setSubtitleBackgroundColor(color);
},
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color,
border: Border.all(
color:
settings.subtitleBackgroundColor == color
? Colors.white
: Colors.transparent,
width: 2,
const SizedBox(height: 8),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
Colors.white,
Colors.grey.shade200,
Colors.yellowAccent,
Colors.blueAccent,
Colors.greenAccent,
Colors.orangeAccent,
Colors.redAccent,
].map((color) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: InkWell(
onTap: () {
settings.setSubtitleColor(color);
},
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color,
border: Border.all(
color: settings.subtitleColor == color
? Colors.white
: Colors.transparent,
width: 2,
),
borderRadius: BorderRadius.circular(8),
),
borderRadius: BorderRadius.circular(8),
),
),
);
}).toList(),
),
),
const SizedBox(height: 16),
const Text(
'Background Color',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
Colors.transparent,
Colors.black,
Colors.grey.shade900,
Colors.grey.shade800,
Colors.grey.shade700,
Colors.blue.shade900,
Colors.brown.shade900,
].map((color) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: InkWell(
onTap: () {
settings.setSubtitleBackgroundColor(color);
},
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color,
border: Border.all(
color: settings.subtitleBackgroundColor ==
color
? Colors.white
: Colors.transparent,
width: 2,
),
borderRadius: BorderRadius.circular(8),
),
),
),
);
}).toList(),
),
),
const SizedBox(height: 16),
Row(
children: [
const Text(
'Background Opacity',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
);
}).toList(),
),
),
const SizedBox(height: 16),
Row(
children: [
const Text(
'Background Opacity',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Text(
'${(settings.subtitleOpacity * 100).round()}%',
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
const SizedBox(width: 8),
Text(
'${(settings.subtitleOpacity * 100).round()}%',
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
),
],
),
Slider(
value: settings.subtitleOpacity,
min: 0.0,
max: 1.0,
divisions: 10,
onChanged: (value) {
settings.setSubtitleOpacity(value);
},
),
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(8),
],
),
child: Center(
child: Text(
'Sample Subtitle Text',
style: TextStyle(
color: Colors.white,
fontSize: 16,
backgroundColor:
settings.subtitleBackgroundColor.withValues(
alpha: settings.subtitleOpacity,
Slider(
value: settings.subtitleOpacity,
min: 0.0,
max: 1.0,
divisions: 10,
onChanged: (value) {
settings.setSubtitleOpacity(value);
},
),
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'Sample Subtitle Text',
style: TextStyle(
color: Colors.white,
fontSize: 16,
backgroundColor:
settings.subtitleBackgroundColor.withValues(
alpha: settings.subtitleOpacity,
),
),
),
),
),
),
],
),
);
},
);
},
],
),
);
},
);
},
),
);
}
}

View file

@ -5,7 +5,8 @@ class VideoSettingsProvider extends ChangeNotifier {
double _subtitleDelay = 0.0;
double _audioDelay = 0.0;
bool _isLocked = false;
Color _subtitleBackgroundColor = Colors.black;
Color _subtitleBackgroundColor = Colors.transparent;
Color _subtitleColor = Colors.white;
double _subtitleOpacity = 0.6;
bool _isFilled = false;
@ -14,6 +15,7 @@ class VideoSettingsProvider extends ChangeNotifier {
double get audioDelay => _audioDelay;
bool get isLocked => _isLocked;
Color get subtitleBackgroundColor => _subtitleBackgroundColor;
Color get subtitleColor => _subtitleColor;
double get subtitleOpacity => _subtitleOpacity;
bool get isFilled => _isFilled;
@ -57,6 +59,11 @@ class VideoSettingsProvider extends ChangeNotifier {
notifyListeners();
}
void setSubtitleColor(Color color) {
_subtitleColor = color;
notifyListeners();
}
void setSubtitleOpacity(double opacity) {
_subtitleOpacity = opacity;
notifyListeners();

View file

@ -1,11 +1,16 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:madari_client/features/video_player/container/options/settings_sheet.dart';
import 'package:madari_client/features/video_player/container/state/video_settings.dart';
import 'package:madari_client/features/video_player/container/video_play.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:provider/provider.dart';
import 'package:rxdart/src/subjects/behavior_subject.dart';
import '../../streamio_addons/models/stremio_base_types.dart' as types;
import '../widgets/video_selector.dart';
import 'options/audio_track_selector.dart';
import 'options/scale_option.dart';
import 'options/subtitle_selector.dart';
@ -13,11 +18,17 @@ import 'options/subtitle_selector.dart';
class VideoMobile extends StatefulWidget {
final VideoController controller;
final types.Meta? meta;
final OnVideoChangeCallback onVideoChange;
final int index;
final BehaviorSubject<int> updateSubject;
const VideoMobile({
super.key,
required this.controller,
required this.meta,
required this.onVideoChange,
required this.index,
required this.updateSubject,
});
@override
@ -37,6 +48,11 @@ class _VideoMobileState extends State<VideoMobile> {
});
}
@override
void dispose() {
super.dispose();
}
void _toggleLock(BuildContext context) {
final settings = context.read<VideoSettingsProvider>();
settings.toggleLock();
@ -140,14 +156,10 @@ class _VideoMobileState extends State<VideoMobile> {
),
if (widget.meta?.currentVideo != null)
Expanded(
child: Text(
"${widget.meta?.name} - ${widget.meta?.currentVideo?.name ?? widget.meta?.currentVideo?.title} - S${widget.meta!.currentVideo?.season} E${widget.meta?.currentVideo?.episode}",
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
child: VideoTitle(
meta: widget.meta!,
index: widget.index,
updateSubject: widget.updateSubject,
),
),
if (widget.meta?.currentVideo == null)
@ -169,6 +181,13 @@ class _VideoMobileState extends State<VideoMobile> {
color: Colors.white,
),
),
if (widget.meta is types.Meta && widget.meta?.type == "series")
SeasonSource(
onVideoChange: widget.onVideoChange,
meta: widget.meta!,
isMobile: true,
updateSubject: widget.updateSubject,
),
],
seekBarThumbColor: Theme.of(context).primaryColorLight,
seekBarColor: Theme.of(context).primaryColor,
@ -176,6 +195,12 @@ class _VideoMobileState extends State<VideoMobile> {
bottomButtonBar: [
const MaterialPlayOrPauseButton(),
const MaterialSkipNextButton(),
if (widget.meta is types.Meta && widget.meta?.type == "series")
NextVideo(
updateSubject: widget.updateSubject,
onVideoChange: widget.onVideoChange,
meta: widget.meta!,
),
const SizedBox(width: 12),
const MaterialPositionIndicator(),
const Spacer(),
@ -204,6 +229,13 @@ class _VideoMobileState extends State<VideoMobile> {
fullscreen: getFullscreenControl(),
normal: const MaterialVideoControlsThemeData(),
child: Video(
subtitleViewConfiguration: SubtitleViewConfiguration(
textScaler: TextScaler.linear(data.subtitleSize),
style: TextStyle(
color: data.subtitleColor,
backgroundColor: data.subtitleBackgroundColor,
),
),
key: key,
onEnterFullscreen: () async {
await defaultEnterNativeFullscreen();
@ -220,3 +252,107 @@ class _VideoMobileState extends State<VideoMobile> {
);
}
}
class VideoTitle extends StatefulWidget {
final types.Meta meta;
final int index;
final BehaviorSubject<int> updateSubject;
const VideoTitle({
super.key,
required this.meta,
required this.index,
required this.updateSubject,
});
@override
State<VideoTitle> createState() => _VideoTitleState();
}
class _VideoTitleState extends State<VideoTitle> {
late int index = widget.index;
late StreamSubscription<int> _updateStatus;
@override
void initState() {
super.initState();
_updateStatus = widget.updateSubject.listen((index) {
setState(() {
this.index = index;
});
});
}
@override
void dispose() {
super.dispose();
_updateStatus.cancel();
}
@override
Widget build(BuildContext context) {
final video = widget.meta
.copyWith(
selectedVideoIndex: index,
)
.currentVideo;
return Text(
"${widget.meta.name} - ${video?.name ?? video?.title} - S${video?.season} E${video?.episode}",
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
);
}
}
class NextVideo extends StatefulWidget {
final BehaviorSubject<int> updateSubject;
final types.Meta meta;
final OnVideoChangeCallback onVideoChange;
const NextVideo({
super.key,
required this.updateSubject,
required this.meta,
required this.onVideoChange,
});
@override
State<NextVideo> createState() => _NextVideoState();
}
class _NextVideoState extends State<NextVideo> {
bool isLoading = false;
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () async {
setState(() {
isLoading = true;
});
await widget.onVideoChange(widget.updateSubject.value + 1);
setState(() {
isLoading = false;
});
},
icon: isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(),
)
: const Icon(
Icons.skip_next_outlined,
),
);
}
}

View file

@ -1,12 +1,22 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:madari_client/features/settings/model/playback_settings_model.dart';
import 'package:madari_client/features/video_player/container/state/video_settings.dart';
import 'package:madari_client/features/video_player/container/video_desktop.dart';
import 'package:madari_client/features/video_player/container/video_mobile.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:provider/provider.dart';
import 'package:rxdart/src/subjects/behavior_subject.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../streamio_addons/models/stremio_base_types.dart';
import '../service/video_eventer_default_track.dart';
typedef OnVideoChangeCallback = Future<bool> Function(
int selectedIndex,
);
class VideoPlay extends StatefulWidget {
final bool enabledHardwareAcceleration;
@ -16,6 +26,9 @@ class VideoPlay extends StatefulWidget {
final int index;
final String stream;
final int bufferSize;
final OnVideoChangeCallback onVideoChange;
final BehaviorSubject<int> updateSubject;
final PlaybackSettings data;
const VideoPlay({
super.key,
@ -27,6 +40,9 @@ class VideoPlay extends StatefulWidget {
required this.index,
required this.stream,
required this.bufferSize,
required this.onVideoChange,
required this.updateSubject,
required this.data,
});
@override
@ -35,6 +51,7 @@ class VideoPlay extends StatefulWidget {
class _VideoPlayState extends State<VideoPlay> {
late String stream = widget.stream;
late int index = widget.index;
late final player = Player(
configuration: PlayerConfiguration(
@ -49,11 +66,25 @@ class _VideoPlayState extends State<VideoPlay> {
enableHardwareAcceleration: widget.enabledHardwareAcceleration,
),
);
late VideoSettingsProvider _settings;
late Debouncer _debouncer;
late VideoEventerDefaultTrackSetter setter;
@override
void initState() {
super.initState();
_settings = context.read<VideoSettingsProvider>();
_debouncer = Debouncer(
duration: const Duration(milliseconds: 500),
);
_settings.addListener(_onSettingsChanged);
setter = VideoEventerDefaultTrackSetter(
player,
widget.data,
);
player.open(
Media(
widget.stream,
@ -65,10 +96,37 @@ class _VideoPlayState extends State<VideoPlay> {
player.play();
}
void _onSettingsChanged() {
final platform = player.platform;
if (platform is NativePlayer) {
_debouncer.run(() {
platform.setProperty('sub-delay', "${-_settings.subtitleDelay}");
platform.setProperty('audio-delay', "${-_settings.audioDelay}");
});
}
}
@override
void didUpdateWidget(covariant VideoPlay oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.stream != stream) {
stream = widget.stream;
player.open(
Media(
stream,
),
);
}
index = widget.index;
}
@override
void dispose() {
super.dispose();
_settings.removeListener(_onSettingsChanged);
setter.dispose();
player.dispose();
}
@ -80,6 +138,9 @@ class _VideoPlayState extends State<VideoPlay> {
return VideoMobile(
controller: controller,
meta: widget.meta,
onVideoChange: widget.onVideoChange,
index: index,
updateSubject: widget.updateSubject,
);
}
@ -89,3 +150,21 @@ class _VideoPlayState extends State<VideoPlay> {
);
}
}
class Debouncer {
final Duration duration;
Timer? _timer;
Debouncer({
this.duration = const Duration(milliseconds: 500),
});
void run(VoidCallback action) {
_timer?.cancel();
_timer = Timer(duration, action);
}
void dispose() {
_timer?.cancel();
}
}

View file

@ -6,6 +6,8 @@ import 'package:madari_client/features/settings/model/playback_settings_model.da
import 'package:madari_client/features/settings/service/playback_setting_service.dart';
import 'package:madari_client/features/streamio_addons/models/stremio_base_types.dart';
import 'package:madari_client/features/video_player/container/video_play.dart';
import 'package:madari_client/features/widgetter/plugins/stremio/containers/streamio_background.dart';
import 'package:rxdart/rxdart.dart';
class VideoPlayer extends StatefulWidget {
final String stream;
@ -13,6 +15,7 @@ class VideoPlayer extends StatefulWidget {
final String id;
final String type;
final String? selectedIndex;
final String? bingGroup;
const VideoPlayer({
super.key,
@ -21,28 +24,32 @@ class VideoPlayer extends StatefulWidget {
required this.meta,
required this.stream,
this.selectedIndex,
this.bingGroup,
});
@override
State<VideoPlayer> createState() => _VideoPlayerState();
int get index {
if (selectedIndex == "null" || selectedIndex == "") {
return 0;
}
return int.tryParse(selectedIndex ?? "0") ?? 0;
}
}
class _VideoPlayerState extends State<VideoPlayer> with WidgetsBindingObserver {
final _logger = Logger('VideoPlayer');
late final Query<PlaybackSettings> _playbackSettings;
bool _isMounted = false;
late String stream = widget.stream;
String? _errorMessage;
int get index {
if (widget.selectedIndex == "null" || widget.selectedIndex == "") {
return 0;
}
return int.tryParse(widget.selectedIndex ?? "0") ?? 0;
}
late Meta meta = widget.meta;
late int index = widget.index;
late final BehaviorSubject<int> updateSubject = BehaviorSubject.seeded(
widget.index,
);
@override
void initState() {
@ -149,11 +156,33 @@ class _VideoPlayerState extends State<VideoPlayer> with WidgetsBindingObserver {
extendBody: true,
extendBodyBehindAppBar: true,
body: VideoPlay(
stream: widget.stream,
meta: widget.meta,
updateSubject: updateSubject,
onVideoChange: (index) async {
final result = await openVideoStream(
context,
widget.meta.copyWith(
selectedVideoIndex: index,
),
shouldPop: true,
bingGroup: widget.bingGroup,
);
if (result == null) return false;
setState(() {
this.index = index;
stream = result;
});
updateSubject.add(index);
return true;
},
stream: stream,
meta: meta,
data: state.data!,
bufferSize: state.data?.bufferSize ?? 32,
index: index,
key: ValueKey('${widget.id}_${widget.selectedIndex}'),
enabledHardwareAcceleration:
state.data?.disableHardwareAcceleration != true,
poster: widget.meta.poster,

View file

@ -0,0 +1,84 @@
import 'dart:async';
import 'package:logging/logging.dart';
import 'package:madari_client/features/settings/model/playback_settings_model.dart';
import 'package:media_kit/media_kit.dart';
class VideoEventerDefaultTrackSetter {
final _logger = Logger("VideoEventerDefaultTrackSetter");
final Player player;
final PlaybackSettings data;
final List<StreamSubscription> _listeners = [];
bool defaultConfigSelected = false;
bool audioSelectionHandled = false;
bool subtitleSelectionHandled = false;
VideoEventerDefaultTrackSetter(
this.player,
this.data,
) {
_logger.info("VideoEventerDefaultTrackSetter");
_listeners.add(
player.stream.tracks.listen(
(tracks) {
if (defaultConfigSelected == true &&
(tracks.audio.length <= 1 || tracks.audio.length <= 1)) {
return;
}
defaultConfigSelected = true;
player.setRate(data.playbackSpeed);
final defaultSubtitle = data.defaultSubtitleTrack;
final defaultAudio = data.defaultAudioTrack;
for (final item in tracks.audio) {
if ((defaultAudio == item.id ||
defaultAudio == item.language ||
defaultAudio == item.title) &&
audioSelectionHandled == false) {
player.setAudioTrack(item);
_logger.info("message player.setAudioTrack(item) = $item");
audioSelectionHandled = true;
break;
}
}
if (data.disableSubtitles) {
for (final item in tracks.subtitle) {
if ((item.id == "no" ||
item.language == "no" ||
item.title == "no") &&
subtitleSelectionHandled == false) {
player.setSubtitleTrack(item);
_logger.info("message player.setSubtitleTrack(item) = $item");
subtitleSelectionHandled = true;
}
}
} else {
for (final item in tracks.subtitle) {
if ((defaultSubtitle == item.id ||
defaultSubtitle == item.language ||
defaultSubtitle == item.title) &&
subtitleSelectionHandled == false) {
subtitleSelectionHandled = true;
player.setSubtitleTrack(item);
_logger.info("message player.setSubtitleTrack(item) = $item");
break;
}
}
}
},
),
);
}
dispose() {
_logger.info("VideoEventerDefaultTrackSetter.dispose()");
for (final item in _listeners) {
item.cancel();
}
}
}

View file

@ -0,0 +1,292 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:madari_client/features/video_player/container/video_play.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:rxdart/src/subjects/behavior_subject.dart';
import '../../streamio_addons/models/stremio_base_types.dart';
class SeasonSource extends StatefulWidget {
final Meta meta;
final bool isMobile;
final OnVideoChangeCallback onVideoChange;
final BehaviorSubject<int> updateSubject;
const SeasonSource({
super.key,
required this.meta,
required this.isMobile,
required this.onVideoChange,
required this.updateSubject,
});
@override
State<SeasonSource> createState() => _SeasonSourceState();
}
class _SeasonSourceState extends State<SeasonSource> {
@override
void initState() {
super.initState();
}
@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: widget.meta,
onVideoChange: widget.onVideoChange,
updateSubject: widget.updateSubject,
);
},
);
}
onSelectMobile(BuildContext context) {
showCupertinoDialog(
context: context,
builder: (context) {
return VideoSelectView(
meta: widget.meta,
onVideoChange: widget.onVideoChange,
updateSubject: widget.updateSubject,
);
},
);
}
}
class VideoSelectView extends StatefulWidget {
final Meta meta;
final OnVideoChangeCallback onVideoChange;
final BehaviorSubject<int> updateSubject;
const VideoSelectView({
super.key,
required this.meta,
required this.onVideoChange,
required this.updateSubject,
});
@override
State<VideoSelectView> createState() => _VideoSelectViewState();
}
class _VideoSelectViewState extends State<VideoSelectView> {
final ScrollController controller = ScrollController();
int? isLoading;
late final videos = widget.meta.videos;
@override
void initState() {
super.initState();
videos?.sort((v1, v2) {
if (v1.season == null && v2.season == null) return 0;
if (v1.season == null) return 1;
if (v2.season == null) return -1;
final seasonComparison = v1.season!.compareTo(v2.season!);
if (seasonComparison != 0) {
return seasonComparison;
}
if (v1.number == null && v2.number == null) return 0;
if (v1.number == null) return 1;
if (v2.number == null) return -1;
return v1.number!.compareTo(v2.number!);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
const itemWidth = 240.0 + 16.0;
final offset = widget.updateSubject.value * 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.black87,
appBar: AppBar(
backgroundColor: Colors.transparent,
title: const Text("Episodes"),
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
),
),
SizedBox(
height: 150,
child: ListView.builder(
controller: controller,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final video = videos![index];
return Stack(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
setState(() {
isLoading = index;
});
final res = await widget.onVideoChange(index);
if (res == false) {
return;
}
if (context.mounted) Navigator.of(context).pop();
if (mounted) {
setState(() {
isLoading = null;
});
}
},
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.updateSubject.value == 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),
],
),
),
),
),
],
),
),
),
if (index == isLoading)
const Positioned.fill(
child: Center(
child: CircularProgressIndicator(),
),
),
],
);
},
itemCount: (widget.meta.videos ?? []).length,
),
),
],
),
),
),
);
}
}

View file

@ -12,10 +12,12 @@ final _logger = Logger('StreamioStreamList');
class StreamioStreamList extends StatefulWidget {
final Meta meta;
final bool shouldPop;
const StreamioStreamList({
super.key,
required this.meta,
required this.shouldPop,
});
@override
@ -37,11 +39,11 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
final Set<String> _selectedAddons = {};
final Map<String, List<StreamWithAddon>> streamsByAddon = {};
Set<String> _resolutions = {};
Set<String> _qualities = {};
Set<String> _codecs = {};
Set<String> _audios = {};
Set<String> _sizes = {};
final Set<String> _resolutions = {};
final Set<String> _qualities = {};
final Set<String> _codecs = {};
final Set<String> _audios = {};
final Set<String> _sizes = {};
final Set<String> _addons = {};
@override
@ -385,6 +387,7 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
return StreamCard(
streamWithAddon: streamData,
meta: widget.meta,
shouldPop: widget.shouldPop,
);
},
),
@ -399,7 +402,10 @@ class StreamWithAddon {
final VideoStream stream;
final String? addonName;
StreamWithAddon({required this.stream, this.addonName});
StreamWithAddon({
required this.stream,
this.addonName,
});
StreamWithAddon copy() {
return StreamWithAddon(
@ -451,15 +457,17 @@ class StreamTag extends StatelessWidget {
class StreamCard extends StatelessWidget {
final StreamWithAddon streamWithAddon;
final Meta meta;
final bool shouldPop;
const StreamCard({
super.key,
required this.meta,
required this.streamWithAddon,
required this.shouldPop,
});
VideoStream get stream {
return streamWithAddon.stream.copyWith();
return streamWithAddon.stream;
}
@override
@ -472,6 +480,11 @@ class StreamCard extends StatelessWidget {
onTap: stream.url != null
? () async {
if (stream.url != null) {
if (shouldPop) {
Navigator.pop(context, stream.url);
return;
}
final settings =
await PlaybackSettingsService.instance.getSettings();

View file

@ -2,13 +2,42 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:madari_client/features/streamio_addons/models/stremio_base_types.dart';
import 'package:madari_client/utils/array-extension.dart';
import '../../../../library/container/add_to_list_button.dart';
import '../../../../streamio_addons/service/stremio_addon_service.dart';
import 'stream_list.dart';
final _logger = Logger('StreamioComponents');
Future<void> openVideoStream(BuildContext context, Meta meta) async {
Future<String?> openVideoStream(
BuildContext context,
Meta meta, {
bool shouldPop = false,
String? bingGroup,
}) async {
final service = StremioAddonService.instance;
if (bingGroup != null) {
final result = await Future(() async {
final List<VideoStream> items = [];
await service.getStreams(meta, callback: (item, addonName, error) {
if (item != null) items.addAll(item);
});
return items;
});
final firstVideo = result.firstWhereOrNull((item) {
return item.behaviorHints?["bingeGroup"] == bingGroup && item.url != null;
});
if (firstVideo != null) {
return firstVideo.url!;
}
}
return showModalBottomSheet(
enableDrag: true,
constraints: const BoxConstraints(
@ -20,6 +49,7 @@ Future<void> openVideoStream(BuildContext context, Meta meta) async {
builder: (context) {
return Scaffold(
body: StreamioStreamList(
shouldPop: shouldPop,
meta: meta.type == "series"
? meta.copyWith(
selectedVideoIndex: meta.selectedVideoIndex ?? 0,

View file

@ -64,7 +64,7 @@ class StremioCatalogPlugin extends PluginBase {
}
for (final catalog in item.catalogs!) {
final hasSearch = catalog.extraRequired?.contains("search") ?? false;
final hasSearch = catalog.extraSupported?.contains("search") ?? false;
final result = PresetWidgetConfig(
title: "${catalog.name ?? ""} ${catalog.type.capitalize}",