mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08: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",
|
||||
"screenBrightness": "Screen brightness",
|
||||
"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/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class VideoPlayerSettingsModel {
|
||||
final double? screenBrightness;
|
||||
final BoxFit videoFit;
|
||||
final bool fillScreen;
|
||||
final bool hardwareAccel;
|
||||
final bool useLibass;
|
||||
final double internalVolume;
|
||||
final String? audioDevice;
|
||||
const VideoPlayerSettingsModel({
|
||||
this.screenBrightness,
|
||||
this.videoFit = BoxFit.contain,
|
||||
this.fillScreen = false,
|
||||
this.hardwareAccel = true,
|
||||
this.useLibass = false,
|
||||
this.internalVolume = 100,
|
||||
this.audioDevice,
|
||||
});
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
part 'video_player_settings.freezed.dart';
|
||||
part 'video_player_settings.g.dart';
|
||||
|
||||
@freezed
|
||||
class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
||||
const VideoPlayerSettingsModel._();
|
||||
|
||||
factory VideoPlayerSettingsModel({
|
||||
double? screenBrightness,
|
||||
@Default(BoxFit.contain) BoxFit videoFit,
|
||||
@Default(false) bool fillScreen,
|
||||
@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) {
|
||||
TargetPlatform.android || TargetPlatform.iOS => 100,
|
||||
_ => internalVolume,
|
||||
};
|
||||
|
||||
VideoPlayerSettingsModel copyWith({
|
||||
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)';
|
||||
}
|
||||
factory VideoPlayerSettingsModel.fromJson(Map<String, dynamic> json) => _$VideoPlayerSettingsModelFromJson(json);
|
||||
|
||||
bool playerSame(VideoPlayerSettingsModel other) {
|
||||
return other.hardwareAccel == hardwareAccel && other.useLibass == useLibass;
|
||||
|
|
@ -111,3 +60,23 @@ class VideoPlayerSettingsModel {
|
|||
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> {
|
||||
VideoPlayerSettingsProviderNotifier(this.ref) : super(const VideoPlayerSettingsModel());
|
||||
VideoPlayerSettingsProviderNotifier(this.ref) : super(VideoPlayerSettingsModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
|
|||
|
||||
void setScreenBrightness(double? value) async {
|
||||
state = state.copyWith(
|
||||
screenBrightness: () => value,
|
||||
screenBrightness: value,
|
||||
);
|
||||
if (state.screenBrightness != null) {
|
||||
ScreenBrightness().setScreenBrightness(state.screenBrightness!);
|
||||
|
|
@ -45,13 +45,13 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
|
|||
}
|
||||
|
||||
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 setUseLibass(bool? value) => state = state.copyWith(useLibass: value);
|
||||
void setHardwareAccel(bool? value) => state = state.copyWith(hardwareAccel: value ?? true);
|
||||
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) {
|
||||
state = state.copyWith(internalVolume: value);
|
||||
|
|
|
|||
|
|
@ -170,15 +170,15 @@ class SharedUtility {
|
|||
|
||||
VideoPlayerSettingsModel get videoPlayerSettings {
|
||||
try {
|
||||
return VideoPlayerSettingsModel.fromJson(sharedPreferences.getString(_videoPlayerSettingsKey) ?? "");
|
||||
return VideoPlayerSettingsModel.fromJson(jsonDecode(sharedPreferences.getString(_videoPlayerSettingsKey) ?? ""));
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return const VideoPlayerSettingsModel();
|
||||
return VideoPlayerSettingsModel();
|
||||
}
|
||||
}
|
||||
|
||||
set videoPlayerSettings(VideoPlayerSettingsModel settings) {
|
||||
sharedPreferences.setString(_videoPlayerSettingsKey, settings.toJson());
|
||||
sharedPreferences.setString(_videoPlayerSettingsKey, jsonEncode(settings.toJson()));
|
||||
}
|
||||
|
||||
PhotoViewSettingsModel get photoViewSettings {
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
|
|||
double dragUpDelta = 0.0;
|
||||
|
||||
final controller = TextEditingController();
|
||||
late final timerController =
|
||||
RestarableTimerController(ref.read(photoViewSettingsProvider).timer, const Duration(milliseconds: 32), () {
|
||||
late final timerController = RestarableTimerController(
|
||||
ref.read(photoViewSettingsProvider).timer, const Duration(milliseconds: 32), onTimeout: () {
|
||||
if (widget.pageController.page == widget.itemCount - 1) {
|
||||
widget.pageController.animateToPage(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut);
|
||||
} else {
|
||||
|
|
@ -314,6 +314,13 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
|
|||
),
|
||||
ProgressFloatingButton(
|
||||
controller: timerController,
|
||||
onLongPress: (duration) {
|
||||
if (duration != null) {
|
||||
ref
|
||||
.read(photoViewSettingsProvider.notifier)
|
||||
.update((state) => state.copyWith(timer: duration));
|
||||
}
|
||||
},
|
||||
),
|
||||
].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: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/screens/settings/settings_list_tile.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/localization_helper.dart';
|
||||
import 'package:fladder/util/option_dialogue.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PlayerSettingsPage extends ConsumerStatefulWidget {
|
||||
|
|
@ -104,6 +109,34 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
: 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(
|
||||
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
||||
enum MessageType {
|
||||
info,
|
||||
warning,
|
||||
error;
|
||||
|
||||
Color color(BuildContext context) {
|
||||
switch (this) {
|
||||
case info:
|
||||
return Theme.of(context).colorScheme.surface;
|
||||
case warning:
|
||||
return Theme.of(context).colorScheme.primaryContainer;
|
||||
case error:
|
||||
return Theme.of(context).colorScheme.errorContainer;
|
||||
}
|
||||
}
|
||||
Color color(BuildContext context) => switch (this) {
|
||||
MessageType.info => Theme.of(context).colorScheme.secondaryContainer,
|
||||
MessageType.warning => Theme.of(context).colorScheme.primaryContainer,
|
||||
MessageType.error => Theme.of(context).colorScheme.errorContainer,
|
||||
};
|
||||
}
|
||||
|
||||
class SettingsMessageBox extends ConsumerWidget {
|
||||
|
|
@ -38,7 +37,20 @@ class SettingsMessageBox extends ConsumerWidget {
|
|||
color: messageType.color(context),
|
||||
child: Padding(
|
||||
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/providers/settings/video_player_settings_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/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/themes_data.dart';
|
||||
|
|
@ -87,12 +88,9 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
|
|||
}
|
||||
lastScale = 0.0;
|
||||
},
|
||||
child: Hero(
|
||||
tag: "HeroPlayer",
|
||||
child: Stack(
|
||||
children: [
|
||||
if (playerController != null)
|
||||
Padding(
|
||||
child: VideoPlayerNextWrapper(
|
||||
video: playerController != null
|
||||
? Padding(
|
||||
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
|
||||
child: OrientationBuilder(builder: (context, orientation) {
|
||||
return Video(
|
||||
|
|
@ -107,11 +105,12 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
|
|||
controls: NoVideoControls,
|
||||
);
|
||||
}),
|
||||
),
|
||||
const DesktopControls(),
|
||||
if (errorPlaying) const _VideoErrorWidget(),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
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: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/providers/settings/video_player_settings_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/list_padding.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
const videoPlayerHeroTag = "HeroPlayer";
|
||||
|
||||
class FloatingPlayerBar extends ConsumerStatefulWidget {
|
||||
const FloatingPlayerBar({super.key});
|
||||
|
|
@ -97,7 +101,7 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
|||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: "HeroPlayer",
|
||||
tag: videoPlayerHeroTag,
|
||||
child: Video(
|
||||
controller: player.controller!,
|
||||
fit: BoxFit.fitHeight,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:async/async.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:square_progress_indicator/square_progress_indicator.dart';
|
||||
|
||||
import 'package:fladder/util/simple_duration_picker.dart';
|
||||
|
||||
class RestarableTimerController {
|
||||
late Duration _steps = const Duration(milliseconds: 32);
|
||||
RestartableTimer? _timer;
|
||||
late Duration _duration = const Duration(seconds: 1);
|
||||
late Function() _onTimeout;
|
||||
late Function()? _onTimeout;
|
||||
|
||||
late Duration _timeLeft = _duration;
|
||||
set setTimeLeft(Duration value) {
|
||||
|
|
@ -24,7 +25,7 @@ class RestarableTimerController {
|
|||
final StreamController<Duration> _timeLeftController = StreamController<Duration>.broadcast();
|
||||
final StreamController<bool> _isActiveController = StreamController<bool>.broadcast();
|
||||
|
||||
RestarableTimerController(Duration duration, Duration steps, Function() onTimeout) {
|
||||
RestarableTimerController(Duration duration, Duration steps, {Function()? onTimeout}) {
|
||||
_steps = steps;
|
||||
_duration = duration;
|
||||
_onTimeout = onTimeout;
|
||||
|
|
@ -50,7 +51,7 @@ class RestarableTimerController {
|
|||
() {
|
||||
if (_timeLeft < _steps) {
|
||||
setTimeLeft = _duration;
|
||||
_onTimeout.call();
|
||||
_onTimeout?.call();
|
||||
} else {
|
||||
setTimeLeft = _timeLeft - _steps;
|
||||
}
|
||||
|
|
@ -86,7 +87,8 @@ class RestarableTimerController {
|
|||
class ProgressFloatingButton extends ConsumerStatefulWidget {
|
||||
final RestarableTimerController? controller;
|
||||
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
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _ProgressFloatingButtonState();
|
||||
|
|
@ -106,7 +108,7 @@ class _ProgressFloatingButtonState extends ConsumerState<ProgressFloatingButton>
|
|||
RestarableTimerController(
|
||||
const Duration(seconds: 1),
|
||||
const Duration(milliseconds: 32),
|
||||
widget.onTimeOut ?? () {},
|
||||
onTimeout: widget.onTimeOut ?? () {},
|
||||
);
|
||||
subscriptions.addAll([
|
||||
timer.timeLeft.listen((event) => setState(() => timeLeft = event)),
|
||||
|
|
@ -132,18 +134,21 @@ class _ProgressFloatingButtonState extends ConsumerState<ProgressFloatingButton>
|
|||
timer.reset();
|
||||
});
|
||||
},
|
||||
onLongPress: () async {
|
||||
HapticFeedback.vibrate();
|
||||
final newTimer =
|
||||
await showSimpleDurationPicker(context: context, initialValue: timer._duration, showNever: false);
|
||||
if (newTimer != null) {
|
||||
setState(() {
|
||||
ref.read(photoViewSettingsProvider.notifier).update((state) => state.copyWith(timer: newTimer));
|
||||
timer.setDuration(newTimer);
|
||||
});
|
||||
}
|
||||
},
|
||||
onLongPress: widget.onLongPress != null
|
||||
? () async {
|
||||
HapticFeedback.vibrate();
|
||||
final newTimer =
|
||||
await showSimpleDurationPicker(context: context, initialValue: timer._duration, showNever: false);
|
||||
widget.onLongPress?.call(newTimer);
|
||||
if (newTimer != null) {
|
||||
setState(() {
|
||||
timer.setDuration(newTimer);
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: isActive ? timer.cancel : timer.play,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue