296 lines
9.6 KiB
Dart
296 lines
9.6 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_meedu_videoplayer/meedu_player.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart' as riv;
|
|
import 'package:mangayomi/models/chapter.dart';
|
|
import 'package:mangayomi/models/video.dart';
|
|
import 'package:mangayomi/modules/anime/providers/stream_controller_provider.dart';
|
|
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
|
import 'package:mangayomi/services/get_anime_servers.dart';
|
|
import 'package:mangayomi/utils/media_query.dart';
|
|
|
|
class AnimeStreamView extends riv.ConsumerWidget {
|
|
final Chapter episode;
|
|
const AnimeStreamView({
|
|
super.key,
|
|
required this.episode,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky,
|
|
overlays: []);
|
|
final serversData = ref.watch(getAnimeServersProvider(
|
|
chapter: episode,
|
|
));
|
|
return serversData.when(
|
|
data: (data) {
|
|
if (data.isEmpty &&
|
|
(episode.manga.value!.isLocalArchive ?? false) == false) {
|
|
return Scaffold(
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
appBar: AppBar(
|
|
title: const Text(''),
|
|
leading: BackButton(
|
|
onPressed: () {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
|
overlays: SystemUiOverlay.values);
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
),
|
|
body: WillPopScope(
|
|
onWillPop: () async {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
|
overlays: SystemUiOverlay.values);
|
|
Navigator.pop(context);
|
|
return false;
|
|
},
|
|
child: const Center(
|
|
child: Text("Error"),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return AnimeStreamPage(
|
|
episode: episode,
|
|
videos: data,
|
|
);
|
|
},
|
|
error: (error, stackTrace) => Scaffold(
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
appBar: AppBar(
|
|
title: const Text(''),
|
|
leading: BackButton(
|
|
onPressed: () {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
|
overlays: SystemUiOverlay.values);
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
),
|
|
body: WillPopScope(
|
|
onWillPop: () async {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
|
overlays: SystemUiOverlay.values);
|
|
Navigator.pop(context);
|
|
return false;
|
|
},
|
|
child: Center(
|
|
child: Text(error.toString()),
|
|
),
|
|
),
|
|
),
|
|
loading: () {
|
|
return Scaffold(
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
body: WillPopScope(
|
|
onWillPop: () async {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
|
overlays: SystemUiOverlay.values);
|
|
Navigator.pop(context);
|
|
return false;
|
|
},
|
|
child: Stack(
|
|
children: [
|
|
MeeduVideoPlayer(
|
|
header: (context, controller, responsive) => AppBar(
|
|
leading: BackButton(
|
|
color: Colors.white,
|
|
onPressed: () {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
|
overlays: SystemUiOverlay.values);
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
),
|
|
controller: MeeduPlayerController(
|
|
autoHideControls: false,
|
|
enabledButtons:
|
|
const EnabledButtons(playPauseAndRepeat: false),
|
|
screenManager: const ScreenManager(
|
|
forceLandScapeInFullscreen: false,
|
|
),
|
|
),
|
|
),
|
|
const ProgressCenter(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class AnimeStreamPage extends StatefulWidget {
|
|
final List<Video> videos;
|
|
final Chapter episode;
|
|
const AnimeStreamPage({Key? key, required this.videos, required this.episode})
|
|
: super(key: key);
|
|
|
|
@override
|
|
State<AnimeStreamPage> createState() => _AnimeStreamPageState();
|
|
}
|
|
|
|
class _AnimeStreamPageState extends State<AnimeStreamPage> {
|
|
final _controller = MeeduPlayerController(
|
|
screenManager: const ScreenManager(
|
|
forceLandScapeInFullscreen: false,
|
|
),
|
|
);
|
|
late final streamController = AnimeStreamController(episode: widget.episode);
|
|
|
|
/// listener for the video quality
|
|
final ValueNotifier<Video?> _video = ValueNotifier(null);
|
|
|
|
late Duration _currentPosition =
|
|
streamController.geTCurrentPosition(); // to save the video position
|
|
|
|
/// subscription to listen the video position changes
|
|
StreamSubscription? _currentPositionSubs;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_video.value = widget.videos[0]; // set the default video quality (480p)
|
|
|
|
// listen the video position
|
|
_currentPositionSubs = _controller.onPositionChanged.listen(
|
|
(Duration position) {
|
|
_currentPosition = position; // save the video position
|
|
|
|
streamController.setCurrentPosition(position.inMilliseconds);
|
|
streamController.setAnimeHistoryUpdate();
|
|
},
|
|
);
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_setDataSource();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_currentPositionSubs?.cancel(); // cancel the subscription
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onChangeVideoQuality() {
|
|
showCupertinoModalPopup(
|
|
context: context,
|
|
builder: (_) => CupertinoActionSheet(
|
|
actions: List.generate(
|
|
widget.videos.length,
|
|
(index) {
|
|
final quality = widget.videos[index];
|
|
return CupertinoActionSheetAction(
|
|
child: Text(quality.quality),
|
|
onPressed: () {
|
|
_video.value = quality; // change the video quality
|
|
_setDataSource(); // update the datasource
|
|
Navigator.maybePop(_);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
cancelButton: CupertinoActionSheetAction(
|
|
onPressed: () => Navigator.maybePop(_),
|
|
isDestructiveAction: true,
|
|
child: const Text("Cancel"),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _setDataSource() async {
|
|
// set the data source and play the video in the last video position
|
|
await _controller.setDataSource(
|
|
DataSource(
|
|
type: DataSourceType.network,
|
|
source: _video.value!.originalUrl,
|
|
httpHeaders: _video.value!.headers),
|
|
autoplay: true,
|
|
seekTo: _currentPosition,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: WillPopScope(
|
|
onWillPop: () async {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
|
overlays: SystemUiOverlay.values);
|
|
Navigator.pop(context);
|
|
return false;
|
|
},
|
|
child: MeeduVideoPlayer(
|
|
controller: _controller,
|
|
header: (ctx, controller, responsive) {
|
|
// creates a responsive fontSize using the size of video container
|
|
final double fontSize = responsive.ip(3);
|
|
|
|
return AppBar(
|
|
title: ListTile(
|
|
dense: true,
|
|
title: SizedBox(
|
|
width: mediaWidth(context, 0.8),
|
|
child: Text(
|
|
widget.episode.manga.value!.name!,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold, color: Colors.white),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
subtitle: SizedBox(
|
|
width: mediaWidth(context, 0.8),
|
|
child: Text(
|
|
widget.episode.name!,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w400,
|
|
color: Colors.white),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
),
|
|
leading: BackButton(
|
|
color: Colors.white,
|
|
onPressed: () {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
|
overlays: SystemUiOverlay.values);
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
actions: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: CupertinoButton(
|
|
padding: const EdgeInsets.all(5),
|
|
onPressed: _onChangeVideoQuality,
|
|
child: ValueListenableBuilder<Video?>(
|
|
valueListenable: _video,
|
|
builder: (context, Video? video, child) {
|
|
return Text(
|
|
video!.quality,
|
|
style: TextStyle(
|
|
fontSize: fontSize > 18 ? 18 : fontSize,
|
|
color: Colors.white,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
)
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|