mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -07:00
feature: Auto next-up preview, skip to next in queue. (#96)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
f72ae9e3ca
commit
66f2b6cd4e
13 changed files with 971 additions and 137 deletions
|
|
@ -1073,5 +1073,11 @@
|
||||||
"off": "Off",
|
"off": "Off",
|
||||||
"screenBrightness": "Screen brightness",
|
"screenBrightness": "Screen brightness",
|
||||||
"scale":"Scale",
|
"scale":"Scale",
|
||||||
"playBackSettings": "Playback Settings"
|
"playBackSettings": "Playback Settings",
|
||||||
|
"settingsAutoNextTitle": "Next-up preview",
|
||||||
|
"settingsAutoNextDesc": "Displays a next-up preview near the end if another item is queued",
|
||||||
|
"autoNextOffSmartTitle": "Smart",
|
||||||
|
"autoNextOffSmartDesc": "Shows the next-up screen when the credits start if no more then 10 seconds remain after the credits. Else it shows the next-up screen with 30 seconds of playtime remaining",
|
||||||
|
"autoNextOffStaticTitle": "Static",
|
||||||
|
"autoNextOffStaticDesc": "Show the next-up screen when 30 seconds of playtime remain"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,35 @@
|
||||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class VideoPlayerSettingsModel {
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
final double? screenBrightness;
|
|
||||||
final BoxFit videoFit;
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
final bool fillScreen;
|
|
||||||
final bool hardwareAccel;
|
part 'video_player_settings.freezed.dart';
|
||||||
final bool useLibass;
|
part 'video_player_settings.g.dart';
|
||||||
final double internalVolume;
|
|
||||||
final String? audioDevice;
|
@freezed
|
||||||
const VideoPlayerSettingsModel({
|
class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
||||||
this.screenBrightness,
|
const VideoPlayerSettingsModel._();
|
||||||
this.videoFit = BoxFit.contain,
|
|
||||||
this.fillScreen = false,
|
factory VideoPlayerSettingsModel({
|
||||||
this.hardwareAccel = true,
|
double? screenBrightness,
|
||||||
this.useLibass = false,
|
@Default(BoxFit.contain) BoxFit videoFit,
|
||||||
this.internalVolume = 100,
|
@Default(false) bool fillScreen,
|
||||||
this.audioDevice,
|
@Default(true) bool hardwareAccel,
|
||||||
});
|
@Default(false) bool useLibass,
|
||||||
|
@Default(100) double internalVolume,
|
||||||
|
@Default(AutoNextType.static) AutoNextType nextVideoType,
|
||||||
|
String? audioDevice,
|
||||||
|
}) = _VideoPlayerSettingsModel;
|
||||||
|
|
||||||
double get volume => switch (defaultTargetPlatform) {
|
double get volume => switch (defaultTargetPlatform) {
|
||||||
TargetPlatform.android || TargetPlatform.iOS => 100,
|
TargetPlatform.android || TargetPlatform.iOS => 100,
|
||||||
_ => internalVolume,
|
_ => internalVolume,
|
||||||
};
|
};
|
||||||
|
|
||||||
VideoPlayerSettingsModel copyWith({
|
factory VideoPlayerSettingsModel.fromJson(Map<String, dynamic> json) => _$VideoPlayerSettingsModelFromJson(json);
|
||||||
ValueGetter<double?>? screenBrightness,
|
|
||||||
BoxFit? videoFit,
|
|
||||||
bool? fillScreen,
|
|
||||||
bool? hardwareAccel,
|
|
||||||
bool? useLibass,
|
|
||||||
double? internalVolume,
|
|
||||||
ValueGetter<String?>? audioDevice,
|
|
||||||
}) {
|
|
||||||
return VideoPlayerSettingsModel(
|
|
||||||
screenBrightness: screenBrightness != null ? screenBrightness() : this.screenBrightness,
|
|
||||||
videoFit: videoFit ?? this.videoFit,
|
|
||||||
fillScreen: fillScreen ?? this.fillScreen,
|
|
||||||
hardwareAccel: hardwareAccel ?? this.hardwareAccel,
|
|
||||||
useLibass: useLibass ?? this.useLibass,
|
|
||||||
internalVolume: internalVolume ?? this.internalVolume,
|
|
||||||
audioDevice: audioDevice != null ? audioDevice() : this.audioDevice,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'screenBrightness': screenBrightness,
|
|
||||||
'videoFit': videoFit.name,
|
|
||||||
'fillScreen': fillScreen,
|
|
||||||
'hardwareAccel': hardwareAccel,
|
|
||||||
'useLibass': useLibass,
|
|
||||||
'internalVolume': internalVolume,
|
|
||||||
'audioDevice': audioDevice,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory VideoPlayerSettingsModel.fromMap(Map<String, dynamic> map) {
|
|
||||||
return VideoPlayerSettingsModel(
|
|
||||||
screenBrightness: map['screenBrightness']?.toDouble(),
|
|
||||||
videoFit: BoxFit.values.firstWhereOrNull((element) => element.name == map['videoFit']) ?? BoxFit.contain,
|
|
||||||
fillScreen: map['fillScreen'] ?? false,
|
|
||||||
hardwareAccel: map['hardwareAccel'] ?? false,
|
|
||||||
useLibass: map['useLibass'] ?? false,
|
|
||||||
internalVolume: map['internalVolume']?.toDouble() ?? 0.0,
|
|
||||||
audioDevice: map['audioDevice'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory VideoPlayerSettingsModel.fromJson(String source) => VideoPlayerSettingsModel.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, internalVolume: $internalVolume, audioDevice: $audioDevice)';
|
|
||||||
}
|
|
||||||
|
|
||||||
bool playerSame(VideoPlayerSettingsModel other) {
|
bool playerSame(VideoPlayerSettingsModel other) {
|
||||||
return other.hardwareAccel == hardwareAccel && other.useLibass == useLibass;
|
return other.hardwareAccel == hardwareAccel && other.useLibass == useLibass;
|
||||||
|
|
@ -111,3 +60,23 @@ class VideoPlayerSettingsModel {
|
||||||
audioDevice.hashCode;
|
audioDevice.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AutoNextType {
|
||||||
|
off,
|
||||||
|
smart,
|
||||||
|
static;
|
||||||
|
|
||||||
|
const AutoNextType();
|
||||||
|
|
||||||
|
String label(BuildContext context) => switch (this) {
|
||||||
|
AutoNextType.off => context.localized.off,
|
||||||
|
AutoNextType.smart => context.localized.autoNextOffSmartTitle,
|
||||||
|
AutoNextType.static => context.localized.autoNextOffStaticTitle,
|
||||||
|
};
|
||||||
|
|
||||||
|
String desc(BuildContext context) => switch (this) {
|
||||||
|
AutoNextType.off => context.localized.off,
|
||||||
|
AutoNextType.smart => context.localized.autoNextOffSmartDesc,
|
||||||
|
AutoNextType.static => context.localized.autoNextOffStaticDesc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
319
lib/models/settings/video_player_settings.freezed.dart
Normal file
319
lib/models/settings/video_player_settings.freezed.dart
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'video_player_settings.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
VideoPlayerSettingsModel _$VideoPlayerSettingsModelFromJson(
|
||||||
|
Map<String, dynamic> json) {
|
||||||
|
return _VideoPlayerSettingsModel.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$VideoPlayerSettingsModel {
|
||||||
|
double? get screenBrightness => throw _privateConstructorUsedError;
|
||||||
|
BoxFit get videoFit => throw _privateConstructorUsedError;
|
||||||
|
bool get fillScreen => throw _privateConstructorUsedError;
|
||||||
|
bool get hardwareAccel => throw _privateConstructorUsedError;
|
||||||
|
bool get useLibass => throw _privateConstructorUsedError;
|
||||||
|
double get internalVolume => throw _privateConstructorUsedError;
|
||||||
|
AutoNextType get nextVideoType => throw _privateConstructorUsedError;
|
||||||
|
String? get audioDevice => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this VideoPlayerSettingsModel to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of VideoPlayerSettingsModel
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$VideoPlayerSettingsModelCopyWith<VideoPlayerSettingsModel> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $VideoPlayerSettingsModelCopyWith<$Res> {
|
||||||
|
factory $VideoPlayerSettingsModelCopyWith(VideoPlayerSettingsModel value,
|
||||||
|
$Res Function(VideoPlayerSettingsModel) then) =
|
||||||
|
_$VideoPlayerSettingsModelCopyWithImpl<$Res, VideoPlayerSettingsModel>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{double? screenBrightness,
|
||||||
|
BoxFit videoFit,
|
||||||
|
bool fillScreen,
|
||||||
|
bool hardwareAccel,
|
||||||
|
bool useLibass,
|
||||||
|
double internalVolume,
|
||||||
|
AutoNextType nextVideoType,
|
||||||
|
String? audioDevice});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
|
||||||
|
$Val extends VideoPlayerSettingsModel>
|
||||||
|
implements $VideoPlayerSettingsModelCopyWith<$Res> {
|
||||||
|
_$VideoPlayerSettingsModelCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of VideoPlayerSettingsModel
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? screenBrightness = freezed,
|
||||||
|
Object? videoFit = null,
|
||||||
|
Object? fillScreen = null,
|
||||||
|
Object? hardwareAccel = null,
|
||||||
|
Object? useLibass = null,
|
||||||
|
Object? internalVolume = null,
|
||||||
|
Object? nextVideoType = null,
|
||||||
|
Object? audioDevice = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
screenBrightness: freezed == screenBrightness
|
||||||
|
? _value.screenBrightness
|
||||||
|
: screenBrightness // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double?,
|
||||||
|
videoFit: null == videoFit
|
||||||
|
? _value.videoFit
|
||||||
|
: videoFit // ignore: cast_nullable_to_non_nullable
|
||||||
|
as BoxFit,
|
||||||
|
fillScreen: null == fillScreen
|
||||||
|
? _value.fillScreen
|
||||||
|
: fillScreen // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
hardwareAccel: null == hardwareAccel
|
||||||
|
? _value.hardwareAccel
|
||||||
|
: hardwareAccel // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
useLibass: null == useLibass
|
||||||
|
? _value.useLibass
|
||||||
|
: useLibass // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
internalVolume: null == internalVolume
|
||||||
|
? _value.internalVolume
|
||||||
|
: internalVolume // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
nextVideoType: null == nextVideoType
|
||||||
|
? _value.nextVideoType
|
||||||
|
: nextVideoType // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AutoNextType,
|
||||||
|
audioDevice: freezed == audioDevice
|
||||||
|
? _value.audioDevice
|
||||||
|
: audioDevice // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$VideoPlayerSettingsModelImplCopyWith<$Res>
|
||||||
|
implements $VideoPlayerSettingsModelCopyWith<$Res> {
|
||||||
|
factory _$$VideoPlayerSettingsModelImplCopyWith(
|
||||||
|
_$VideoPlayerSettingsModelImpl value,
|
||||||
|
$Res Function(_$VideoPlayerSettingsModelImpl) then) =
|
||||||
|
__$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{double? screenBrightness,
|
||||||
|
BoxFit videoFit,
|
||||||
|
bool fillScreen,
|
||||||
|
bool hardwareAccel,
|
||||||
|
bool useLibass,
|
||||||
|
double internalVolume,
|
||||||
|
AutoNextType nextVideoType,
|
||||||
|
String? audioDevice});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
|
||||||
|
extends _$VideoPlayerSettingsModelCopyWithImpl<$Res,
|
||||||
|
_$VideoPlayerSettingsModelImpl>
|
||||||
|
implements _$$VideoPlayerSettingsModelImplCopyWith<$Res> {
|
||||||
|
__$$VideoPlayerSettingsModelImplCopyWithImpl(
|
||||||
|
_$VideoPlayerSettingsModelImpl _value,
|
||||||
|
$Res Function(_$VideoPlayerSettingsModelImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of VideoPlayerSettingsModel
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? screenBrightness = freezed,
|
||||||
|
Object? videoFit = null,
|
||||||
|
Object? fillScreen = null,
|
||||||
|
Object? hardwareAccel = null,
|
||||||
|
Object? useLibass = null,
|
||||||
|
Object? internalVolume = null,
|
||||||
|
Object? nextVideoType = null,
|
||||||
|
Object? audioDevice = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$VideoPlayerSettingsModelImpl(
|
||||||
|
screenBrightness: freezed == screenBrightness
|
||||||
|
? _value.screenBrightness
|
||||||
|
: screenBrightness // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double?,
|
||||||
|
videoFit: null == videoFit
|
||||||
|
? _value.videoFit
|
||||||
|
: videoFit // ignore: cast_nullable_to_non_nullable
|
||||||
|
as BoxFit,
|
||||||
|
fillScreen: null == fillScreen
|
||||||
|
? _value.fillScreen
|
||||||
|
: fillScreen // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
hardwareAccel: null == hardwareAccel
|
||||||
|
? _value.hardwareAccel
|
||||||
|
: hardwareAccel // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
useLibass: null == useLibass
|
||||||
|
? _value.useLibass
|
||||||
|
: useLibass // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
internalVolume: null == internalVolume
|
||||||
|
? _value.internalVolume
|
||||||
|
: internalVolume // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
nextVideoType: null == nextVideoType
|
||||||
|
? _value.nextVideoType
|
||||||
|
: nextVideoType // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AutoNextType,
|
||||||
|
audioDevice: freezed == audioDevice
|
||||||
|
? _value.audioDevice
|
||||||
|
: audioDevice // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
||||||
|
with DiagnosticableTreeMixin {
|
||||||
|
_$VideoPlayerSettingsModelImpl(
|
||||||
|
{this.screenBrightness,
|
||||||
|
this.videoFit = BoxFit.contain,
|
||||||
|
this.fillScreen = false,
|
||||||
|
this.hardwareAccel = true,
|
||||||
|
this.useLibass = false,
|
||||||
|
this.internalVolume = 100,
|
||||||
|
this.nextVideoType = AutoNextType.static,
|
||||||
|
this.audioDevice})
|
||||||
|
: super._();
|
||||||
|
|
||||||
|
factory _$VideoPlayerSettingsModelImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$VideoPlayerSettingsModelImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final double? screenBrightness;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final BoxFit videoFit;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final bool fillScreen;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final bool hardwareAccel;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final bool useLibass;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final double internalVolume;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final AutoNextType nextVideoType;
|
||||||
|
@override
|
||||||
|
final String? audioDevice;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||||
|
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, internalVolume: $internalVolume, nextVideoType: $nextVideoType, audioDevice: $audioDevice)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties
|
||||||
|
..add(DiagnosticsProperty('type', 'VideoPlayerSettingsModel'))
|
||||||
|
..add(DiagnosticsProperty('screenBrightness', screenBrightness))
|
||||||
|
..add(DiagnosticsProperty('videoFit', videoFit))
|
||||||
|
..add(DiagnosticsProperty('fillScreen', fillScreen))
|
||||||
|
..add(DiagnosticsProperty('hardwareAccel', hardwareAccel))
|
||||||
|
..add(DiagnosticsProperty('useLibass', useLibass))
|
||||||
|
..add(DiagnosticsProperty('internalVolume', internalVolume))
|
||||||
|
..add(DiagnosticsProperty('nextVideoType', nextVideoType))
|
||||||
|
..add(DiagnosticsProperty('audioDevice', audioDevice));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of VideoPlayerSettingsModel
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$VideoPlayerSettingsModelImplCopyWith<_$VideoPlayerSettingsModelImpl>
|
||||||
|
get copyWith => __$$VideoPlayerSettingsModelImplCopyWithImpl<
|
||||||
|
_$VideoPlayerSettingsModelImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$VideoPlayerSettingsModelImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
|
||||||
|
factory _VideoPlayerSettingsModel(
|
||||||
|
{final double? screenBrightness,
|
||||||
|
final BoxFit videoFit,
|
||||||
|
final bool fillScreen,
|
||||||
|
final bool hardwareAccel,
|
||||||
|
final bool useLibass,
|
||||||
|
final double internalVolume,
|
||||||
|
final AutoNextType nextVideoType,
|
||||||
|
final String? audioDevice}) = _$VideoPlayerSettingsModelImpl;
|
||||||
|
_VideoPlayerSettingsModel._() : super._();
|
||||||
|
|
||||||
|
factory _VideoPlayerSettingsModel.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$VideoPlayerSettingsModelImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? get screenBrightness;
|
||||||
|
@override
|
||||||
|
BoxFit get videoFit;
|
||||||
|
@override
|
||||||
|
bool get fillScreen;
|
||||||
|
@override
|
||||||
|
bool get hardwareAccel;
|
||||||
|
@override
|
||||||
|
bool get useLibass;
|
||||||
|
@override
|
||||||
|
double get internalVolume;
|
||||||
|
@override
|
||||||
|
AutoNextType get nextVideoType;
|
||||||
|
@override
|
||||||
|
String? get audioDevice;
|
||||||
|
|
||||||
|
/// Create a copy of VideoPlayerSettingsModel
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$VideoPlayerSettingsModelImplCopyWith<_$VideoPlayerSettingsModelImpl>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
52
lib/models/settings/video_player_settings.g.dart
Normal file
52
lib/models/settings/video_player_settings.g.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'video_player_settings.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$VideoPlayerSettingsModelImpl _$$VideoPlayerSettingsModelImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$VideoPlayerSettingsModelImpl(
|
||||||
|
screenBrightness: (json['screenBrightness'] as num?)?.toDouble(),
|
||||||
|
videoFit: $enumDecodeNullable(_$BoxFitEnumMap, json['videoFit']) ??
|
||||||
|
BoxFit.contain,
|
||||||
|
fillScreen: json['fillScreen'] as bool? ?? false,
|
||||||
|
hardwareAccel: json['hardwareAccel'] as bool? ?? true,
|
||||||
|
useLibass: json['useLibass'] as bool? ?? false,
|
||||||
|
internalVolume: (json['internalVolume'] as num?)?.toDouble() ?? 100,
|
||||||
|
nextVideoType:
|
||||||
|
$enumDecodeNullable(_$AutoNextTypeEnumMap, json['nextVideoType']) ??
|
||||||
|
AutoNextType.static,
|
||||||
|
audioDevice: json['audioDevice'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$VideoPlayerSettingsModelImplToJson(
|
||||||
|
_$VideoPlayerSettingsModelImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'screenBrightness': instance.screenBrightness,
|
||||||
|
'videoFit': _$BoxFitEnumMap[instance.videoFit]!,
|
||||||
|
'fillScreen': instance.fillScreen,
|
||||||
|
'hardwareAccel': instance.hardwareAccel,
|
||||||
|
'useLibass': instance.useLibass,
|
||||||
|
'internalVolume': instance.internalVolume,
|
||||||
|
'nextVideoType': _$AutoNextTypeEnumMap[instance.nextVideoType]!,
|
||||||
|
'audioDevice': instance.audioDevice,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$BoxFitEnumMap = {
|
||||||
|
BoxFit.fill: 'fill',
|
||||||
|
BoxFit.contain: 'contain',
|
||||||
|
BoxFit.cover: 'cover',
|
||||||
|
BoxFit.fitWidth: 'fitWidth',
|
||||||
|
BoxFit.fitHeight: 'fitHeight',
|
||||||
|
BoxFit.none: 'none',
|
||||||
|
BoxFit.scaleDown: 'scaleDown',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$AutoNextTypeEnumMap = {
|
||||||
|
AutoNextType.off: 'off',
|
||||||
|
AutoNextType.smart: 'smart',
|
||||||
|
AutoNextType.static: 'static',
|
||||||
|
};
|
||||||
|
|
@ -13,7 +13,7 @@ final videoPlayerSettingsProvider =
|
||||||
});
|
});
|
||||||
|
|
||||||
class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSettingsModel> {
|
class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSettingsModel> {
|
||||||
VideoPlayerSettingsProviderNotifier(this.ref) : super(const VideoPlayerSettingsModel());
|
VideoPlayerSettingsProviderNotifier(this.ref) : super(VideoPlayerSettingsModel());
|
||||||
|
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
|
||||||
|
|
||||||
void setScreenBrightness(double? value) async {
|
void setScreenBrightness(double? value) async {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
screenBrightness: () => value,
|
screenBrightness: value,
|
||||||
);
|
);
|
||||||
if (state.screenBrightness != null) {
|
if (state.screenBrightness != null) {
|
||||||
ScreenBrightness().setScreenBrightness(state.screenBrightness!);
|
ScreenBrightness().setScreenBrightness(state.screenBrightness!);
|
||||||
|
|
@ -45,13 +45,13 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
|
||||||
}
|
}
|
||||||
|
|
||||||
void setFillScreen(bool? value, {BuildContext? context}) {
|
void setFillScreen(bool? value, {BuildContext? context}) {
|
||||||
state = state.copyWith(fillScreen: value);
|
state = state.copyWith(fillScreen: value ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setHardwareAccel(bool? value) => state = state.copyWith(hardwareAccel: value);
|
void setHardwareAccel(bool? value) => state = state.copyWith(hardwareAccel: value ?? true);
|
||||||
void setUseLibass(bool? value) => state = state.copyWith(useLibass: value);
|
void setUseLibass(bool? value) => state = state.copyWith(useLibass: value ?? false);
|
||||||
|
|
||||||
void setFitType(BoxFit? value) => state = state.copyWith(videoFit: value);
|
void setFitType(BoxFit? value) => state = state.copyWith(videoFit: value ?? BoxFit.contain);
|
||||||
|
|
||||||
void setVolume(double value) {
|
void setVolume(double value) {
|
||||||
state = state.copyWith(internalVolume: value);
|
state = state.copyWith(internalVolume: value);
|
||||||
|
|
|
||||||
|
|
@ -170,15 +170,15 @@ class SharedUtility {
|
||||||
|
|
||||||
VideoPlayerSettingsModel get videoPlayerSettings {
|
VideoPlayerSettingsModel get videoPlayerSettings {
|
||||||
try {
|
try {
|
||||||
return VideoPlayerSettingsModel.fromJson(sharedPreferences.getString(_videoPlayerSettingsKey) ?? "");
|
return VideoPlayerSettingsModel.fromJson(jsonDecode(sharedPreferences.getString(_videoPlayerSettingsKey) ?? ""));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
return const VideoPlayerSettingsModel();
|
return VideoPlayerSettingsModel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set videoPlayerSettings(VideoPlayerSettingsModel settings) {
|
set videoPlayerSettings(VideoPlayerSettingsModel settings) {
|
||||||
sharedPreferences.setString(_videoPlayerSettingsKey, settings.toJson());
|
sharedPreferences.setString(_videoPlayerSettingsKey, jsonEncode(settings.toJson()));
|
||||||
}
|
}
|
||||||
|
|
||||||
PhotoViewSettingsModel get photoViewSettings {
|
PhotoViewSettingsModel get photoViewSettings {
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
|
||||||
double dragUpDelta = 0.0;
|
double dragUpDelta = 0.0;
|
||||||
|
|
||||||
final controller = TextEditingController();
|
final controller = TextEditingController();
|
||||||
late final timerController =
|
late final timerController = RestarableTimerController(
|
||||||
RestarableTimerController(ref.read(photoViewSettingsProvider).timer, const Duration(milliseconds: 32), () {
|
ref.read(photoViewSettingsProvider).timer, const Duration(milliseconds: 32), onTimeout: () {
|
||||||
if (widget.pageController.page == widget.itemCount - 1) {
|
if (widget.pageController.page == widget.itemCount - 1) {
|
||||||
widget.pageController.animateToPage(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut);
|
widget.pageController.animateToPage(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -314,6 +314,13 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
|
||||||
),
|
),
|
||||||
ProgressFloatingButton(
|
ProgressFloatingButton(
|
||||||
controller: timerController,
|
controller: timerController,
|
||||||
|
onLongPress: (duration) {
|
||||||
|
if (duration != null) {
|
||||||
|
ref
|
||||||
|
.read(photoViewSettingsProvider.notifier)
|
||||||
|
.update((state) => state.copyWith(timer: duration));
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
].addPadding(const EdgeInsets.symmetric(horizontal: 8)),
|
].addPadding(const EdgeInsets.symmetric(horizontal: 8)),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||||
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
||||||
|
|
@ -10,10 +18,7 @@ import 'package:fladder/util/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/box_fit_extension.dart';
|
import 'package:fladder/util/box_fit_extension.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
import 'package:fladder/util/option_dialogue.dart';
|
import 'package:fladder/util/option_dialogue.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'dart:io' show Platform;
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class PlayerSettingsPage extends ConsumerStatefulWidget {
|
class PlayerSettingsPage extends ConsumerStatefulWidget {
|
||||||
|
|
@ -104,6 +109,34 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
||||||
: Container(),
|
: Container(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
SettingsListTile(
|
||||||
|
label: Text(context.localized.settingsAutoNextTitle),
|
||||||
|
subLabel: Text(context.localized.settingsAutoNextDesc),
|
||||||
|
trailing: EnumBox(
|
||||||
|
current: ref.watch(
|
||||||
|
videoPlayerSettingsProvider.select(
|
||||||
|
(value) => value.nextVideoType.label(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
itemBuilder: (context) => AutoNextType.values
|
||||||
|
.map(
|
||||||
|
(entry) => PopupMenuItem(
|
||||||
|
value: entry,
|
||||||
|
child: Text(entry.label(context)),
|
||||||
|
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||||
|
ref.read(videoPlayerSettingsProvider).copyWith(nextVideoType: entry),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedFadeSize(
|
||||||
|
child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) {
|
||||||
|
AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)),
|
||||||
|
AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)),
|
||||||
|
_ => const SizedBox.shrink(),
|
||||||
|
},
|
||||||
|
),
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
||||||
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
|
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:ficonsax/ficonsax.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/util/list_padding.dart';
|
||||||
|
|
||||||
enum MessageType {
|
enum MessageType {
|
||||||
info,
|
info,
|
||||||
warning,
|
warning,
|
||||||
error;
|
error;
|
||||||
|
|
||||||
Color color(BuildContext context) {
|
Color color(BuildContext context) => switch (this) {
|
||||||
switch (this) {
|
MessageType.info => Theme.of(context).colorScheme.secondaryContainer,
|
||||||
case info:
|
MessageType.warning => Theme.of(context).colorScheme.primaryContainer,
|
||||||
return Theme.of(context).colorScheme.surface;
|
MessageType.error => Theme.of(context).colorScheme.errorContainer,
|
||||||
case warning:
|
};
|
||||||
return Theme.of(context).colorScheme.primaryContainer;
|
|
||||||
case error:
|
|
||||||
return Theme.of(context).colorScheme.errorContainer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsMessageBox extends ConsumerWidget {
|
class SettingsMessageBox extends ConsumerWidget {
|
||||||
|
|
@ -38,7 +37,20 @@ class SettingsMessageBox extends ConsumerWidget {
|
||||||
color: messageType.color(context),
|
color: messageType.color(context),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(message),
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
switch (messageType) {
|
||||||
|
MessageType.info => IconsaxOutline.information,
|
||||||
|
MessageType.warning => IconsaxOutline.warning_2,
|
||||||
|
MessageType.error => IconsaxOutline.danger,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(message),
|
||||||
|
),
|
||||||
|
].addInBetween(const SizedBox(width: 12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,428 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:ficonsax/ficonsax.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/item_base_model.dart';
|
||||||
|
import 'package:fladder/models/items/movie_model.dart';
|
||||||
|
import 'package:fladder/models/media_playback_model.dart';
|
||||||
|
import 'package:fladder/models/playback/playback_model.dart';
|
||||||
|
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||||
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
|
import 'package:fladder/providers/user_provider.dart';
|
||||||
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
|
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||||
|
import 'package:fladder/screens/shared/default_titlebar.dart';
|
||||||
|
import 'package:fladder/util/adaptive_layout.dart';
|
||||||
|
import 'package:fladder/util/fladder_image.dart';
|
||||||
|
import 'package:fladder/util/list_padding.dart';
|
||||||
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart';
|
||||||
|
import 'package:fladder/widgets/shared/progress_floating_button.dart';
|
||||||
|
|
||||||
|
class VideoPlayerNextWrapper extends ConsumerStatefulWidget {
|
||||||
|
final Widget video;
|
||||||
|
final Widget controls;
|
||||||
|
final List<Widget> overlays;
|
||||||
|
const VideoPlayerNextWrapper({
|
||||||
|
required this.video,
|
||||||
|
required this.controls,
|
||||||
|
this.overlays = const [],
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConsumerStatefulWidget> createState() => _VideoPlayerNextWrapperState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoPlayerNextWrapperState extends ConsumerState<VideoPlayerNextWrapper> {
|
||||||
|
bool show = false;
|
||||||
|
bool showOverwrite = false;
|
||||||
|
late RestarableTimerController timerController =
|
||||||
|
RestarableTimerController(const Duration(seconds: 30), const Duration(milliseconds: 33), onTimeout: onTimeOut);
|
||||||
|
|
||||||
|
void onTimeOut() {
|
||||||
|
timerController.cancel();
|
||||||
|
if (showOverwrite == true) return;
|
||||||
|
final nextUp = ref.read(playBackModel.select((value) => value?.nextVideo));
|
||||||
|
if (nextUp != null) {
|
||||||
|
ref.read(playbackModelHelper).loadNewVideo(nextUp);
|
||||||
|
}
|
||||||
|
hideNextUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showNextScreen(MediaPlaybackModel model) {
|
||||||
|
final nextUp = ref.read(playBackModel.select((value) => value?.nextVideo));
|
||||||
|
if (nextUp == null) return;
|
||||||
|
if (show) return;
|
||||||
|
if (showOverwrite) return;
|
||||||
|
if (!model.playing) return;
|
||||||
|
if (model.buffering) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
show = true;
|
||||||
|
timerController.reset();
|
||||||
|
timerController.play();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void determineShow(MediaPlaybackModel model) {
|
||||||
|
final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state));
|
||||||
|
if (playerState != VideoPlayerState.fullScreen) {
|
||||||
|
showOverwrite = false;
|
||||||
|
show = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final nextType = ref.read(videoPlayerSettingsProvider.select((value) => value.nextVideoType));
|
||||||
|
if (nextType == AutoNextType.off || model.duration < const Duration(seconds: 40)) {
|
||||||
|
showOverwrite = false;
|
||||||
|
show = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final credits = ref.read(playBackModel)?.introSkipModel?.credits;
|
||||||
|
|
||||||
|
if (nextType == AutoNextType.static || credits == null) {
|
||||||
|
if ((model.duration - model.position).abs() < const Duration(seconds: 32)) {
|
||||||
|
showNextScreen(model);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (nextType == AutoNextType.smart) {
|
||||||
|
final maxTime = ref.read(userProvider.select((value) => value?.serverConfiguration?.maxResumePct ?? 90));
|
||||||
|
final resumeDuration = model.duration * (maxTime / 100);
|
||||||
|
final timeLeft = model.duration - credits.end;
|
||||||
|
if (credits.end > resumeDuration && timeLeft < const Duration(seconds: 30)) {
|
||||||
|
if (model.position >= credits.start) {
|
||||||
|
showNextScreen(model);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if ((model.duration - model.position).abs() < const Duration(seconds: 32)) {
|
||||||
|
showNextScreen(model);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
show = false;
|
||||||
|
showOverwrite = false;
|
||||||
|
timerController.cancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void hideNextUp() {
|
||||||
|
timerController.cancel();
|
||||||
|
setState(() {
|
||||||
|
show = false;
|
||||||
|
showOverwrite = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timerController.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const animSpeed = Duration(milliseconds: 250);
|
||||||
|
final nextUp = ref.watch(playBackModel.select((value) => value?.nextVideo));
|
||||||
|
final currentItem = ref.watch(playBackModel.select((value) => value?.item));
|
||||||
|
final portraitMode = MediaQuery.sizeOf(context).width < MediaQuery.sizeOf(context).height;
|
||||||
|
|
||||||
|
double padding = show ? 16 : 0;
|
||||||
|
|
||||||
|
ref.listen(mediaPlaybackProvider, (previous, next) => determineShow(next));
|
||||||
|
return Hero(
|
||||||
|
tag: videoPlayerHeroTag,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerLowest.withOpacity(0.2),
|
||||||
|
),
|
||||||
|
if (nextUp != null)
|
||||||
|
AnimatedAlign(
|
||||||
|
duration: animSpeed,
|
||||||
|
alignment: portraitMode ? Alignment.bottomCenter : Alignment.centerRight,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: animSpeed,
|
||||||
|
opacity: show ? 1 : 0,
|
||||||
|
child: Padding(
|
||||||
|
padding: MediaQuery.paddingOf(context).add(const EdgeInsets.all(32)),
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
widthFactor: portraitMode ? null : 0.35,
|
||||||
|
heightFactor: portraitMode ? 0.5 : null,
|
||||||
|
child: Card(
|
||||||
|
elevation: 10,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
context.localized.nextUp,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold, fontSize: 24.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox.square(
|
||||||
|
dimension: 45.0,
|
||||||
|
child: ProgressFloatingButton(
|
||||||
|
controller: timerController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].addInBetween(
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Flexible(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: _NextUpInformation(
|
||||||
|
item: nextUp,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].addInBetween(const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
width: 8,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedAlign(
|
||||||
|
duration: animSpeed,
|
||||||
|
alignment: portraitMode ? Alignment.topCenter : Alignment.centerLeft,
|
||||||
|
child: AnimatedPadding(
|
||||||
|
duration: animSpeed,
|
||||||
|
padding: EdgeInsets.all(padding).add(show ? MediaQuery.paddingOf(context) : EdgeInsets.zero),
|
||||||
|
child: AnimatedFractionallySizedBox(
|
||||||
|
duration: animSpeed,
|
||||||
|
heightFactor: show ? (portraitMode ? 0.40 : 0.9) : 1.0,
|
||||||
|
widthFactor: show ? (portraitMode ? 1 : 0.60) : 1.0,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (currentItem != null)
|
||||||
|
AnimatedFadeSize(
|
||||||
|
duration: animSpeed,
|
||||||
|
child: show
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(currentItem.title,
|
||||||
|
style: Theme.of(context).textTheme.displaySmall)),
|
||||||
|
if (currentItem.label(context) != null)
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
currentItem.label(context)!,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: () => hideNextUp(),
|
||||||
|
tooltip: "Resume video",
|
||||||
|
icon: const Icon(IconsaxBold.maximize_4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.passthrough,
|
||||||
|
children: [
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: animSpeed,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black,
|
||||||
|
borderRadius: BorderRadius.circular(show ? 16 : 0),
|
||||||
|
),
|
||||||
|
child: widget.video,
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
ignoring: show,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: show ? 0 : 1,
|
||||||
|
duration: animSpeed,
|
||||||
|
child: widget.controls,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedFadeSize(
|
||||||
|
duration: animSpeed,
|
||||||
|
child: show
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
child: _SimpleControls(
|
||||||
|
skip: nextUp != null ? () => onTimeOut() : null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (AdaptiveLayout.of(context).isDesktop)
|
||||||
|
const Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: DefaultTitleBar(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NextUpInformation extends StatelessWidget {
|
||||||
|
final ItemBaseModel item;
|
||||||
|
const _NextUpInformation({
|
||||||
|
required this.item,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return switch (item) {
|
||||||
|
MovieModel _ => Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 150),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 0.67,
|
||||||
|
child: Card(
|
||||||
|
child: FladderImage(
|
||||||
|
image: item.images?.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
item.title,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
].addInBetween(
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.localized.overview,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Text(item.overview.summary),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
].addInBetween(
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.title,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
if (item.label(context) != null)
|
||||||
|
Text(
|
||||||
|
item.label(context)!,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 2.1,
|
||||||
|
child: Card(
|
||||||
|
child: FladderImage(
|
||||||
|
image: item.images?.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
context.localized.overview,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
Text(item.overview.summary),
|
||||||
|
const SizedBox(height: 12)
|
||||||
|
].addInBetween(
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SimpleControls extends ConsumerWidget {
|
||||||
|
final Function()? skip;
|
||||||
|
const _SimpleControls({
|
||||||
|
this.skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final player = ref.watch(videoPlayerProvider.select((value) => value.controller?.player));
|
||||||
|
final isPlaying = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: () => player?.playOrPause(),
|
||||||
|
icon: Icon(isPlaying ? IconsaxBold.pause : IconsaxBold.play),
|
||||||
|
),
|
||||||
|
if (skip != null)
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: skip,
|
||||||
|
tooltip: "Play next video",
|
||||||
|
icon: const Icon(IconsaxBold.next),
|
||||||
|
)
|
||||||
|
].addInBetween(const SizedBox(width: 4)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import 'package:media_kit_video/media_kit_video.dart';
|
||||||
import 'package:fladder/models/media_playback_model.dart';
|
import 'package:fladder/models/media_playback_model.dart';
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
|
import 'package:fladder/screens/video_player/components/video_player_next_wrapper.dart';
|
||||||
import 'package:fladder/screens/video_player/video_player_controls.dart';
|
import 'package:fladder/screens/video_player/video_player_controls.dart';
|
||||||
import 'package:fladder/util/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/themes_data.dart';
|
import 'package:fladder/util/themes_data.dart';
|
||||||
|
|
@ -87,12 +88,9 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
|
||||||
}
|
}
|
||||||
lastScale = 0.0;
|
lastScale = 0.0;
|
||||||
},
|
},
|
||||||
child: Hero(
|
child: VideoPlayerNextWrapper(
|
||||||
tag: "HeroPlayer",
|
video: playerController != null
|
||||||
child: Stack(
|
? Padding(
|
||||||
children: [
|
|
||||||
if (playerController != null)
|
|
||||||
Padding(
|
|
||||||
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
|
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
|
||||||
child: OrientationBuilder(builder: (context, orientation) {
|
child: OrientationBuilder(builder: (context, orientation) {
|
||||||
return Video(
|
return Video(
|
||||||
|
|
@ -107,11 +105,12 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
|
||||||
controls: NoVideoControls,
|
controls: NoVideoControls,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
const DesktopControls(),
|
: const SizedBox.shrink(),
|
||||||
if (errorPlaying) const _VideoErrorWidget(),
|
controls: const DesktopControls(),
|
||||||
],
|
overlays: [
|
||||||
),
|
if (errorPlaying) const _VideoErrorWidget(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:ficonsax/ficonsax.dart';
|
import 'package:ficonsax/ficonsax.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:media_kit_video/media_kit_video.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/media_playback_model.dart';
|
import 'package:fladder/models/media_playback_model.dart';
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
|
|
@ -9,11 +16,8 @@ import 'package:fladder/util/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/duration_extensions.dart';
|
import 'package:fladder/util/duration_extensions.dart';
|
||||||
import 'package:fladder/util/list_padding.dart';
|
import 'package:fladder/util/list_padding.dart';
|
||||||
import 'package:fladder/util/refresh_state.dart';
|
import 'package:fladder/util/refresh_state.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
const videoPlayerHeroTag = "HeroPlayer";
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
|
||||||
import 'package:window_manager/window_manager.dart';
|
|
||||||
|
|
||||||
class FloatingPlayerBar extends ConsumerStatefulWidget {
|
class FloatingPlayerBar extends ConsumerStatefulWidget {
|
||||||
const FloatingPlayerBar({super.key});
|
const FloatingPlayerBar({super.key});
|
||||||
|
|
@ -97,7 +101,7 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Hero(
|
Hero(
|
||||||
tag: "HeroPlayer",
|
tag: videoPlayerHeroTag,
|
||||||
child: Video(
|
child: Video(
|
||||||
controller: player.controller!,
|
controller: player.controller!,
|
||||||
fit: BoxFit.fitHeight,
|
fit: BoxFit.fitHeight,
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:async/async.dart';
|
import 'package:async/async.dart';
|
||||||
import 'package:ficonsax/ficonsax.dart';
|
import 'package:ficonsax/ficonsax.dart';
|
||||||
import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
|
|
||||||
import 'package:fladder/util/simple_duration_picker.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:square_progress_indicator/square_progress_indicator.dart';
|
import 'package:square_progress_indicator/square_progress_indicator.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/util/simple_duration_picker.dart';
|
||||||
|
|
||||||
class RestarableTimerController {
|
class RestarableTimerController {
|
||||||
late Duration _steps = const Duration(milliseconds: 32);
|
late Duration _steps = const Duration(milliseconds: 32);
|
||||||
RestartableTimer? _timer;
|
RestartableTimer? _timer;
|
||||||
late Duration _duration = const Duration(seconds: 1);
|
late Duration _duration = const Duration(seconds: 1);
|
||||||
late Function() _onTimeout;
|
late Function()? _onTimeout;
|
||||||
|
|
||||||
late Duration _timeLeft = _duration;
|
late Duration _timeLeft = _duration;
|
||||||
set setTimeLeft(Duration value) {
|
set setTimeLeft(Duration value) {
|
||||||
|
|
@ -24,7 +25,7 @@ class RestarableTimerController {
|
||||||
final StreamController<Duration> _timeLeftController = StreamController<Duration>.broadcast();
|
final StreamController<Duration> _timeLeftController = StreamController<Duration>.broadcast();
|
||||||
final StreamController<bool> _isActiveController = StreamController<bool>.broadcast();
|
final StreamController<bool> _isActiveController = StreamController<bool>.broadcast();
|
||||||
|
|
||||||
RestarableTimerController(Duration duration, Duration steps, Function() onTimeout) {
|
RestarableTimerController(Duration duration, Duration steps, {Function()? onTimeout}) {
|
||||||
_steps = steps;
|
_steps = steps;
|
||||||
_duration = duration;
|
_duration = duration;
|
||||||
_onTimeout = onTimeout;
|
_onTimeout = onTimeout;
|
||||||
|
|
@ -50,7 +51,7 @@ class RestarableTimerController {
|
||||||
() {
|
() {
|
||||||
if (_timeLeft < _steps) {
|
if (_timeLeft < _steps) {
|
||||||
setTimeLeft = _duration;
|
setTimeLeft = _duration;
|
||||||
_onTimeout.call();
|
_onTimeout?.call();
|
||||||
} else {
|
} else {
|
||||||
setTimeLeft = _timeLeft - _steps;
|
setTimeLeft = _timeLeft - _steps;
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +87,8 @@ class RestarableTimerController {
|
||||||
class ProgressFloatingButton extends ConsumerStatefulWidget {
|
class ProgressFloatingButton extends ConsumerStatefulWidget {
|
||||||
final RestarableTimerController? controller;
|
final RestarableTimerController? controller;
|
||||||
final Function()? onTimeOut;
|
final Function()? onTimeOut;
|
||||||
const ProgressFloatingButton({this.controller, this.onTimeOut, super.key});
|
final Function(Duration? newDuration)? onLongPress;
|
||||||
|
const ProgressFloatingButton({this.controller, this.onTimeOut, this.onLongPress, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ConsumerStatefulWidget> createState() => _ProgressFloatingButtonState();
|
ConsumerState<ConsumerStatefulWidget> createState() => _ProgressFloatingButtonState();
|
||||||
|
|
@ -106,7 +108,7 @@ class _ProgressFloatingButtonState extends ConsumerState<ProgressFloatingButton>
|
||||||
RestarableTimerController(
|
RestarableTimerController(
|
||||||
const Duration(seconds: 1),
|
const Duration(seconds: 1),
|
||||||
const Duration(milliseconds: 32),
|
const Duration(milliseconds: 32),
|
||||||
widget.onTimeOut ?? () {},
|
onTimeout: widget.onTimeOut ?? () {},
|
||||||
);
|
);
|
||||||
subscriptions.addAll([
|
subscriptions.addAll([
|
||||||
timer.timeLeft.listen((event) => setState(() => timeLeft = event)),
|
timer.timeLeft.listen((event) => setState(() => timeLeft = event)),
|
||||||
|
|
@ -132,18 +134,21 @@ class _ProgressFloatingButtonState extends ConsumerState<ProgressFloatingButton>
|
||||||
timer.reset();
|
timer.reset();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onLongPress: () async {
|
onLongPress: widget.onLongPress != null
|
||||||
HapticFeedback.vibrate();
|
? () async {
|
||||||
final newTimer =
|
HapticFeedback.vibrate();
|
||||||
await showSimpleDurationPicker(context: context, initialValue: timer._duration, showNever: false);
|
final newTimer =
|
||||||
if (newTimer != null) {
|
await showSimpleDurationPicker(context: context, initialValue: timer._duration, showNever: false);
|
||||||
setState(() {
|
widget.onLongPress?.call(newTimer);
|
||||||
ref.read(photoViewSettingsProvider.notifier).update((state) => state.copyWith(timer: newTimer));
|
if (newTimer != null) {
|
||||||
timer.setDuration(newTimer);
|
setState(() {
|
||||||
});
|
timer.setDuration(newTimer);
|
||||||
}
|
});
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
|
heroTag: null,
|
||||||
onPressed: isActive ? timer.cancel : timer.play,
|
onPressed: isActive ? timer.cancel : timer.play,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue