feature: Added settings to force the player into certain orientations (#108)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2024-11-02 18:44:18 +01:00 committed by GitHub
parent 691293648b
commit c32f71b368
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 170 additions and 5 deletions

View file

@ -1099,5 +1099,11 @@
"deleteFilterConfirmation": "Are you sure you want to delete this filter?", "deleteFilterConfirmation": "Are you sure you want to delete this filter?",
"libraryFiltersLimitReached" : "Filter limit reached (10) remove some filters", "libraryFiltersLimitReached" : "Filter limit reached (10) remove some filters",
"libraryFiltersRemoveAll": "Remove all filters", "libraryFiltersRemoveAll": "Remove all filters",
"libraryFiltersRemoveAllConfirm": "This will delete all saved filters for every library" "libraryFiltersRemoveAllConfirm": "This will delete all saved filters for every library",
"playerSettingsOrientationTitle": "Player orientation",
"playerSettingsOrientationDesc": "Force the video player into certain orientations",
"deviceOrientationPortraitUp": "Portrait Up",
"deviceOrientationPortraitDown": "Portrait Down",
"deviceOrientationLandscapeLeft": "Landscape Left",
"deviceOrientationLandscapeRight": "Landscape Right"
} }

View file

@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -20,6 +21,7 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
@Default(true) bool hardwareAccel, @Default(true) bool hardwareAccel,
@Default(false) bool useLibass, @Default(false) bool useLibass,
@Default(100) double internalVolume, @Default(100) double internalVolume,
Set<DeviceOrientation>? allowedOrientations,
@Default(AutoNextType.static) AutoNextType nextVideoType, @Default(AutoNextType.static) AutoNextType nextVideoType,
String? audioDevice, String? audioDevice,
}) = _VideoPlayerSettingsModel; }) = _VideoPlayerSettingsModel;

View file

@ -27,6 +27,8 @@ mixin _$VideoPlayerSettingsModel {
bool get hardwareAccel => throw _privateConstructorUsedError; bool get hardwareAccel => throw _privateConstructorUsedError;
bool get useLibass => throw _privateConstructorUsedError; bool get useLibass => throw _privateConstructorUsedError;
double get internalVolume => throw _privateConstructorUsedError; double get internalVolume => throw _privateConstructorUsedError;
Set<DeviceOrientation>? get allowedOrientations =>
throw _privateConstructorUsedError;
AutoNextType get nextVideoType => throw _privateConstructorUsedError; AutoNextType get nextVideoType => throw _privateConstructorUsedError;
String? get audioDevice => throw _privateConstructorUsedError; String? get audioDevice => throw _privateConstructorUsedError;
@ -53,6 +55,7 @@ abstract class $VideoPlayerSettingsModelCopyWith<$Res> {
bool hardwareAccel, bool hardwareAccel,
bool useLibass, bool useLibass,
double internalVolume, double internalVolume,
Set<DeviceOrientation>? allowedOrientations,
AutoNextType nextVideoType, AutoNextType nextVideoType,
String? audioDevice}); String? audioDevice});
} }
@ -79,6 +82,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
Object? hardwareAccel = null, Object? hardwareAccel = null,
Object? useLibass = null, Object? useLibass = null,
Object? internalVolume = null, Object? internalVolume = null,
Object? allowedOrientations = freezed,
Object? nextVideoType = null, Object? nextVideoType = null,
Object? audioDevice = freezed, Object? audioDevice = freezed,
}) { }) {
@ -107,6 +111,10 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
? _value.internalVolume ? _value.internalVolume
: internalVolume // ignore: cast_nullable_to_non_nullable : internalVolume // ignore: cast_nullable_to_non_nullable
as double, as double,
allowedOrientations: freezed == allowedOrientations
? _value.allowedOrientations
: allowedOrientations // ignore: cast_nullable_to_non_nullable
as Set<DeviceOrientation>?,
nextVideoType: null == nextVideoType nextVideoType: null == nextVideoType
? _value.nextVideoType ? _value.nextVideoType
: nextVideoType // ignore: cast_nullable_to_non_nullable : nextVideoType // ignore: cast_nullable_to_non_nullable
@ -135,6 +143,7 @@ abstract class _$$VideoPlayerSettingsModelImplCopyWith<$Res>
bool hardwareAccel, bool hardwareAccel,
bool useLibass, bool useLibass,
double internalVolume, double internalVolume,
Set<DeviceOrientation>? allowedOrientations,
AutoNextType nextVideoType, AutoNextType nextVideoType,
String? audioDevice}); String? audioDevice});
} }
@ -160,6 +169,7 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
Object? hardwareAccel = null, Object? hardwareAccel = null,
Object? useLibass = null, Object? useLibass = null,
Object? internalVolume = null, Object? internalVolume = null,
Object? allowedOrientations = freezed,
Object? nextVideoType = null, Object? nextVideoType = null,
Object? audioDevice = freezed, Object? audioDevice = freezed,
}) { }) {
@ -188,6 +198,10 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
? _value.internalVolume ? _value.internalVolume
: internalVolume // ignore: cast_nullable_to_non_nullable : internalVolume // ignore: cast_nullable_to_non_nullable
as double, as double,
allowedOrientations: freezed == allowedOrientations
? _value._allowedOrientations
: allowedOrientations // ignore: cast_nullable_to_non_nullable
as Set<DeviceOrientation>?,
nextVideoType: null == nextVideoType nextVideoType: null == nextVideoType
? _value.nextVideoType ? _value.nextVideoType
: nextVideoType // ignore: cast_nullable_to_non_nullable : nextVideoType // ignore: cast_nullable_to_non_nullable
@ -211,9 +225,11 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
this.hardwareAccel = true, this.hardwareAccel = true,
this.useLibass = false, this.useLibass = false,
this.internalVolume = 100, this.internalVolume = 100,
final Set<DeviceOrientation>? allowedOrientations,
this.nextVideoType = AutoNextType.static, this.nextVideoType = AutoNextType.static,
this.audioDevice}) this.audioDevice})
: super._(); : _allowedOrientations = allowedOrientations,
super._();
factory _$VideoPlayerSettingsModelImpl.fromJson(Map<String, dynamic> json) => factory _$VideoPlayerSettingsModelImpl.fromJson(Map<String, dynamic> json) =>
_$$VideoPlayerSettingsModelImplFromJson(json); _$$VideoPlayerSettingsModelImplFromJson(json);
@ -235,6 +251,17 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
@override @override
@JsonKey() @JsonKey()
final double internalVolume; final double internalVolume;
final Set<DeviceOrientation>? _allowedOrientations;
@override
Set<DeviceOrientation>? get allowedOrientations {
final value = _allowedOrientations;
if (value == null) return null;
if (_allowedOrientations is EqualUnmodifiableSetView)
return _allowedOrientations;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(value);
}
@override @override
@JsonKey() @JsonKey()
final AutoNextType nextVideoType; final AutoNextType nextVideoType;
@ -243,7 +270,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, internalVolume: $internalVolume, nextVideoType: $nextVideoType, audioDevice: $audioDevice)'; return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, audioDevice: $audioDevice)';
} }
@override @override
@ -257,6 +284,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
..add(DiagnosticsProperty('hardwareAccel', hardwareAccel)) ..add(DiagnosticsProperty('hardwareAccel', hardwareAccel))
..add(DiagnosticsProperty('useLibass', useLibass)) ..add(DiagnosticsProperty('useLibass', useLibass))
..add(DiagnosticsProperty('internalVolume', internalVolume)) ..add(DiagnosticsProperty('internalVolume', internalVolume))
..add(DiagnosticsProperty('allowedOrientations', allowedOrientations))
..add(DiagnosticsProperty('nextVideoType', nextVideoType)) ..add(DiagnosticsProperty('nextVideoType', nextVideoType))
..add(DiagnosticsProperty('audioDevice', audioDevice)); ..add(DiagnosticsProperty('audioDevice', audioDevice));
} }
@ -286,6 +314,7 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
final bool hardwareAccel, final bool hardwareAccel,
final bool useLibass, final bool useLibass,
final double internalVolume, final double internalVolume,
final Set<DeviceOrientation>? allowedOrientations,
final AutoNextType nextVideoType, final AutoNextType nextVideoType,
final String? audioDevice}) = _$VideoPlayerSettingsModelImpl; final String? audioDevice}) = _$VideoPlayerSettingsModelImpl;
_VideoPlayerSettingsModel._() : super._(); _VideoPlayerSettingsModel._() : super._();
@ -306,6 +335,8 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
@override @override
double get internalVolume; double get internalVolume;
@override @override
Set<DeviceOrientation>? get allowedOrientations;
@override
AutoNextType get nextVideoType; AutoNextType get nextVideoType;
@override @override
String? get audioDevice; String? get audioDevice;

View file

@ -16,6 +16,9 @@ _$VideoPlayerSettingsModelImpl _$$VideoPlayerSettingsModelImplFromJson(
hardwareAccel: json['hardwareAccel'] as bool? ?? true, hardwareAccel: json['hardwareAccel'] as bool? ?? true,
useLibass: json['useLibass'] as bool? ?? false, useLibass: json['useLibass'] as bool? ?? false,
internalVolume: (json['internalVolume'] as num?)?.toDouble() ?? 100, internalVolume: (json['internalVolume'] as num?)?.toDouble() ?? 100,
allowedOrientations: (json['allowedOrientations'] as List<dynamic>?)
?.map((e) => $enumDecode(_$DeviceOrientationEnumMap, e))
.toSet(),
nextVideoType: nextVideoType:
$enumDecodeNullable(_$AutoNextTypeEnumMap, json['nextVideoType']) ?? $enumDecodeNullable(_$AutoNextTypeEnumMap, json['nextVideoType']) ??
AutoNextType.static, AutoNextType.static,
@ -31,6 +34,9 @@ Map<String, dynamic> _$$VideoPlayerSettingsModelImplToJson(
'hardwareAccel': instance.hardwareAccel, 'hardwareAccel': instance.hardwareAccel,
'useLibass': instance.useLibass, 'useLibass': instance.useLibass,
'internalVolume': instance.internalVolume, 'internalVolume': instance.internalVolume,
'allowedOrientations': instance.allowedOrientations
?.map((e) => _$DeviceOrientationEnumMap[e]!)
.toList(),
'nextVideoType': _$AutoNextTypeEnumMap[instance.nextVideoType]!, 'nextVideoType': _$AutoNextTypeEnumMap[instance.nextVideoType]!,
'audioDevice': instance.audioDevice, 'audioDevice': instance.audioDevice,
}; };
@ -45,6 +51,13 @@ const _$BoxFitEnumMap = {
BoxFit.scaleDown: 'scaleDown', BoxFit.scaleDown: 'scaleDown',
}; };
const _$DeviceOrientationEnumMap = {
DeviceOrientation.portraitUp: 'portraitUp',
DeviceOrientation.landscapeLeft: 'landscapeLeft',
DeviceOrientation.portraitDown: 'portraitDown',
DeviceOrientation.landscapeRight: 'landscapeRight',
};
const _$AutoNextTypeEnumMap = { const _$AutoNextTypeEnumMap = {
AutoNextType.off: 'off', AutoNextType.off: 'off',
AutoNextType.smart: 'smart', AutoNextType.smart: 'smart',

View file

@ -6,7 +6,7 @@ part of 'library_filters_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$libraryFiltersHash() => r'7b4661651df7e0c019dca5bb7eb6433bcd8b3ebb'; String _$libraryFiltersHash() => r'fd98699d8d7c1db6daefa6e53d5d90f989a8f776';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.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:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
@ -63,4 +64,7 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
state = state.copyWith(internalVolume: value); state = state.copyWith(internalVolume: value);
ref.read(videoPlayerProvider).setVolume(value); ref.read(videoPlayerProvider).setVolume(value);
} }
void toggleOrientation(Set<DeviceOrientation>? orientation) =>
state = state.copyWith(allowedOrientations: orientation);
} }

View file

@ -22,7 +22,7 @@ final showSyncButtonProviderProvider = AutoDisposeProvider<bool>.internal(
); );
typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef<bool>; typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef<bool>;
String _$userHash() => r'418b3d4ade830479db9f48c7793ac5b646778b82'; String _$userHash() => r'e83369c0d569d5a862aa1b92f3f0a45a9d1fe446';
/// See also [User]. /// See also [User].
@ProviderFor(User) @ProviderFor(User)

View file

@ -14,6 +14,7 @@ import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
import 'package:fladder/screens/settings/widgets/settings_message_box.dart'; import 'package:fladder/screens/settings/widgets/settings_message_box.dart';
import 'package:fladder/screens/settings/widgets/subtitle_editor.dart'; import 'package:fladder/screens/settings/widgets/subtitle_editor.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart';
import 'package:fladder/util/adaptive_layout.dart'; 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';
@ -151,6 +152,12 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
); );
}, },
), ),
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
SettingsListTile(
label: Text(context.localized.playerSettingsOrientationTitle),
subLabel: Text(context.localized.playerSettingsOrientationDesc),
onTap: () => showOrientationOptions(context, ref),
),
], ],
), ),
); );

