feat: refactor player components to use provider

This commit is contained in:
22
2025-09-14 20:56:51 +08:00
parent 18c9f0f6c3
commit 849f5ee5c0
15 changed files with 283 additions and 297 deletions

View File

@@ -368,8 +368,8 @@ MediaKitPlayer useMediaKitPlayer(BuildContext context) {
position: duration == Duration.zero ? Duration.zero : position,
duration: duration,
buffer: duration == Duration.zero ? Duration.zero : buffer,
width: videoParams?.w?.toDouble(),
height: videoParams?.h?.toDouble(),
width: videoParams?.w?.toDouble() ?? 0,
height: videoParams?.h?.toDouble() ?? 0,
saveProgress: saveProgress,
play: play,
pause: pause,

View File

@@ -1,10 +1,13 @@
import 'dart:ui';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:iris/models/player.dart';
import 'package:iris/utils/logger.dart';
import 'package:provider/provider.dart';
void useAppLifecycle() {
final context = useContext();
final saveProgress = context.read<MediaPlayer>().saveProgress;
void useAppLifecycle(
Future<void> Function() saveProgress,
) {
AppLifecycleState? appLifecycleState = useAppLifecycleState();
useEffect(() {

View File

@@ -2,14 +2,18 @@ import 'package:collection/collection.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_zustand/flutter_zustand.dart';
import 'package:iris/models/file.dart';
import 'package:iris/models/player.dart';
import 'package:iris/models/storages/local.dart';
import 'package:iris/models/storages/storage.dart';
import 'package:iris/store/use_play_queue_store.dart';
import 'package:iris/store/use_storage_store.dart';
import 'package:iris/utils/files_filter.dart';
import 'package:provider/provider.dart';
FileItem? useCover(bool isPlaying) {
FileItem? useCover() {
final context = useContext();
final isPlaying =
context.select<MediaPlayer, bool>((player) => player.isPlaying);
final playQueue =
usePlayQueueStore().select(context, (state) => state.playQueue);

View File

@@ -9,6 +9,7 @@ import 'package:iris/store/use_app_store.dart';
import 'package:iris/store/use_player_ui_store.dart';
import 'package:iris/utils/platform.dart';
import 'package:iris/utils/resize_window.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
class Gesture {
@@ -55,13 +56,25 @@ class Gesture {
}
Gesture useGesture({
required MediaPlayer player,
required void Function() showControl,
required void Function() hideControl,
required void Function() showProgress,
}) {
final context = useContext();
final isPlaying =
context.select<MediaPlayer, bool>((player) => player.isPlaying);
final progress =
context.select<MediaPlayer, ({Duration position, Duration duration})>(
(player) => (position: player.position, duration: player.duration),
);
final play = context.read<MediaPlayer>().play;
final pause = context.read<MediaPlayer>().pause;
final forward = context.read<MediaPlayer>().forward;
final backward = context.read<MediaPlayer>().backward;
final seek = context.read<MediaPlayer>().seek;
final aspectRatio =
usePlayerUiStore().select(context, (state) => state.aspectRatio);
@@ -112,22 +125,22 @@ Gesture useGesture({
} else {
showProgress();
}
await player.forward(5);
await forward(5);
} else if (position < 0.25) {
if (isShowControl) {
showControl();
} else {
showProgress();
}
player.backward(5);
backward(5);
} else {
if (player.isPlaying == true) {
if (isPlaying == true) {
await useAppStore().updateAutoPlay(false);
player.pause();
pause();
showControl();
} else {
await useAppStore().updateAutoPlay(true);
player.play();
play();
}
}
} else {
@@ -141,7 +154,7 @@ Gesture useGesture({
}
void onLongPressStart(LongPressStartDetails details) {
if (isTouch.value && player.isPlaying == true) {
if (isTouch.value && isPlaying == true) {
isLongPress.value = true;
useAppStore().updateRate(2.0);
}
@@ -200,14 +213,14 @@ Gesture useGesture({
// 水平滑动
if (isHorizontalGesture.value && isSeeking) {
double dx = details.delta.dx;
int seconds = (dx * 2 + player.position.inSeconds).toInt();
int seconds = (dx * 2 + progress.position.inSeconds).toInt();
Duration position = Duration(
seconds: seconds < 0
? 0
: seconds > player.duration.inSeconds
? player.duration.inSeconds
: seconds > progress.duration.inSeconds
? progress.duration.inSeconds
: seconds);
player.seek(position);
seek(position);
if (isShowControl) {
showControl();
} else {
@@ -259,7 +272,7 @@ Gesture useGesture({
isRightGesture.value = false;
startPosition.value = null;
if (isSeeking) {
await player.seek(player.position);
await seek(progress.position);
usePlayerUiStore().updateIsSeeking(false);
}
}
@@ -272,7 +285,7 @@ Gesture useGesture({
startPosition.value = null;
if (isSeeking) {
isTouch.value = false;
await player.seek(player.position);
await seek(progress.position);
usePlayerUiStore().updateIsSeeking(false);
}
}
@@ -285,10 +298,10 @@ Gesture useGesture({
}
final cursor = useMemoized(() {
return player.isPlaying == false
return isPlaying == false
? SystemMouseCursors.basic
: SystemMouseCursors.none;
}, [player.isPlaying]);
}, [isPlaying]);
return Gesture(
onTapDown: onTapDown,

View File

@@ -16,17 +16,22 @@ import 'package:iris/utils/platform.dart';
import 'package:iris/widgets/bottom_sheets/show_open_link_bottom_sheet.dart';
import 'package:iris/widgets/dialogs/show_open_link_dialog.dart';
import 'package:iris/widgets/popup.dart';
import 'package:provider/provider.dart';
typedef KeyboardEvent = void Function(KeyEvent event);
KeyboardEvent useKeyboard({
required MediaPlayer player,
required void Function() showControl,
required Future<void> Function(Future<void>) showControlForHover,
required void Function() showProgress,
}) {
final context = useContext();
final player = context.read<MediaPlayer>();
final isPlaying =
context.select<MediaPlayer, bool>((player) => player.isPlaying);
final shuffle = useAppStore().state.shuffle;
final isFullScreen = usePlayerUiStore().state.isFullScreen;
final isShowControl = usePlayerUiStore().state.isShowControl;
@@ -130,7 +135,7 @@ KeyboardEvent useKeyboard({
case LogicalKeyboardKey.space:
case LogicalKeyboardKey.mediaPlayPause:
showControl();
if (player.isPlaying) {
if (isPlaying) {
useAppStore().updateAutoPlay(false);
player.pause();
} else {
@@ -173,7 +178,7 @@ KeyboardEvent useKeyboard({
showControlForHover(
showPopup(
context: context,
child: SubtitleAndAudioTrack(player: player),
child: SubtitleAndAudioTrack(),
direction: PopupDirection.right,
),
);

View File

@@ -11,8 +11,8 @@ class MediaPlayer {
final Duration position;
final Duration duration;
final Duration buffer;
final double? width;
final double? height;
final double width;
final double height;
final Future<void> Function() saveProgress;
final Future<void> Function() play;
final Future<void> Function() pause;

View File

@@ -5,16 +5,17 @@ import 'package:iris/models/player.dart';
import 'package:iris/utils/get_localizations.dart';
import 'package:iris/utils/logger.dart';
import 'package:media_kit/media_kit.dart';
import 'package:provider/provider.dart';
class AudioTrackList extends HookWidget {
const AudioTrackList({super.key, required this.player});
final MediaPlayer player;
const AudioTrackList({super.key});
@override
Widget build(BuildContext context) {
final t = getLocalizations(context);
final player = context.read<MediaPlayer>();
final focusNode = useFocusNode();
useEffect(() {
@@ -25,47 +26,41 @@ class AudioTrackList extends HookWidget {
if (player is MediaKitPlayer) {
return ListView(
children: [
...(player as MediaKitPlayer).audios.map(
(audio) => ListTile(
focusNode: (player as MediaKitPlayer).audio == audio
? focusNode
: null,
title: Text(
audio == AudioTrack.auto()
? t.auto
: audio == AudioTrack.no()
? t.off
: audio.title ?? audio.language ?? audio.id,
style: (player as MediaKitPlayer).audio == audio
? TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
)
: TextStyle(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
),
tileColor: (player as MediaKitPlayer).audio == audio
? Theme.of(context).hoverColor
: null,
onTap: () {
logger(
'Set audio track: ${audio.title ?? audio.language ?? audio.id}');
(player as MediaKitPlayer).player.setAudioTrack(audio);
Navigator.of(context).pop();
},
),
...player.audios.map(
(audio) => ListTile(
focusNode: player.audio == audio ? focusNode : null,
title: Text(
audio == AudioTrack.auto()
? t.auto
: audio == AudioTrack.no()
? t.off
: audio.title ?? audio.language ?? audio.id,
style: player.audio == audio
? TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
)
: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
tileColor:
player.audio == audio ? Theme.of(context).hoverColor : null,
onTap: () {
logger(
'Set audio track: ${audio.title ?? audio.language ?? audio.id}');
player.player.setAudioTrack(audio);
Navigator.of(context).pop();
},
),
),
],
);
}
if (player is FvpPlayer) {
final audios =
(player as FvpPlayer).controller.getMediaInfo()?.audio ?? [];
final activeAudioTracks =
(player as FvpPlayer).controller.getActiveAudioTracks() ?? [];
final audios = player.controller.getMediaInfo()?.audio ?? [];
final activeAudioTracks = player.controller.getActiveAudioTracks() ?? [];
return ListView(
children: [
ListTile(
@@ -85,7 +80,7 @@ class AudioTrackList extends HookWidget {
activeAudioTracks.isEmpty ? Theme.of(context).hoverColor : null,
onTap: () {
logger('Set audio track: ${t.off}');
(player as FvpPlayer).controller.setAudioTracks([]);
player.controller.setAudioTracks([]);
Navigator.of(context).pop();
},
),
@@ -113,9 +108,7 @@ class AudioTrackList extends HookWidget {
onTap: () {
logger(
'Set audio track: ${audio.metadata['title'] ?? audio.metadata['language'] ?? audios.indexOf(audio).toString()}');
(player as FvpPlayer)
.controller
.setAudioTracks([audios.indexOf(audio)]);
player.controller.setAudioTracks([audios.indexOf(audio)]);
Navigator.of(context).pop();
},
),

View File

@@ -23,18 +23,17 @@ import 'package:iris/utils/platform.dart';
import 'package:iris/utils/resize_window.dart';
import 'package:iris/widgets/popup.dart';
import 'package:iris/pages/storages/storages.dart';
import 'package:provider/provider.dart';
class ControlBar extends HookWidget {
const ControlBar({
super.key,
required this.player,
required this.showControl,
required this.showControlForHover,
this.color,
this.overlayColor,
});
final MediaPlayer player;
final void Function() showControl;
final Future<void> Function(Future<void> callback) showControlForHover;
final Color? color;
@@ -44,6 +43,15 @@ class ControlBar extends HookWidget {
Widget build(BuildContext context) {
final t = getLocalizations(context);
final isPlaying =
context.select<MediaPlayer, bool>((player) => player.isPlaying);
final isInitializing =
context.select<MediaPlayer, bool>((player) => player.isInitializing);
final play = context.read<MediaPlayer>().play;
final pause = context.read<MediaPlayer>().pause;
final rate = useAppStore().select(context, (state) => state.rate);
final volume = useAppStore().select(context, (state) => state.volume);
final isMuted = useAppStore().select(context, (state) => state.isMuted);
@@ -64,14 +72,14 @@ class ControlBar extends HookWidget {
final isSeeking =
usePlayerUiStore().select(context, (state) => state.isSeeking);
final displayIsPlaying = useState(player.isPlaying);
final displayIsPlaying = useState(isPlaying);
useEffect(() {
if (!isSeeking) {
displayIsPlaying.value = player.isPlaying;
displayIsPlaying.value = isPlaying;
}
return null;
}, [player.isPlaying]);
}, [isPlaying]);
return Container(
padding: const EdgeInsets.all(8),
@@ -92,7 +100,6 @@ class ControlBar extends HookWidget {
Visibility(
visible: MediaQuery.of(context).size.width < 1024 || !isDesktop,
child: ControlBarSlider(
player: player,
showControl: showControl,
color: color,
),
@@ -117,17 +124,17 @@ class ControlBar extends HookWidget {
),
onPressed: () {
showControl();
if (player.isPlaying == true) {
if (isPlaying == true) {
useAppStore().updateAutoPlay(false);
player.pause();
pause();
} else {
useAppStore().updateAutoPlay(true);
player.play();
play();
}
},
style: ButtonStyle(overlayColor: overlayColor),
),
if (player.isInitializing)
if (isInitializing)
SizedBox(
width: 36,
height: 36,
@@ -148,7 +155,7 @@ class ControlBar extends HookWidget {
onPressed: () {
showControl();
useAppStore().updateAutoPlay(false);
player.pause();
pause();
usePlayQueueStore().updateCurrentIndex(-1);
},
style: ButtonStyle(overlayColor: overlayColor),
@@ -331,7 +338,6 @@ class ControlBar extends HookWidget {
visible:
MediaQuery.of(context).size.width >= 1024 && isDesktop,
child: ControlBarSlider(
player: player,
showControl: showControl,
color: color,
),
@@ -349,7 +355,10 @@ class ControlBar extends HookWidget {
showControlForHover(
showPopup(
context: context,
child: SubtitleAndAudioTrack(player: player),
child: Provider<MediaPlayer>.value(
value: context.read<MediaPlayer>(),
child: const SubtitleAndAudioTrack(),
),
direction: PopupDirection.right,
),
);
@@ -602,7 +611,10 @@ class ControlBar extends HookWidget {
onTap: () => showControlForHover(
showPopup(
context: context,
child: SubtitleAndAudioTrack(player: player),
child: Provider<MediaPlayer>.value(
value: context.read<MediaPlayer>(),
child: const SubtitleAndAudioTrack(),
),
direction: PopupDirection.right,
),
),

View File

@@ -5,18 +5,16 @@ import 'package:iris/models/player.dart';
import 'package:iris/store/use_app_store.dart';
import 'package:iris/store/use_player_ui_store.dart';
import 'package:iris/utils/format_duration_to_minutes.dart';
import 'package:provider/provider.dart';
class ControlBarSlider extends HookWidget {
const ControlBarSlider({
super.key,
required this.player,
required this.showControl,
this.disabled = false,
this.color,
});
final MediaPlayer player;
final void Function() showControl;
final bool disabled;
final Color? color;
@@ -25,6 +23,24 @@ class ControlBarSlider extends HookWidget {
Widget build(BuildContext context) {
final autoPlay = useAppStore().select(context, (state) => state.autoPlay);
final progress = context.select<
MediaPlayer,
({
Duration position,
Duration duration,
Duration buffer,
})>(
(player) => (
position: player.position,
duration: player.duration,
buffer: player.buffer
),
);
final play = context.read<MediaPlayer>().play;
final pause = context.read<MediaPlayer>().pause;
final seek = context.read<MediaPlayer>().seek;
return ExcludeFocus(
child: Container(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
@@ -33,7 +49,7 @@ class ControlBarSlider extends HookWidget {
Visibility(
visible: !disabled,
child: Text(
formatDurationToMinutes(player.position),
formatDurationToMinutes(progress.position),
style: TextStyle(
color: color,
height: 2,
@@ -58,12 +74,12 @@ class ControlBarSlider extends HookWidget {
trackHeight: 3,
),
child: Slider(
value: player.buffer.inMilliseconds.toDouble() >
player.duration.inMilliseconds.toDouble()
value: progress.buffer.inMilliseconds.toDouble() >
progress.duration.inMilliseconds.toDouble()
? 0
: player.buffer.inMilliseconds.toDouble(),
: progress.buffer.inMilliseconds.toDouble(),
min: 0,
max: player.duration.inMilliseconds.toDouble(),
max: progress.duration.inMilliseconds.toDouble(),
onChanged: null,
),
),
@@ -83,23 +99,23 @@ class ControlBarSlider extends HookWidget {
trackHeight: 4,
),
child: Slider(
value: player.position.inMilliseconds.toDouble() >
player.duration.inMilliseconds.toDouble()
value: progress.position.inMilliseconds.toDouble() >
progress.duration.inMilliseconds.toDouble()
? 0
: player.position.inMilliseconds.toDouble(),
: progress.position.inMilliseconds.toDouble(),
min: 0,
max: player.duration.inMilliseconds.toDouble(),
max: progress.duration.inMilliseconds.toDouble(),
onChangeStart: (value) {
usePlayerUiStore().updateIsSeeking(true);
player.pause();
pause();
},
onChanged: (value) {
showControl();
player.seek(Duration(milliseconds: value.toInt()));
seek(Duration(milliseconds: value.toInt()));
},
onChangeEnd: (value) async {
if (autoPlay) {
player.play();
play();
}
usePlayerUiStore().updateIsSeeking(false);
},
@@ -111,7 +127,7 @@ class ControlBarSlider extends HookWidget {
Visibility(
visible: !disabled,
child: Text(
formatDurationToMinutes(player.duration),
formatDurationToMinutes(progress.duration),
style: TextStyle(
color: color,
height: 2,

View File

@@ -13,11 +13,11 @@ import 'package:iris/utils/format_duration_to_minutes.dart';
import 'package:iris/utils/resize_window.dart';
import 'package:iris/widgets/drag_aria.dart';
import 'package:iris/widgets/title_bar.dart';
import 'package:provider/provider.dart';
class ControlsOverlay extends HookWidget {
const ControlsOverlay({
super.key,
required this.player,
required this.currentPlay,
required this.title,
required this.showControl,
@@ -26,7 +26,6 @@ class ControlsOverlay extends HookWidget {
required this.showProgress,
});
final MediaPlayer player;
final PlayQueueItem? currentPlay;
final String title;
final Function() showControl;
@@ -36,6 +35,16 @@ class ControlsOverlay extends HookWidget {
@override
Widget build(BuildContext context) {
final isPlaying =
context.select<MediaPlayer, bool>((player) => player.isPlaying);
final progress =
context.select<MediaPlayer, ({Duration position, Duration duration})>(
(player) => (position: player.position, duration: player.duration),
);
final saveProgress = context.read<MediaPlayer>().saveProgress;
final rate = useAppStore().select(context, (state) => state.rate);
final aspectRatio =
@@ -46,7 +55,6 @@ class ControlsOverlay extends HookWidget {
usePlayerUiStore().select(context, (state) => state.isShowProgress);
final gesture = useGesture(
player: player,
showControl: showControl,
hideControl: hideControl,
showProgress: showProgress,
@@ -78,7 +86,7 @@ class ControlsOverlay extends HookWidget {
right: 0,
bottom: 0,
child: MouseRegion(
cursor: isShowControl || player.isPlaying == false
cursor: isShowControl || isPlaying == false
? SystemMouseCursors.basic
: SystemMouseCursors.none,
onHover: gesture.onHover,
@@ -231,7 +239,6 @@ class ControlsOverlay extends HookWidget {
bottom: -16,
height: 32,
child: ControlBarSlider(
player: player,
showControl: showControl,
disabled: true,
),
@@ -274,7 +281,7 @@ class ControlsOverlay extends HookWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${formatDurationToMinutes(player.position)} / ${formatDurationToMinutes(player.duration)}',
'${formatDurationToMinutes(progress.position)} / ${formatDurationToMinutes(progress.duration)}',
style: TextStyle(
color: Colors.white,
fontSize: 16,
@@ -317,7 +324,7 @@ class ControlsOverlay extends HookWidget {
actions: [const SizedBox(width: 8)],
color: contentColor,
overlayColor: overlayColor,
saveProgress: () => player.saveProgress(),
saveProgress: () => saveProgress(),
resizeWindow: () => resizeWindow(aspectRatio),
),
),
@@ -343,7 +350,6 @@ class ControlsOverlay extends HookWidget {
child: GestureDetector(
onTap: () => showControl(),
child: ControlBar(
player: player,
showControl: showControl,
showControlForHover: showControlForHover,
color: contentColor,

View File

@@ -22,20 +22,27 @@ import 'package:iris/utils/platform.dart';
import 'package:iris/store/use_app_store.dart';
import 'package:iris/store/use_play_queue_store.dart';
import 'package:iris/utils/get_localizations.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
class Player extends HookWidget {
const Player({super.key, required this.player});
final MediaPlayer player;
const Player({super.key});
@override
Widget build(BuildContext context) {
final t = getLocalizations(context);
useAppLifecycle(player.saveProgress);
final isPlaying =
context.select<MediaPlayer, bool>((player) => player.isPlaying);
final width = context.select<MediaPlayer, double>((player) => player.width);
final height =
context.select<MediaPlayer, double>((player) => player.height);
final cover = useCover(player.isPlaying);
final saveProgress = context.read<MediaPlayer>().saveProgress;
useAppLifecycle();
final cover = useCover();
final playerUiStore = usePlayerUiStore();
@@ -167,7 +174,7 @@ class Player extends HookWidget {
final showControlForHover = useCallback((Future<void> callback) async {
try {
player.saveProgress();
saveProgress();
showControl();
updateIsHovering(true);
await callback;
@@ -183,7 +190,6 @@ class Player extends HookWidget {
}, [updateIsShowProgress, resetBottomProgressTimer]);
final onKeyEvent = useKeyboard(
player: player,
showControl: showControl,
showControlForHover: showControlForHover,
showProgress: showProgress,
@@ -203,7 +209,7 @@ class Player extends HookWidget {
windowManager.setTitle(title);
}
return;
}, [title, player.isPlaying]);
}, [title, isPlaying]);
useEffect(() {
if (isShowControl || currentPlay?.file.type == ContentType.video) {
@@ -232,18 +238,12 @@ class Player extends HookWidget {
);
final videoViewSize = useMemoized(() {
if (fit != BoxFit.none || player.width == 0 || player.height == 0) {
if (fit != BoxFit.none || width == 0 || height == 0) {
return MediaQuery.of(context).size;
} else {
return Size(player.width! / scaleFactor, player.height! / scaleFactor);
return Size(width / scaleFactor, height / scaleFactor);
}
}, [
fit,
MediaQuery.of(context).size,
player.width,
player.height,
scaleFactor
]);
}, [fit, MediaQuery.of(context).size, width, height, scaleFactor]);
final videoViewOffset = useMemoized(
() => fit == BoxFit.none
@@ -285,7 +285,7 @@ class Player extends HookWidget {
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) async {
if (!didPop) {
await player.saveProgress();
await saveProgress();
if (!canPop.value) {
canPop.value = true;
if (context.mounted) {
@@ -311,7 +311,6 @@ class Player extends HookWidget {
height: videoViewSize.height,
child: VideoView(
key: ValueKey(currentPlay?.file.uri),
player: player,
fit: fit,
),
),
@@ -330,7 +329,6 @@ class Player extends HookWidget {
right: 0,
bottom: 0,
child: ControlsOverlay(
player: player,
currentPlay: currentPlay,
title: title,
showControl: showControl,

View File

@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:iris/hooks/player/use_fvp_player.dart';
import 'package:iris/hooks/player/use_media_kit_player.dart';
import 'package:iris/models/player.dart';
import 'package:iris/models/store/app_state.dart';
import 'package:iris/pages/player/player.dart';
import 'package:provider/provider.dart';
class PlayerView extends HookWidget {
const PlayerView({super.key, required this.playerBackend});
@@ -27,7 +29,10 @@ class _MediaKitPlayerHost extends HookWidget {
@override
Widget build(BuildContext context) {
final player = useMediaKitPlayer(context);
return Player(player: player);
return Provider<MediaPlayer>.value(
value: player,
child: const Player(key: ValueKey('media_kit_player')),
);
}
}
@@ -37,6 +42,9 @@ class _FvpPlayerHost extends HookWidget {
@override
Widget build(BuildContext context) {
final player = useFvpPlayer(context);
return Player(player: player);
return Provider<MediaPlayer>.value(
value: player,
child: const Player(key: ValueKey('fvp_player')),
);
}
}

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:iris/models/player.dart';
import 'package:iris/pages/player/audio_track_list.dart';
import 'package:iris/pages/player/subtitle_list.dart';
import 'package:iris/utils/get_localizations.dart';
@@ -16,17 +15,15 @@ class ITab {
}
class SubtitleAndAudioTrack extends HookWidget {
const SubtitleAndAudioTrack({super.key, required this.player});
final MediaPlayer player;
const SubtitleAndAudioTrack({super.key});
@override
Widget build(BuildContext context) {
final t = getLocalizations(context);
List<ITab> tabs = [
ITab(title: t.subtitle, child: SubtitleList(player: player)),
ITab(title: t.audio_track, child: AudioTrackList(player: player)),
ITab(title: t.subtitle, child: SubtitleList()),
ITab(title: t.audio_track, child: AudioTrackList()),
];
final tabController = useTabController(initialLength: tabs.length);

View File

@@ -10,20 +10,17 @@ import 'package:iris/utils/get_localizations.dart';
import 'package:iris/utils/logger.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_stream/media_stream.dart';
import 'package:provider/provider.dart';
class SubtitleList extends HookWidget {
const SubtitleList({super.key, required this.player});
final MediaPlayer player;
const SubtitleList({super.key});
@override
Widget build(BuildContext context) {
final t = getLocalizations(context);
final focusNode = useFocusNode();
MediaStream mediaStream = MediaStream();
final streamUrl = mediaStream.url;
final player = context.watch<MediaPlayer>();
final playQueue =
usePlayQueueStore().select(context, (state) => state.playQueue);
@@ -34,191 +31,124 @@ class SubtitleList extends HookWidget {
() => playQueue.indexWhere((element) => element.index == currentIndex),
[playQueue, currentIndex]);
final PlayQueueItem? currentPlay = useMemoized(
final file = useMemoized(
() => playQueue.isEmpty || currentPlayIndex < 0
? null
: playQueue[currentPlayIndex],
: playQueue[currentPlayIndex].file,
[playQueue, currentPlayIndex]);
useEffect(() {
focusNode.requestFocus();
return () => focusNode.unfocus();
return focusNode.unfocus;
}, []);
if (player is MediaKitPlayer) {
final activeSubtitle = context.select<MediaPlayer, SubtitleTrack>(
(p) => p is MediaKitPlayer ? p.subtitle : SubtitleTrack.no());
final subtitles = context.select<MediaPlayer, List<SubtitleTrack>>(
(p) => p is MediaKitPlayer ? p.subtitles : []);
final externalSubtitles = context.select<MediaPlayer, List<Subtitle>>(
(p) => p is MediaKitPlayer ? p.externalSubtitles : []);
return ListView(
children: [
...(player as MediaKitPlayer).subtitles.map(
(subtitle) => ListTile(
focusNode: (player as MediaKitPlayer).subtitle == subtitle
? focusNode
: null,
title: Text(
subtitle == SubtitleTrack.no()
? t.off
: subtitle.title ?? subtitle.language ?? subtitle.id,
style: (player as MediaKitPlayer).subtitle == subtitle
? TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
)
: TextStyle(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
),
tileColor: (player as MediaKitPlayer).subtitle == subtitle
? Theme.of(context).hoverColor
: null,
onTap: () {
logger(
'Set subtitle: ${subtitle.title ?? subtitle.language ?? subtitle.id}');
(player as MediaKitPlayer)
.player
.setSubtitleTrack(subtitle);
Navigator.of(context).pop();
},
),
),
...(player as MediaKitPlayer).externalSubtitles.map(
(subtitle) => ListTile(
title: Text(
subtitle.name,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
onTap: () {
logger('Set external subtitle: $subtitle');
final uri = currentPlay?.file.storageType == StorageType.ftp
? '$streamUrl/${subtitle.uri}'
: subtitle.uri;
logger('External subtitle uri: $uri');
(player as MediaKitPlayer).player.setSubtitleTrack(
SubtitleTrack.uri(
uri,
title: subtitle.name,
),
);
Navigator.of(context).pop();
},
),
...subtitles.map((subtitle) {
final bool isActive = activeSubtitle == subtitle;
return ListTile(
focusNode: isActive ? focusNode : null,
title: Text(
subtitle == SubtitleTrack.no()
? t.off
: subtitle.title ?? subtitle.language ?? subtitle.id,
style: isActive
? TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary)
: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
tileColor: isActive ? Theme.of(context).hoverColor : null,
onTap: () {
logger('Set subtitle: ${subtitle.id}');
player.player.setSubtitleTrack(subtitle);
Navigator.of(context).pop();
},
);
}),
...externalSubtitles.map((subtitle) {
return ListTile(
title: Text(subtitle.name,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant)),
onTap: () {
logger('Set external subtitle: $subtitle');
final mediaStream = MediaStream();
final uri = file?.storageType == StorageType.ftp
? '${mediaStream.url}/${subtitle.uri}'
: subtitle.uri;
player.player.setSubtitleTrack(
SubtitleTrack.uri(uri, title: subtitle.name));
Navigator.of(context).pop();
},
);
}),
],
);
}
if (player is FvpPlayer) {
final subtitles =
(player as FvpPlayer).controller.getMediaInfo()?.subtitle ?? [];
final activeSubtitles =
(player as FvpPlayer).controller.getActiveSubtitleTracks() ?? [];
final activeSubtitles = useListenableSelector(player.controller,
() => player.controller.getActiveSubtitleTracks() ?? []);
final externalSubtitleIndex = useValueListenable(player.externalSubtitle);
final subtitles = player.controller.getMediaInfo()?.subtitle ?? [];
final externalSubtitles = player.externalSubtitles;
return ListView(
children: [
ListTile(
focusNode: (player as FvpPlayer).externalSubtitle.value == null &&
activeSubtitles.isEmpty
selected: externalSubtitleIndex == null && activeSubtitles.isEmpty,
focusNode: externalSubtitleIndex == null && activeSubtitles.isEmpty
? focusNode
: null,
title: Text(
t.off,
style: (player as FvpPlayer).externalSubtitle.value == null &&
activeSubtitles.isEmpty
? TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
)
: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
tileColor: (player as FvpPlayer).externalSubtitle.value == null &&
activeSubtitles.isEmpty
? Theme.of(context).hoverColor
: null,
title: Text(t.off),
onTap: () {
logger('Set subtitle: ${t.off}');
(player as FvpPlayer).externalSubtitle.value = null;
(player as FvpPlayer).controller.setSubtitleTracks([]);
player.externalSubtitle.value = null;
player.controller.setSubtitleTracks([]);
Navigator.of(context).pop();
},
),
...subtitles.map(
(subtitle) => ListTile(
focusNode: (player as FvpPlayer).externalSubtitle.value == null &&
activeSubtitles.contains(subtitles.indexOf(subtitle))
? focusNode
: null,
title: Text(
subtitle.metadata['title'] ??
subtitle.metadata['language'] ??
subtitle.index.toString(),
style: (player as FvpPlayer).externalSubtitle.value == null &&
activeSubtitles.contains(subtitles.indexOf(subtitle))
? TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
)
: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
tileColor: (player as FvpPlayer).externalSubtitle.value == null &&
activeSubtitles.contains(subtitles.indexOf(subtitle))
? Theme.of(context).hoverColor
: null,
...subtitles.map((subtitle) {
final int index = subtitles.indexOf(subtitle);
final bool isActive = externalSubtitleIndex == null &&
activeSubtitles.contains(index);
return ListTile(
selected: isActive,
focusNode: isActive ? focusNode : null,
title: Text(subtitle.metadata['title'] ??
subtitle.metadata['language'] ??
subtitle.index.toString()),
onTap: () {
logger(
'Set subtitle: ${subtitle.metadata['title'] ?? subtitle.metadata['language'] ?? subtitle.index.toString()}');
(player as FvpPlayer).externalSubtitle.value = null;
(player as FvpPlayer)
.controller
.setSubtitleTracks([subtitles.indexOf(subtitle)]);
player.externalSubtitle.value = null;
player.controller.setSubtitleTracks([index]);
Navigator.of(context).pop();
},
),
),
...(player as FvpPlayer).externalSubtitles.map(
(subtitle) => ListTile(
focusNode: (player as FvpPlayer).externalSubtitle.value ==
(player as FvpPlayer)
.externalSubtitles
.indexOf(subtitle)
? focusNode
: null,
title: (player as FvpPlayer).externalSubtitle.value ==
(player as FvpPlayer)
.externalSubtitles
.indexOf(subtitle)
? Text(
subtitle.name,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary),
)
: Text(
subtitle.name,
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
),
tileColor: (player as FvpPlayer).externalSubtitle.value ==
(player as FvpPlayer)
.externalSubtitles
.indexOf(subtitle)
? Theme.of(context).hoverColor
: null,
onTap: () {
logger('Set external subtitle: $subtitle');
(player as FvpPlayer).externalSubtitle.value =
(player as FvpPlayer)
.externalSubtitles
.indexOf(subtitle);
Navigator.of(context).pop();
},
),
),
);
}),
...externalSubtitles.map((subtitle) {
final int index = externalSubtitles.indexOf(subtitle);
final bool isActive = externalSubtitleIndex == index;
return ListTile(
selected: isActive,
focusNode: isActive ? focusNode : null,
title: Text(subtitle.name),
onTap: () {
player.externalSubtitle.value = index;
Navigator.of(context).pop();
},
);
}),
],
);
}

View File

@@ -2,20 +2,21 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:iris/models/player.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart';
class VideoView extends HookWidget {
const VideoView({
super.key,
required this.player,
required this.fit,
});
final MediaPlayer player;
final BoxFit fit;
@override
Widget build(BuildContext context) {
final player = context.read<MediaPlayer>();
return switch (player) {
MediaKitPlayer player => Video(
controller: player.controller,