mirror of
https://github.com/K3vinb5/Unyo.git
synced 2026-06-13 13:49:43 +00:00
rewrite: Progress on the video screen
This commit is contained in:
@@ -159,9 +159,11 @@ class AnimeDetailsCubit extends Cubit<AnimeDetailsState> with EffectMixin<AnimeD
|
||||
_logger.i("Navigating to Video Player with selected video from ${state.selectedExtension?.name}");
|
||||
_videoInfoNotifier.updateVideoInfo(
|
||||
VideoInfoModel(
|
||||
currentVideo: selectedVideo,
|
||||
playlistIndex: episodeIndex,
|
||||
)
|
||||
currentVideo: selectedVideo,
|
||||
alternativeVideos: state.extensionVideoResults,
|
||||
videoIndex: state.extensionVideoResults.indexOf(selectedVideo),
|
||||
playlistIndex: episodeIndex,
|
||||
)
|
||||
);
|
||||
pushRouteEffect(path: "/video");
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:unyo/domain/entities/anime.dart';
|
||||
import 'package:unyo/domain/entities/user.dart';
|
||||
import 'package:unyo/core/di/locator.dart';
|
||||
import 'package:unyo/domain/entities/video_info.dart';
|
||||
import 'package:unyo/presentation/dialogs/warning_dialog.dart';
|
||||
|
||||
class VideoCubit extends Cubit<VideoState> with EffectMixin<VideoState> {
|
||||
// Repositories
|
||||
@@ -73,7 +74,15 @@ class VideoCubit extends Cubit<VideoState> with EffectMixin<VideoState> {
|
||||
emit(state.copyWith(loggedUser: loggedUser));
|
||||
});
|
||||
_videoInfoSubscription = _videoInfoNotifier.videoInfoStream.listen((videoInfo) {
|
||||
_videoService = VideoService(video: videoInfo.currentVideo, playlistIndex: videoInfo.playlistIndex, lowLatency: false);
|
||||
|
||||
_videoService = VideoService(
|
||||
video: videoInfo.currentVideo,
|
||||
alternativeVideos: videoInfo.alternativeVideos,
|
||||
videoIndex: videoInfo.videoIndex,
|
||||
playlistIndex: videoInfo.playlistIndex,
|
||||
onErrorCallback: (errorTitle) => showWidgetDialogEffect(dialog: WarningDialog(width: 500, height: 200, title: errorTitle)),
|
||||
lowLatency: false
|
||||
);
|
||||
_videoService.setPreventSleep(true);
|
||||
_videoServiceInitialized = true;
|
||||
emit(state.copyWith(videoInfo: videoInfo, isLoading: false));
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fvp/mdk.dart' as mdk;
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
@@ -23,24 +24,37 @@ class VideoService {
|
||||
final Logger _logger = sl<Logger>();
|
||||
final HttpService _httpService = sl<HttpService>();
|
||||
|
||||
ext.Video _video;
|
||||
final ext.Video _video;
|
||||
final List<ext.Video> _alternativeVideos;
|
||||
int _videoIndex;
|
||||
// This will be used for getting the correct video out of a playlist from a magnet / torrent
|
||||
int _playlistIndex;
|
||||
late final mdk.Player _player;
|
||||
late Timer seekTimer;
|
||||
final List<ext.Track> captionTracks = [];
|
||||
final List<ext.Track> audioTracks = [];
|
||||
ClosedCaptionFile? _currentCaptionFile;
|
||||
ext.Track? _currentCaptionTrack;
|
||||
ext.Track? _currentAudioTrack;
|
||||
bool _isBuffering = true;
|
||||
final bool _lowLatency;
|
||||
static const mdk.SeekFlag _seekFlags = mdk.SeekFlag(mdk.SeekFlag.fromStart | mdk.SeekFlag.inCache);
|
||||
final void Function(String) _onErrorCallback;
|
||||
|
||||
VideoService({required ext.Video video, required int playlistIndex, bool lowLatency = false})
|
||||
: _playlistIndex = playlistIndex,
|
||||
_video = video,
|
||||
_lowLatency = lowLatency {
|
||||
final bool _lowLatency;
|
||||
late Timer seekTimer;
|
||||
bool _isBuffering = true;
|
||||
static const mdk.SeekFlag _seekFlags = mdk.SeekFlag(mdk.SeekFlag.fromStart | mdk.SeekFlag.inCache | mdk.SeekFlag.fast);
|
||||
|
||||
VideoService({
|
||||
required ext.Video video,
|
||||
required List<ext.Video> alternativeVideos,
|
||||
required int videoIndex,
|
||||
required int playlistIndex,
|
||||
required void Function(String) onErrorCallback,
|
||||
bool lowLatency = false,
|
||||
}) : _playlistIndex = playlistIndex,
|
||||
_videoIndex = videoIndex,
|
||||
_video = video,
|
||||
_alternativeVideos = alternativeVideos,
|
||||
_onErrorCallback = onErrorCallback,
|
||||
_lowLatency = lowLatency {
|
||||
_player = mdk.Player();
|
||||
// Set player ffmpeg properties
|
||||
_configureDecoder();
|
||||
@@ -61,7 +75,6 @@ class VideoService {
|
||||
_initCaptionsAndAudiotracks();
|
||||
_player.state = mdk.PlaybackState.playing;
|
||||
setVolume(1.0);
|
||||
return false;
|
||||
}
|
||||
if (newStatus.test(mdk.MediaStatus.buffering)) {
|
||||
_isBuffering = true;
|
||||
@@ -69,8 +82,9 @@ class VideoService {
|
||||
if (newStatus.test(mdk.MediaStatus.buffered)) {
|
||||
_isBuffering = false;
|
||||
}
|
||||
if(newStatus.test(mdk.MediaStatus.invalid)) {
|
||||
// TODO Warn user about invalid media and stop playback / leave
|
||||
if (newStatus.test(mdk.MediaStatus.invalid)) {
|
||||
_onErrorCallback("Failed to load media. Please try again later.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -99,10 +113,9 @@ class VideoService {
|
||||
final streams = _player.mediaInfo.video;
|
||||
if (streams != null && streams.isNotEmpty) {
|
||||
final codec = streams.first.codec;
|
||||
// codec.width and codec.height are ints
|
||||
return codec.width / codec.height;
|
||||
}
|
||||
return 16 / 9;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Setters
|
||||
@@ -134,10 +147,23 @@ class VideoService {
|
||||
}
|
||||
|
||||
bool seekTo(Duration newDuration) {
|
||||
_player.seek(position: newDuration.inMilliseconds, flags: _seekFlags);
|
||||
if (isPlaying) {
|
||||
_player.seek(position: newDuration.inMilliseconds, flags: _seekFlags);
|
||||
} else {
|
||||
_player.seek(position: newDuration.inMilliseconds, flags: _seekFlags);
|
||||
_player.state = mdk.PlaybackState.paused;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool reverse(Duration reverseDuration) {
|
||||
return seekTo(Duration(milliseconds: position.inMilliseconds - reverseDuration.inMilliseconds));
|
||||
}
|
||||
|
||||
bool forward(Duration forwardDuration) {
|
||||
return seekTo(Duration(milliseconds: position.inMilliseconds + forwardDuration.inMilliseconds));
|
||||
}
|
||||
|
||||
bool setCaptionOffset(Duration duration) {
|
||||
return false;
|
||||
}
|
||||
@@ -149,9 +175,7 @@ class VideoService {
|
||||
}
|
||||
_currentCaptionTrack = captionTracks[captionIndex];
|
||||
if (_currentCaptionTrack!.embedded) {
|
||||
_player.activeSubtitleTracks = [
|
||||
_currentCaptionTrack?.embeddedIndex ?? 0
|
||||
];
|
||||
_player.activeSubtitleTracks = [_currentCaptionTrack?.embeddedIndex ?? 0];
|
||||
} else {
|
||||
_currentCaptionFile = await _loadExternalCaption(_currentCaptionTrack!);
|
||||
_player.setMedia(_currentCaptionTrack!.url, mdk.MediaType.subtitle);
|
||||
@@ -166,9 +190,7 @@ class VideoService {
|
||||
}
|
||||
_currentAudioTrack = audioTracks[audioTrackIndex];
|
||||
if (_currentAudioTrack!.embedded) {
|
||||
_player.activeAudioTracks = [
|
||||
_currentAudioTrack?.embeddedIndex ?? 0
|
||||
];
|
||||
_player.activeAudioTracks = [_currentAudioTrack?.embeddedIndex ?? 0];
|
||||
} else {
|
||||
_player.setMedia(_currentAudioTrack!.url, mdk.MediaType.audio);
|
||||
}
|
||||
@@ -181,7 +203,7 @@ class VideoService {
|
||||
}
|
||||
|
||||
bool setPreventSleep(bool preventSleep) {
|
||||
WakelockPlus.enable();
|
||||
WakelockPlus.toggle(enable: preventSleep);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -198,25 +220,19 @@ class VideoService {
|
||||
// Utilities
|
||||
void _configureDecoder() {
|
||||
final vd = {
|
||||
'windows': [
|
||||
'MFT:d3d=11',
|
||||
"D3D11",
|
||||
"DXVA",
|
||||
'CUDA',
|
||||
'hap',
|
||||
'FFmpeg',
|
||||
'dav1d'
|
||||
],
|
||||
'windows': ['MFT:d3d=11', "D3D11", "DXVA", 'CUDA', 'hap', 'FFmpeg', 'dav1d'],
|
||||
'macos': ['VT', 'hap', 'FFmpeg', 'dav1d'],
|
||||
'linux': ['VAAPI', 'CUDA', 'VDPAU', 'hap', 'FFmpeg', 'dav1d'],
|
||||
};
|
||||
_player.setDecoders(mdk.MediaType.video, vd[Platform.operatingSystem]!);
|
||||
}
|
||||
|
||||
void _configurePlayer() {
|
||||
_player.setProperty(
|
||||
'avio.protocol_whitelist',
|
||||
'file,ftp,rtmp,http,https,tls,rtp,tcp,udp,crypto,httpproxy,data,concatf,concat,subfile'
|
||||
'avio.protocol_whitelist',
|
||||
'file,ftp,rtmp,http,https,tls,rtp,tcp,udp,crypto,httpproxy,data,concatf,concat,subfile',
|
||||
);
|
||||
// Not sure about this flag
|
||||
_player.setProperty('video.decoder', 'shader_resource=0');
|
||||
_player.setProperty('avformat.strict', 'experimental');
|
||||
_player.setProperty('avformat.safe', '0');
|
||||
@@ -231,49 +247,44 @@ class VideoService {
|
||||
_player.setProperty('avformat.analyzeduration', '100000');
|
||||
_player.setBufferRange(min: 0, max: 1000, drop: true);
|
||||
} else {
|
||||
_player.setBufferRange(min: 0, max: 4000, drop: false);
|
||||
_player.setBufferRange(min: 0, max: 5000, drop: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _setPlayerHttpHeaders(ext.Headers? headers) {
|
||||
if (headers == null || headers.headersMap.isEmpty) return;
|
||||
final userAgent = headers.headersMap.entries
|
||||
.firstWhere(
|
||||
(e) => e.key.toLowerCase() == 'user-agent',
|
||||
orElse: () => const MapEntry('', '')
|
||||
)
|
||||
.value;
|
||||
if (headers == null || headers.headersMap.isEmpty) return;
|
||||
final userAgent = headers.headersMap.entries
|
||||
.firstWhere((e) => e.key.toLowerCase() == 'user-agent', orElse: () => const MapEntry('', ''))
|
||||
.value;
|
||||
|
||||
if (userAgent.isNotEmpty) {
|
||||
_player.setProperty('user_agent', userAgent);
|
||||
}
|
||||
final formattedHeaders = headers.headersMap.entries
|
||||
.where((e) => e.key.toLowerCase() != 'user-agent') // Filter out UA
|
||||
.map((e) {
|
||||
// Fix cookie separator logic (HTTP spec requires '; ' not ',')
|
||||
final value = e.key.toLowerCase() == 'cookie'
|
||||
? e.value.replaceAll(',', '; ')
|
||||
: e.value;
|
||||
return '${e.key}: $value';
|
||||
})
|
||||
.join('\r\n'); // Join with CRLF
|
||||
if (userAgent.isNotEmpty) {
|
||||
_player.setProperty('user_agent', userAgent);
|
||||
}
|
||||
final formattedHeaders = headers.headersMap.entries
|
||||
// .where((e) => e.key.toLowerCase() != 'user-agent') // Filter out UA
|
||||
.map((e) {
|
||||
// Fix cookie separator logic (HTTP spec requires '; ' not ',')
|
||||
final value = e.key.toLowerCase() == 'cookie' ? e.value.replaceAll(',', '; ') : e.value;
|
||||
return '${e.key}: $value';
|
||||
})
|
||||
.join('\r\n'); // Join with CRLF
|
||||
|
||||
if (formattedHeaders.isNotEmpty) {
|
||||
_player.setProperty('headers', formattedHeaders);
|
||||
_player.setProperty('avio.headers', formattedHeaders);
|
||||
if (formattedHeaders.isNotEmpty) {
|
||||
_player.setProperty('headers', formattedHeaders);
|
||||
_player.setProperty('avio.headers', formattedHeaders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _setPlayerLogsHandler() {
|
||||
mdk.setLogHandler((mdk.LogLevel level, String message) {
|
||||
if (!message.contains("unloaded media's position")) {
|
||||
switch (level) {
|
||||
case mdk.LogLevel.debug:
|
||||
// _logger.d("MDK Log: $message");
|
||||
// _logger.d("MDK Log: $message");
|
||||
case mdk.LogLevel.info:
|
||||
// _logger.i("MDK Log: $message");
|
||||
// _logger.i("MDK Log: $message");
|
||||
case mdk.LogLevel.warning:
|
||||
// _logger.w("MDK Log: $message");
|
||||
// _logger.w("MDK Log: $message");
|
||||
case mdk.LogLevel.error:
|
||||
_logger.e("MDK Log: $message");
|
||||
case mdk.LogLevel.off:
|
||||
@@ -299,20 +310,24 @@ class VideoService {
|
||||
}
|
||||
}
|
||||
captionTracks.addAll(_video.subtitleTracks);
|
||||
setCaption(0);
|
||||
if (_player.mediaInfo.audio != null && _player.mediaInfo.audio!.length > 1) {
|
||||
for (mdk.AudioStreamInfo audioStreamInfo in _player.mediaInfo.audio!) {
|
||||
audioTracks.add(
|
||||
ext.Track(
|
||||
url: "",
|
||||
lang:
|
||||
"${audioStreamInfo.metadata["title"] ?? ""} (${audioStreamInfo.metadata["language"]} - Embedded)",
|
||||
"${audioStreamInfo.metadata["title"] ?? ""} (${audioStreamInfo.metadata["language"]} - Embedded)",
|
||||
embedded: true,
|
||||
embeddedIndex: audioStreamInfo.index
|
||||
embeddedIndex: audioStreamInfo.index,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
audioTracks.addAll(_video.audioTracks);
|
||||
if (audioTracks.isNotEmpty) {
|
||||
setAudioTrack(0);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ClosedCaptionFile> _loadExternalCaption(ext.Track captionTrack) async {
|
||||
|
||||
@@ -7,20 +7,26 @@ part 'video_info.g.dart';
|
||||
|
||||
abstract class VideoInfo {
|
||||
final ext.Video currentVideo;
|
||||
final List<ext.Video> alternativeVideos;
|
||||
final int videoIndex;
|
||||
final int playlistIndex;
|
||||
|
||||
const VideoInfo({required this.currentVideo, required this.playlistIndex});
|
||||
const VideoInfo({required this.currentVideo, required this.alternativeVideos, required this.videoIndex, required this.playlistIndex});
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class VideoInfoModel with _$VideoInfoModel implements VideoInfo {
|
||||
const factory VideoInfoModel({
|
||||
@ext.VideoConverter() required ext.Video currentVideo,
|
||||
@ext.VideoConverter() required List<ext.Video> alternativeVideos,
|
||||
required int videoIndex,
|
||||
required int playlistIndex,
|
||||
}) = _VideoInfoModel;
|
||||
|
||||
factory VideoInfoModel.empty() => VideoInfoModel(
|
||||
currentVideo: ext.Video.empty(),
|
||||
alternativeVideos: [],
|
||||
videoIndex: -1,
|
||||
playlistIndex: -1
|
||||
);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$VideoInfoModel {
|
||||
|
||||
@ext.VideoConverter() ext.Video get currentVideo; int get playlistIndex;
|
||||
@ext.VideoConverter() ext.Video get currentVideo;@ext.VideoConverter() List<ext.Video> get alternativeVideos; int get videoIndex; int get playlistIndex;
|
||||
/// Create a copy of VideoInfoModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -28,16 +28,16 @@ $VideoInfoModelCopyWith<VideoInfoModel> get copyWith => _$VideoInfoModelCopyWith
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is VideoInfoModel&&(identical(other.currentVideo, currentVideo) || other.currentVideo == currentVideo)&&(identical(other.playlistIndex, playlistIndex) || other.playlistIndex == playlistIndex));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is VideoInfoModel&&(identical(other.currentVideo, currentVideo) || other.currentVideo == currentVideo)&&const DeepCollectionEquality().equals(other.alternativeVideos, alternativeVideos)&&(identical(other.videoIndex, videoIndex) || other.videoIndex == videoIndex)&&(identical(other.playlistIndex, playlistIndex) || other.playlistIndex == playlistIndex));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,currentVideo,playlistIndex);
|
||||
int get hashCode => Object.hash(runtimeType,currentVideo,const DeepCollectionEquality().hash(alternativeVideos),videoIndex,playlistIndex);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VideoInfoModel(currentVideo: $currentVideo, playlistIndex: $playlistIndex)';
|
||||
return 'VideoInfoModel(currentVideo: $currentVideo, alternativeVideos: $alternativeVideos, videoIndex: $videoIndex, playlistIndex: $playlistIndex)';
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ abstract mixin class $VideoInfoModelCopyWith<$Res> {
|
||||
factory $VideoInfoModelCopyWith(VideoInfoModel value, $Res Function(VideoInfoModel) _then) = _$VideoInfoModelCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@ext.VideoConverter() ext.Video currentVideo, int playlistIndex
|
||||
@ext.VideoConverter() ext.Video currentVideo,@ext.VideoConverter() List<ext.Video> alternativeVideos, int videoIndex, int playlistIndex
|
||||
});
|
||||
|
||||
|
||||
@@ -65,10 +65,12 @@ class _$VideoInfoModelCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of VideoInfoModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? currentVideo = null,Object? playlistIndex = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? currentVideo = null,Object? alternativeVideos = null,Object? videoIndex = null,Object? playlistIndex = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
currentVideo: null == currentVideo ? _self.currentVideo : currentVideo // ignore: cast_nullable_to_non_nullable
|
||||
as ext.Video,playlistIndex: null == playlistIndex ? _self.playlistIndex : playlistIndex // ignore: cast_nullable_to_non_nullable
|
||||
as ext.Video,alternativeVideos: null == alternativeVideos ? _self.alternativeVideos : alternativeVideos // ignore: cast_nullable_to_non_nullable
|
||||
as List<ext.Video>,videoIndex: null == videoIndex ? _self.videoIndex : videoIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,playlistIndex: null == playlistIndex ? _self.playlistIndex : playlistIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
@@ -154,10 +156,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@ext.VideoConverter() ext.Video currentVideo, int playlistIndex)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@ext.VideoConverter() ext.Video currentVideo, @ext.VideoConverter() List<ext.Video> alternativeVideos, int videoIndex, int playlistIndex)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideoInfoModel() when $default != null:
|
||||
return $default(_that.currentVideo,_that.playlistIndex);case _:
|
||||
return $default(_that.currentVideo,_that.alternativeVideos,_that.videoIndex,_that.playlistIndex);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -175,10 +177,10 @@ return $default(_that.currentVideo,_that.playlistIndex);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@ext.VideoConverter() ext.Video currentVideo, int playlistIndex) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@ext.VideoConverter() ext.Video currentVideo, @ext.VideoConverter() List<ext.Video> alternativeVideos, int videoIndex, int playlistIndex) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideoInfoModel():
|
||||
return $default(_that.currentVideo,_that.playlistIndex);case _:
|
||||
return $default(_that.currentVideo,_that.alternativeVideos,_that.videoIndex,_that.playlistIndex);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -195,10 +197,10 @@ return $default(_that.currentVideo,_that.playlistIndex);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@ext.VideoConverter() ext.Video currentVideo, int playlistIndex)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@ext.VideoConverter() ext.Video currentVideo, @ext.VideoConverter() List<ext.Video> alternativeVideos, int videoIndex, int playlistIndex)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideoInfoModel() when $default != null:
|
||||
return $default(_that.currentVideo,_that.playlistIndex);case _:
|
||||
return $default(_that.currentVideo,_that.alternativeVideos,_that.videoIndex,_that.playlistIndex);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -210,10 +212,18 @@ return $default(_that.currentVideo,_that.playlistIndex);case _:
|
||||
@JsonSerializable()
|
||||
|
||||
class _VideoInfoModel implements VideoInfoModel {
|
||||
const _VideoInfoModel({@ext.VideoConverter() required this.currentVideo, required this.playlistIndex});
|
||||
const _VideoInfoModel({@ext.VideoConverter() required this.currentVideo, @ext.VideoConverter() required final List<ext.Video> alternativeVideos, required this.videoIndex, required this.playlistIndex}): _alternativeVideos = alternativeVideos;
|
||||
factory _VideoInfoModel.fromJson(Map<String, dynamic> json) => _$VideoInfoModelFromJson(json);
|
||||
|
||||
@override@ext.VideoConverter() final ext.Video currentVideo;
|
||||
final List<ext.Video> _alternativeVideos;
|
||||
@override@ext.VideoConverter() List<ext.Video> get alternativeVideos {
|
||||
if (_alternativeVideos is EqualUnmodifiableListView) return _alternativeVideos;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_alternativeVideos);
|
||||
}
|
||||
|
||||
@override final int videoIndex;
|
||||
@override final int playlistIndex;
|
||||
|
||||
/// Create a copy of VideoInfoModel
|
||||
@@ -229,16 +239,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _VideoInfoModel&&(identical(other.currentVideo, currentVideo) || other.currentVideo == currentVideo)&&(identical(other.playlistIndex, playlistIndex) || other.playlistIndex == playlistIndex));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _VideoInfoModel&&(identical(other.currentVideo, currentVideo) || other.currentVideo == currentVideo)&&const DeepCollectionEquality().equals(other._alternativeVideos, _alternativeVideos)&&(identical(other.videoIndex, videoIndex) || other.videoIndex == videoIndex)&&(identical(other.playlistIndex, playlistIndex) || other.playlistIndex == playlistIndex));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,currentVideo,playlistIndex);
|
||||
int get hashCode => Object.hash(runtimeType,currentVideo,const DeepCollectionEquality().hash(_alternativeVideos),videoIndex,playlistIndex);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VideoInfoModel(currentVideo: $currentVideo, playlistIndex: $playlistIndex)';
|
||||
return 'VideoInfoModel(currentVideo: $currentVideo, alternativeVideos: $alternativeVideos, videoIndex: $videoIndex, playlistIndex: $playlistIndex)';
|
||||
}
|
||||
|
||||
|
||||
@@ -249,7 +259,7 @@ abstract mixin class _$VideoInfoModelCopyWith<$Res> implements $VideoInfoModelCo
|
||||
factory _$VideoInfoModelCopyWith(_VideoInfoModel value, $Res Function(_VideoInfoModel) _then) = __$VideoInfoModelCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
@ext.VideoConverter() ext.Video currentVideo, int playlistIndex
|
||||
@ext.VideoConverter() ext.Video currentVideo,@ext.VideoConverter() List<ext.Video> alternativeVideos, int videoIndex, int playlistIndex
|
||||
});
|
||||
|
||||
|
||||
@@ -266,10 +276,12 @@ class __$VideoInfoModelCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of VideoInfoModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? currentVideo = null,Object? playlistIndex = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? currentVideo = null,Object? alternativeVideos = null,Object? videoIndex = null,Object? playlistIndex = null,}) {
|
||||
return _then(_VideoInfoModel(
|
||||
currentVideo: null == currentVideo ? _self.currentVideo : currentVideo // ignore: cast_nullable_to_non_nullable
|
||||
as ext.Video,playlistIndex: null == playlistIndex ? _self.playlistIndex : playlistIndex // ignore: cast_nullable_to_non_nullable
|
||||
as ext.Video,alternativeVideos: null == alternativeVideos ? _self._alternativeVideos : alternativeVideos // ignore: cast_nullable_to_non_nullable
|
||||
as List<ext.Video>,videoIndex: null == videoIndex ? _self.videoIndex : videoIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,playlistIndex: null == playlistIndex ? _self.playlistIndex : playlistIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -11,11 +11,21 @@ _VideoInfoModel _$VideoInfoModelFromJson(Map<String, dynamic> json) =>
|
||||
currentVideo: const VideoConverter().fromJson(
|
||||
json['currentVideo'] as Map<String, dynamic>,
|
||||
),
|
||||
alternativeVideos: (json['alternativeVideos'] as List<dynamic>)
|
||||
.map(
|
||||
(e) => const VideoConverter().fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
videoIndex: (json['videoIndex'] as num).toInt(),
|
||||
playlistIndex: (json['playlistIndex'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$VideoInfoModelToJson(_VideoInfoModel instance) =>
|
||||
<String, dynamic>{
|
||||
'currentVideo': const VideoConverter().toJson(instance.currentVideo),
|
||||
'alternativeVideos': instance.alternativeVideos
|
||||
.map(const VideoConverter().toJson)
|
||||
.toList(),
|
||||
'videoIndex': instance.videoIndex,
|
||||
'playlistIndex': instance.playlistIndex,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:fvp/fvp.dart' as fvp;
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
@@ -32,18 +31,16 @@ void main() async {
|
||||
setupLocatorAfterHiveInit();
|
||||
// Setup Window Manager options
|
||||
WindowOptions windowOptions = const WindowOptions(
|
||||
size: Size(1280, 720),
|
||||
center: true,
|
||||
minimumSize: Size(1280, 720),
|
||||
center: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
titleBarStyle: TitleBarStyle.normal,
|
||||
title: "Unyo",
|
||||
);
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
windowManager.show();
|
||||
windowManager.focus();
|
||||
windowManager.setPreventClose(true);
|
||||
});
|
||||
//Run Flutter app with localization and screen utilities
|
||||
// Run Flutter app with localization and screen utilities
|
||||
runApp(
|
||||
EasyLocalization(
|
||||
supportedLocales: const [Locale('en', 'GB')],
|
||||
|
||||
@@ -37,6 +37,7 @@ class _TextFieldDialogState extends State<TextFieldDialog> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
SizedBox(height: 5.h,),
|
||||
Text(widget.title, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 10.h),
|
||||
UnyoTextfield(
|
||||
|
||||
37
lib/presentation/dialogs/warning_dialog.dart
Normal file
37
lib/presentation/dialogs/warning_dialog.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:unyo/presentation/widgets/styled/dark_unyo_button.dart';
|
||||
|
||||
class WarningDialog extends StatelessWidget {
|
||||
final double width;
|
||||
final double height;
|
||||
final String title;
|
||||
|
||||
const WarningDialog({super.key, required this.width, required this.height, required this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0.w, vertical: 24.0.h),
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 15.h),
|
||||
DarkUnyoButton(
|
||||
text: "Confirm",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
@@ -5,7 +7,14 @@ class DarkUnyoButton extends StatelessWidget {
|
||||
final String? text;
|
||||
final Widget? child;
|
||||
final Color color;
|
||||
final Color textColor;
|
||||
final bool isEnabled;
|
||||
final double? maxWidth;
|
||||
final double? maxHeight;
|
||||
final double? minWidth;
|
||||
final double? minHeight;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final void Function() onPressed;
|
||||
|
||||
const DarkUnyoButton({
|
||||
@@ -15,20 +24,32 @@ class DarkUnyoButton extends StatelessWidget {
|
||||
this.isEnabled = true,
|
||||
this.child,
|
||||
this.color = const Color.fromARGB(255, 37, 37, 37),
|
||||
this.textColor = Colors.white,
|
||||
this.width,
|
||||
this.height,
|
||||
this.minWidth,
|
||||
this.minHeight,
|
||||
this.maxWidth,
|
||||
this.maxHeight,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
minimumSize: WidgetStatePropertyAll( Size(80.w, 45.h)),
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
color,
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: isEnabled ? onPressed : null,
|
||||
highlightColor: Color.lerp(Colors.white, color, 1.0),
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: Container(
|
||||
width: width ?? 80.w.clamp(minWidth ?? 0, maxWidth ?? math.max(minWidth ?? 0, 80.w)),
|
||||
height: height ?? 45.h.clamp(minHeight ?? 0, maxHeight ?? math.max(minHeight ?? 0, 45.h)),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20.0), color: color.withOpacity(0.9)),
|
||||
child: Center(
|
||||
child: Text(text ?? "", style: TextStyle(color: textColor, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
),
|
||||
foregroundColor: const WidgetStatePropertyAll(Colors.white),
|
||||
),
|
||||
onPressed: isEnabled ? onPressed : null,
|
||||
child: text != null ? Text(text!, style: TextStyle(fontSize: 10.sp),) : child ?? const SizedBox(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
import 'package:unyo/application/cubits/video_cubit.dart';
|
||||
import 'package:unyo/application/states/video_state.dart';
|
||||
|
||||
// Internal dependencies
|
||||
import 'package:unyo/core/services/video/video_service.dart';
|
||||
import 'package:unyo/presentation/widgets/styled/dark_unyo_button.dart';
|
||||
|
||||
class UnyoVideoControls extends StatefulWidget {
|
||||
final VideoCubit videoCubit;
|
||||
@@ -23,25 +25,35 @@ class _UnyoVideoControlsState extends State<UnyoVideoControls> with TickerProvid
|
||||
late AnimationController _controller;
|
||||
late Timer _refreshTimer;
|
||||
late VideoService _videoService;
|
||||
late Timer _hideControlsTimer;
|
||||
late bool _controlsVisible;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controlsVisible = true;
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 400), // Adjust the duration as needed
|
||||
);
|
||||
_controller.animateTo(1.0);
|
||||
_videoService = widget.videoService;
|
||||
_refreshTimer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||
if (mounted && _videoService.isPlaying) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
_hideControlsTimer = Timer(const Duration(seconds: 3), () {
|
||||
if (mounted && _videoService.isPlaying) {
|
||||
_controlsVisible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshTimer.cancel();
|
||||
_hideControlsTimer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -51,92 +63,264 @@ class _UnyoVideoControlsState extends State<UnyoVideoControls> with TickerProvid
|
||||
value: widget.videoCubit,
|
||||
child: BlocBuilder<VideoCubit, VideoState>(
|
||||
builder: (context, state) {
|
||||
return Stack(
|
||||
children: [
|
||||
// Header
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
return !_videoService.isBuffering
|
||||
? MouseRegion(
|
||||
onHover: (event) {
|
||||
_hideControlsTimer.cancel();
|
||||
_controlsVisible = true;
|
||||
setState(() {});
|
||||
_hideControlsTimer = Timer(const Duration(seconds: 3), () {
|
||||
if (mounted && _videoService.isPlaying) {
|
||||
_controlsVisible = false;
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
},
|
||||
cursor: _controlsVisible ? SystemMouseCursors.basic : SystemMouseCursors.none,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
opacity: _controlsVisible ? 1 : 0,
|
||||
child: Stack(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => context.read<VideoCubit>().navigateBackToAnimeDetailsPage(context),
|
||||
icon: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: ColorScheme.of(context).tertiary,
|
||||
// Screen Gestures
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (_videoService.isPlaying) {
|
||||
_refreshTimer.cancel();
|
||||
_videoService.pause();
|
||||
_controlsVisible = true;
|
||||
setState(() {});
|
||||
_controller.animateTo(0.0);
|
||||
}
|
||||
},
|
||||
),
|
||||
// Header
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
context.read<VideoCubit>().navigateBackToAnimeDetailsPage(context),
|
||||
icon: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: ColorScheme.of(context).tertiary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
"${state.selectedAnime.title.userPreferred} - Episode ${state.videoInfo.playlistIndex + 1}",
|
||||
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
"${state.selectedAnime.title.userPreferred} - Episode ${state.videoInfo.playlistIndex + 1}",
|
||||
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
|
||||
// Video Controls Overlay
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
opacity: _videoService.isPlaying ? 0 : 1,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: IgnorePointer(
|
||||
ignoring: _videoService.isPlaying,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_videoService.reverse(const Duration(seconds: 15));
|
||||
setState(() {});
|
||||
},
|
||||
icon: Tooltip(
|
||||
waitDuration: const Duration(milliseconds: 2000),
|
||||
textAlign: TextAlign.center,
|
||||
message: "Reverse 15s",
|
||||
child: Icon(Icons.fast_rewind_rounded, color: Colors.white, size: 80.w),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 20.w),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: Tooltip(
|
||||
waitDuration: const Duration(milliseconds: 2000),
|
||||
textAlign: TextAlign.center,
|
||||
message: "Resume Play",
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (!_videoService.isPlaying) {
|
||||
_videoService.play();
|
||||
setState(() {});
|
||||
_refreshTimer = Timer.periodic(
|
||||
const Duration(milliseconds: 100),
|
||||
(_) {
|
||||
if (mounted && _videoService.isPlaying) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
);
|
||||
_hideControlsTimer = Timer(const Duration(seconds: 3), () {
|
||||
if (mounted && _videoService.isPlaying) {
|
||||
_controlsVisible = false;
|
||||
}
|
||||
});
|
||||
_controller.animateTo(1.0);
|
||||
}
|
||||
},
|
||||
// highlightColor: Colors.white12,
|
||||
hoverColor: Colors.white10,
|
||||
splashColor: Colors.white12,
|
||||
borderRadius: BorderRadius.circular(60.w),
|
||||
child: AnimatedIcon(
|
||||
icon: AnimatedIcons.play_pause,
|
||||
size: 100.w,
|
||||
color: Colors.white,
|
||||
progress: _controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 20.w),
|
||||
Tooltip(
|
||||
waitDuration: const Duration(milliseconds: 2000),
|
||||
textAlign: TextAlign.center,
|
||||
message: "Forward 15s",
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
_videoService.forward(const Duration(seconds: 15));
|
||||
setState(() {});
|
||||
},
|
||||
icon: Icon(Icons.fast_forward_rounded, color: Colors.white, size: 80.w),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Video Slider And Controls
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SizedBox(
|
||||
height: 135,
|
||||
child: Column(
|
||||
children: [
|
||||
// Video Slider
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// TODO - AniSkip
|
||||
Opacity(
|
||||
opacity: 0.7,
|
||||
child: DarkUnyoButton(
|
||||
text: "${state.loggedUser.settings.manualSkipTime.toString()}s",
|
||||
color: ColorScheme.of(context).primary,
|
||||
width: 80.w,
|
||||
maxWidth: 90,
|
||||
maxHeight: 45,
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
SizedBox(width: 20.w),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15,),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 20.w),
|
||||
SizedBox(
|
||||
width: 75,
|
||||
child: Text(
|
||||
_videoService.position.toString().substring(0, 7),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w400),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
min: 0,
|
||||
max: _videoService.duration.inMilliseconds.toDouble(),
|
||||
value: _videoService.position.inMilliseconds.toDouble(),
|
||||
label: _videoService.formattedPosition,
|
||||
divisions: _videoService.duration.inMilliseconds.toDouble() > 0
|
||||
? _videoService.duration.inMilliseconds
|
||||
: null,
|
||||
onChanged: (value) {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
_videoService.seekTo(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 75,
|
||||
child: Text(
|
||||
_videoService.duration.toString().substring(0, 7),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w400),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 15.w),
|
||||
],
|
||||
),
|
||||
// Video Controls
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Left Aligned
|
||||
Row(mainAxisAlignment: MainAxisAlignment.start, children: []),
|
||||
// Right Aligned
|
||||
Row(mainAxisAlignment: MainAxisAlignment.end, children: []),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Video Controls Overlay
|
||||
const Align(
|
||||
alignment: Alignment.center,
|
||||
child: Row(children: []),
|
||||
),
|
||||
// Video Slider And Controls
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SizedBox(
|
||||
height: 90,
|
||||
child: Column(
|
||||
children: [
|
||||
// Video Slider
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(width: 20.w),
|
||||
SizedBox(
|
||||
width: 75,
|
||||
child: Text(
|
||||
_videoService.position.toString().substring(0, 7),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
)
|
||||
: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
context.read<VideoCubit>().navigateBackToAnimeDetailsPage(context),
|
||||
icon: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: ColorScheme.of(context).tertiary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
"${state.selectedAnime.title.userPreferred} - Episode ${state.videoInfo.playlistIndex + 1}",
|
||||
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
min: 0,
|
||||
max: _videoService.duration.inMilliseconds.toDouble(),
|
||||
value: _videoService.position.inMilliseconds.toDouble(),
|
||||
label: _videoService.formattedPosition,
|
||||
divisions: _videoService.duration.inMilliseconds.toDouble() > 0
|
||||
? _videoService.duration.inMilliseconds
|
||||
: null,
|
||||
onChanged: (value) {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
_videoService.seekTo(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 75,
|
||||
child: Text(
|
||||
_videoService.duration.toString().substring(0, 7),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 15.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Video Controls
|
||||
Row(children: []),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Center(
|
||||
child: LoadingAnimationWidget.fourRotatingDots(color: Colors.white, size: 100.w),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:unyo/core/services/video/video_service.dart';
|
||||
|
||||
@@ -19,6 +21,17 @@ class _UnyoVideoTextureState extends State<UnyoVideoTexture> {
|
||||
super.initState();
|
||||
_videoService = widget.videoService;
|
||||
aspectRatio = _videoService.aspectRatio;
|
||||
if (aspectRatio <= 0) {
|
||||
aspectRatio = 16 / 9; // Default aspect ratio
|
||||
Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
||||
if (_videoService.aspectRatio > 0) {
|
||||
setState(() {
|
||||
aspectRatio = _videoService.aspectRatio;
|
||||
});
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
_videoService.updateTexture();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- mdk (~> 0.33.1)
|
||||
- mdk (0.33.1)
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -25,6 +27,8 @@ PODS:
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- window_manager (0.5.0):
|
||||
- FlutterMacOS
|
||||
|
||||
@@ -34,11 +38,13 @@ DEPENDENCIES:
|
||||
- flutter_discord_rpc (from `Flutter/ephemeral/.symlinks/plugins/flutter_discord_rpc/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- fvp (from `Flutter/ephemeral/.symlinks/plugins/fvp/darwin`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -56,6 +62,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral
|
||||
fvp:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/fvp/darwin
|
||||
package_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||
path_provider_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||
screen_retriever_macos:
|
||||
@@ -66,6 +74,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
video_player_avfoundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin
|
||||
wakelock_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||
window_manager:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
@@ -76,11 +86,13 @@ SPEC CHECKSUMS:
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
fvp: ed100b827d10aff53789fb9cb9dfdd85a87d037d
|
||||
mdk: 622e3452cea55a982c0712ec9ce931d8ec718b22
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
|
||||
window_manager: b729e31d38fb04905235df9ea896128991cad99e
|
||||
|
||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||
|
||||
Reference in New Issue
Block a user