mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-04-20 14:12:04 +00:00
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
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:
parent
309be2be2c
commit
87a9f0bf76
13 changed files with 897 additions and 152 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
292
lib/features/video_player/widgets/video_selector.dart
Normal file
292
lib/features/video_player/widgets/video_selector.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
Loading…
Reference in a new issue