View file

@ -1,4 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
@ -19,6 +21,7 @@ import 'package:fladder/screens/playlists/add_to_playlists.dart';
import 'package:fladder/screens/video_player/components/video_player_queue.dart'; import 'package:fladder/screens/video_player/components/video_player_queue.dart';
import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart'; import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/device_orientation_extension.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/refresh_state.dart';
@ -176,6 +179,11 @@ class _VideoOptionsMobileState extends ConsumerState<VideoOptions> {
], ],
), ),
), ),
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
SpacedListTile(
title: Text(context.localized.playerSettingsOrientationTitle),
onTap: () => showOrientationOptions(context, ref),
),
ListTile( ListTile(
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -492,3 +500,70 @@ Future<void> showPlaybackSpeed(BuildContext context) {
}, },
); );
} }
Future<void> showOrientationOptions(BuildContext context, WidgetRef ref) async {
Set<DeviceOrientation> orientations = ref
.read(videoPlayerSettingsProvider
.select((value) => value.allowedOrientations ?? Set.from(DeviceOrientation.values)))
.toSet();
void toggleOrientation(DeviceOrientation orientation) {
if (orientations.contains(orientation) && orientations.length > 1) {
orientations.remove(orientation);
} else {
orientations.add(orientation);
}
}
await showDialog(
context: context,
builder: (context) {
return StatefulBuilder(builder: (context, state) {
return SimpleDialog(
contentPadding: const EdgeInsets.only(top: 8, bottom: 24),
title: Row(children: [Text(context.localized.playbackRate)]),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12).copyWith(top: 6),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Divider(),
...DeviceOrientation.values.map(
(orientation) => CheckboxListTile.adaptive(
title: Text(orientation.label(context)),
value: orientations.contains(orientation),
onChanged: (value) {
state(() => toggleOrientation(orientation));
},
),
),
const Divider(),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(context.localized.cancel),
),
FilledButton(
onPressed: () {
Navigator.of(context).pop();
ref.read(videoPlayerSettingsProvider.notifier).toggleOrientation(orientations);
},
child: Text(context.localized.save),
),
].addInBetween(const SizedBox(width: 8)),
)
].addInBetween(const SizedBox(width: 8)),
),
)
],
);
});
},
);
}

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.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:media_kit_video/media_kit_video.dart'; import 'package:media_kit_video/media_kit_video.dart';
@ -49,6 +50,7 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
super.dispose(); super.dispose();
} }
@ -58,6 +60,9 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
Future.microtask(() { Future.microtask(() {
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(state: VideoPlayerState.fullScreen)); ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(state: VideoPlayerState.fullScreen));
final orientations = ref.read(videoPlayerSettingsProvider.select((value) => value.allowedOrientations));
SystemChrome.setPreferredOrientations(
orientations?.isNotEmpty == true ? orientations!.toList() : DeviceOrientation.values);
return ref.read(videoPlayerSettingsProvider.notifier).setSavedBrightness(); return ref.read(videoPlayerSettingsProvider.notifier).setSavedBrightness();
}); });
} }
@ -70,6 +75,15 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
final playerController = ref.watch(videoPlayerProvider.select((value) => value.controller)); final playerController = ref.watch(videoPlayerProvider.select((value) => value.controller));
ref.listen(
videoPlayerSettingsProvider.select((value) => value.allowedOrientations),
(previous, next) {
if (previous != next) {
SystemChrome.setPreferredOrientations(next?.isNotEmpty == true ? next!.toList() : DeviceOrientation.values);
}
},
);
return Material( return Material(
color: Colors.black, color: Colors.black,
child: Theme( child: Theme(

View file

@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fladder/util/localization_helper.dart';
extension DeviceOrientationExtension on DeviceOrientation {
String label(BuildContext context) => switch (this) {
DeviceOrientation.portraitUp => context.localized.deviceOrientationPortraitUp,
DeviceOrientation.landscapeLeft => context.localized.deviceOrientationLandscapeLeft,
DeviceOrientation.portraitDown => context.localized.deviceOrientationPortraitDown,
DeviceOrientation.landscapeRight => context.localized.deviceOrientationLandscapeRight,
};
}