mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
feat: Android TV support (#503)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
7ab8c015b9
commit
c299492d6d
168 changed files with 12019 additions and 3073 deletions
|
|
@ -679,6 +679,12 @@ class FakeJellyfinOpenApi extends JellyfinOpenApi {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<chopper.Response<BrandingOptions>> brandingConfigurationGet() async => chopper.Response(
|
||||
FakeHelper.fakeCorrectResponse,
|
||||
const BrandingOptions(loginDisclaimer: "Test server"),
|
||||
);
|
||||
}
|
||||
|
||||
class FakeHelper {
|
||||
|
|
|
|||
|
|
@ -1171,6 +1171,7 @@
|
|||
"phone": "Phone",
|
||||
"tablet": "Tablet",
|
||||
"desktop": "Desktop",
|
||||
"television": "Television",
|
||||
"layoutModeSingle": "Single",
|
||||
"layoutModeDual": "Dual",
|
||||
"copiedToClipboard": "Copied to clipboard",
|
||||
|
|
@ -1191,6 +1192,7 @@
|
|||
"segmentActionSkip": "Skip",
|
||||
"loading": "Loading",
|
||||
"exitFladderTitle": "Exit Fladder",
|
||||
"exitFladderDesc": "Are you sure you want to close Fladder?",
|
||||
"castAndCrew": "Cast & Crew",
|
||||
"guestActor": "{count, plural, other{Guest Actors} one{Guest Actor}}",
|
||||
"@guestActor": {
|
||||
|
|
@ -1336,5 +1338,8 @@
|
|||
"type": "double"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"quickConnectPostFailed": "Failed to get quick connect code",
|
||||
"quickConnectLoginUsingCode": "Using quick connect",
|
||||
"quickConnectEnterCodeDescription": "Enter the code below to login"
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import 'package:fladder/providers/video_player_provider.dart';
|
|||
import 'package:fladder/routes/auto_router.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/login/lock_screen.dart';
|
||||
import 'package:fladder/src/video_player_helper.g.dart';
|
||||
import 'package:fladder/theme.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/application_info.dart';
|
||||
|
|
@ -36,6 +37,7 @@ import 'package:fladder/util/fladder_config.dart';
|
|||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/util/themes_data.dart';
|
||||
import 'package:fladder/widgets/media_query_scaler.dart';
|
||||
|
||||
bool get _isDesktop {
|
||||
if (kIsWeb) return false;
|
||||
|
|
@ -86,13 +88,16 @@ void main(List<String> args) async {
|
|||
os: !kIsWeb ? defaultTargetPlatform.name.capitalize() : "${defaultTargetPlatform.name.capitalize()} Web",
|
||||
);
|
||||
|
||||
// Check if running on android TV
|
||||
final leanBackEnabled = !kIsWeb && Platform.isAndroid ? await NativeVideoActivity().isLeanBackEnabled() : false;
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWith((ref) => sharedPreferences),
|
||||
applicationInfoProvider.overrideWith((ref) => applicationInfo),
|
||||
crashLogProvider.overrideWith((ref) => crashProvider),
|
||||
argumentsStateProvider.overrideWith((ref) => ArgumentsModel.fromArguments(args)),
|
||||
argumentsStateProvider.overrideWith((ref) => ArgumentsModel.fromArguments(args, leanBackEnabled)),
|
||||
syncProvider.overrideWith((ref) => SyncNotifier(ref, applicationDirectory))
|
||||
],
|
||||
child: AdaptiveLayoutBuilder(
|
||||
|
|
@ -116,7 +121,9 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
|
|||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||
if (ref.read(lockScreenActiveProvider) || ref.read(userProvider) == null) {
|
||||
if (ref.read(lockScreenActiveProvider) ||
|
||||
ref.read(userProvider) == null ||
|
||||
ref.read(videoPlayerProvider).lastState?.playing == true) {
|
||||
dateTime = DateTime.now();
|
||||
return;
|
||||
}
|
||||
|
|
@ -248,11 +255,8 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
|
|||
final language = ref.watch(clientSettingsProvider
|
||||
.select((value) => value.selectedLocale ?? WidgetsBinding.instance.platformDispatcher.locale));
|
||||
final scrollBehaviour = const MaterialScrollBehavior();
|
||||
return Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||
},
|
||||
child: DynamicColorBuilder(builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
||||
return DynamicColorBuilder(
|
||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
||||
final lightTheme = themeColor == null
|
||||
? FladderTheme.theme(lightDynamic ?? FladderTheme.defaultScheme(Brightness.light), schemeVariant)
|
||||
: FladderTheme.theme(themeColor.schemeLight, schemeVariant);
|
||||
|
|
@ -281,9 +285,12 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
|
|||
}
|
||||
return locale;
|
||||
},
|
||||
builder: (context, child) => LocalizationContextWrapper(
|
||||
child: ScaffoldMessenger(child: child ?? Container()),
|
||||
currentLocale: language,
|
||||
builder: (context, child) => MediaQueryScaler(
|
||||
child: LocalizationContextWrapper(
|
||||
child: ScaffoldMessenger(child: child ?? Container()),
|
||||
currentLocale: language,
|
||||
),
|
||||
enable: ref.read(argumentsStateProvider).leanBackMode,
|
||||
),
|
||||
debugShowCheckedModeBanner: false,
|
||||
darkTheme: darkTheme.copyWith(
|
||||
|
|
@ -300,7 +307,7 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
|
|||
routerConfig: autoRouter.config(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ class ItemBaseModel with ItemBaseModelMappable {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> navigateTo(BuildContext context, {WidgetRef? ref}) async {
|
||||
Future<void> navigateTo(BuildContext context, {WidgetRef? ref, Object? tag}) async {
|
||||
switch (this) {
|
||||
case FolderModel _:
|
||||
case BoxSetModel _:
|
||||
|
|
@ -191,7 +191,7 @@ class ItemBaseModel with ItemBaseModelMappable {
|
|||
case SeasonModel _:
|
||||
case PersonModel _:
|
||||
default:
|
||||
context.router.push(DetailsRoute(id: id, item: this));
|
||||
context.router.push(DetailsRoute(id: id, item: this, tag: tag));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable {
|
|||
playlistId: item.playlistItemId,
|
||||
dateAired: item.premiereDate,
|
||||
chapters: Chapter.chaptersFromInfo(item.id ?? "", item.chapters ?? [], ref),
|
||||
images: ImagesData.fromBaseItem(item, ref, getOriginalSize: true),
|
||||
images: ImagesData.fromBaseItem(item, ref),
|
||||
primaryRatio: item.primaryImageAspectRatio,
|
||||
season: item.parentIndexNumber ?? 0,
|
||||
episode: item.indexNumber ?? 0,
|
||||
|
|
|
|||
|
|
@ -38,10 +38,9 @@ class ImagesData {
|
|||
dto.BaseItemDto item,
|
||||
Ref ref, {
|
||||
Size backDrop = const Size(2000, 2000),
|
||||
Size logo = const Size(1000, 1000),
|
||||
Size logo = const Size(500, 500),
|
||||
Size primary = const Size(600, 600),
|
||||
bool getOriginalSize = false,
|
||||
int quality = 95,
|
||||
}) {
|
||||
final itemid = item.id;
|
||||
if (itemid == null) return null;
|
||||
|
|
@ -59,7 +58,6 @@ class ImagesData {
|
|||
type: enums.ImageType.primary,
|
||||
maxHeight: primary.height.toInt(),
|
||||
maxWidth: primary.width.toInt(),
|
||||
quality: quality,
|
||||
),
|
||||
key: "${itemid}_primary_${item.imageTags?['Primary']}",
|
||||
hash: item.imageBlurHashes?.primary?[item.imageTags?['Primary']] ?? "",
|
||||
|
|
@ -77,7 +75,6 @@ class ImagesData {
|
|||
type: enums.ImageType.logo,
|
||||
maxHeight: logo.height.toInt(),
|
||||
maxWidth: logo.width.toInt(),
|
||||
quality: quality,
|
||||
),
|
||||
key: "${itemid}_logo_${item.imageTags?['Logo']}",
|
||||
hash: item.imageBlurHashes?.logo?[item.imageTags?['Logo']] ?? "")
|
||||
|
|
@ -98,7 +95,6 @@ class ImagesData {
|
|||
backdrop,
|
||||
maxHeight: backDrop.height.toInt(),
|
||||
maxWidth: backDrop.width.toInt(),
|
||||
quality: quality,
|
||||
),
|
||||
key: "${itemid}_backdrop_${index}_$backdrop",
|
||||
hash: item.imageBlurHashes?.backdrop?[backdrop] ?? "",
|
||||
|
|
@ -116,9 +112,8 @@ class ImagesData {
|
|||
dto.BaseItemDto item,
|
||||
Ref ref, {
|
||||
Size backDrop = const Size(2000, 2000),
|
||||
Size logo = const Size(1000, 1000),
|
||||
Size logo = const Size(500, 500),
|
||||
Size primary = const Size(600, 600),
|
||||
int quality = 95,
|
||||
}) {
|
||||
if (item.seriesId == null && item.parentId == null) return null;
|
||||
|
||||
|
|
@ -132,7 +127,6 @@ class ImagesData {
|
|||
type: enums.ImageType.primary,
|
||||
maxHeight: primary.height.toInt(),
|
||||
maxWidth: primary.width.toInt(),
|
||||
quality: quality,
|
||||
),
|
||||
key: "${item.seriesId}_primary_${item.seriesPrimaryImageTag ?? ""}",
|
||||
hash: item.imageBlurHashes?.primary?[item.seriesPrimaryImageTag] ?? "")
|
||||
|
|
@ -144,7 +138,6 @@ class ImagesData {
|
|||
type: enums.ImageType.logo,
|
||||
maxHeight: logo.height.toInt(),
|
||||
maxWidth: logo.width.toInt(),
|
||||
quality: quality,
|
||||
),
|
||||
key: "${item.seriesId}_logo_${item.parentLogoImageTag ?? ""}",
|
||||
hash: item.imageBlurHashes?.logo?[item.parentLogoImageTag] ?? "")
|
||||
|
|
@ -161,7 +154,6 @@ class ImagesData {
|
|||
backdrop,
|
||||
maxHeight: backDrop.height.toInt(),
|
||||
maxWidth: backDrop.width.toInt(),
|
||||
quality: quality,
|
||||
),
|
||||
key: "${itemId}_backdrop_${index}_$backdrop",
|
||||
hash: item.imageBlurHashes?.backdrop?[backdrop] ?? "",
|
||||
|
|
@ -180,8 +172,7 @@ class ImagesData {
|
|||
Ref ref, {
|
||||
Size backDrop = const Size(2000, 2000),
|
||||
Size logo = const Size(1000, 1000),
|
||||
Size primary = const Size(2000, 2000),
|
||||
int quality = 95,
|
||||
Size primary = const Size(500, 500),
|
||||
}) {
|
||||
return ImagesData(
|
||||
primary: (item.primaryImageTag != null && item.imageBlurHashes != null)
|
||||
|
|
@ -191,7 +182,6 @@ class ImagesData {
|
|||
type: enums.ImageType.primary,
|
||||
maxHeight: primary.height.toInt(),
|
||||
maxWidth: primary.width.toInt(),
|
||||
quality: quality,
|
||||
),
|
||||
key: "${item.id ?? ""}_primary_${item.primaryImageTag ?? ''}",
|
||||
hash: item.imageBlurHashes?.primary?[item.primaryImageTag] ?? '')
|
||||
|
|
|
|||
|
|
@ -74,6 +74,11 @@ class MovieModel extends ItemStreamModel with MovieModelMappable {
|
|||
@override
|
||||
MediaStreamsModel? get streamModel => mediaStreams;
|
||||
|
||||
@override
|
||||
String? label(BuildContext context) {
|
||||
return name;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get syncAble => true;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import 'package:fladder/models/items/images_models.dart';
|
||||
import 'package:fladder/models/items/overview_model.dart';
|
||||
import 'package:dart_mappable/dart_mappable.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/images_models.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/models/items/movie_model.dart';
|
||||
import 'package:fladder/models/items/overview_model.dart';
|
||||
import 'package:fladder/models/items/series_model.dart';
|
||||
|
||||
import 'package:dart_mappable/dart_mappable.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
part 'person_model.mapper.dart';
|
||||
|
||||
@MappableClass()
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class SeriesModel extends ItemBaseModel with SeriesModelMappable {
|
|||
userData: UserData.fromDto(item.userData),
|
||||
parentId: item.parentId,
|
||||
playlistId: item.playlistItemId,
|
||||
images: ImagesData.fromBaseItem(item, ref, getOriginalSize: true),
|
||||
images: ImagesData.fromBaseItem(item, ref),
|
||||
primaryRatio: item.primaryImageAspectRatio,
|
||||
originalTitle: item.originalTitle ?? "",
|
||||
sortName: item.sortName ?? "",
|
||||
|
|
|
|||
|
|
@ -1,26 +1,34 @@
|
|||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/models/credentials_model.dart';
|
||||
|
||||
class LoginScreenModel {
|
||||
final List<AccountModel> accounts;
|
||||
final CredentialsModel tempCredentials;
|
||||
final bool loading;
|
||||
LoginScreenModel({
|
||||
required this.accounts,
|
||||
required this.tempCredentials,
|
||||
required this.loading,
|
||||
});
|
||||
part 'login_screen_model.freezed.dart';
|
||||
|
||||
LoginScreenModel copyWith({
|
||||
List<AccountModel>? accounts,
|
||||
CredentialsModel? tempCredentials,
|
||||
bool? loading,
|
||||
}) {
|
||||
return LoginScreenModel(
|
||||
accounts: accounts ?? this.accounts,
|
||||
tempCredentials: tempCredentials ?? this.tempCredentials,
|
||||
loading: loading ?? this.loading,
|
||||
);
|
||||
}
|
||||
enum LoginScreenType {
|
||||
users,
|
||||
login,
|
||||
code,
|
||||
}
|
||||
|
||||
@Freezed(copyWith: true)
|
||||
abstract class LoginScreenModel with _$LoginScreenModel {
|
||||
factory LoginScreenModel({
|
||||
@Default([]) List<AccountModel> accounts,
|
||||
@Default(LoginScreenType.users) LoginScreenType screen,
|
||||
ServerLoginModel? serverLoginModel,
|
||||
String? errorMessage,
|
||||
@Default(false) bool hasBaseUrl,
|
||||
@Default(false) bool loading,
|
||||
}) = _LoginScreenModel;
|
||||
}
|
||||
|
||||
@Freezed(copyWith: true)
|
||||
abstract class ServerLoginModel with _$ServerLoginModel {
|
||||
factory ServerLoginModel({
|
||||
required CredentialsModel tempCredentials,
|
||||
@Default([]) List<AccountModel> accounts,
|
||||
String? serverMessage,
|
||||
@Default(false) bool hasQuickConnect,
|
||||
}) = _ServerLoginModel;
|
||||
}
|
||||
|
|
|
|||
774
lib/models/login_screen_model.freezed.dart
Normal file
774
lib/models/login_screen_model.freezed.dart
Normal file
|
|
@ -0,0 +1,774 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// 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 'login_screen_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LoginScreenModel {
|
||||
List<AccountModel> get accounts;
|
||||
LoginScreenType get screen;
|
||||
ServerLoginModel? get serverLoginModel;
|
||||
String? get errorMessage;
|
||||
bool get hasBaseUrl;
|
||||
bool get loading;
|
||||
|
||||
/// Create a copy of LoginScreenModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$LoginScreenModelCopyWith<LoginScreenModel> get copyWith =>
|
||||
_$LoginScreenModelCopyWithImpl<LoginScreenModel>(
|
||||
this as LoginScreenModel, _$identity);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, loading: $loading)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $LoginScreenModelCopyWith<$Res> {
|
||||
factory $LoginScreenModelCopyWith(
|
||||
LoginScreenModel value, $Res Function(LoginScreenModel) _then) =
|
||||
_$LoginScreenModelCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{List<AccountModel> accounts,
|
||||
LoginScreenType screen,
|
||||
ServerLoginModel? serverLoginModel,
|
||||
String? errorMessage,
|
||||
bool hasBaseUrl,
|
||||
bool loading});
|
||||
|
||||
$ServerLoginModelCopyWith<$Res>? get serverLoginModel;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$LoginScreenModelCopyWithImpl<$Res>
|
||||
implements $LoginScreenModelCopyWith<$Res> {
|
||||
_$LoginScreenModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final LoginScreenModel _self;
|
||||
final $Res Function(LoginScreenModel) _then;
|
||||
|
||||
/// Create a copy of LoginScreenModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? accounts = null,
|
||||
Object? screen = null,
|
||||
Object? serverLoginModel = freezed,
|
||||
Object? errorMessage = freezed,
|
||||
Object? hasBaseUrl = null,
|
||||
Object? loading = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
accounts: null == accounts
|
||||
? _self.accounts
|
||||
: accounts // ignore: cast_nullable_to_non_nullable
|
||||
as List<AccountModel>,
|
||||
screen: null == screen
|
||||
? _self.screen
|
||||
: screen // ignore: cast_nullable_to_non_nullable
|
||||
as LoginScreenType,
|
||||
serverLoginModel: freezed == serverLoginModel
|
||||
? _self.serverLoginModel
|
||||
: serverLoginModel // ignore: cast_nullable_to_non_nullable
|
||||
as ServerLoginModel?,
|
||||
errorMessage: freezed == errorMessage
|
||||
? _self.errorMessage
|
||||
: errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
hasBaseUrl: null == hasBaseUrl
|
||||
? _self.hasBaseUrl
|
||||
: hasBaseUrl // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
loading: null == loading
|
||||
? _self.loading
|
||||
: loading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of LoginScreenModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ServerLoginModelCopyWith<$Res>? get serverLoginModel {
|
||||
if (_self.serverLoginModel == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ServerLoginModelCopyWith<$Res>(_self.serverLoginModel!, (value) {
|
||||
return _then(_self.copyWith(serverLoginModel: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds pattern-matching-related methods to [LoginScreenModel].
|
||||
extension LoginScreenModelPatterns on LoginScreenModel {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>(
|
||||
TResult Function(_LoginScreenModel value)? $default, {
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LoginScreenModel() when $default != null:
|
||||
return $default(_that);
|
||||
case _:
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>(
|
||||
TResult Function(_LoginScreenModel value) $default,
|
||||
) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LoginScreenModel():
|
||||
return $default(_that);
|
||||
case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
}
|
||||
}
|
||||
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>(
|
||||
TResult? Function(_LoginScreenModel value)? $default,
|
||||
) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LoginScreenModel() when $default != null:
|
||||
return $default(_that);
|
||||
case _:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>(
|
||||
TResult Function(
|
||||
List<AccountModel> accounts,
|
||||
LoginScreenType screen,
|
||||
ServerLoginModel? serverLoginModel,
|
||||
String? errorMessage,
|
||||
bool hasBaseUrl,
|
||||
bool loading)?
|
||||
$default, {
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LoginScreenModel() when $default != null:
|
||||
return $default(_that.accounts, _that.screen, _that.serverLoginModel,
|
||||
_that.errorMessage, _that.hasBaseUrl, _that.loading);
|
||||
case _:
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>(
|
||||
TResult Function(
|
||||
List<AccountModel> accounts,
|
||||
LoginScreenType screen,
|
||||
ServerLoginModel? serverLoginModel,
|
||||
String? errorMessage,
|
||||
bool hasBaseUrl,
|
||||
bool loading)
|
||||
$default,
|
||||
) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LoginScreenModel():
|
||||
return $default(_that.accounts, _that.screen, _that.serverLoginModel,
|
||||
_that.errorMessage, _that.hasBaseUrl, _that.loading);
|
||||
case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
}
|
||||
}
|
||||
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>(
|
||||
TResult? Function(
|
||||
List<AccountModel> accounts,
|
||||
LoginScreenType screen,
|
||||
ServerLoginModel? serverLoginModel,
|
||||
String? errorMessage,
|
||||
bool hasBaseUrl,
|
||||
bool loading)?
|
||||
$default,
|
||||
) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LoginScreenModel() when $default != null:
|
||||
return $default(_that.accounts, _that.screen, _that.serverLoginModel,
|
||||
_that.errorMessage, _that.hasBaseUrl, _that.loading);
|
||||
case _:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _LoginScreenModel implements LoginScreenModel {
|
||||
_LoginScreenModel(
|
||||
{final List<AccountModel> accounts = const [],
|
||||
this.screen = LoginScreenType.users,
|
||||
this.serverLoginModel,
|
||||
this.errorMessage,
|
||||
this.hasBaseUrl = false,
|
||||
this.loading = false})
|
||||
: _accounts = accounts;
|
||||
|
||||
final List<AccountModel> _accounts;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<AccountModel> get accounts {
|
||||
if (_accounts is EqualUnmodifiableListView) return _accounts;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_accounts);
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final LoginScreenType screen;
|
||||
@override
|
||||
final ServerLoginModel? serverLoginModel;
|
||||
@override
|
||||
final String? errorMessage;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool hasBaseUrl;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool loading;
|
||||
|
||||
/// Create a copy of LoginScreenModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$LoginScreenModelCopyWith<_LoginScreenModel> get copyWith =>
|
||||
__$LoginScreenModelCopyWithImpl<_LoginScreenModel>(this, _$identity);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, loading: $loading)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$LoginScreenModelCopyWith<$Res>
|
||||
implements $LoginScreenModelCopyWith<$Res> {
|
||||
factory _$LoginScreenModelCopyWith(
|
||||
_LoginScreenModel value, $Res Function(_LoginScreenModel) _then) =
|
||||
__$LoginScreenModelCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{List<AccountModel> accounts,
|
||||
LoginScreenType screen,
|
||||
ServerLoginModel? serverLoginModel,
|
||||
String? errorMessage,
|
||||
bool hasBaseUrl,
|
||||
bool loading});
|
||||
|
||||
@override
|
||||
$ServerLoginModelCopyWith<$Res>? get serverLoginModel;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$LoginScreenModelCopyWithImpl<$Res>
|
||||
implements _$LoginScreenModelCopyWith<$Res> {
|
||||
__$LoginScreenModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _LoginScreenModel _self;
|
||||
final $Res Function(_LoginScreenModel) _then;
|
||||
|
||||
/// Create a copy of LoginScreenModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? accounts = null,
|
||||
Object? screen = null,
|
||||
Object? serverLoginModel = freezed,
|
||||
Object? errorMessage = freezed,
|
||||
Object? hasBaseUrl = null,
|
||||
Object? loading = null,
|
||||
}) {
|
||||
return _then(_LoginScreenModel(
|
||||
accounts: null == accounts
|
||||
? _self._accounts
|
||||
: accounts // ignore: cast_nullable_to_non_nullable
|
||||
as List<AccountModel>,
|
||||
screen: null == screen
|
||||
? _self.screen
|
||||
: screen // ignore: cast_nullable_to_non_nullable
|
||||
as LoginScreenType,
|
||||
serverLoginModel: freezed == serverLoginModel
|
||||
? _self.serverLoginModel
|
||||
: serverLoginModel // ignore: cast_nullable_to_non_nullable
|
||||
as ServerLoginModel?,
|
||||
errorMessage: freezed == errorMessage
|
||||
? _self.errorMessage
|
||||
: errorMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
hasBaseUrl: null == hasBaseUrl
|
||||
? _self.hasBaseUrl
|
||||
: hasBaseUrl // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
loading: null == loading
|
||||
? _self.loading
|
||||
: loading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of LoginScreenModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ServerLoginModelCopyWith<$Res>? get serverLoginModel {
|
||||
if (_self.serverLoginModel == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ServerLoginModelCopyWith<$Res>(_self.serverLoginModel!, (value) {
|
||||
return _then(_self.copyWith(serverLoginModel: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ServerLoginModel {
|
||||
CredentialsModel get tempCredentials;
|
||||
List<AccountModel> get accounts;
|
||||
String? get serverMessage;
|
||||
bool get hasQuickConnect;
|
||||
|
||||
/// Create a copy of ServerLoginModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ServerLoginModelCopyWith<ServerLoginModel> get copyWith =>
|
||||
_$ServerLoginModelCopyWithImpl<ServerLoginModel>(
|
||||
this as ServerLoginModel, _$identity);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerLoginModel(tempCredentials: $tempCredentials, accounts: $accounts, serverMessage: $serverMessage, hasQuickConnect: $hasQuickConnect)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ServerLoginModelCopyWith<$Res> {
|
||||
factory $ServerLoginModelCopyWith(
|
||||
ServerLoginModel value, $Res Function(ServerLoginModel) _then) =
|
||||
_$ServerLoginModelCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{CredentialsModel tempCredentials,
|
||||
List<AccountModel> accounts,
|
||||
String? serverMessage,
|
||||
bool hasQuickConnect});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ServerLoginModelCopyWithImpl<$Res>
|
||||
implements $ServerLoginModelCopyWith<$Res> {
|
||||
_$ServerLoginModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ServerLoginModel _self;
|
||||
final $Res Function(ServerLoginModel) _then;
|
||||
|
||||
/// Create a copy of ServerLoginModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? tempCredentials = null,
|
||||
Object? accounts = null,
|
||||
Object? serverMessage = freezed,
|
||||
Object? hasQuickConnect = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
tempCredentials: null == tempCredentials
|
||||
? _self.tempCredentials
|
||||
: tempCredentials // ignore: cast_nullable_to_non_nullable
|
||||
as CredentialsModel,
|
||||
accounts: null == accounts
|
||||
? _self.accounts
|
||||
: accounts // ignore: cast_nullable_to_non_nullable
|
||||
as List<AccountModel>,
|
||||
serverMessage: freezed == serverMessage
|
||||
? _self.serverMessage
|
||||
: serverMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
hasQuickConnect: null == hasQuickConnect
|
||||
? _self.hasQuickConnect
|
||||
: hasQuickConnect // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds pattern-matching-related methods to [ServerLoginModel].
|
||||
extension ServerLoginModelPatterns on ServerLoginModel {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>(
|
||||
TResult Function(_ServerLoginModel value)? $default, {
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerLoginModel() when $default != null:
|
||||
return $default(_that);
|
||||
case _:
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>(
|
||||
TResult Function(_ServerLoginModel value) $default,
|
||||
) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerLoginModel():
|
||||
return $default(_that);
|
||||
case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
}
|
||||
}
|
||||
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>(
|
||||
TResult? Function(_ServerLoginModel value)? $default,
|
||||
) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerLoginModel() when $default != null:
|
||||
return $default(_that);
|
||||
case _:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>(
|
||||
TResult Function(
|
||||
CredentialsModel tempCredentials,
|
||||
List<AccountModel> accounts,
|
||||
String? serverMessage,
|
||||
bool hasQuickConnect)?
|
||||
$default, {
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerLoginModel() when $default != null:
|
||||
return $default(_that.tempCredentials, _that.accounts,
|
||||
_that.serverMessage, _that.hasQuickConnect);
|
||||
case _:
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>(
|
||||
TResult Function(
|
||||
CredentialsModel tempCredentials,
|
||||
List<AccountModel> accounts,
|
||||
String? serverMessage,
|
||||
bool hasQuickConnect)
|
||||
$default,
|
||||
) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerLoginModel():
|
||||
return $default(_that.tempCredentials, _that.accounts,
|
||||
_that.serverMessage, _that.hasQuickConnect);
|
||||
case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
}
|
||||
}
|
||||
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>(
|
||||
TResult? Function(
|
||||
CredentialsModel tempCredentials,
|
||||
List<AccountModel> accounts,
|
||||
String? serverMessage,
|
||||
bool hasQuickConnect)?
|
||||
$default,
|
||||
) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerLoginModel() when $default != null:
|
||||
return $default(_that.tempCredentials, _that.accounts,
|
||||
_that.serverMessage, _that.hasQuickConnect);
|
||||
case _:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _ServerLoginModel implements ServerLoginModel {
|
||||
_ServerLoginModel(
|
||||
{required this.tempCredentials,
|
||||
final List<AccountModel> accounts = const [],
|
||||
this.serverMessage,
|
||||
this.hasQuickConnect = false})
|
||||
: _accounts = accounts;
|
||||
|
||||
@override
|
||||
final CredentialsModel tempCredentials;
|
||||
final List<AccountModel> _accounts;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<AccountModel> get accounts {
|
||||
if (_accounts is EqualUnmodifiableListView) return _accounts;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_accounts);
|
||||
}
|
||||
|
||||
@override
|
||||
final String? serverMessage;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool hasQuickConnect;
|
||||
|
||||
/// Create a copy of ServerLoginModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ServerLoginModelCopyWith<_ServerLoginModel> get copyWith =>
|
||||
__$ServerLoginModelCopyWithImpl<_ServerLoginModel>(this, _$identity);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerLoginModel(tempCredentials: $tempCredentials, accounts: $accounts, serverMessage: $serverMessage, hasQuickConnect: $hasQuickConnect)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ServerLoginModelCopyWith<$Res>
|
||||
implements $ServerLoginModelCopyWith<$Res> {
|
||||
factory _$ServerLoginModelCopyWith(
|
||||
_ServerLoginModel value, $Res Function(_ServerLoginModel) _then) =
|
||||
__$ServerLoginModelCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{CredentialsModel tempCredentials,
|
||||
List<AccountModel> accounts,
|
||||
String? serverMessage,
|
||||
bool hasQuickConnect});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$ServerLoginModelCopyWithImpl<$Res>
|
||||
implements _$ServerLoginModelCopyWith<$Res> {
|
||||
__$ServerLoginModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ServerLoginModel _self;
|
||||
final $Res Function(_ServerLoginModel) _then;
|
||||
|
||||
/// Create a copy of ServerLoginModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? tempCredentials = null,
|
||||
Object? accounts = null,
|
||||
Object? serverMessage = freezed,
|
||||
Object? hasQuickConnect = null,
|
||||
}) {
|
||||
return _then(_ServerLoginModel(
|
||||
tempCredentials: null == tempCredentials
|
||||
? _self.tempCredentials
|
||||
: tempCredentials // ignore: cast_nullable_to_non_nullable
|
||||
as CredentialsModel,
|
||||
accounts: null == accounts
|
||||
? _self._accounts
|
||||
: accounts // ignore: cast_nullable_to_non_nullable
|
||||
as List<AccountModel>,
|
||||
serverMessage: freezed == serverMessage
|
||||
? _self.serverMessage
|
||||
: serverMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
hasQuickConnect: null == hasQuickConnect
|
||||
? _self.hasQuickConnect
|
||||
: hasQuickConnect // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
|
@ -137,7 +137,7 @@ class PlaybackModelHelper {
|
|||
oldModel: currentModel,
|
||||
);
|
||||
if (newModel == null) return null;
|
||||
ref.read(videoPlayerProvider.notifier).loadPlaybackItem(newModel, startPosition: Duration.zero);
|
||||
ref.read(videoPlayerProvider.notifier).loadPlaybackItem(newModel, Duration.zero);
|
||||
return newModel;
|
||||
}
|
||||
|
||||
|
|
@ -502,7 +502,7 @@ class PlaybackModelHelper {
|
|||
}
|
||||
if (newModel == null) return;
|
||||
if (newModel.runtimeType != playbackModel.runtimeType || newModel is TranscodePlaybackModel) {
|
||||
ref.read(videoPlayerProvider.notifier).loadPlaybackItem(newModel, startPosition: currentPosition);
|
||||
ref.read(videoPlayerProvider.notifier).loadPlaybackItem(newModel, currentPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ abstract class ArgumentsModel with _$ArgumentsModel {
|
|||
|
||||
factory ArgumentsModel({
|
||||
@Default(false) bool htpcMode,
|
||||
@Default(false) bool leanBackMode,
|
||||
}) = _ArgumentsModel;
|
||||
|
||||
factory ArgumentsModel.fromArguments(List<String> arguments) {
|
||||
factory ArgumentsModel.fromArguments(List<String> arguments, bool leanBackEnabled) {
|
||||
arguments = arguments.map((e) => e.trim()).toList();
|
||||
return ArgumentsModel(
|
||||
htpcMode: arguments.contains('--htpc'),
|
||||
htpcMode: arguments.contains('--htpc') || leanBackEnabled,
|
||||
leanBackMode: leanBackEnabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@ T _$identity<T>(T value) => value;
|
|||
/// @nodoc
|
||||
mixin _$ArgumentsModel {
|
||||
bool get htpcMode;
|
||||
bool get leanBackMode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ArgumentsModel(htpcMode: $htpcMode)';
|
||||
return 'ArgumentsModel(htpcMode: $htpcMode, leanBackMode: $leanBackMode)';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,13 +116,13 @@ extension ArgumentsModelPatterns on ArgumentsModel {
|
|||
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>(
|
||||
TResult Function(bool htpcMode)? $default, {
|
||||
TResult Function(bool htpcMode, bool leanBackMode)? $default, {
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ArgumentsModel() when $default != null:
|
||||
return $default(_that.htpcMode);
|
||||
return $default(_that.htpcMode, _that.leanBackMode);
|
||||
case _:
|
||||
return orElse();
|
||||
}
|
||||
|
|
@ -142,12 +143,12 @@ extension ArgumentsModelPatterns on ArgumentsModel {
|
|||
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>(
|
||||
TResult Function(bool htpcMode) $default,
|
||||
TResult Function(bool htpcMode, bool leanBackMode) $default,
|
||||
) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ArgumentsModel():
|
||||
return $default(_that.htpcMode);
|
||||
return $default(_that.htpcMode, _that.leanBackMode);
|
||||
case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
}
|
||||
|
|
@ -167,12 +168,12 @@ extension ArgumentsModelPatterns on ArgumentsModel {
|
|||
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>(
|
||||
TResult? Function(bool htpcMode)? $default,
|
||||
TResult? Function(bool htpcMode, bool leanBackMode)? $default,
|
||||
) {
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ArgumentsModel() when $default != null:
|
||||
return $default(_that.htpcMode);
|
||||
return $default(_that.htpcMode, _that.leanBackMode);
|
||||
case _:
|
||||
return null;
|
||||
}
|
||||
|
|
@ -182,15 +183,19 @@ extension ArgumentsModelPatterns on ArgumentsModel {
|
|||
/// @nodoc
|
||||
|
||||
class _ArgumentsModel extends ArgumentsModel {
|
||||
_ArgumentsModel({this.htpcMode = false}) : super._();
|
||||
_ArgumentsModel({this.htpcMode = false, this.leanBackMode = false})
|
||||
: super._();
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool htpcMode;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool leanBackMode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ArgumentsModel(htpcMode: $htpcMode)';
|
||||
return 'ArgumentsModel(htpcMode: $htpcMode, leanBackMode: $leanBackMode)';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ T selectAvailableOrSmaller<T>(T value, Set<T> availableOptions, List<T> allOptio
|
|||
enum HomeBanner {
|
||||
hide,
|
||||
carousel,
|
||||
banner;
|
||||
banner,
|
||||
detailedBanner;
|
||||
|
||||
const HomeBanner();
|
||||
|
||||
|
|
@ -48,6 +49,7 @@ enum HomeBanner {
|
|||
HomeBanner.hide => context.localized.hide,
|
||||
HomeBanner.carousel => context.localized.homeBannerCarousel,
|
||||
HomeBanner.banner => context.localized.homeBannerSlideshow,
|
||||
HomeBanner.detailedBanner => 'Detailed banner'
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,12 +47,14 @@ const _$ViewSizeEnumMap = {
|
|||
ViewSize.phone: 'phone',
|
||||
ViewSize.tablet: 'tablet',
|
||||
ViewSize.desktop: 'desktop',
|
||||
ViewSize.television: 'television',
|
||||
};
|
||||
|
||||
const _$HomeBannerEnumMap = {
|
||||
HomeBanner.hide: 'hide',
|
||||
HomeBanner.carousel: 'carousel',
|
||||
HomeBanner.banner: 'banner',
|
||||
HomeBanner.detailedBanner: 'detailedBanner',
|
||||
};
|
||||
|
||||
const _$HomeCarouselSettingsEnumMap = {
|
||||
|
|
|
|||
|
|
@ -127,11 +127,17 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
|||
|
||||
enum PlayerOptions {
|
||||
libMDK,
|
||||
libMPV;
|
||||
libMPV,
|
||||
nativePlayer;
|
||||
|
||||
const PlayerOptions();
|
||||
|
||||
static Iterable<PlayerOptions> get available => kIsWeb ? {PlayerOptions.libMPV} : PlayerOptions.values;
|
||||
static Iterable<PlayerOptions> get available => kIsWeb
|
||||
? {PlayerOptions.libMPV}
|
||||
: switch (defaultTargetPlatform) {
|
||||
TargetPlatform.android => PlayerOptions.values,
|
||||
_ => {PlayerOptions.libMDK, PlayerOptions.libMPV},
|
||||
};
|
||||
|
||||
static PlayerOptions get platformDefaults {
|
||||
if (kIsWeb) return PlayerOptions.libMPV;
|
||||
|
|
@ -143,6 +149,7 @@ enum PlayerOptions {
|
|||
String label(BuildContext context) => switch (this) {
|
||||
PlayerOptions.libMDK => "MDK",
|
||||
PlayerOptions.libMPV => "MPV",
|
||||
PlayerOptions.nativePlayer => "Native",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ const _$BoxFitEnumMap = {
|
|||
const _$PlayerOptionsEnumMap = {
|
||||
PlayerOptions.libMDK: 'libMDK',
|
||||
PlayerOptions.libMPV: 'libMPV',
|
||||
PlayerOptions.nativePlayer: 'nativePlayer',
|
||||
};
|
||||
|
||||
const _$DeviceOrientationEnumMap = {
|
||||
|
|
|
|||
|
|
@ -37,10 +37,14 @@ class JellyRequest implements Interceptor {
|
|||
FutureOr<Response<BodyType>> intercept<BodyType>(Chain<BodyType> chain) async {
|
||||
final connectivityNotifier = ref.read(connectivityStatusProvider.notifier);
|
||||
try {
|
||||
final serverUrl = Uri.parse(ref.read(userProvider)?.server ?? ref.read(authProvider).tempCredentials.server);
|
||||
final serverUrl = Uri.parse(
|
||||
ref.read(userProvider)?.server ?? ref.read(authProvider).serverLoginModel?.tempCredentials.server ?? "");
|
||||
|
||||
//Use current logged in user otherwise use the authprovider
|
||||
var loginModel = ref.read(userProvider)?.credentials ?? ref.read(authProvider).tempCredentials;
|
||||
var loginModel = ref.read(userProvider)?.credentials ?? ref.read(authProvider).serverLoginModel?.tempCredentials;
|
||||
|
||||
if (loginModel == null) throw UnimplementedError();
|
||||
|
||||
var headers = loginModel.header(ref);
|
||||
final Response<BodyType> response = await chain.proceed(
|
||||
applyHeaders(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:chopper/chopper.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/models/credentials_model.dart';
|
||||
import 'package:fladder/models/login_screen_model.dart';
|
||||
|
|
@ -13,44 +16,133 @@ import 'package:fladder/providers/service_provider.dart';
|
|||
import 'package:fladder/providers/shared_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/providers/views_provider.dart';
|
||||
import 'package:fladder/screens/login/lock_screen.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/fladder_config.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
|
||||
final authProvider = StateNotifierProvider<AuthNotifier, LoginScreenModel>((ref) {
|
||||
return AuthNotifier(ref);
|
||||
});
|
||||
|
||||
class AuthNotifier extends StateNotifier<LoginScreenModel> {
|
||||
AuthNotifier(this.ref)
|
||||
: super(
|
||||
LoginScreenModel(
|
||||
accounts: [],
|
||||
tempCredentials: CredentialsModel.createNewCredentials(),
|
||||
loading: false,
|
||||
),
|
||||
);
|
||||
AuthNotifier(this.ref) : super(LoginScreenModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<Response<List<AccountModel>>?> getPublicUsers() async {
|
||||
try {
|
||||
var response = await api.usersPublicGet(state.tempCredentials);
|
||||
if (response.isSuccessful && response.body != null) {
|
||||
var models = response.body ?? [];
|
||||
BuildContext? context;
|
||||
|
||||
return response.copyWith(body: models.toList());
|
||||
Future<void> initModel(BuildContext newContext) async {
|
||||
context ??= newContext;
|
||||
ref.read(userProvider.notifier).clear();
|
||||
final currentAccounts = ref.read(authProvider.notifier).getSavedAccounts();
|
||||
ref.read(lockScreenActiveProvider.notifier).update((state) => true);
|
||||
if (FladderConfig.baseUrl != null) {
|
||||
final url = FladderConfig.baseUrl;
|
||||
state = state.copyWith(
|
||||
hasBaseUrl: true,
|
||||
);
|
||||
if (url != null) {
|
||||
await setServer(url);
|
||||
}
|
||||
return response.copyWith(body: []);
|
||||
}
|
||||
state = state.copyWith(
|
||||
accounts: currentAccounts,
|
||||
screen: currentAccounts.isEmpty ? LoginScreenType.login : LoginScreenType.users,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _fetchServerInfo(String url) async {
|
||||
try {
|
||||
final newCredentials = CredentialsModel.createNewCredentials().copyWith(server: url.rtrim('/'));
|
||||
final newLoginModel = ServerLoginModel(tempCredentials: newCredentials);
|
||||
state = state.copyWith(
|
||||
serverLoginModel: newLoginModel,
|
||||
loading: true,
|
||||
);
|
||||
final publicUsers = (await getPublicUsers())?.body ?? [];
|
||||
final quickConnectStatus = (await api.quickConnectEnabled()).body ?? false;
|
||||
final branding = await api.getBranding();
|
||||
final serverResponse = await api.systemInfoPublicGet();
|
||||
state = state.copyWith(
|
||||
screen: quickConnectStatus ? LoginScreenType.code : LoginScreenType.login,
|
||||
serverLoginModel: newLoginModel.copyWith(
|
||||
tempCredentials: newCredentials.copyWith(
|
||||
serverName: serverResponse.body?.serverName,
|
||||
serverId: serverResponse.body?.id,
|
||||
),
|
||||
accounts: publicUsers,
|
||||
hasQuickConnect: quickConnectStatus,
|
||||
serverMessage: branding.body?.loginDisclaimer,
|
||||
),
|
||||
loading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
state = state.copyWith(
|
||||
errorMessage: context?.localized.invalidUrl,
|
||||
loading: false,
|
||||
);
|
||||
if (context != null) {
|
||||
fladderSnackbar(context!, title: context!.localized.unableToConnectHost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? _parseUrl(String url) {
|
||||
if (url.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (!Uri.parse(url).isAbsolute) {
|
||||
return context?.localized.invalidUrl;
|
||||
}
|
||||
|
||||
if (!url.startsWith('https://') && !url.startsWith('http://')) {
|
||||
return context?.localized.invalidUrlDesc;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Response<List<AccountModel>>?> getPublicUsers() async {
|
||||
try {
|
||||
state = state.copyWith(loading: true);
|
||||
final credentials = state.serverLoginModel?.tempCredentials;
|
||||
if (credentials == null) return null;
|
||||
var response = await api.usersPublicGet(credentials);
|
||||
if (response.isSuccessful && response.body != null) {
|
||||
var models = response.body ?? [];
|
||||
return response.copyWith(body: models.toList());
|
||||
}
|
||||
state = state.copyWith(
|
||||
serverLoginModel: state.serverLoginModel?.copyWith(
|
||||
accounts: response.body ?? [],
|
||||
),
|
||||
);
|
||||
return response.copyWith(body: []);
|
||||
} catch (e) {
|
||||
return null;
|
||||
} finally {
|
||||
state = state.copyWith(loading: false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response<AccountModel>?> authenticateUsingSecret(String secret) async {
|
||||
clearAllProviders();
|
||||
var response = await api.quickConnectAuthenticate(secret);
|
||||
return _createAccountModel(response);
|
||||
}
|
||||
|
||||
Future<Response<AccountModel>?> authenticateByName(String userName, String password) async {
|
||||
state = state.copyWith(loading: true);
|
||||
clearAllProviders();
|
||||
var response = await api.usersAuthenticateByNamePost(userName: userName, password: password);
|
||||
CredentialsModel credentials = state.tempCredentials;
|
||||
return _createAccountModel(response);
|
||||
}
|
||||
|
||||
Future<Response<AccountModel>?> _createAccountModel(Response<AuthenticationResult> response) async {
|
||||
CredentialsModel? credentials = state.serverLoginModel?.tempCredentials;
|
||||
if (credentials == null) return null;
|
||||
if (response.isSuccessful && (response.body?.accessToken?.isNotEmpty ?? false)) {
|
||||
var serverResponse = await api.systemInfoPublicGet();
|
||||
credentials = credentials.copyWith(
|
||||
|
|
@ -68,16 +160,21 @@ class AuthNotifier extends StateNotifier<LoginScreenModel> {
|
|||
);
|
||||
ref.read(sharedUtilityProvider).addAccount(newUser);
|
||||
ref.read(userProvider.notifier).userState = newUser;
|
||||
state = state.copyWith(loading: false);
|
||||
final currentAccounts = ref.read(authProvider.notifier).getSavedAccounts();
|
||||
|
||||
state = state.copyWith(
|
||||
serverLoginModel: null,
|
||||
accounts: currentAccounts,
|
||||
);
|
||||
|
||||
return Response(response.base, newUser);
|
||||
}
|
||||
state = state.copyWith(loading: false);
|
||||
return Response(response.base, null);
|
||||
}
|
||||
|
||||
Future<Response?> logOutUser() async {
|
||||
final currentUser = ref.read(userProvider);
|
||||
state = state.copyWith(tempCredentials: CredentialsModel.createNewCredentials());
|
||||
state = state.copyWith(serverLoginModel: null);
|
||||
await ref.read(sharedUtilityProvider).removeAccount(currentUser);
|
||||
clearAllProviders();
|
||||
return null;
|
||||
|
|
@ -95,10 +192,17 @@ class AuthNotifier extends StateNotifier<LoginScreenModel> {
|
|||
ref.read(libraryScreenProvider.notifier).clear();
|
||||
}
|
||||
|
||||
void setServer(String server) {
|
||||
Future<void> setServer(String server) async {
|
||||
final url = (state.hasBaseUrl ? FladderConfig.baseUrl : server);
|
||||
if (url == null || server.isEmpty) return;
|
||||
final isUrlValid = _parseUrl(url);
|
||||
state = state.copyWith(
|
||||
tempCredentials: state.tempCredentials.copyWith(server: server),
|
||||
errorMessage: isUrlValid,
|
||||
serverLoginModel: null,
|
||||
);
|
||||
if (isUrlValid == null) {
|
||||
await _fetchServerInfo(url);
|
||||
}
|
||||
}
|
||||
|
||||
List<AccountModel> getSavedAccounts() {
|
||||
|
|
@ -113,4 +217,27 @@ class AuthNotifier extends StateNotifier<LoginScreenModel> {
|
|||
accounts.insert(newIndex, original);
|
||||
ref.read(sharedUtilityProvider).saveAccounts(accounts);
|
||||
}
|
||||
|
||||
void addNewUser() {
|
||||
state = state.copyWith(
|
||||
screen: LoginScreenType.login,
|
||||
);
|
||||
}
|
||||
|
||||
void goUserSelect() {
|
||||
state = state.copyWith(
|
||||
serverLoginModel: state.hasBaseUrl ? state.serverLoginModel : null,
|
||||
screen: LoginScreenType.users,
|
||||
);
|
||||
}
|
||||
|
||||
void tryParseUrl(String server) {
|
||||
if (server.isNotEmpty && state.errorMessage != null) {
|
||||
final url = server;
|
||||
final isUrlValid = _parseUrl(url);
|
||||
state = state.copyWith(
|
||||
errorMessage: isUrlValid,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,16 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
final viewTypes =
|
||||
ref.read(viewsProvider.select((value) => value.dashboardViews)).map((e) => e.collectionType).toSet().toList();
|
||||
|
||||
final imagesToFetch = {
|
||||
ImageType.logo,
|
||||
ImageType.primary,
|
||||
ImageType.backdrop,
|
||||
ImageType.banner,
|
||||
}.toList();
|
||||
|
||||
if (viewTypes.containsAny([CollectionType.movies, CollectionType.tvshows])) {
|
||||
final resumeVideoResponse = await api.usersUserIdItemsResumeGet(
|
||||
limit: 16,
|
||||
enableImageTypes: imagesToFetch,
|
||||
fields: [
|
||||
ItemFields.parentid,
|
||||
ItemFields.mediastreams,
|
||||
|
|
@ -36,6 +43,8 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
ItemFields.overview,
|
||||
ItemFields.genres,
|
||||
],
|
||||
mediaTypes: [MediaType.video],
|
||||
enableTotalRecordCount: false,
|
||||
|
|
@ -48,7 +57,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
|
||||
if (viewTypes.contains(CollectionType.music)) {
|
||||
final resumeAudioResponse = await api.usersUserIdItemsResumeGet(
|
||||
limit: 16,
|
||||
enableImageTypes: imagesToFetch,
|
||||
fields: [
|
||||
ItemFields.parentid,
|
||||
ItemFields.mediastreams,
|
||||
|
|
@ -56,6 +65,8 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
ItemFields.overview,
|
||||
ItemFields.genres,
|
||||
],
|
||||
mediaTypes: [MediaType.audio],
|
||||
enableTotalRecordCount: false,
|
||||
|
|
@ -68,7 +79,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
|
||||
if (viewTypes.contains(CollectionType.books)) {
|
||||
final resumeBookResponse = await api.usersUserIdItemsResumeGet(
|
||||
limit: 16,
|
||||
enableImageTypes: imagesToFetch,
|
||||
fields: [
|
||||
ItemFields.parentid,
|
||||
ItemFields.mediastreams,
|
||||
|
|
@ -76,6 +87,8 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
ItemFields.overview,
|
||||
ItemFields.genres,
|
||||
],
|
||||
mediaTypes: [MediaType.book],
|
||||
enableTotalRecordCount: false,
|
||||
|
|
@ -87,7 +100,6 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
}
|
||||
|
||||
final nextResponse = await api.showsNextUpGet(
|
||||
limit: 16,
|
||||
nextUpDateCutoff: DateTime.now().subtract(
|
||||
ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))),
|
||||
fields: [
|
||||
|
|
@ -97,6 +109,8 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
|
|||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
ItemFields.overview,
|
||||
ItemFields.genres,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:fladder/providers/user_provider.dart';
|
|||
|
||||
const _defaultHeight = 576;
|
||||
const _defaultWidth = 384;
|
||||
const _defaultQuality = 96;
|
||||
const _defaultQuality = 90;
|
||||
|
||||
final imageUtilityProvider = Provider<ImageNotifier>((ref) {
|
||||
return ImageNotifier(ref: ref);
|
||||
|
|
@ -19,7 +19,7 @@ class ImageNotifier {
|
|||
});
|
||||
|
||||
String get currentServerUrl {
|
||||
return ref.read(userProvider)?.server ?? ref.read(authProvider).tempCredentials.server;
|
||||
return ref.read(userProvider)?.server ?? ref.read(authProvider).serverLoginModel?.tempCredentials.server ?? "";
|
||||
}
|
||||
|
||||
String getUserImageUrl(String id) {
|
||||
|
|
|
|||
0
lib/providers/lock_screen_provider.dart
Normal file
0
lib/providers/lock_screen_provider.dart
Normal file
|
|
@ -77,7 +77,7 @@ class JellyService {
|
|||
final JellyfinOpenApi _api;
|
||||
|
||||
JellyfinOpenApi get api {
|
||||
var authServer = ref.read(authProvider).tempCredentials.server;
|
||||
var authServer = ref.read(authProvider).serverLoginModel?.tempCredentials.server ?? "";
|
||||
var currentServer = ref.read(userProvider)?.credentials.server;
|
||||
if ((authServer.isNotEmpty ? authServer : currentServer) == FakeHelper.fakeTestServerUrl) {
|
||||
return FakeJellyfinOpenApi();
|
||||
|
|
@ -1126,6 +1126,8 @@ class JellyService {
|
|||
|
||||
Future<Response<bool>> quickConnectEnabled() async => api.quickConnectEnabledGet();
|
||||
|
||||
Future<Response<BrandingOptions>> getBranding() async => api.brandingConfigurationGet();
|
||||
|
||||
Future<Response<dynamic>> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId);
|
||||
|
||||
Future<UserConfiguration?> _updateUserConfiguration(UserConfiguration newUserConfiguration) async {
|
||||
|
|
@ -1161,6 +1163,22 @@ class JellyService {
|
|||
);
|
||||
return _updateUserConfiguration(updated);
|
||||
}
|
||||
|
||||
Future<Response<QuickConnectResult>> quickConnectInitiate() async {
|
||||
return api.quickConnectInitiatePost();
|
||||
}
|
||||
|
||||
Future<Response<QuickConnectResult>> quickConnectConnectGet({
|
||||
String? secret,
|
||||
}) async {
|
||||
return api.quickConnectConnectGet(secret: secret);
|
||||
}
|
||||
|
||||
Future<Response<AuthenticationResult>> quickConnectAuthenticate(String secret) async {
|
||||
return api.usersAuthenticateWithQuickConnectPost(
|
||||
body: QuickConnectDto(secret: secret),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ParsedMap on Map<String, dynamic> {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@ import 'package:collection/collection.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
|
||||
import 'package:fladder/models/items/media_segments_model.dart';
|
||||
import 'package:fladder/models/settings/key_combinations.dart';
|
||||
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||
import 'package:fladder/providers/shared_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/src/player_settings_helper.g.dart' as pigeon;
|
||||
|
||||
final videoPlayerSettingsProvider =
|
||||
StateNotifierProvider<VideoPlayerSettingsProviderNotifier, VideoPlayerSettingsModel>((ref) {
|
||||
|
|
@ -30,6 +33,30 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
|
|||
if (!oldState.playerSame(value)) {
|
||||
ref.read(videoPlayerProvider.notifier).init();
|
||||
}
|
||||
final userData = ref.read(userProvider);
|
||||
pigeon.PlayerSettingsPigeon().sendPlayerSettings(
|
||||
pigeon.PlayerSettings(
|
||||
skipTypes: value.segmentSkipSettings.map(
|
||||
(key, value) => MapEntry(
|
||||
switch (key) {
|
||||
MediaSegmentType.unknown => pigeon.SegmentType.intro,
|
||||
MediaSegmentType.commercial => pigeon.SegmentType.commercial,
|
||||
MediaSegmentType.preview => pigeon.SegmentType.preview,
|
||||
MediaSegmentType.recap => pigeon.SegmentType.recap,
|
||||
MediaSegmentType.outro => pigeon.SegmentType.outro,
|
||||
MediaSegmentType.intro => pigeon.SegmentType.intro,
|
||||
},
|
||||
switch (value) {
|
||||
SegmentSkip.none => pigeon.SegmentSkip.none,
|
||||
SegmentSkip.askToSkip => pigeon.SegmentSkip.ask,
|
||||
SegmentSkip.skip => pigeon.SegmentSkip.skip,
|
||||
},
|
||||
),
|
||||
),
|
||||
skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds,
|
||||
skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void setScreenBrightness(double? value) async {
|
||||
|
|
|
|||
|
|
@ -55,10 +55,30 @@ class SharedUtility {
|
|||
}
|
||||
|
||||
Future<bool?> addAccount(AccountModel account) async {
|
||||
return await saveAccounts(getAccounts()
|
||||
..add(account.copyWith(
|
||||
lastUsed: DateTime.now(),
|
||||
)));
|
||||
final newAccount = account.copyWith(
|
||||
lastUsed: DateTime.now(),
|
||||
);
|
||||
|
||||
List<AccountModel> accounts = getAccounts().toList();
|
||||
if (accounts.any((element) => element.sameIdentity(newAccount))) {
|
||||
accounts = accounts
|
||||
.map(
|
||||
(e) => e.sameIdentity(newAccount)
|
||||
? e.copyWith(
|
||||
credentials: newAccount.credentials,
|
||||
lastUsed: newAccount.lastUsed,
|
||||
)
|
||||
: e,
|
||||
)
|
||||
.toList();
|
||||
} else {
|
||||
accounts = [
|
||||
...accounts,
|
||||
newAccount,
|
||||
];
|
||||
}
|
||||
|
||||
return await saveAccounts(accounts);
|
||||
}
|
||||
|
||||
Future<bool?> removeAccount(AccountModel? account) async {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/media_playback_model.dart';
|
||||
|
|
@ -77,6 +79,8 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
|
|||
mediaState.update(
|
||||
(state) => state.playing == event ? state : state.copyWith(playing: event),
|
||||
);
|
||||
final currentState = playbackState;
|
||||
ref.read(playBackModel)?.updatePlaybackPosition(currentState.position, playbackState.playing, ref);
|
||||
}
|
||||
|
||||
Future<void> updatePosition(Duration event) async {
|
||||
|
|
@ -105,7 +109,7 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<bool> loadPlaybackItem(PlaybackModel model, {Duration? startPosition}) async {
|
||||
Future<bool> loadPlaybackItem(PlaybackModel model, Duration startPosition) async {
|
||||
await state.stop();
|
||||
mediaState
|
||||
.update((state) => state.copyWith(state: VideoPlayerState.fullScreen, buffering: true, errorPlaying: false));
|
||||
|
|
@ -114,13 +118,13 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
|
|||
PlaybackModel? newPlaybackModel = model;
|
||||
|
||||
if (media != null) {
|
||||
await state.open(media.url, false);
|
||||
await state.loadVideo(model, startPosition, false);
|
||||
await state.setVolume(ref.read(videoPlayerSettingsProvider).volume);
|
||||
state.stateStream?.takeWhile((event) => event.buffering == true).listen(
|
||||
null,
|
||||
onDone: () async {
|
||||
final start = startPosition ?? await model.startDuration();
|
||||
if (start != null) {
|
||||
final start = startPosition;
|
||||
if (start != Duration.zero) {
|
||||
await state.seek(start);
|
||||
}
|
||||
await state.setAudioTrack(null, model);
|
||||
|
|
@ -138,4 +142,6 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
|
|||
mediaState.update((state) => state.copyWith(errorPlaying: true));
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> openPlayer(BuildContext context) async => state.openPlayer(context);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ class ViewsNotifier extends StateNotifier<ViewsModel> {
|
|||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
ItemFields.overview,
|
||||
],
|
||||
);
|
||||
return e.copyWith(recentlyAdded: recents.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList());
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/login/lock_screen.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/navigation_body.dart';
|
||||
|
||||
const settingsPageRoute = "settings";
|
||||
|
||||
|
|
@ -53,18 +54,22 @@ final List<AutoRoute> homeRoutes = [
|
|||
page: DashboardRoute.page,
|
||||
initial: true,
|
||||
path: 'dashboard',
|
||||
maintainState: false,
|
||||
),
|
||||
AutoRoute(
|
||||
page: FavouritesRoute.page,
|
||||
path: 'favourites',
|
||||
maintainState: false,
|
||||
),
|
||||
AutoRoute(
|
||||
page: SyncedRoute.page,
|
||||
path: 'synced',
|
||||
maintainState: false,
|
||||
),
|
||||
AutoRoute(
|
||||
page: LibraryRoute.page,
|
||||
path: 'libraries',
|
||||
maintainState: false,
|
||||
),
|
||||
];
|
||||
|
||||
|
|
@ -76,7 +81,7 @@ final List<AutoRoute> detailsRoutes = [
|
|||
|
||||
final List<AutoRoute> _defaultRoutes = [
|
||||
AutoRoute(page: SplashRoute.page, path: '/splash'),
|
||||
AutoRoute(page: LoginRoute.page, path: '/login'),
|
||||
AutoRoute(page: LoginRoute.page, path: '/login', maintainState: false),
|
||||
];
|
||||
|
||||
final List<AutoRoute> _settingsChildren = [
|
||||
|
|
@ -117,6 +122,8 @@ class AuthGuard extends AutoRouteGuard {
|
|||
if (ref.read(userProvider) != null ||
|
||||
resolver.routeName == const LoginRoute().routeName ||
|
||||
resolver.routeName == SplashRoute().routeName) {
|
||||
// We assume the last main focus is no longer active after navigating
|
||||
lastMainFocus = null;
|
||||
return resolver.next(true);
|
||||
}
|
||||
|
||||
|
|
@ -127,5 +134,9 @@ class AuthGuard extends AutoRouteGuard {
|
|||
router.replace(const LoginRoute());
|
||||
}
|
||||
}));
|
||||
|
||||
// We assume the last main focus is no longer active after navigating
|
||||
lastMainFocus = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,11 +93,12 @@ class DetailsRoute extends _i18.PageRouteInfo<DetailsRouteArgs> {
|
|||
DetailsRoute({
|
||||
String id = '',
|
||||
_i19.ItemBaseModel? item,
|
||||
Object? tag,
|
||||
_i20.Key? key,
|
||||
List<_i18.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
DetailsRoute.name,
|
||||
args: DetailsRouteArgs(id: id, item: item, key: key),
|
||||
args: DetailsRouteArgs(id: id, item: item, tag: tag, key: key),
|
||||
rawQueryParams: {'id': id},
|
||||
initialChildren: children,
|
||||
);
|
||||
|
|
@ -111,34 +112,44 @@ class DetailsRoute extends _i18.PageRouteInfo<DetailsRouteArgs> {
|
|||
final args = data.argsAs<DetailsRouteArgs>(
|
||||
orElse: () => DetailsRouteArgs(id: queryParams.getString('id', '')),
|
||||
);
|
||||
return _i4.DetailsScreen(id: args.id, item: args.item, key: args.key);
|
||||
return _i4.DetailsScreen(
|
||||
id: args.id,
|
||||
item: args.item,
|
||||
tag: args.tag,
|
||||
key: args.key,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class DetailsRouteArgs {
|
||||
const DetailsRouteArgs({this.id = '', this.item, this.key});
|
||||
const DetailsRouteArgs({this.id = '', this.item, this.tag, this.key});
|
||||
|
||||
final String id;
|
||||
|
||||
final _i19.ItemBaseModel? item;
|
||||
|
||||
final Object? tag;
|
||||
|
||||
final _i20.Key? key;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DetailsRouteArgs{id: $id, item: $item, key: $key}';
|
||||
return 'DetailsRouteArgs{id: $id, item: $item, tag: $tag, key: $key}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DetailsRouteArgs) return false;
|
||||
return id == other.id && item == other.item && key == other.key;
|
||||
return id == other.id &&
|
||||
item == other.item &&
|
||||
tag == other.tag &&
|
||||
key == other.key;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ item.hashCode ^ key.hashCode;
|
||||
int get hashCode => id.hashCode ^ item.hashCode ^ tag.hashCode ^ key.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
|
|
|
|||
|
|
@ -13,7 +13,13 @@ import 'package:fladder/util/fladder_image.dart';
|
|||
class DetailsScreen extends ConsumerStatefulWidget {
|
||||
final String id;
|
||||
final ItemBaseModel? item;
|
||||
const DetailsScreen({@QueryParam() this.id = '', this.item, super.key});
|
||||
final Object? tag;
|
||||
const DetailsScreen({
|
||||
@QueryParam() this.id = '',
|
||||
this.item,
|
||||
this.tag,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _DetailsScreenState();
|
||||
|
|
@ -66,7 +72,7 @@ class _DetailsScreenState extends ConsumerState<DetailsScreen> {
|
|||
key: Key(widget.id),
|
||||
children: [
|
||||
Hero(
|
||||
tag: widget.id,
|
||||
tag: widget.tag ?? UniqueKey(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface.withValues(alpha: 1.0),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/providers/settings/book_viewer_settings_provider.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/fladder_slider.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/modal_side_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
Future<void> showBookViewerSettings(
|
||||
BuildContext context,
|
||||
|
|
@ -80,10 +83,9 @@ class BookViewerSettingsScreen extends ConsumerWidget {
|
|||
label: const Text("Read direction"),
|
||||
current: settings.readDirection.name.toUpperCaseSplit(),
|
||||
itemBuilder: (context) => ReadDirection.values
|
||||
.map((value) => PopupMenuItem(
|
||||
value: value,
|
||||
child: Text(value.name.toUpperCaseSplit()),
|
||||
onTap: () => ref
|
||||
.map((value) => ItemActionButton(
|
||||
label: Text(value.name.toUpperCaseSplit()),
|
||||
action: () => ref
|
||||
.read(bookViewerSettingsProvider.notifier)
|
||||
.update((state) => state.copyWith(readDirection: value)),
|
||||
))
|
||||
|
|
@ -102,10 +104,9 @@ class BookViewerSettingsScreen extends ConsumerWidget {
|
|||
label: const Text("Init zoom"),
|
||||
current: settings.initZoomState.name.toUpperCaseSplit(),
|
||||
itemBuilder: (context) => InitZoomState.values
|
||||
.map((value) => PopupMenuItem(
|
||||
value: value,
|
||||
child: Text(value.name.toUpperCaseSplit()),
|
||||
onTap: () => ref
|
||||
.map((value) => ItemActionButton(
|
||||
label: Text(value.name.toUpperCaseSplit()),
|
||||
action: () => ref
|
||||
.read(bookViewerSettingsProvider.notifier)
|
||||
.update((state) => state.copyWith(initZoomState: value)),
|
||||
))
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:fladder/util/list_padding.dart';
|
|||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
|
||||
final _selectedWarningProvider = StateProvider<ErrorType?>((ref) => null);
|
||||
|
||||
|
|
@ -41,16 +42,14 @@ class CrashScreen extends ConsumerWidget {
|
|||
EnumBox(
|
||||
current: selectedType == null ? context.localized.all : selectedType.name.capitalize(),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: null,
|
||||
child: Text(context.localized.all),
|
||||
onTap: () => ref.read(_selectedWarningProvider.notifier).update((state) => null),
|
||||
ItemActionButton(
|
||||
label: Text(context.localized.all),
|
||||
action: () => ref.read(_selectedWarningProvider.notifier).update((state) => null),
|
||||
),
|
||||
...ErrorType.values.map(
|
||||
(entry) => PopupMenuItem(
|
||||
value: entry,
|
||||
child: Text(entry.name.capitalize()),
|
||||
onTap: () => ref.read(_selectedWarningProvider.notifier).update((state) => entry),
|
||||
(entry) => ItemActionButton(
|
||||
label: Text(entry.name.capitalize()),
|
||||
action: () => ref.read(_selectedWarningProvider.notifier).update((state) => entry),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
||||
|
|
@ -23,6 +24,7 @@ import 'package:fladder/screens/shared/media/poster_row.dart';
|
|||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
|
|
@ -45,6 +47,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
late final Timer _timer;
|
||||
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
|
||||
|
||||
final textController = TextEditingController();
|
||||
|
||||
ItemBaseModel? selectedPoster;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -70,6 +76,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final padding = AdaptiveLayout.adaptivePadding(context);
|
||||
final bannerType = ref.watch(homeSettingsProvider.select((value) => value.homeBanner));
|
||||
|
||||
final dashboardData = ref.watch(dashboardProvider);
|
||||
final views = ref.watch(viewsProvider);
|
||||
|
|
@ -87,10 +94,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
HomeCarouselSettings.cont => allResume,
|
||||
};
|
||||
|
||||
final viewSize = AdaptiveLayout.viewSizeOf(context);
|
||||
|
||||
return MediaQuery.removeViewInsets(
|
||||
context: context,
|
||||
child: NestedScaffold(
|
||||
background: BackgroundImage(items: [...homeCarouselItems, ...dashboardData.nextUp, ...allResume]),
|
||||
background: BackgroundImage(
|
||||
items: selectedPoster != null
|
||||
? [selectedPoster!]
|
||||
: [...homeCarouselItems, ...dashboardData.nextUp, ...allResume]),
|
||||
body: PullToRefresh(
|
||||
refreshKey: _refreshIndicatorKey,
|
||||
displacement: 80 + MediaQuery.of(context).viewPadding.top,
|
||||
|
|
@ -101,8 +113,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
controller: AdaptiveLayout.scrollOf(context, HomeTabs.dashboard),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
const DefaultSliverTopBadding(),
|
||||
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
||||
if (bannerType != HomeBanner.detailedBanner) const DefaultSliverTopBadding(),
|
||||
if (viewSize == ViewSize.phone)
|
||||
NestedSliverAppBar(
|
||||
route: LibrarySearchRoute(),
|
||||
parent: context,
|
||||
|
|
@ -114,7 +126,16 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
context,
|
||||
horizontalPadding: 0,
|
||||
),
|
||||
child: HomeBannerWidget(posters: homeCarouselItems),
|
||||
child: HomeBannerWidget(
|
||||
posters: homeCarouselItems,
|
||||
onSelect: (selected) {
|
||||
// if (selectedPoster != selected) {
|
||||
// setState(() {
|
||||
// selectedPoster = selected;
|
||||
// });
|
||||
// }
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
|
|
@ -130,80 +151,84 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
...[
|
||||
if (resumeVideo.isNotEmpty &&
|
||||
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
|
||||
SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinueWatching,
|
||||
posters: resumeVideo,
|
||||
),
|
||||
PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinueWatching,
|
||||
posters: resumeVideo,
|
||||
),
|
||||
if (resumeAudio.isNotEmpty &&
|
||||
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
|
||||
SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinueListening,
|
||||
posters: resumeAudio,
|
||||
),
|
||||
PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinueListening,
|
||||
posters: resumeAudio,
|
||||
),
|
||||
if (resumeBooks.isNotEmpty &&
|
||||
(homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate))
|
||||
SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinueReading,
|
||||
posters: resumeBooks,
|
||||
),
|
||||
PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinueReading,
|
||||
posters: resumeBooks,
|
||||
),
|
||||
if (dashboardData.nextUp.isNotEmpty &&
|
||||
(homeSettings.nextUp == HomeNextUp.nextUp || homeSettings.nextUp == HomeNextUp.separate))
|
||||
SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.nextUp,
|
||||
posters: dashboardData.nextUp,
|
||||
),
|
||||
PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.nextUp,
|
||||
posters: dashboardData.nextUp,
|
||||
),
|
||||
if ([...allResume, ...dashboardData.nextUp].isNotEmpty && homeSettings.nextUp == HomeNextUp.combined)
|
||||
SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinue,
|
||||
posters: [...allResume, ...dashboardData.nextUp],
|
||||
PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardContinue,
|
||||
posters: [...allResume, ...dashboardData.nextUp],
|
||||
),
|
||||
...views.dashboardViews.where((element) => element.recentlyAdded.isNotEmpty).map(
|
||||
(view) => PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardRecentlyAdded(view.name),
|
||||
collectionAspectRatio: view.collectionType.aspectRatio,
|
||||
onLabelClick: () => context.router.push(
|
||||
LibrarySearchRoute(
|
||||
viewModelId: view.id,
|
||||
types: switch (view.collectionType) {
|
||||
CollectionType.tvshows => {
|
||||
FladderItemType.episode: true,
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
sortingOptions: switch (view.collectionType) {
|
||||
CollectionType.books ||
|
||||
CollectionType.boxsets ||
|
||||
CollectionType.folders ||
|
||||
CollectionType.music =>
|
||||
SortingOptions.dateLastContentAdded,
|
||||
_ => SortingOptions.dateAdded,
|
||||
},
|
||||
sortOrder: SortingOrder.descending,
|
||||
recursive: true,
|
||||
),
|
||||
),
|
||||
posters: view.recentlyAdded,
|
||||
),
|
||||
),
|
||||
]
|
||||
.nonNulls
|
||||
.toList()
|
||||
.mapIndexed(
|
||||
(index, child) => SliverToBoxAdapter(
|
||||
child: FocusProvider(
|
||||
autoFocus: bannerType != HomeBanner.detailedBanner ? index == 0 : false,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
.addInBetween(
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(height: 16),
|
||||
),
|
||||
),
|
||||
...views.dashboardViews
|
||||
.where((element) => element.recentlyAdded.isNotEmpty)
|
||||
.map((view) => SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.dashboardRecentlyAdded(view.name),
|
||||
collectionAspectRatio: view.collectionType.aspectRatio,
|
||||
onLabelClick: () => context.router.push(
|
||||
LibrarySearchRoute(
|
||||
viewModelId: view.id,
|
||||
types: switch (view.collectionType) {
|
||||
CollectionType.tvshows => {
|
||||
FladderItemType.episode: true,
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
sortingOptions: switch (view.collectionType) {
|
||||
CollectionType.books ||
|
||||
CollectionType.boxsets ||
|
||||
CollectionType.folders ||
|
||||
CollectionType.music =>
|
||||
SortingOptions.dateLastContentAdded,
|
||||
_ => SortingOptions.dateAdded,
|
||||
},
|
||||
sortOrder: SortingOrder.descending,
|
||||
recursive: true,
|
||||
),
|
||||
),
|
||||
posters: view.recentlyAdded,
|
||||
),
|
||||
)),
|
||||
].nonNulls.toList().addInBetween(const SliverToBoxAdapter(child: SizedBox(height: 16))),
|
||||
const DefautlSliverBottomPadding(),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,17 +5,26 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/screens/shared/media/carousel_banner.dart';
|
||||
import 'package:fladder/screens/shared/media/detailed_banner.dart';
|
||||
import 'package:fladder/screens/shared/media/media_banner.dart';
|
||||
|
||||
class HomeBannerWidget extends ConsumerWidget {
|
||||
final List<ItemBaseModel> posters;
|
||||
const HomeBannerWidget({required this.posters, super.key});
|
||||
final Function(ItemBaseModel selected) onSelect;
|
||||
|
||||
const HomeBannerWidget({
|
||||
required this.posters,
|
||||
required this.onSelect,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bannerType = ref.watch(homeSettingsProvider.select((value) => value.homeBanner));
|
||||
final maxHeight = (MediaQuery.sizeOf(context).shortestSide * 0.6).clamp(125.0, 375.0);
|
||||
|
||||
return switch (bannerType) {
|
||||
HomeBanner.carousel => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -34,6 +43,12 @@ class HomeBannerWidget extends ConsumerWidget {
|
|||
maxHeight: maxHeight,
|
||||
),
|
||||
),
|
||||
HomeBanner.detailedBanner => AnimatedFadeSize(
|
||||
child: DetailedBanner(
|
||||
posters: posters,
|
||||
onSelect: onSelect,
|
||||
),
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/models/items/media_streams_model.dart';
|
||||
import 'package:fladder/screens/details_screens/components/label_title_item.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
|
||||
class MediaStreamInformation extends ConsumerWidget {
|
||||
final MediaStreamsModel mediaStream;
|
||||
|
|
@ -30,15 +32,13 @@ class MediaStreamInformation extends ConsumerWidget {
|
|||
label: Text(context.localized.version),
|
||||
current: mediaStream.currentVersionStream?.name ?? "",
|
||||
itemBuilder: (context) => mediaStream.versionStreams
|
||||
.map((e) => PopupMenuItem(
|
||||
value: e,
|
||||
padding: EdgeInsets.zero,
|
||||
child: textWidget(
|
||||
.map((e) => ItemActionButton(
|
||||
selected: mediaStream.currentVersionStream == e,
|
||||
label: textWidget(
|
||||
context,
|
||||
selected: mediaStream.currentVersionStream == e,
|
||||
label: e.name,
|
||||
),
|
||||
onTap: () => onVersionIndexChanged?.call(e.index),
|
||||
action: () => onVersionIndexChanged?.call(e.index),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
|
|
@ -48,10 +48,8 @@ class MediaStreamInformation extends ConsumerWidget {
|
|||
current: (mediaStream.videoStreams.first).prettyName,
|
||||
itemBuilder: (context) => mediaStream.videoStreams
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Text(e.prettyName),
|
||||
(e) => ItemActionButton(
|
||||
label: Text(e.prettyName),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
@ -62,12 +60,13 @@ class MediaStreamInformation extends ConsumerWidget {
|
|||
current: mediaStream.currentAudioStream?.displayTitle ?? "",
|
||||
itemBuilder: (context) => [AudioStreamModel.no(), ...mediaStream.audioStreams]
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e,
|
||||
padding: EdgeInsets.zero,
|
||||
child: textWidget(context,
|
||||
selected: mediaStream.currentAudioStream?.index == e.index, label: e.displayTitle),
|
||||
onTap: () => onAudioIndexChanged?.call(e.index),
|
||||
(e) => ItemActionButton(
|
||||
selected: mediaStream.currentAudioStream?.index == e.index,
|
||||
label: textWidget(
|
||||
context,
|
||||
label: e.displayTitle,
|
||||
),
|
||||
action: () => onAudioIndexChanged?.call(e.index),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
@ -78,12 +77,13 @@ class MediaStreamInformation extends ConsumerWidget {
|
|||
current: mediaStream.currentSubStream?.displayTitle ?? "",
|
||||
itemBuilder: (context) => [SubStreamModel.no(), ...mediaStream.subStreams]
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e,
|
||||
padding: EdgeInsets.zero,
|
||||
child: textWidget(context,
|
||||
selected: mediaStream.currentSubStream?.index == e.index, label: e.displayTitle),
|
||||
onTap: () => onSubIndexChanged?.call(e.index),
|
||||
(e) => ItemActionButton(
|
||||
selected: mediaStream.currentSubStream?.index == e.index,
|
||||
label: textWidget(
|
||||
context,
|
||||
label: e.displayTitle,
|
||||
),
|
||||
action: () => onSubIndexChanged?.call(e.index),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
@ -92,22 +92,12 @@ class MediaStreamInformation extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget textWidget(BuildContext context, {required bool selected, required String label}) {
|
||||
return Container(
|
||||
height: kMinInteractiveDimension,
|
||||
width: double.maxFinite,
|
||||
color: selected ? Theme.of(context).colorScheme.primary : null,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: selected ? Theme.of(context).colorScheme.onPrimary : null,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Widget textWidget(BuildContext context, {required String label}) {
|
||||
return Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -115,7 +105,7 @@ class MediaStreamInformation extends ConsumerWidget {
|
|||
class _StreamOptionSelect<T> extends StatelessWidget {
|
||||
final Text label;
|
||||
final String current;
|
||||
final List<PopupMenuEntry<T>> Function(BuildContext context) itemBuilder;
|
||||
final List<ItemAction> Function(BuildContext context) itemBuilder;
|
||||
const _StreamOptionSelect({
|
||||
required this.label,
|
||||
required this.current,
|
||||
|
|
@ -124,47 +114,14 @@ class _StreamOptionSelect<T> extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textStyle = Theme.of(context).textTheme.titleMedium;
|
||||
const padding = EdgeInsets.all(6);
|
||||
final itemList = itemBuilder(context);
|
||||
return LabelTitleItem(
|
||||
title: label,
|
||||
content: Flexible(
|
||||
child: PopupMenuButton(
|
||||
tooltip: '',
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
enabled: itemList.length > 1,
|
||||
itemBuilder: itemBuilder,
|
||||
enableFeedback: false,
|
||||
menuPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: padding,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Material(
|
||||
textStyle: textStyle?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: itemList.length > 1 ? Theme.of(context).colorScheme.primary : null),
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
current,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
if (itemList.length > 1)
|
||||
Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: LabelTitleItem(
|
||||
title: label,
|
||||
content: Flexible(
|
||||
child: EnumBox(
|
||||
current: current,
|
||||
itemBuilder: itemBuilder,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ class OverviewHeader extends ConsumerWidget {
|
|||
final EdgeInsets? padding;
|
||||
final String? subTitle;
|
||||
final String? originalTitle;
|
||||
final Alignment logoAlignment;
|
||||
final Function()? onTitleClicked;
|
||||
final int? productionYear;
|
||||
final String? summary;
|
||||
final Duration? runTime;
|
||||
final String? officialRating;
|
||||
final double? communityRating;
|
||||
|
|
@ -32,8 +34,10 @@ class OverviewHeader extends ConsumerWidget {
|
|||
this.padding,
|
||||
this.subTitle,
|
||||
this.originalTitle,
|
||||
this.logoAlignment = Alignment.bottomCenter,
|
||||
this.onTitleClicked,
|
||||
this.productionYear,
|
||||
this.summary,
|
||||
this.runTime,
|
||||
this.officialRating,
|
||||
this.communityRating,
|
||||
|
|
@ -68,83 +72,101 @@ class OverviewHeader extends ConsumerWidget {
|
|||
crossAxisAlignment: crossAlignment,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MediaHeader(
|
||||
name: name,
|
||||
logo: image?.logo,
|
||||
onTap: onTitleClicked,
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: crossAlignment,
|
||||
children: [
|
||||
if (subTitle != null)
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
subTitle ?? "",
|
||||
textAlign: TextAlign.center,
|
||||
style: mainStyle,
|
||||
),
|
||||
),
|
||||
if (name.toLowerCase() != originalTitle?.toLowerCase() && originalTitle != null)
|
||||
SelectableText(
|
||||
originalTitle.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: subStyle,
|
||||
),
|
||||
].addInBetween(const SizedBox(height: 4)),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: crossAlignment,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
if (officialRating != null)
|
||||
ChipButton(
|
||||
label: officialRating.toString(),
|
||||
),
|
||||
if (productionYear != null)
|
||||
SelectableText(
|
||||
productionYear.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: subStyle,
|
||||
),
|
||||
if (runTime != null && (runTime?.inSeconds ?? 0) > 1)
|
||||
SelectableText(
|
||||
runTime.humanize.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: subStyle,
|
||||
),
|
||||
if (communityRating != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.star_rate_rounded,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Text(
|
||||
communityRating?.toStringAsFixed(2) ?? "",
|
||||
style: subStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
].addInBetween(CircleAvatar(
|
||||
radius: 3,
|
||||
backgroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
)),
|
||||
Flexible(
|
||||
child: ExcludeFocus(
|
||||
child: MediaHeader(
|
||||
name: name,
|
||||
logo: image?.logo,
|
||||
onTap: onTitleClicked,
|
||||
alignment: logoAlignment,
|
||||
),
|
||||
if (genres.isNotEmpty)
|
||||
Genres(
|
||||
genres: genres.take(6).toList(),
|
||||
),
|
||||
),
|
||||
ExcludeFocus(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: crossAlignment,
|
||||
children: [
|
||||
if (subTitle != null)
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
subTitle ?? "",
|
||||
textAlign: TextAlign.center,
|
||||
style: mainStyle,
|
||||
),
|
||||
),
|
||||
if (name.toLowerCase() != originalTitle?.toLowerCase() && originalTitle != null)
|
||||
SelectableText(
|
||||
originalTitle.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: subStyle,
|
||||
),
|
||||
].addInBetween(const SizedBox(height: 4)),
|
||||
),
|
||||
),
|
||||
ExcludeFocus(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: crossAlignment,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
if (officialRating != null)
|
||||
ChipButton(
|
||||
label: officialRating.toString(),
|
||||
),
|
||||
if (productionYear != null)
|
||||
SelectableText(
|
||||
productionYear.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: subStyle,
|
||||
),
|
||||
if (runTime != null && (runTime?.inSeconds ?? 0) > 1)
|
||||
SelectableText(
|
||||
runTime.humanize.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: subStyle,
|
||||
),
|
||||
if (communityRating != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.star_rate_rounded,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Text(
|
||||
communityRating?.toStringAsFixed(2) ?? "",
|
||||
style: subStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
].addInBetween(CircleAvatar(
|
||||
radius: 3,
|
||||
backgroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
)),
|
||||
),
|
||||
].addInBetween(const SizedBox(height: 10)),
|
||||
if (summary?.isNotEmpty == true)
|
||||
Flexible(
|
||||
child: Text(
|
||||
summary ?? "",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
if (genres.isNotEmpty)
|
||||
Genres(
|
||||
genres: genres.take(6).toList(),
|
||||
),
|
||||
].addInBetween(const SizedBox(height: 10)),
|
||||
),
|
||||
),
|
||||
if (centerButtons != null) centerButtons!,
|
||||
].addInBetween(const SizedBox(height: 21)),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/providers/items/episode_details_provider.dart';
|
||||
|
|
@ -25,6 +25,8 @@ import 'package:fladder/util/localization_helper.dart';
|
|||
import 'package:fladder/util/people_extension.dart';
|
||||
import 'package:fladder/util/router_extension.dart';
|
||||
import 'package:fladder/util/widget_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||
import 'package:fladder/widgets/shared/selectable_icon_button.dart';
|
||||
|
||||
class EpisodeDetailScreen extends ConsumerStatefulWidget {
|
||||
|
|
@ -77,19 +79,62 @@ class _ItemDetailScreenState extends ConsumerState<EpisodeDetailScreen> {
|
|||
OverviewHeader(
|
||||
name: details.series?.name ?? "",
|
||||
image: seasonDetails.images,
|
||||
centerButtons: episodeDetails.playAble
|
||||
? MediaPlayButton(
|
||||
item: episodeDetails,
|
||||
onPressed: () async {
|
||||
await details.episode.play(context, ref);
|
||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||
},
|
||||
onLongPressed: () async {
|
||||
await details.episode.play(context, ref, showPlaybackOption: true);
|
||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
centerButtons: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: wrapAlignment,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
episodeDetails.playAble
|
||||
? MediaPlayButton(
|
||||
item: episodeDetails,
|
||||
onPressed: () async {
|
||||
await details.episode.play(context, ref);
|
||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||
},
|
||||
onLongPressed: () async {
|
||||
await details.episode.play(context, ref, showPlaybackOption: true);
|
||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(userProvider.notifier)
|
||||
.setAsFavorite(!(episodeDetails.userData.isFavourite), episodeDetails.id);
|
||||
},
|
||||
selected: episodeDetails.userData.isFavourite,
|
||||
selectedIcon: IconsaxPlusBold.heart,
|
||||
icon: IconsaxPlusLinear.heart,
|
||||
),
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(userProvider.notifier)
|
||||
.markAsPlayed(!(episodeDetails.userData.played), episodeDetails.id);
|
||||
},
|
||||
selected: episodeDetails.userData.played,
|
||||
selectedIcon: IconsaxPlusBold.tick_circle,
|
||||
icon: IconsaxPlusLinear.tick_circle,
|
||||
),
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
controller: scrollController,
|
||||
shrinkWrap: true,
|
||||
children:
|
||||
episodeDetails.generateActions(context, ref).listTileItems(context, useIcons: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
selected: false,
|
||||
icon: IconsaxPlusLinear.more,
|
||||
),
|
||||
].nonNulls.toList().addPadding(const EdgeInsets.symmetric(horizontal: 6)),
|
||||
),
|
||||
padding: padding,
|
||||
subTitle: details.episode?.detailedName(context),
|
||||
originalTitle: details.series?.originalTitle,
|
||||
|
|
@ -101,34 +146,6 @@ class _ItemDetailScreenState extends ConsumerState<EpisodeDetailScreen> {
|
|||
officialRating: details.series?.overview.parentalRating,
|
||||
communityRating: details.series?.overview.communityRating,
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: wrapAlignment,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(userProvider.notifier)
|
||||
.setAsFavorite(!(episodeDetails.userData.isFavourite), episodeDetails.id);
|
||||
},
|
||||
selected: episodeDetails.userData.isFavourite,
|
||||
selectedIcon: IconsaxPlusBold.heart,
|
||||
icon: IconsaxPlusLinear.heart,
|
||||
),
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(userProvider.notifier)
|
||||
.markAsPlayed(!(episodeDetails.userData.played), episodeDetails.id);
|
||||
},
|
||||
selected: episodeDetails.userData.played,
|
||||
selectedIcon: IconsaxPlusBold.tick_circle,
|
||||
icon: IconsaxPlusLinear.tick_circle,
|
||||
),
|
||||
].addPadding(const EdgeInsets.symmetric(horizontal: 6)),
|
||||
).padding(padding),
|
||||
if (details.episode?.mediaStreams != null)
|
||||
Padding(
|
||||
padding: padding,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import 'package:fladder/util/item_base_model/play_item_helpers.dart';
|
|||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/router_extension.dart';
|
||||
import 'package:fladder/util/widget_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||
import 'package:fladder/widgets/shared/selectable_icon_button.dart';
|
||||
|
||||
class MovieDetailScreen extends ConsumerStatefulWidget {
|
||||
|
|
@ -71,23 +73,63 @@ class _ItemDetailScreenState extends ConsumerState<MovieDetailScreen> {
|
|||
name: details.name,
|
||||
image: details.images,
|
||||
padding: padding,
|
||||
centerButtons: MediaPlayButton(
|
||||
item: details,
|
||||
onLongPressed: () async {
|
||||
await details.play(
|
||||
context,
|
||||
ref,
|
||||
showPlaybackOption: true,
|
||||
);
|
||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||
},
|
||||
onPressed: () async {
|
||||
await details.play(
|
||||
context,
|
||||
ref,
|
||||
);
|
||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||
},
|
||||
centerButtons: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: wrapAlignment,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
MediaPlayButton(
|
||||
item: details,
|
||||
onLongPressed: () async {
|
||||
await details.play(
|
||||
context,
|
||||
ref,
|
||||
showPlaybackOption: true,
|
||||
);
|
||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||
},
|
||||
onPressed: () async {
|
||||
await details.play(
|
||||
context,
|
||||
ref,
|
||||
);
|
||||
ref.read(providerInstance.notifier).fetchDetails(widget.item);
|
||||
},
|
||||
),
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(userProvider.notifier)
|
||||
.setAsFavorite(!details.userData.isFavourite, details.id);
|
||||
},
|
||||
selected: details.userData.isFavourite,
|
||||
selectedIcon: IconsaxPlusBold.heart,
|
||||
icon: IconsaxPlusLinear.heart,
|
||||
),
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref.read(userProvider.notifier).markAsPlayed(!details.userData.played, details.id);
|
||||
},
|
||||
selected: details.userData.played,
|
||||
selectedIcon: IconsaxPlusBold.tick_circle,
|
||||
icon: IconsaxPlusLinear.tick_circle,
|
||||
),
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
controller: scrollController,
|
||||
shrinkWrap: true,
|
||||
children: details.generateActions(context, ref).listTileItems(context, useIcons: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
selected: false,
|
||||
icon: IconsaxPlusLinear.more,
|
||||
),
|
||||
],
|
||||
),
|
||||
originalTitle: details.originalTitle,
|
||||
productionYear: details.overview.productionYear,
|
||||
|
|
@ -97,32 +139,6 @@ class _ItemDetailScreenState extends ConsumerState<MovieDetailScreen> {
|
|||
officialRating: details.overview.parentalRating,
|
||||
communityRating: details.overview.communityRating,
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: wrapAlignment,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(userProvider.notifier)
|
||||
.setAsFavorite(!details.userData.isFavourite, details.id);
|
||||
},
|
||||
selected: details.userData.isFavourite,
|
||||
selectedIcon: IconsaxPlusBold.heart,
|
||||
icon: IconsaxPlusLinear.heart,
|
||||
),
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref.read(userProvider.notifier).markAsPlayed(!details.userData.played, details.id);
|
||||
},
|
||||
selected: details.userData.played,
|
||||
selectedIcon: IconsaxPlusBold.tick_circle,
|
||||
icon: IconsaxPlusLinear.tick_circle,
|
||||
),
|
||||
],
|
||||
).padding(padding),
|
||||
if (details.mediaStreams.isNotEmpty)
|
||||
MediaStreamInformation(
|
||||
onVersionIndexChanged: (index) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/series_model.dart';
|
||||
|
|
@ -25,6 +25,8 @@ import 'package:fladder/util/list_padding.dart';
|
|||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/router_extension.dart';
|
||||
import 'package:fladder/util/widget_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||
import 'package:fladder/widgets/shared/selectable_icon_button.dart';
|
||||
|
||||
class SeriesDetailScreen extends ConsumerStatefulWidget {
|
||||
|
|
@ -74,20 +76,60 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
|||
OverviewHeader(
|
||||
name: details.name,
|
||||
image: details.images,
|
||||
centerButtons: MediaPlayButton(
|
||||
item: details.nextUp,
|
||||
onPressed: details.nextUp != null
|
||||
? () async {
|
||||
await details.nextUp.play(context, ref);
|
||||
ref.read(providerId.notifier).fetchDetails(widget.item);
|
||||
}
|
||||
: null,
|
||||
onLongPressed: details.nextUp != null
|
||||
? () async {
|
||||
await details.nextUp.play(context, ref, showPlaybackOption: true);
|
||||
ref.read(providerId.notifier).fetchDetails(widget.item);
|
||||
}
|
||||
: null,
|
||||
centerButtons: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: wrapAlignment,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
MediaPlayButton(
|
||||
item: details.nextUp,
|
||||
onPressed: details.nextUp != null
|
||||
? () async {
|
||||
await details.nextUp.play(context, ref);
|
||||
ref.read(providerId.notifier).fetchDetails(widget.item);
|
||||
}
|
||||
: null,
|
||||
onLongPressed: details.nextUp != null
|
||||
? () async {
|
||||
await details.nextUp.play(context, ref, showPlaybackOption: true);
|
||||
ref.read(providerId.notifier).fetchDetails(widget.item);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(userProvider.notifier)
|
||||
.setAsFavorite(!details.userData.isFavourite, details.id);
|
||||
},
|
||||
selected: details.userData.isFavourite,
|
||||
selectedIcon: IconsaxPlusBold.heart,
|
||||
icon: IconsaxPlusLinear.heart,
|
||||
),
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref.read(userProvider.notifier).markAsPlayed(!details.userData.played, details.id);
|
||||
},
|
||||
selected: details.userData.played,
|
||||
selectedIcon: IconsaxPlusBold.tick_circle,
|
||||
icon: IconsaxPlusLinear.tick_circle,
|
||||
),
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
controller: scrollController,
|
||||
shrinkWrap: true,
|
||||
children: details.generateActions(context, ref).listTileItems(context, useIcons: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
selected: false,
|
||||
icon: IconsaxPlusLinear.more,
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: padding,
|
||||
originalTitle: details.originalTitle,
|
||||
|
|
@ -98,32 +140,6 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
|||
genres: details.overview.genreItems,
|
||||
communityRating: details.overview.communityRating,
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: wrapAlignment,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(userProvider.notifier)
|
||||
.setAsFavorite(!details.userData.isFavourite, details.id);
|
||||
},
|
||||
selected: details.userData.isFavourite,
|
||||
selectedIcon: IconsaxPlusBold.heart,
|
||||
icon: IconsaxPlusLinear.heart,
|
||||
),
|
||||
SelectableIconButton(
|
||||
onPressed: () async {
|
||||
await ref.read(userProvider.notifier).markAsPlayed(!details.userData.played, details.id);
|
||||
},
|
||||
selected: details.userData.played,
|
||||
selectedIcon: IconsaxPlusBold.tick_circle,
|
||||
icon: IconsaxPlusLinear.tick_circle,
|
||||
),
|
||||
],
|
||||
).padding(padding),
|
||||
if (details.nextUp != null)
|
||||
NextUpEpisode(
|
||||
nextEpisode: details.nextUp!,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/library_filter_model.dart';
|
||||
|
|
@ -12,6 +13,7 @@ import 'package:fladder/screens/shared/media/poster_row.dart';
|
|||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
|
||||
|
|
@ -54,9 +56,9 @@ class FavouritesScreen extends ConsumerWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
...favourites.favourites.entries.where((element) => element.value.isNotEmpty).map(
|
||||
(e) => SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
...[
|
||||
...favourites.favourites.entries.where((element) => element.value.isNotEmpty).map(
|
||||
(e) => PosterRow(
|
||||
contentPadding: padding,
|
||||
onLabelClick: () => context.pushRoute(
|
||||
LibrarySearchRoute().withFilter(
|
||||
|
|
@ -71,15 +73,17 @@ class FavouritesScreen extends ConsumerWidget {
|
|||
posters: e.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (favourites.people.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
if (favourites.people.isNotEmpty)
|
||||
PosterRow(
|
||||
contentPadding: padding,
|
||||
label: context.localized.actor(favourites.people.length),
|
||||
posters: favourites.people,
|
||||
),
|
||||
].mapIndexed(
|
||||
(index, e) => SliverToBoxAdapter(
|
||||
child: FocusProvider(hasFocus: false, autoFocus: index == 0, child: e),
|
||||
),
|
||||
),
|
||||
const DefautlSliverBottomPadding(),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ import 'package:fladder/providers/library_screen_provider.dart';
|
|||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/home_screen.dart';
|
||||
import 'package:fladder/screens/metadata/refresh_metadata.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/screens/shared/nested_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/theme.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
|
||||
|
|
@ -233,9 +233,10 @@ class LibraryRow extends ConsumerWidget {
|
|||
label: context.localized.library(views.length),
|
||||
items: views,
|
||||
height: 155,
|
||||
autoFocus: true,
|
||||
startIndex: selectedView != null ? views.indexOf(selectedView!) : null,
|
||||
contentPadding: padding,
|
||||
itemBuilder: (context, index) {
|
||||
itemBuilder: (context, index, selected) {
|
||||
final view = views[index];
|
||||
final isSelected = selectedView == view;
|
||||
final List<ItemActionButton> viewActions = [
|
||||
|
|
@ -250,25 +251,26 @@ class LibraryRow extends ConsumerWidget {
|
|||
action: () => showRefreshPopup(context, view.id, view.name),
|
||||
)
|
||||
];
|
||||
return FlatButton(
|
||||
onTap: isSelected ? null : () => onSelected?.call(view),
|
||||
onLongPress: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)),
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position =
|
||||
RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: viewActions.popupMenuItems(useIcons: true),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FocusButton(
|
||||
key: Key(view.id),
|
||||
onTap: isSelected ? null : () => onSelected?.call(view),
|
||||
onLongPress: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)),
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position =
|
||||
RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: viewActions.popupMenuItems(useIcons: true),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: FladderTheme.defaultShape.borderRadius,
|
||||
),
|
||||
|
|
@ -294,15 +296,15 @@ class LibraryRow extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
view.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.start,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
view.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.start,
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -352,49 +352,27 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
child: Tooltip(
|
||||
message: librarySearchResults.nestedCurrentItem?.type.label(context) ??
|
||||
context.localized.library(1),
|
||||
child: InkWell(
|
||||
onTapUp: (details) async {
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) {
|
||||
double left = details.globalPosition.dx;
|
||||
double top = details.globalPosition.dy;
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(left, top, 40, 100),
|
||||
items: <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
child: Text(librarySearchResults.nestedCurrentItem?.type.label(context) ??
|
||||
context.localized.library(0))),
|
||||
itemCountWidget.toPopupMenuItem(useIcons: true),
|
||||
refreshAction.toPopupMenuItem(useIcons: true),
|
||||
itemViewAction.toPopupMenuItem(useIcons: true),
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
await showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: [
|
||||
itemCountWidget.toListItem(context, useIcons: true),
|
||||
refreshAction.toListItem(context, useIcons: true),
|
||||
itemViewAction.toListItem(context, useIcons: true),
|
||||
if (librarySearchResults.views.hasEnabled == true)
|
||||
showSavedFiltersDialogue.toPopupMenuItem(useIcons: true),
|
||||
if (itemActions.isNotEmpty) ItemActionDivider().toPopupMenuItem(),
|
||||
...itemActions.popupMenuItems(useIcons: true),
|
||||
showSavedFiltersDialogue.toListItem(context, useIcons: true),
|
||||
if (itemActions.isNotEmpty) ItemActionDivider().toListItem(context),
|
||||
...itemActions.listTileItems(context, useIcons: true),
|
||||
],
|
||||
elevation: 8.0,
|
||||
);
|
||||
} else {
|
||||
await showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: [
|
||||
itemCountWidget.toListItem(context, useIcons: true),
|
||||
refreshAction.toListItem(context, useIcons: true),
|
||||
itemViewAction.toListItem(context, useIcons: true),
|
||||
if (librarySearchResults.views.hasEnabled == true)
|
||||
showSavedFiltersDialogue.toPopupMenuItem(useIcons: true),
|
||||
if (itemActions.isNotEmpty) ItemActionDivider().toListItem(context),
|
||||
...itemActions.listTileItems(context, useIcons: true),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
icon: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Icon(
|
||||
isFavorite
|
||||
? librarySearchResults.nestedCurrentItem?.type.selectedicon
|
||||
|
|
|
|||
|
|
@ -149,16 +149,19 @@ class _LibraryFilterChipsState extends ConsumerState<LibraryFilterChips> {
|
|||
),
|
||||
];
|
||||
|
||||
return Row(
|
||||
spacing: 4,
|
||||
children: chips.mapIndexed(
|
||||
(index, element) {
|
||||
final position = index == 0
|
||||
? PositionContext.first
|
||||
: (index == chips.length - 1 ? PositionContext.last : PositionContext.middle);
|
||||
return PositionProvider(position: position, child: element);
|
||||
},
|
||||
).toList(),
|
||||
return FocusTraversalGroup(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
children: chips.mapIndexed(
|
||||
(index, element) {
|
||||
final position = index == 0
|
||||
? PositionContext.first
|
||||
: (index == chips.length - 1 ? PositionContext.last : PositionContext.middle);
|
||||
return PositionProvider(position: position, child: element);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,11 +23,14 @@ import 'package:fladder/routes/auto_router.gr.dart';
|
|||
import 'package:fladder/screens/shared/media/poster_list_item.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_widget.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/util/theme_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
||||
import 'package:fladder/widgets/shared/grid_focus_traveler.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
|
||||
final libraryViewTypeProvider = StateProvider<LibraryViewTypes>((ref) {
|
||||
|
|
@ -63,12 +66,12 @@ class LibraryViews extends ConsumerWidget {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
sliver: SliverAnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: _getWidget(ref, context),
|
||||
child: _getWidget(context, ref),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getWidget(WidgetRef ref, BuildContext context) {
|
||||
Widget _getWidget(BuildContext context, WidgetRef ref) {
|
||||
final selected = ref.watch(librarySearchProvider(key!).select((value) => value.selectedPosters));
|
||||
final posterSizeMultiplier = ref.watch(clientSettingsProvider.select((value) => value.posterSize));
|
||||
final libraryProvider = ref.read(librarySearchProvider(key!).notifier);
|
||||
|
|
@ -111,21 +114,24 @@ class LibraryViews extends ConsumerWidget {
|
|||
switch (ref.watch(libraryViewTypeProvider)) {
|
||||
case LibraryViewTypes.grid:
|
||||
Widget createGrid(List<ItemBaseModel> items) {
|
||||
return SliverGrid.builder(
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final cellWidth = (width / posterSize).floorToDouble();
|
||||
final crossAxisCount = ((width / cellWidth).floor()).clamp(2, 10);
|
||||
return GridFocusTraveler(
|
||||
itemCount: items.length,
|
||||
crossAxisCount: crossAxisCount,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: posterSize.clamp(2, double.maxFinite).toInt(),
|
||||
mainAxisSpacing: (8 * decimal) + 8,
|
||||
crossAxisSpacing: (8 * decimal) + 8,
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
childAspectRatio: items.getMostCommonType.aspectRatio,
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
itemBuilder: (other, selectedIndex, index) {
|
||||
final item = items[index];
|
||||
return PosterWidget(
|
||||
key: Key(item.id),
|
||||
poster: item,
|
||||
maxLines: 2,
|
||||
heroTag: true,
|
||||
subTitle: item.subTitle(sortingOptions),
|
||||
excludeActions: excludeActions,
|
||||
otherActions: otherActions(item),
|
||||
|
|
@ -134,6 +140,11 @@ class LibraryViews extends ConsumerWidget {
|
|||
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
|
||||
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
|
||||
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
|
||||
onFocusChanged: (focus) {
|
||||
if (focus) {
|
||||
other.ensureVisible();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -165,16 +176,19 @@ class LibraryViews extends ConsumerWidget {
|
|||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final poster = items[index];
|
||||
return PosterListItem(
|
||||
poster: poster,
|
||||
selected: selected.contains(poster),
|
||||
excludeActions: excludeActions,
|
||||
otherActions: otherActions(poster),
|
||||
subTitle: poster.subTitle(sortingOptions),
|
||||
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
|
||||
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
|
||||
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
|
||||
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
|
||||
return FocusProvider(
|
||||
autoFocus: index == 0,
|
||||
child: PosterListItem(
|
||||
poster: poster,
|
||||
selected: selected.contains(poster),
|
||||
excludeActions: excludeActions,
|
||||
otherActions: otherActions(poster),
|
||||
subTitle: poster.subTitle(sortingOptions),
|
||||
onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData),
|
||||
onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]),
|
||||
onItemUpdated: (newItem) => libraryProvider.updateItem(newItem),
|
||||
onPressed: (action, item) async => onItemPressed(action, key, item, ref, context),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -228,7 +242,6 @@ class LibraryViews extends ConsumerWidget {
|
|||
aspectRatio: item.primaryRatio,
|
||||
selected: selected.contains(item),
|
||||
inlineTitle: true,
|
||||
heroTag: true,
|
||||
subTitle: item.subTitle(sortingOptions),
|
||||
excludeActions: excludeActions,
|
||||
otherActions: otherActions(group[index]),
|
||||
|
|
@ -257,7 +270,6 @@ class LibraryViews extends ConsumerWidget {
|
|||
aspectRatio: item.primaryRatio,
|
||||
selected: selected.contains(item),
|
||||
inlineTitle: true,
|
||||
heroTag: true,
|
||||
excludeActions: excludeActions,
|
||||
otherActions: otherActions(item),
|
||||
subTitle: item.subTitle(sortingOptions),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:page_transition/page_transition.dart';
|
|||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/providers/library_search_provider.dart';
|
||||
import 'package:fladder/screens/shared/outlined_text_field.dart';
|
||||
import 'package:fladder/theme.dart';
|
||||
import 'package:fladder/util/debouncer.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
|
|
@ -87,7 +88,7 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
|
|||
),
|
||||
child: child,
|
||||
),
|
||||
builder: (context, controller, focusNode) => TextField(
|
||||
builder: (context, controller, focusNode) => OutlinedTextField(
|
||||
focusNode: focusNode,
|
||||
controller: controller,
|
||||
onSubmitted: (value) {
|
||||
|
|
@ -99,6 +100,7 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
|
|||
isEmpty = value.isEmpty;
|
||||
});
|
||||
},
|
||||
placeHolder: widget.title ?? "${context.localized.search}...",
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.title ?? "${context.localized.search}...",
|
||||
prefixIcon: const Icon(IconsaxPlusLinear.search_normal),
|
||||
|
|
|
|||
0
lib/screens/login/controllers/login_controller.dart
Normal file
0
lib/screens/login/controllers/login_controller.dart
Normal file
140
lib/screens/login/login_code_dialog.dart
Normal file
140
lib/screens/login/login_code_dialog.dart
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
Future<void> openLoginCodeDialog(
|
||||
BuildContext context, {
|
||||
required QuickConnectResult quickConnectInfo,
|
||||
required Function(BuildContext context, String secret) onAuthenticated,
|
||||
}) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => LoginCodeDialog(
|
||||
quickConnectInfo: quickConnectInfo,
|
||||
onAuthenticated: onAuthenticated,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class LoginCodeDialog extends ConsumerStatefulWidget {
|
||||
final QuickConnectResult quickConnectInfo;
|
||||
final Function(BuildContext context, String secret) onAuthenticated;
|
||||
const LoginCodeDialog({
|
||||
required this.quickConnectInfo,
|
||||
required this.onAuthenticated,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<LoginCodeDialog> createState() => _LoginCodeDialogState();
|
||||
}
|
||||
|
||||
class _LoginCodeDialogState extends ConsumerState<LoginCodeDialog> {
|
||||
late QuickConnectResult quickConnectInfo = widget.quickConnectInfo;
|
||||
|
||||
RestartableTimer? timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void createTimer() {
|
||||
timer?.cancel();
|
||||
timer = RestartableTimer(const Duration(seconds: 1), () async {
|
||||
final result = await ref.read(jellyApiProvider).quickConnectConnectGet(
|
||||
secret: quickConnectInfo.secret,
|
||||
);
|
||||
final newSecret = result.body?.secret;
|
||||
if (result.isSuccessful && result.body?.authenticated == true && newSecret != null) {
|
||||
widget.onAuthenticated.call(context, newSecret);
|
||||
} else {
|
||||
timer?.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final code = quickConnectInfo.code;
|
||||
final serverName = ref.watch(authProvider.select((value) => value.serverLoginModel?.tempCredentials.serverName));
|
||||
return Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 16,
|
||||
children: [
|
||||
Text(
|
||||
serverName?.isNotEmpty == true
|
||||
? "${context.localized.quickConnectTitle} - $serverName"
|
||||
: context.localized.quickConnectTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Divider(),
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
if (code != null) ...[
|
||||
Text(
|
||||
context.localized.quickConnectEnterCodeDescription,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
IntrinsicWidth(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
code,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
wordSpacing: 8,
|
||||
letterSpacing: 8,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
semanticsLabel: code,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final response = await ref.read(jellyApiProvider).quickConnectInitiate();
|
||||
if (response.isSuccessful && response.body != null) {
|
||||
setState(() {
|
||||
quickConnectInfo = response.body!;
|
||||
});
|
||||
createTimer();
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
context.localized.refresh,
|
||||
),
|
||||
)
|
||||
].addInBetween(const SizedBox(height: 16)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
|
|
@ -7,25 +5,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/models/login_screen_model.dart';
|
||||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/providers/shared_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/login/lock_screen.dart';
|
||||
import 'package:fladder/screens/login/login_edit_user.dart';
|
||||
import 'package:fladder/screens/login/login_screen_credentials.dart';
|
||||
import 'package:fladder/screens/login/login_user_grid.dart';
|
||||
import 'package:fladder/screens/login/widgets/discover_servers_widget.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/screens/shared/fladder_logo.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/screens/shared/outlined_text_field.dart';
|
||||
import 'package:fladder/screens/shared/passcode_input.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/auth_service.dart';
|
||||
import 'package:fladder/util/fladder_config.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart';
|
||||
|
||||
@RoutePage()
|
||||
|
|
@ -37,373 +24,85 @@ class LoginScreen extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _LoginPageState extends ConsumerState<LoginScreen> {
|
||||
List<AccountModel> users = const [];
|
||||
bool loading = false;
|
||||
String? invalidUrl = "";
|
||||
bool startCheckingForErrors = false;
|
||||
bool addingNewUser = false;
|
||||
bool editingUsers = false;
|
||||
late final TextEditingController serverTextController = TextEditingController(text: '');
|
||||
|
||||
final usernameController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
void startAddingNewUser() {
|
||||
setState(() {
|
||||
addingNewUser = true;
|
||||
editingUsers = false;
|
||||
});
|
||||
if (FladderConfig.baseUrl != null) {
|
||||
serverTextController.text = FladderConfig.baseUrl!;
|
||||
_parseUrl(FladderConfig.baseUrl!);
|
||||
retrieveListOfUsers();
|
||||
}
|
||||
}
|
||||
bool editUsersMode = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.microtask(() {
|
||||
ref.read(userProvider.notifier).clear();
|
||||
final currentAccounts = ref.read(authProvider.notifier).getSavedAccounts();
|
||||
addingNewUser = currentAccounts.isEmpty;
|
||||
ref.read(lockScreenActiveProvider.notifier).update((state) => true);
|
||||
if (FladderConfig.baseUrl != null) {
|
||||
serverTextController.text = FladderConfig.baseUrl!;
|
||||
_parseUrl(FladderConfig.baseUrl!);
|
||||
retrieveListOfUsers();
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(authProvider.notifier).initModel(context);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loggedInUsers = ref.watch(authProvider.select((value) => value.accounts));
|
||||
final authLoading = ref.watch(authProvider.select((value) => value.loading));
|
||||
final screen = ref.watch(authProvider.select((value) => value.screen));
|
||||
final accounts = ref.watch(authProvider.select((value) => value.accounts));
|
||||
return Scaffold(
|
||||
appBar: const FladderAppBar(),
|
||||
floatingActionButton: !addingNewUser
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (!AdaptiveLayout.of(context).isDesktop)
|
||||
FloatingActionButton(
|
||||
key: const Key("edit_button"),
|
||||
heroTag: "edit_button",
|
||||
backgroundColor: editingUsers ? Theme.of(context).colorScheme.errorContainer : null,
|
||||
child: const Icon(IconsaxPlusLinear.edit_2),
|
||||
onPressed: () => setState(() => editingUsers = !editingUsers),
|
||||
),
|
||||
FloatingActionButton(
|
||||
key: const Key("new_button"),
|
||||
heroTag: "new_button",
|
||||
child: const Icon(IconsaxPlusLinear.add_square),
|
||||
onPressed: startAddingNewUser,
|
||||
),
|
||||
].addInBetween(const SizedBox(width: 16)),
|
||||
)
|
||||
: null,
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 32),
|
||||
extendBody: true,
|
||||
extendBodyBehindAppBar: true,
|
||||
floatingActionButton: switch (screen) {
|
||||
LoginScreenType.users => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
spacing: 16,
|
||||
children: [
|
||||
const Center(
|
||||
child: FladderLogo(),
|
||||
if (!AdaptiveLayout.of(context).isDesktop)
|
||||
FloatingActionButton(
|
||||
key: const Key("edit_user_button"),
|
||||
heroTag: "edit_user_button",
|
||||
backgroundColor: editUsersMode ? Theme.of(context).colorScheme.errorContainer : null,
|
||||
child: const Icon(IconsaxPlusLinear.edit_2),
|
||||
onPressed: () => setState(() => editUsersMode = !editUsersMode),
|
||||
),
|
||||
FloatingActionButton(
|
||||
key: const Key("new_user_button"),
|
||||
heroTag: "new_user_button",
|
||||
child: const Icon(IconsaxPlusLinear.add_square),
|
||||
onPressed: () => ref.read(authProvider.notifier).addNewUser(),
|
||||
),
|
||||
AnimatedFadeSize(
|
||||
child: addingNewUser
|
||||
? addUserFields(loggedInUsers, authLoading)
|
||||
: Column(
|
||||
key: UniqueKey(),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
LoginUserGrid(
|
||||
users: loggedInUsers,
|
||||
editMode: editingUsers,
|
||||
onPressed: (user) async => tapLoggedInAccount(user),
|
||||
onLongPress: (user) => openUserEditDialogue(context, user),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
].addPadding(const EdgeInsets.symmetric(vertical: 16)),
|
||||
],
|
||||
),
|
||||
_ => null,
|
||||
},
|
||||
body: Center(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: MediaQuery.paddingOf(context).add(const EdgeInsetsGeometry.all(16)),
|
||||
children: [
|
||||
const FladderLogo(),
|
||||
const SizedBox(height: 24),
|
||||
AnimatedFadeSize(
|
||||
child: switch (screen) {
|
||||
LoginScreenType.login || LoginScreenType.code => const LoginScreenCredentials(),
|
||||
_ => LoginUserGrid(
|
||||
users: accounts,
|
||||
editMode: editUsersMode,
|
||||
onPressed: (user) => tapLoggedInAccount(context, user, ref),
|
||||
onLongPress: (user) => openUserEditDialogue(context, user),
|
||||
),
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _parseUrl(String url) {
|
||||
setState(() {
|
||||
ref.read(authProvider.notifier).setServer("");
|
||||
users = [];
|
||||
|
||||
if (url.isEmpty) {
|
||||
invalidUrl = "";
|
||||
return;
|
||||
}
|
||||
if (!Uri.parse(url).isAbsolute) {
|
||||
invalidUrl = context.localized.invalidUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url.startsWith('https://') && !url.startsWith('http://')) {
|
||||
invalidUrl = context.localized.invalidUrlDesc;
|
||||
return;
|
||||
}
|
||||
|
||||
invalidUrl = null;
|
||||
|
||||
if (invalidUrl == null) {
|
||||
ref.read(authProvider.notifier).setServer(url.rtrim('/'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void openUserEditDialogue(BuildContext context, AccountModel user) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => LoginEditUser(
|
||||
user: user,
|
||||
onTapServer: (value) {
|
||||
setState(() {
|
||||
_parseUrl(value);
|
||||
serverTextController.text = value;
|
||||
startAddingNewUser();
|
||||
});
|
||||
ref.read(authProvider.notifier).setServer(value);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void tapLoggedInAccount(AccountModel user) async {
|
||||
switch (user.authMethod) {
|
||||
case Authentication.autoLogin:
|
||||
handleLogin(user);
|
||||
break;
|
||||
case Authentication.biometrics:
|
||||
final authenticated = await AuthService.authenticateUser(context, user);
|
||||
if (authenticated) {
|
||||
handleLogin(user);
|
||||
}
|
||||
break;
|
||||
case Authentication.passcode:
|
||||
if (context.mounted) {
|
||||
showPassCodeDialog(context, (newPin) {
|
||||
if (newPin == user.localPin) {
|
||||
handleLogin(user);
|
||||
} else {
|
||||
fladderSnackbar(context, title: context.localized.incorrectPinTryAgain);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case Authentication.none:
|
||||
handleLogin(user);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleLogin(AccountModel user) async {
|
||||
await ref.read(authProvider.notifier).switchUser();
|
||||
await ref.read(sharedUtilityProvider).updateAccountInfo(user.copyWith(
|
||||
lastUsed: DateTime.now(),
|
||||
));
|
||||
ref.read(userProvider.notifier).updateUser(user.copyWith(lastUsed: DateTime.now()));
|
||||
|
||||
loggedInGoToHome();
|
||||
}
|
||||
|
||||
void loggedInGoToHome() {
|
||||
ref.read(lockScreenActiveProvider.notifier).update((state) => false);
|
||||
if (context.mounted) {
|
||||
context.router.replaceAll([const DashboardRoute()]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Null> Function()? get enterCredentialsTryLogin => emptyFields()
|
||||
? null
|
||||
: () async {
|
||||
serverTextController.text = serverTextController.text.rtrim('/');
|
||||
ref.read(authProvider.notifier).setServer(serverTextController.text.rtrim('/'));
|
||||
final response = await ref.read(authProvider.notifier).authenticateByName(
|
||||
usernameController.text,
|
||||
passwordController.text,
|
||||
);
|
||||
if (response?.isSuccessful == false) {
|
||||
fladderSnackbar(context,
|
||||
title:
|
||||
"(${response?.base.statusCode}) ${response?.base.reasonPhrase ?? context.localized.somethingWentWrongPasswordCheck}");
|
||||
} else if (response?.body != null) {
|
||||
loggedInGoToHome();
|
||||
}
|
||||
};
|
||||
|
||||
bool emptyFields() => usernameController.text.isEmpty;
|
||||
|
||||
void retrieveListOfUsers() async {
|
||||
serverTextController.text = serverTextController.text.rtrim('/');
|
||||
ref.read(authProvider.notifier).setServer(serverTextController.text);
|
||||
setState(() => loading = true);
|
||||
final response = await ref.read(authProvider.notifier).getPublicUsers();
|
||||
if ((response == null || response.isSuccessful == false) && context.mounted) {
|
||||
fladderSnackbar(context, title: response?.base.reasonPhrase ?? context.localized.unableToConnectHost);
|
||||
setState(() => startCheckingForErrors = true);
|
||||
}
|
||||
if (response?.body?.isEmpty == true) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
setState(() {
|
||||
users = response?.body ?? [];
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Widget addUserFields(List<AccountModel> accounts, bool authLoading) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (accounts.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: IconButton.filledTonal(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
addingNewUser = false;
|
||||
loading = false;
|
||||
startCheckingForErrors = false;
|
||||
serverTextController.text = "";
|
||||
usernameController.text = "";
|
||||
passwordController.text = "";
|
||||
invalidUrl = "";
|
||||
});
|
||||
ref.read(authProvider.notifier).setServer("");
|
||||
},
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.arrow_left_2,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (FladderConfig.baseUrl == null) ...[
|
||||
Flexible(
|
||||
child: OutlinedTextField(
|
||||
controller: serverTextController,
|
||||
onChanged: _parseUrl,
|
||||
onSubmitted: (value) => retrieveListOfUsers(),
|
||||
autoFillHints: const [AutofillHints.url],
|
||||
keyboardType: TextInputType.url,
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.go,
|
||||
label: context.localized.server,
|
||||
errorText: (invalidUrl == null || serverTextController.text.isEmpty || !startCheckingForErrors)
|
||||
? null
|
||||
: invalidUrl,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Tooltip(
|
||||
message: context.localized.retrievePublicListOfUsers,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: IconButton.filled(
|
||||
onPressed: () => retrieveListOfUsers(),
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.refresh,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
AnimatedFadeSize(
|
||||
child: invalidUrl == null
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (loading || users.isNotEmpty)
|
||||
AnimatedFadeSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: loading
|
||||
? CircularProgressIndicator(key: UniqueKey(), strokeCap: StrokeCap.round)
|
||||
: LoginUserGrid(
|
||||
users: users,
|
||||
onPressed: (value) {
|
||||
usernameController.text = value.name;
|
||||
passwordController.text = "";
|
||||
focusNode.requestFocus();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
AutofillGroup(
|
||||
child: Column(
|
||||
children: [
|
||||
OutlinedTextField(
|
||||
controller: usernameController,
|
||||
autoFillHints: const [AutofillHints.username],
|
||||
textInputAction: TextInputAction.next,
|
||||
autocorrect: false,
|
||||
onChanged: (value) => setState(() {}),
|
||||
label: context.localized.userName,
|
||||
),
|
||||
OutlinedTextField(
|
||||
controller: passwordController,
|
||||
autoFillHints: const [AutofillHints.password],
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
focusNode: focusNode,
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (value) => enterCredentialsTryLogin?.call(),
|
||||
onChanged: (value) => setState(() {}),
|
||||
label: context.localized.password,
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: enterCredentialsTryLogin,
|
||||
child: authLoading
|
||||
? SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.inversePrimary,
|
||||
strokeCap: StrokeCap.round),
|
||||
)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(context.localized.login),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(IconsaxPlusBold.send_1),
|
||||
],
|
||||
),
|
||||
),
|
||||
].addPadding(const EdgeInsets.symmetric(vertical: 4)),
|
||||
),
|
||||
),
|
||||
].addPadding(const EdgeInsets.symmetric(vertical: 10)),
|
||||
)
|
||||
: DiscoverServersWidget(
|
||||
serverCredentials: accounts.map((e) => e.credentials).toList(),
|
||||
onPressed: (server) {
|
||||
serverTextController.text = server.address;
|
||||
_parseUrl(server.address);
|
||||
retrieveListOfUsers();
|
||||
},
|
||||
),
|
||||
)
|
||||
].addPadding(const EdgeInsets.symmetric(vertical: 8)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
315
lib/screens/login/login_screen_credentials.dart
Normal file
315
lib/screens/login/login_screen_credentials.dart
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/providers/shared_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/login/lock_screen.dart';
|
||||
import 'package:fladder/screens/login/login_code_dialog.dart';
|
||||
import 'package:fladder/screens/login/login_user_grid.dart';
|
||||
import 'package:fladder/screens/login/widgets/discover_servers_widget.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/screens/shared/outlined_text_field.dart';
|
||||
import 'package:fladder/screens/shared/passcode_input.dart';
|
||||
import 'package:fladder/util/auth_service.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
class LoginScreenCredentials extends ConsumerStatefulWidget {
|
||||
const LoginScreenCredentials({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _LoginScreenCredentialsState();
|
||||
}
|
||||
|
||||
class _LoginScreenCredentialsState extends ConsumerState<LoginScreenCredentials> {
|
||||
late final TextEditingController serverTextController = TextEditingController(text: '');
|
||||
final usernameController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
||||
bool loggingIn = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final existingUsers = ref.watch(authProvider.select((value) => value.accounts));
|
||||
final otherCredentials = existingUsers.map((e) => e.credentials).toList();
|
||||
final serverCredentials = ref.watch(authProvider.select((value) => value.serverLoginModel));
|
||||
final users = serverCredentials?.accounts ?? [];
|
||||
final provider = ref.read(authProvider.notifier);
|
||||
final loading = ref.watch(authProvider.select((value) => value.loading));
|
||||
final hasBaseUrl = ref.watch(authProvider.select((value) => value.hasBaseUrl));
|
||||
final urlError = ref.watch(authProvider.select((value) => value.errorMessage));
|
||||
final hasQuickConnect = ref.watch(authProvider.select((value) => value.serverLoginModel?.hasQuickConnect ?? false));
|
||||
|
||||
ref.listen(
|
||||
authProvider.select((value) => value.serverLoginModel),
|
||||
(previous, next) {
|
||||
if (next?.tempCredentials.server.isNotEmpty == true) {
|
||||
serverTextController.text = next?.tempCredentials.server ?? "";
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 16,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (existingUsers.isNotEmpty)
|
||||
IconButton.filledTonal(
|
||||
onPressed: () => provider.goUserSelect(),
|
||||
iconSize: 28,
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.arrow_left_2,
|
||||
),
|
||||
),
|
||||
if (!hasBaseUrl)
|
||||
Flexible(
|
||||
child: OutlinedTextField(
|
||||
controller: serverTextController,
|
||||
onChanged: (value) => provider.tryParseUrl(value),
|
||||
onSubmitted: (value) => provider.setServer(value),
|
||||
autoFillHints: const [AutofillHints.url],
|
||||
keyboardType: TextInputType.url,
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.go,
|
||||
label: context.localized.server,
|
||||
errorText: urlError,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: context.localized.retrievePublicListOfUsers,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: IconButton.filled(
|
||||
onPressed: () => provider.setServer(serverTextController.text),
|
||||
iconSize: 28,
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.refresh,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (serverCredentials == null)
|
||||
DiscoverServersWidget(
|
||||
serverCredentials: otherCredentials,
|
||||
onPressed: (info) => provider.setServer(info.address),
|
||||
)
|
||||
else ...[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 16,
|
||||
children: [
|
||||
if (loading || users.isNotEmpty)
|
||||
AnimatedFadeSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: loading
|
||||
? CircularProgressIndicator(key: UniqueKey(), strokeCap: StrokeCap.round)
|
||||
: LoginUserGrid(
|
||||
users: users,
|
||||
onPressed: (value) {
|
||||
usernameController.text = value.name;
|
||||
passwordController.text = "";
|
||||
focusNode.requestFocus();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
AutofillGroup(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Flexible(
|
||||
child: OutlinedTextField(
|
||||
controller: usernameController,
|
||||
autoFillHints: const [AutofillHints.username],
|
||||
textInputAction: TextInputAction.next,
|
||||
autocorrect: false,
|
||||
onChanged: (value) => setState(() {}),
|
||||
label: context.localized.userName,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: OutlinedTextField(
|
||||
controller: passwordController,
|
||||
autoFillHints: const [AutofillHints.password],
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
focusNode: focusNode,
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (value) => enterCredentialsTryLogin?.call(),
|
||||
onChanged: (value) => setState(() {}),
|
||||
label: context.localized.password,
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
indent: 32,
|
||||
endIndent: 32,
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: enterCredentialsTryLogin,
|
||||
child: loggingIn
|
||||
? SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.inversePrimary, strokeCap: StrokeCap.round),
|
||||
)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(context.localized.login),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(IconsaxPlusBold.send_1),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (hasQuickConnect)
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final result = await ref.read(jellyApiProvider).quickConnectInitiate();
|
||||
if (result.body != null) {
|
||||
await openLoginCodeDialog(
|
||||
context,
|
||||
quickConnectInfo: result.body!,
|
||||
onAuthenticated: (context, secret) async {
|
||||
context.pop();
|
||||
if (secret.isNotEmpty) {
|
||||
await loginUsingSecret(secret);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
fladderSnackbar(context, title: context.localized.quickConnectPostFailed);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(context.localized.quickConnectLoginUsingCode),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(IconsaxPlusBold.scan_barcode),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (serverCredentials.serverMessage?.isEmpty == false) ...[
|
||||
const Divider(),
|
||||
Text(
|
||||
serverCredentials.serverMessage ?? "",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> Function()? get enterCredentialsTryLogin => emptyFields() ? null : () => loginUsingCredentials();
|
||||
|
||||
Future<void> loginUsingCredentials() async {
|
||||
setState(() {
|
||||
loggingIn = true;
|
||||
});
|
||||
final response = await ref.read(authProvider.notifier).authenticateByName(
|
||||
usernameController.text,
|
||||
passwordController.text,
|
||||
);
|
||||
if (response?.isSuccessful == false) {
|
||||
fladderSnackbar(context,
|
||||
title:
|
||||
"(${response?.base.statusCode}) ${response?.base.reasonPhrase ?? context.localized.somethingWentWrongPasswordCheck}");
|
||||
} else if (response?.body != null) {
|
||||
loggedInGoToHome(context, ref);
|
||||
}
|
||||
setState(() {
|
||||
loggingIn = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> loginUsingSecret(String secret) async {
|
||||
setState(() {
|
||||
loggingIn = true;
|
||||
});
|
||||
final response = await ref.read(authProvider.notifier).authenticateUsingSecret(secret);
|
||||
if (response?.isSuccessful == false) {
|
||||
fladderSnackbar(context,
|
||||
title:
|
||||
"(${response?.base.statusCode}) ${response?.base.reasonPhrase ?? context.localized.somethingWentWrongPasswordCheck}");
|
||||
} else if (response?.body != null) {
|
||||
loggedInGoToHome(context, ref);
|
||||
}
|
||||
setState(() {
|
||||
loggingIn = false;
|
||||
});
|
||||
}
|
||||
|
||||
bool emptyFields() => usernameController.text.isEmpty;
|
||||
}
|
||||
|
||||
void loggedInGoToHome(BuildContext context, WidgetRef ref) {
|
||||
ref.read(lockScreenActiveProvider.notifier).update((state) => false);
|
||||
if (context.mounted) {
|
||||
context.router.replaceAll([const DashboardRoute()]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleLogin(BuildContext context, AccountModel user, WidgetRef ref) async {
|
||||
await ref.read(authProvider.notifier).switchUser();
|
||||
await ref.read(sharedUtilityProvider).updateAccountInfo(user.copyWith(
|
||||
lastUsed: DateTime.now(),
|
||||
));
|
||||
ref.read(userProvider.notifier).updateUser(user.copyWith(lastUsed: DateTime.now()));
|
||||
|
||||
loggedInGoToHome(context, ref);
|
||||
}
|
||||
|
||||
void tapLoggedInAccount(BuildContext context, AccountModel user, WidgetRef ref) async {
|
||||
Future<void> loginFunction() => _handleLogin(context, user, ref);
|
||||
switch (user.authMethod) {
|
||||
case Authentication.autoLogin:
|
||||
loginFunction();
|
||||
break;
|
||||
case Authentication.biometrics:
|
||||
final authenticated = await AuthService.authenticateUser(context, user);
|
||||
if (authenticated) {
|
||||
loginFunction();
|
||||
}
|
||||
break;
|
||||
case Authentication.passcode:
|
||||
if (context.mounted) {
|
||||
showPassCodeDialog(context, (newPin) {
|
||||
if (newPin == user.localPin) {
|
||||
loginFunction();
|
||||
} else {
|
||||
fladderSnackbar(context, title: context.localized.incorrectPinTryAgain);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case Authentication.none:
|
||||
loginFunction();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,9 @@ import 'package:reorderable_grid/reorderable_grid.dart';
|
|||
|
||||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/user_icon.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
||||
class LoginUserGrid extends ConsumerWidget {
|
||||
|
|
@ -21,127 +21,118 @@ class LoginUserGrid extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mainAxisExtent = 175.0;
|
||||
final maxCount = (MediaQuery.of(context).size.width ~/ mainAxisExtent).clamp(1, 3);
|
||||
final maxCount = (MediaQuery.of(context).size.width / mainAxisExtent).floor().clamp(1, 3);
|
||||
|
||||
return ReorderableGridView.builder(
|
||||
onReorder: (oldIndex, newIndex) => ref.read(authProvider.notifier).reOrderUsers(oldIndex, newIndex),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
autoScroll: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: users.length == 1 ? 1 : maxCount,
|
||||
mainAxisSpacing: 24,
|
||||
crossAxisSpacing: 24,
|
||||
mainAxisExtent: mainAxisExtent,
|
||||
),
|
||||
itemCount: users.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = users[index];
|
||||
return FlatButton(
|
||||
key: Key(user.id),
|
||||
onTap: () => editMode ? onLongPress?.call(user) : onPressed?.call(user),
|
||||
onLongPress:
|
||||
AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer ? () => onLongPress?.call(user) : null,
|
||||
child: _CardHolder(
|
||||
content: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: UserIcon(
|
||||
labelStyle: Theme.of(context).textTheme.headlineMedium,
|
||||
user: user,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
final crossAxisCount = users.length == 1 ? 1 : maxCount;
|
||||
|
||||
final neededWidth = crossAxisCount * mainAxisExtent + (crossAxisCount - 1) * 24.0;
|
||||
|
||||
return SizedBox(
|
||||
width: neededWidth,
|
||||
child: ReorderableGridView.builder(
|
||||
onReorder: (oldIndex, newIndex) => ref.read(authProvider.notifier).reOrderUsers(oldIndex, newIndex),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
autoScroll: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: (users.length == 1 ? 1 : maxCount).toInt(),
|
||||
mainAxisSpacing: 24,
|
||||
crossAxisSpacing: 24,
|
||||
mainAxisExtent: mainAxisExtent,
|
||||
),
|
||||
itemCount: users.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = users[index];
|
||||
return Center(
|
||||
key: Key(user.id),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: FocusButton(
|
||||
onTap: () => editMode ? onLongPress?.call(user) : onPressed?.call(user),
|
||||
onLongPress: switch (AdaptiveLayout.inputDeviceOf(context)) {
|
||||
InputDevice.dpad || InputDevice.pointer => () => onLongPress?.call(user),
|
||||
InputDevice.touch => null,
|
||||
},
|
||||
darkOverlay: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
user.authMethod.icon,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.name,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
)),
|
||||
],
|
||||
),
|
||||
if (user.credentials.serverName.isNotEmpty)
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
child: Row(
|
||||
child: UserIcon(
|
||||
labelStyle: Theme.of(context).textTheme.headlineMedium,
|
||||
user: user,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Icon(
|
||||
IconsaxPlusBold.driver_2,
|
||||
size: 14,
|
||||
Icon(
|
||||
user.authMethod.icon,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.credentials.serverName,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
user.name,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
)),
|
||||
],
|
||||
),
|
||||
)
|
||||
].addInBetween(const SizedBox(width: 4, height: 4)),
|
||||
),
|
||||
),
|
||||
if (editMode)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
IconsaxPlusBold.edit_2,
|
||||
size: 14,
|
||||
if (user.credentials.serverName.isNotEmpty)
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Icon(
|
||||
IconsaxPlusBold.driver_2,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.credentials.serverName,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
].addInBetween(const SizedBox(width: 4, height: 4)),
|
||||
),
|
||||
),
|
||||
if (editMode)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
IconsaxPlusBold.edit_2,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CardHolder extends StatelessWidget {
|
||||
final Widget content;
|
||||
|
||||
const _CardHolder({
|
||||
required this.content,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 1,
|
||||
shadowColor: Colors.transparent,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150),
|
||||
child: content,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
0
lib/screens/login/screens/server_selection_screen.dart
Normal file
0
lib/screens/login/screens/server_selection_screen.dart
Normal file
0
lib/screens/login/widgets/credentials_input_section.dart
Normal file
0
lib/screens/login/widgets/credentials_input_section.dart
Normal file
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/credentials_model.dart';
|
||||
import 'package:fladder/providers/discovery_provider.dart';
|
||||
|
|
@ -37,6 +37,7 @@ class DiscoverServersWidget extends ConsumerWidget {
|
|||
return ListView(
|
||||
padding: const EdgeInsets.all(6),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
if (existingServers.isNotEmpty) ...[
|
||||
Row(
|
||||
|
|
@ -123,8 +124,8 @@ class _ServerInfoCard extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: () => onPressed(server),
|
||||
child: TextButton(
|
||||
onPressed: () => onPressed(server),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
child: Row(
|
||||
|
|
|
|||
0
lib/screens/login/widgets/server_input_section.dart
Normal file
0
lib/screens/login/widgets/server_input_section.dart
Normal file
0
lib/screens/login/widgets/server_url_input.dart
Normal file
0
lib/screens/login/widgets/server_url_input.dart
Normal file
|
|
@ -1,5 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/providers/edit_item_provider.dart';
|
||||
|
|
@ -12,10 +18,7 @@ import 'package:fladder/util/list_extensions.dart';
|
|||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/adaptive_date_picker.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
|
||||
class EditFields extends ConsumerStatefulWidget {
|
||||
final Map<String, dynamic> fields;
|
||||
|
|
@ -63,14 +66,14 @@ class _EditGeneralState extends ConsumerState<EditFields> {
|
|||
trailing: EnumBox(
|
||||
current: map.entries.firstWhereOrNull((element) => element.value == true)?.key ?? "",
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: const Text(""),
|
||||
onTap: () => ref.read(editItemProvider.notifier).updateField(MapEntry(e.key, "")),
|
||||
ItemActionButton(
|
||||
label: const Text(""),
|
||||
action: () => ref.read(editItemProvider.notifier).updateField(MapEntry(e.key, "")),
|
||||
),
|
||||
...map.entries.map(
|
||||
(mapEntry) => PopupMenuItem(
|
||||
child: Text(mapEntry.key),
|
||||
onTap: () => ref
|
||||
(mapEntry) => ItemActionButton(
|
||||
label: Text(mapEntry.key),
|
||||
action: () => ref
|
||||
.read(editItemProvider.notifier)
|
||||
.updateField(MapEntry(e.key, mapEntry.key)),
|
||||
),
|
||||
|
|
@ -240,9 +243,9 @@ class _EditGeneralState extends ConsumerState<EditFields> {
|
|||
.whereNot(
|
||||
(element) => element == PersonKind.swaggerGeneratedUnknown)
|
||||
.map(
|
||||
(entry) => PopupMenuItem(
|
||||
child: Text(entry.name.toUpperCaseSplit()),
|
||||
onTap: () {
|
||||
(entry) => ItemActionButton(
|
||||
label: Text(entry.name.toUpperCaseSplit()),
|
||||
action: () {
|
||||
setState(() {
|
||||
personType = entry;
|
||||
});
|
||||
|
|
@ -570,9 +573,9 @@ class _EditGeneralState extends ConsumerState<EditFields> {
|
|||
current: (e.value as DisplayOrder).value.toUpperCaseSplit(),
|
||||
itemBuilder: (context) => DisplayOrder.values
|
||||
.map(
|
||||
(mapEntry) => PopupMenuItem(
|
||||
child: Text(mapEntry.value.toUpperCaseSplit()),
|
||||
onTap: () => ref
|
||||
(mapEntry) => ItemActionButton(
|
||||
label: Text(mapEntry.value.toUpperCaseSplit()),
|
||||
action: () => ref
|
||||
.read(editItemProvider.notifier)
|
||||
.updateField(MapEntry(e.key, mapEntry.value)),
|
||||
),
|
||||
|
|
@ -594,9 +597,9 @@ class _EditGeneralState extends ConsumerState<EditFields> {
|
|||
current: (e.value as ShowStatus).value,
|
||||
itemBuilder: (context) => ShowStatus.values
|
||||
.map(
|
||||
(mapEntry) => PopupMenuItem(
|
||||
child: Text(mapEntry.value),
|
||||
onTap: () => ref
|
||||
(mapEntry) => ItemActionButton(
|
||||
label: Text(mapEntry.value),
|
||||
action: () => ref
|
||||
.read(editItemProvider.notifier)
|
||||
.updateField(MapEntry(e.key, mapEntry.value)),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
|||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
|
||||
Future<void> showRefreshPopup(BuildContext context, String itemId, String itemName) async {
|
||||
return showDialog(
|
||||
|
|
@ -69,10 +70,9 @@ class _RefreshPopupDialogState extends ConsumerState<RefreshPopupDialog> {
|
|||
child: EnumBox(
|
||||
current: refreshMode.label(context),
|
||||
itemBuilder: (context) => MetadataRefresh.values
|
||||
.map((value) => PopupMenuItem(
|
||||
value: value,
|
||||
child: Text(value.label(context)),
|
||||
onTap: () => setState(() {
|
||||
.map((value) => ItemActionButton(
|
||||
label: Text(value.label(context)),
|
||||
action: () => setState(() {
|
||||
refreshMode = value;
|
||||
}),
|
||||
))
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
|
@ -35,6 +35,7 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
late final BasePlayer player = switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) {
|
||||
PlayerOptions.libMDK => LibMDK(),
|
||||
PlayerOptions.libMPV => LibMPV(),
|
||||
_ => LibMDK(),
|
||||
};
|
||||
late String videoUrl = "";
|
||||
|
||||
|
|
@ -102,7 +103,7 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
duration = event.duration;
|
||||
});
|
||||
}));
|
||||
await player.open(videoUrl, !ref.watch(photoViewSettingsProvider).autoPlay);
|
||||
await player.loadVideo(videoUrl, !ref.watch(photoViewSettingsProvider).autoPlay);
|
||||
await player.setVolume(ref.watch(photoViewSettingsProvider.select((value) => value.mute)) ? 0 : 100);
|
||||
await player.loop(ref.watch(photoViewSettingsProvider.select((value) => value.repeat)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:fladder/screens/settings/widgets/settings_label_divider.dart';
|
|||
import 'package:fladder/screens/settings/widgets/settings_list_group.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
|
||||
List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
|
||||
final clientSettings = ref.watch(clientSettingsProvider);
|
||||
|
|
@ -28,10 +29,9 @@ List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
|
|||
),
|
||||
itemBuilder: (context) => HomeBanner.values
|
||||
.map(
|
||||
(entry) => PopupMenuItem(
|
||||
value: entry,
|
||||
child: Text(entry.label(context)),
|
||||
onTap: () =>
|
||||
(entry) => ItemActionButton(
|
||||
label: Text(entry.label(context)),
|
||||
action: () =>
|
||||
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)),
|
||||
),
|
||||
)
|
||||
|
|
@ -48,10 +48,9 @@ List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
|
|||
),
|
||||
itemBuilder: (context) => HomeCarouselSettings.values
|
||||
.map(
|
||||
(entry) => PopupMenuItem(
|
||||
value: entry,
|
||||
child: Text(entry.label(context)),
|
||||
onTap: () => ref
|
||||
(entry) => ItemActionButton(
|
||||
label: Text(entry.label(context)),
|
||||
action: () => ref
|
||||
.read(homeSettingsProvider.notifier)
|
||||
.update((context) => context.copyWith(carouselSettings: entry)),
|
||||
),
|
||||
|
|
@ -70,10 +69,9 @@ List<Widget> buildClientSettingsDashboard(BuildContext context, WidgetRef ref) {
|
|||
),
|
||||
itemBuilder: (context) => HomeNextUp.values
|
||||
.map(
|
||||
(entry) => PopupMenuItem(
|
||||
value: entry,
|
||||
child: Text(entry.label(context)),
|
||||
onTap: () =>
|
||||
(entry) => ItemActionButton(
|
||||
label: Text(entry.label(context)),
|
||||
action: () =>
|
||||
ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -89,6 +89,21 @@ List<Widget> buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu
|
|||
return SettingsListTile(
|
||||
label: Text(context.localized.downloadsSyncedData),
|
||||
subLabel: Text(data.byteFormat ?? ""),
|
||||
onTap: () {
|
||||
showDefaultAlertDialog(
|
||||
context,
|
||||
context.localized.downloadsClearTitle,
|
||||
context.localized.downloadsClearDesc,
|
||||
(context) async {
|
||||
await ref.read(syncProvider.notifier).removeAllSyncedData();
|
||||
setState(() {});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
context.localized.clear,
|
||||
(context) => Navigator.of(context).pop(),
|
||||
context.localized.cancel,
|
||||
);
|
||||
},
|
||||
trailing: FilledButton(
|
||||
onPressed: () {
|
||||
showDefaultAlertDialog(
|
||||
|
|
@ -123,21 +138,22 @@ List<Widget> buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu
|
|||
label: Text(context.localized.maxConcurrentDownloadsTitle),
|
||||
subLabel: Text(context.localized.maxConcurrentDownloadsDesc),
|
||||
trailing: SizedBox(
|
||||
width: 100,
|
||||
child: IntInputField(
|
||||
controller: TextEditingController(text: clientSettings.maxConcurrentDownloads.toString()),
|
||||
onSubmitted: (value) {
|
||||
if (value != null) {
|
||||
ref.read(clientSettingsProvider.notifier).update(
|
||||
(current) => current.copyWith(
|
||||
maxConcurrentDownloads: value,
|
||||
),
|
||||
);
|
||||
width: 150,
|
||||
child: IntInputField(
|
||||
controller: TextEditingController(text: clientSettings.maxConcurrentDownloads.toString()),
|
||||
onSubmitted: (value) {
|
||||
if (value != null) {
|
||||
ref.read(clientSettingsProvider.notifier).update(
|
||||
(current) => current.copyWith(
|
||||
maxConcurrentDownloads: value,
|
||||
),
|
||||
);
|
||||
|
||||
ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value);
|
||||
}
|
||||
},
|
||||
)),
|
||||
ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
|||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/fladder_slider.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
|
||||
List<Widget> buildClientSettingsVisual(
|
||||
BuildContext context,
|
||||
|
|
@ -41,16 +42,15 @@ List<Widget> buildClientSettingsVisual(
|
|||
itemBuilder: (context) {
|
||||
return [
|
||||
...AppLocalizations.supportedLocales.map(
|
||||
(entry) => PopupMenuItem(
|
||||
value: entry,
|
||||
child: Localizations.override(
|
||||
(entry) => ItemActionButton(
|
||||
label: Localizations.override(
|
||||
context: context,
|
||||
locale: entry,
|
||||
child: Builder(builder: (context) {
|
||||
return Text("${context.localized.nativeName} (${entry.toDisplayCode()})");
|
||||
}),
|
||||
),
|
||||
onTap: () => ref
|
||||
action: () => ref
|
||||
.read(clientSettingsProvider.notifier)
|
||||
.update((state) => state.copyWith(selectedLocale: entry)),
|
||||
),
|
||||
|
|
@ -95,10 +95,9 @@ List<Widget> buildClientSettingsVisual(
|
|||
current: clientSettings.backgroundImage.label(context),
|
||||
itemBuilder: (context) => BackgroundType.values
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e,
|
||||
child: Text(e.label(context)),
|
||||
onTap: () =>
|
||||
(e) => ItemActionButton(
|
||||
label: Text(e.label(context)),
|
||||
action: () =>
|
||||
ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(backgroundImage: e)),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -94,6 +94,15 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
|
|||
...buildClientSettingsAdvanced(context, ref),
|
||||
if (kDebugMode) ...[
|
||||
const SizedBox(height: 64),
|
||||
SettingsListTile(
|
||||
label: const Text(
|
||||
"Clear cache",
|
||||
),
|
||||
contentColor: Theme.of(context).colorScheme.error,
|
||||
onTap: () {
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
},
|
||||
),
|
||||
SettingsListTile(
|
||||
label: Text(
|
||||
context.localized.clearAllSettings,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
|
||||
import 'package:fladder/models/items/media_segments_model.dart';
|
||||
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||
import 'package:fladder/providers/arguments_provider.dart';
|
||||
import 'package:fladder/providers/connectivity_provider.dart';
|
||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
|
|
@ -27,6 +28,7 @@ import 'package:fladder/util/bitrate_helper.dart';
|
|||
import 'package:fladder/util/box_fit_extension.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PlayerSettingsPage extends ConsumerStatefulWidget {
|
||||
|
|
@ -81,10 +83,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
current: videoSettings.videoFit.label(context),
|
||||
itemBuilder: (context) => BoxFit.values
|
||||
.map(
|
||||
(entry) => PopupMenuItem(
|
||||
value: entry,
|
||||
child: Text(entry.label(context)),
|
||||
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(entry),
|
||||
(entry) => ItemActionButton(
|
||||
label: Text(entry.label(context)),
|
||||
action: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(entry),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
@ -102,10 +103,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
),
|
||||
itemBuilder: (context) => Bitrate.values
|
||||
.map(
|
||||
(entry) => PopupMenuItem(
|
||||
value: entry,
|
||||
child: Text(entry.label(context)),
|
||||
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
(entry) => ItemActionButton(
|
||||
label: Text(entry.label(context)),
|
||||
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
videoSettings.copyWith(maxHomeBitrate: entry),
|
||||
),
|
||||
)
|
||||
|
|
@ -124,10 +124,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
),
|
||||
itemBuilder: (context) => Bitrate.values
|
||||
.map(
|
||||
(entry) => PopupMenuItem(
|
||||
value: entry,
|
||||
child: Text(entry.label(context)),
|
||||
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
(entry) => ItemActionButton(
|
||||
label: Text(entry.label(context)),
|
||||
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
videoSettings.copyWith(maxInternetBitrate: entry),
|
||||
),
|
||||
)
|
||||
|
|
@ -153,10 +152,9 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
current: entry.value.label(context),
|
||||
itemBuilder: (context) => SegmentSkip.values
|
||||
.map(
|
||||
(value) => PopupMenuItem(
|
||||
value: value,
|
||||
child: Text(value.label(context)),
|
||||
onTap: () {
|
||||
(value) => ItemActionButton(
|
||||
label: Text(value.label(context)),
|
||||
action: () {
|
||||
final newEntries = videoSettings.segmentSkipSettings.map(
|
||||
(key, currentValue) => MapEntry(key, key == entry.key ? value : currentValue));
|
||||
ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
|
|
@ -264,145 +262,151 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
...settingsListGroup(context, SettingsLabelDivider(label: context.localized.advanced), [
|
||||
if (PlayerOptions.available.length != 1)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.playerSettingsBackendTitle),
|
||||
subLabel: Text(context.localized.playerSettingsBackendDesc),
|
||||
trailing: Builder(builder: (context) {
|
||||
final wantedPlayer = videoSettings.wantedPlayer;
|
||||
final currentPlayer = videoSettings.playerOptions;
|
||||
return EnumBox(
|
||||
current: currentPlayer == null
|
||||
? "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"
|
||||
: wantedPlayer.label(context),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: null,
|
||||
child:
|
||||
Text("${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"),
|
||||
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
videoSettings.copyWith(playerOptions: null),
|
||||
),
|
||||
...PlayerOptions.available.map(
|
||||
(entry) => PopupMenuItem(
|
||||
value: entry,
|
||||
child: Text(entry.label(context)),
|
||||
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
videoSettings.copyWith(playerOptions: entry),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
AnimatedFadeSize(
|
||||
child: switch (videoSettings.wantedPlayer) {
|
||||
PlayerOptions.libMPV => Column(
|
||||
children: [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
|
||||
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
|
||||
trailing: Switch(
|
||||
value: videoSettings.hardwareAccel,
|
||||
onChanged: (value) => provider.setHardwareAccel(value),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
|
||||
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
|
||||
trailing: Switch(
|
||||
value: videoSettings.useLibass,
|
||||
onChanged: (value) => provider.setUseLibass(value),
|
||||
...settingsListGroup(
|
||||
context,
|
||||
SettingsLabelDivider(label: context.localized.advanced),
|
||||
[
|
||||
if (!ref.read(argumentsStateProvider).leanBackMode) ...[
|
||||
if (PlayerOptions.available.length != 1)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.playerSettingsBackendTitle),
|
||||
subLabel: Text(context.localized.playerSettingsBackendDesc),
|
||||
trailing: Builder(builder: (context) {
|
||||
final wantedPlayer = videoSettings.wantedPlayer;
|
||||
final currentPlayer = videoSettings.playerOptions;
|
||||
return EnumBox(
|
||||
current: currentPlayer == null
|
||||
? "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"
|
||||
: wantedPlayer.label(context),
|
||||
itemBuilder: (context) => [
|
||||
ItemActionButton(
|
||||
label: Text(
|
||||
"${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"),
|
||||
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
videoSettings.copyWith(playerOptions: null),
|
||||
),
|
||||
),
|
||||
if (!videoSettings.useLibass)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
|
||||
onTap: videoSettings.useLibass
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
useSafeArea: false,
|
||||
builder: (context) => const SubtitleEditor(),
|
||||
);
|
||||
},
|
||||
),
|
||||
AnimatedFadeSize(
|
||||
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
|
||||
? SettingsMessageBox(
|
||||
context.localized.settingsPlayerMobileWarning,
|
||||
messageType: MessageType.warning,
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerBufferSizeTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerBufferSizeDesc),
|
||||
trailing: SizedBox(
|
||||
width: 70,
|
||||
child: IntInputField(
|
||||
suffix: 'MB',
|
||||
controller: TextEditingController(text: videoSettings.bufferSize.toString()),
|
||||
onSubmitted: (value) {
|
||||
if (value != null) {
|
||||
provider.setBufferSize(value);
|
||||
}
|
||||
},
|
||||
)),
|
||||
),
|
||||
],
|
||||
...PlayerOptions.available.map(
|
||||
(entry) => ItemActionButton(
|
||||
label: Text(entry.label(context)),
|
||||
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
videoSettings.copyWith(playerOptions: entry),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
_ => SettingsMessageBox(
|
||||
messageType: MessageType.info,
|
||||
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}")
|
||||
},
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
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 =
|
||||
videoSettings.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(),
|
||||
child: switch (videoSettings.wantedPlayer) {
|
||||
PlayerOptions.libMPV => Column(
|
||||
children: [
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
|
||||
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
|
||||
trailing: Switch(
|
||||
value: videoSettings.hardwareAccel,
|
||||
onChanged: (value) => provider.setHardwareAccel(value),
|
||||
),
|
||||
),
|
||||
if (!kIsWeb)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
|
||||
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
|
||||
trailing: Switch(
|
||||
value: videoSettings.useLibass,
|
||||
onChanged: (value) => provider.setUseLibass(value),
|
||||
),
|
||||
),
|
||||
if (!videoSettings.useLibass)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
|
||||
onTap: videoSettings.useLibass
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
useSafeArea: false,
|
||||
builder: (context) => const SubtitleEditor(),
|
||||
);
|
||||
},
|
||||
),
|
||||
AnimatedFadeSize(
|
||||
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
|
||||
? SettingsMessageBox(
|
||||
context.localized.settingsPlayerMobileWarning,
|
||||
messageType: MessageType.warning,
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerBufferSizeTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerBufferSizeDesc),
|
||||
trailing: SizedBox(
|
||||
width: 70,
|
||||
child: IntInputField(
|
||||
suffix: 'MB',
|
||||
controller: TextEditingController(text: videoSettings.bufferSize.toString()),
|
||||
onSubmitted: (value) {
|
||||
if (value != null) {
|
||||
provider.setBufferSize(value);
|
||||
}
|
||||
},
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
PlayerOptions.libMDK => SettingsMessageBox(
|
||||
messageType: MessageType.info,
|
||||
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}"),
|
||||
_ => const SizedBox.shrink()
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.playerSettingsOrientationTitle),
|
||||
subLabel: Text(context.localized.playerSettingsOrientationDesc),
|
||||
onTap: () => showOrientationOptions(context, ref),
|
||||
),
|
||||
]),
|
||||
if (videoSettings.wantedPlayer != PlayerOptions.nativePlayer) ...[
|
||||
Column(
|
||||
children: [
|
||||
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) => ItemActionButton(
|
||||
label: Text(entry.label(context)),
|
||||
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||
videoSettings.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(),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb && !ref.read(argumentsStateProvider).htpcMode)
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.playerSettingsOrientationTitle),
|
||||
subLabel: Text(context.localized.playerSettingsOrientationDesc),
|
||||
onTap: () => showOrientationOptions(context, ref),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/login/widgets/login_icon.dart';
|
||||
import 'package:fladder/screens/shared/outlined_text_field.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
Future<void> openQuickConnectDialog(
|
||||
BuildContext context,
|
||||
|
|
@ -30,18 +31,28 @@ class _QuickConnectDialogState extends ConsumerState<QuickConnectDialog> {
|
|||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(userProvider);
|
||||
return AlertDialog(
|
||||
title: Text(context.localized.quickConnectTitle),
|
||||
title: Text(
|
||||
context.localized.quickConnectTitle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
scrollable: true,
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 12,
|
||||
children: [
|
||||
Text(context.localized.quickConnectAction),
|
||||
Text(
|
||||
context.localized.quickConnectAction,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (user != null) SizedBox(child: LoginIcon(user: user)),
|
||||
Flexible(
|
||||
child: OutlinedTextField(
|
||||
label: context.localized.code,
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.go,
|
||||
onChanged: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
setState(() {
|
||||
|
|
@ -50,6 +61,7 @@ class _QuickConnectDialogState extends ConsumerState<QuickConnectDialog> {
|
|||
});
|
||||
}
|
||||
},
|
||||
onSubmitted: (value) => tryLogin(),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
),
|
||||
),
|
||||
|
|
@ -58,50 +70,24 @@ class _QuickConnectDialogState extends ConsumerState<QuickConnectDialog> {
|
|||
child: error != null || success != null
|
||||
? Card(
|
||||
key: Key(context.localized.error),
|
||||
color: success == null ? Theme.of(context).colorScheme.errorContainer : Theme.of(context).colorScheme.surfaceContainer,
|
||||
color: success == null
|
||||
? Theme.of(context).colorScheme.errorContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
success ?? error ?? "",
|
||||
style: TextStyle(
|
||||
color:
|
||||
success == null ? Theme.of(context).colorScheme.onErrorContainer : Theme.of(context).colorScheme.onSurface),
|
||||
color: success == null
|
||||
? Theme.of(context).colorScheme.onErrorContainer
|
||||
: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: loading
|
||||
? null
|
||||
: () async {
|
||||
setState(() {
|
||||
error = null;
|
||||
loading = true;
|
||||
});
|
||||
final response = await ref.read(userProvider.notifier).quickConnect(controller.text);
|
||||
if (response.isSuccessful) {
|
||||
setState(
|
||||
() {
|
||||
error = null;
|
||||
success = context.localized.loggedIn;
|
||||
},
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
if (controller.text.isEmpty) {
|
||||
error = context.localized.quickConnectInputACode;
|
||||
} else {
|
||||
error = context.localized.quickConnectWrongCode;
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
setState(
|
||||
() {},
|
||||
);
|
||||
controller.text = "";
|
||||
},
|
||||
FilledButton(
|
||||
onPressed: loading ? null : () => tryLogin(),
|
||||
child: loading
|
||||
? const SizedBox.square(
|
||||
child: CircularProgressIndicator(),
|
||||
|
|
@ -109,8 +95,37 @@ class _QuickConnectDialogState extends ConsumerState<QuickConnectDialog> {
|
|||
)
|
||||
: Text(context.localized.login),
|
||||
)
|
||||
].addInBetween(const SizedBox(height: 16)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tryLogin() async {
|
||||
setState(() {
|
||||
error = null;
|
||||
loading = true;
|
||||
});
|
||||
final response = await ref.read(userProvider.notifier).quickConnect(controller.text);
|
||||
if (response.isSuccessful) {
|
||||
setState(
|
||||
() {
|
||||
error = null;
|
||||
success = context.localized.loggedIn;
|
||||
},
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
if (controller.text.isEmpty) {
|
||||
error = context.localized.quickConnectInputACode;
|
||||
} else {
|
||||
error = context.localized.quickConnectWrongCode;
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
setState(
|
||||
() {},
|
||||
);
|
||||
controller.text = "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
||||
|
||||
class SettingsListTile extends StatelessWidget {
|
||||
final Widget label;
|
||||
final Widget? subLabel;
|
||||
final Widget? trailing;
|
||||
final bool selected;
|
||||
final bool autoFocus;
|
||||
final IconData? icon;
|
||||
final Widget? leading;
|
||||
final Color? contentColor;
|
||||
|
|
@ -16,6 +18,7 @@ class SettingsListTile extends StatelessWidget {
|
|||
this.subLabel,
|
||||
this.trailing,
|
||||
this.selected = false,
|
||||
this.autoFocus = false,
|
||||
this.leading,
|
||||
this.icon,
|
||||
this.contentColor,
|
||||
|
|
@ -52,6 +55,12 @@ class SettingsListTile extends StatelessWidget {
|
|||
margin: EdgeInsets.zero,
|
||||
child: FlatButton(
|
||||
onTap: onTap,
|
||||
autoFocus: autoFocus,
|
||||
onFocusChange: (value) {
|
||||
if (value) {
|
||||
context.ensureVisible();
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
|
|
@ -66,6 +75,7 @@ class SettingsListTile extends StatelessWidget {
|
|||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
DefaultTextStyle.merge(
|
||||
|
|
@ -85,7 +95,7 @@ class SettingsListTile extends StatelessWidget {
|
|||
children: [
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||
textStyle: Theme.of(context).textTheme.titleLarge?.copyWith(color: contentColor),
|
||||
child: label,
|
||||
),
|
||||
if (subLabel != null)
|
||||
|
|
@ -93,7 +103,7 @@ class SettingsListTile extends StatelessWidget {
|
|||
opacity: 0.65,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
textStyle: Theme.of(context).textTheme.labelLarge?.copyWith(color: contentColor),
|
||||
child: subLabel,
|
||||
),
|
||||
),
|
||||
|
|
@ -101,9 +111,12 @@ class SettingsListTile extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
if (trailing != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: trailing,
|
||||
ExcludeFocusTraversal(
|
||||
excluding: onTap != null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: trailing,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/providers/arguments_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/shared/user_icon.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
|
|
@ -76,7 +77,7 @@ class SettingsScaffold extends ConsumerWidget {
|
|||
padding: MediaQuery.paddingOf(context).copyWith(bottom: 0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (showBackButtonNested)
|
||||
if (showBackButtonNested && !ref.read(argumentsStateProvider).htpcMode)
|
||||
BackButton(
|
||||
onPressed: () => backAction(context),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
|
@ -13,6 +14,7 @@ import 'package:fladder/routes/auto_router.gr.dart';
|
|||
import 'package:fladder/screens/settings/quick_connect_window.dart';
|
||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
||||
import 'package:fladder/screens/shared/default_alert_dialog.dart';
|
||||
import 'package:fladder/screens/shared/fladder_icon.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
|
|
@ -55,9 +57,9 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(flex: 1, child: _leftPane(context)),
|
||||
Expanded(flex: 2, child: _leftPane(context)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
flex: 3,
|
||||
child: content,
|
||||
),
|
||||
],
|
||||
|
|
@ -88,6 +90,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
return IconsaxPlusLinear.monitor;
|
||||
case ViewSize.desktop:
|
||||
return IconsaxPlusLinear.monitor;
|
||||
case ViewSize.television:
|
||||
return IconsaxPlusLinear.mirroring_screen;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,6 +133,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
SettingsListTile(
|
||||
label: Text(context.localized.settingsClientTitle),
|
||||
subLabel: Text(context.localized.settingsClientDesc),
|
||||
autoFocus: true,
|
||||
selected: containsRoute(const ClientSettingsRoute()),
|
||||
icon: deviceIcon,
|
||||
onTap: () => navigateTo(const ClientSettingsRoute()),
|
||||
|
|
@ -171,83 +176,81 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||
label: Text(context.localized.exitFladderTitle),
|
||||
icon: IconsaxPlusLinear.close_square,
|
||||
onTap: () async {
|
||||
final manager = WindowManager.instance;
|
||||
if (await manager.isClosable()) {
|
||||
manager.close();
|
||||
} else {
|
||||
fladderSnackbar(context, title: context.localized.somethingWentWrong);
|
||||
}
|
||||
showDefaultAlertDialog(
|
||||
context,
|
||||
context.localized.exitFladderTitle,
|
||||
context.localized.exitFladderDesc,
|
||||
(context) async {
|
||||
if (AdaptiveLayout.of(context).isDesktop) {
|
||||
final manager = WindowManager.instance;
|
||||
if (await manager.isClosable()) {
|
||||
manager.close();
|
||||
} else {
|
||||
fladderSnackbar(context, title: context.localized.somethingWentWrong);
|
||||
}
|
||||
} else {
|
||||
SystemNavigator.pop();
|
||||
}
|
||||
},
|
||||
context.localized.close,
|
||||
(context) => context.pop(),
|
||||
context.localized.cancel,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
floatingActionButton: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: MediaQuery.paddingOf(context).horizontal),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const Spacer(),
|
||||
FloatingActionButton(
|
||||
key: Key(context.localized.switchUser),
|
||||
tooltip: context.localized.switchUser,
|
||||
onPressed: () async {
|
||||
await ref.read(userProvider.notifier).logoutUser();
|
||||
context.router.replaceAll([const LoginRoute()]);
|
||||
},
|
||||
child: const Icon(
|
||||
IconsaxPlusLinear.arrow_swap_horizontal,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
FloatingActionButton(
|
||||
heroTag: context.localized.logout,
|
||||
key: Key(context.localized.logout),
|
||||
tooltip: context.localized.logout,
|
||||
backgroundColor: Theme.of(context).colorScheme.errorContainer,
|
||||
onPressed: () {
|
||||
final user = ref.read(userProvider);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")),
|
||||
scrollable: true,
|
||||
content: Text(
|
||||
context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""),
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.localized.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom().copyWith(
|
||||
iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
|
||||
foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
|
||||
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer),
|
||||
),
|
||||
onPressed: () async {
|
||||
await ref.read(authProvider.notifier).logOutUser();
|
||||
if (context.mounted) {
|
||||
context.router.replaceAll([const LoginRoute()]);
|
||||
}
|
||||
},
|
||||
child: Text(context.localized.logout),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
IconsaxPlusLinear.logout,
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const FractionallySizedBox(
|
||||
widthFactor: 0.25,
|
||||
child: Divider(),
|
||||
),
|
||||
),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.switchUser),
|
||||
icon: IconsaxPlusLinear.arrow_swap_horizontal,
|
||||
contentColor: Colors.greenAccent,
|
||||
onTap: () async {
|
||||
await ref.read(userProvider.notifier).logoutUser();
|
||||
context.router.replaceAll([const LoginRoute()]);
|
||||
},
|
||||
),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.logout),
|
||||
icon: IconsaxPlusLinear.logout,
|
||||
contentColor: Theme.of(context).colorScheme.error,
|
||||
onTap: () {
|
||||
final user = ref.read(userProvider);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")),
|
||||
scrollable: true,
|
||||
content: Text(
|
||||
context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""),
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.localized.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom().copyWith(
|
||||
iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
|
||||
foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
|
||||
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer),
|
||||
),
|
||||
onPressed: () async {
|
||||
await ref.read(authProvider.notifier).logOutUser();
|
||||
if (context.mounted) {
|
||||
context.router.replaceAll([const LoginRoute()]);
|
||||
}
|
||||
},
|
||||
child: Text(context.localized.logout),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/providers/arguments_provider.dart';
|
||||
|
||||
Future<void> showDefaultAlertDialog(
|
||||
BuildContext context,
|
||||
String title,
|
||||
|
|
@ -18,9 +22,12 @@ Future<void> showDefaultAlertDialog(
|
|||
content: content != null ? Text(content) : null,
|
||||
actions: [
|
||||
if (decline != null)
|
||||
ElevatedButton(
|
||||
onPressed: () => decline.call(context),
|
||||
child: Text(declineTitle),
|
||||
Consumer(
|
||||
builder: (context, ref, child) => ElevatedButton(
|
||||
autofocus: ref.read(argumentsStateProvider).htpcMode,
|
||||
onPressed: () => decline.call(context),
|
||||
child: Text(declineTitle),
|
||||
),
|
||||
),
|
||||
if (accept != null)
|
||||
ElevatedButton(
|
||||
|
|
|
|||
|
|
@ -44,167 +44,171 @@ class _DefaultTitleBarState extends ConsumerState<DefaultTitleBar> with WindowLi
|
|||
final isOffline = ref.watch(connectivityStatusProvider.select((value) => value == ConnectionState.offline));
|
||||
final surfaceColor = theme.colorScheme.surface;
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (event) => setState(() => hovering = true),
|
||||
onExit: (event) => setState(() => hovering = false),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: isOffline
|
||||
? [
|
||||
theme.colorScheme.errorContainer.withValues(alpha: 0.8),
|
||||
theme.colorScheme.errorContainer.withValues(alpha: 0.25),
|
||||
]
|
||||
: [
|
||||
surfaceColor.withValues(alpha: hovering ? 0.7 : 0),
|
||||
surfaceColor.withValues(alpha: 0),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
)),
|
||||
height: widget.height,
|
||||
child: kIsWeb
|
||||
? const SizedBox.shrink()
|
||||
: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
switch (AdaptiveLayout.of(context).platform) {
|
||||
TargetPlatform.android || TargetPlatform.iOS => SizedBox(height: MediaQuery.paddingOf(context).top),
|
||||
TargetPlatform.windows || TargetPlatform.linux => Container(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0),
|
||||
child: DragToMoveArea(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
color: iconColor,
|
||||
fontSize: 14,
|
||||
return ExcludeFocus(
|
||||
child: MouseRegion(
|
||||
onEnter: (event) => setState(() => hovering = true),
|
||||
onExit: (event) => setState(() => hovering = false),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: isOffline
|
||||
? [
|
||||
theme.colorScheme.errorContainer.withValues(alpha: 0.8),
|
||||
theme.colorScheme.errorContainer.withValues(alpha: 0.25),
|
||||
]
|
||||
: [
|
||||
surfaceColor.withValues(alpha: hovering ? 0.7 : 0),
|
||||
surfaceColor.withValues(alpha: 0),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
)),
|
||||
height: widget.height,
|
||||
child: kIsWeb
|
||||
? const SizedBox.shrink()
|
||||
: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
switch (AdaptiveLayout.of(context).platform) {
|
||||
TargetPlatform.android ||
|
||||
TargetPlatform.iOS =>
|
||||
SizedBox(height: MediaQuery.paddingOf(context).top),
|
||||
TargetPlatform.windows || TargetPlatform.linux => Container(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0),
|
||||
child: DragToMoveArea(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
color: iconColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
child: Text(widget.label ?? ""),
|
||||
),
|
||||
child: Text(widget.label ?? ""),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(boxShadow: [
|
||||
BoxShadow(
|
||||
color: surfaceColor.withValues(alpha: isOffline ? 0 : 0.5),
|
||||
blurRadius: 32,
|
||||
spreadRadius: 10,
|
||||
offset: const Offset(8, -6),
|
||||
),
|
||||
]),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder<List<bool>>(future: Future.microtask(() async {
|
||||
final isMinimized = await windowManager.isMinimized();
|
||||
return [isMinimized];
|
||||
}), builder: (context, snapshot) {
|
||||
final isMinimized = snapshot.data?.firstOrNull ?? false;
|
||||
return IconButton(
|
||||
Container(
|
||||
decoration: BoxDecoration(boxShadow: [
|
||||
BoxShadow(
|
||||
color: surfaceColor.withValues(alpha: isOffline ? 0 : 0.5),
|
||||
blurRadius: 32,
|
||||
spreadRadius: 10,
|
||||
offset: const Offset(8, -6),
|
||||
),
|
||||
]),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder<List<bool>>(future: Future.microtask(() async {
|
||||
final isMinimized = await windowManager.isMinimized();
|
||||
return [isMinimized];
|
||||
}), builder: (context, snapshot) {
|
||||
final isMinimized = snapshot.data?.firstOrNull ?? false;
|
||||
return IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
hoverColor: brightness == Brightness.light
|
||||
? Colors.black.withValues(alpha: 0.1)
|
||||
: Colors.white.withValues(alpha: 0.2),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2))),
|
||||
onPressed: () async {
|
||||
fullScreenHelper.closeFullScreen(ref);
|
||||
if (isMinimized) {
|
||||
windowManager.restore();
|
||||
} else {
|
||||
windowManager.minimize();
|
||||
}
|
||||
},
|
||||
icon: Transform.translate(
|
||||
offset: const Offset(0, -2),
|
||||
child: Icon(
|
||||
Icons.minimize_rounded,
|
||||
color: iconColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
FutureBuilder<List<bool>>(
|
||||
future: Future.microtask(() async {
|
||||
final isMaximized = await windowManager.isMaximized();
|
||||
return [isMaximized];
|
||||
}),
|
||||
builder: (BuildContext context, AsyncSnapshot<List<bool>> snapshot) {
|
||||
final maximized = snapshot.data?.firstOrNull ?? false;
|
||||
return IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
hoverColor: brightness == Brightness.light
|
||||
? Colors.black.withValues(alpha: 0.1)
|
||||
: Colors.white.withValues(alpha: 0.2),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
onPressed: () async {
|
||||
fullScreenHelper.closeFullScreen(ref);
|
||||
if (maximized) {
|
||||
await windowManager.unmaximize();
|
||||
return;
|
||||
}
|
||||
if (!maximized) {
|
||||
await windowManager.maximize();
|
||||
} else {
|
||||
await windowManager.unmaximize();
|
||||
}
|
||||
},
|
||||
icon: Transform.translate(
|
||||
offset: const Offset(0, 0),
|
||||
child: Icon(
|
||||
maximized ? Icons.maximize_rounded : Icons.crop_square_rounded,
|
||||
color: iconColor,
|
||||
size: 19,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
hoverColor: brightness == Brightness.light
|
||||
? Colors.black.withValues(alpha: 0.1)
|
||||
: Colors.white.withValues(alpha: 0.2),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2))),
|
||||
hoverColor: Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
fullScreenHelper.closeFullScreen(ref);
|
||||
if (isMinimized) {
|
||||
windowManager.restore();
|
||||
} else {
|
||||
windowManager.minimize();
|
||||
}
|
||||
windowManager.close();
|
||||
},
|
||||
icon: Transform.translate(
|
||||
offset: const Offset(0, -2),
|
||||
child: Icon(
|
||||
Icons.minimize_rounded,
|
||||
Icons.close_rounded,
|
||||
color: iconColor,
|
||||
size: 20,
|
||||
size: 23,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
FutureBuilder<List<bool>>(
|
||||
future: Future.microtask(() async {
|
||||
final isMaximized = await windowManager.isMaximized();
|
||||
return [isMaximized];
|
||||
}),
|
||||
builder: (BuildContext context, AsyncSnapshot<List<bool>> snapshot) {
|
||||
final maximized = snapshot.data?.firstOrNull ?? false;
|
||||
return IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
hoverColor: brightness == Brightness.light
|
||||
? Colors.black.withValues(alpha: 0.1)
|
||||
: Colors.white.withValues(alpha: 0.2),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
onPressed: () async {
|
||||
fullScreenHelper.closeFullScreen(ref);
|
||||
if (maximized) {
|
||||
await windowManager.unmaximize();
|
||||
return;
|
||||
}
|
||||
if (!maximized) {
|
||||
await windowManager.maximize();
|
||||
} else {
|
||||
await windowManager.unmaximize();
|
||||
}
|
||||
},
|
||||
icon: Transform.translate(
|
||||
offset: const Offset(0, 0),
|
||||
child: Icon(
|
||||
maximized ? Icons.maximize_rounded : Icons.crop_square_rounded,
|
||||
color: iconColor,
|
||||
size: 19,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
hoverColor: Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
windowManager.close();
|
||||
},
|
||||
icon: Transform.translate(
|
||||
offset: const Offset(0, -2),
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
color: iconColor,
|
||||
size: 23,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
TargetPlatform.macOS => const SizedBox.shrink(),
|
||||
_ => Text(widget.label ?? "Fladder"),
|
||||
},
|
||||
const OfflineBanner()
|
||||
],
|
||||
),
|
||||
TargetPlatform.macOS => const SizedBox.shrink(),
|
||||
_ => Text(widget.label ?? "Fladder"),
|
||||
},
|
||||
const OfflineBanner()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,17 +63,20 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final padding = EdgeInsets.symmetric(horizontal: MediaQuery.sizeOf(context).width / 25);
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final padding = EdgeInsets.symmetric(horizontal: size.width / 25);
|
||||
final backGroundColor = Theme.of(context).colorScheme.surface.withValues(alpha: 0.8);
|
||||
final minHeight = 450.0.clamp(0, MediaQuery.sizeOf(context).height).toDouble();
|
||||
final maxHeight = MediaQuery.sizeOf(context).height - 10;
|
||||
final minHeight = 450.0.clamp(0, size.height).toDouble();
|
||||
final maxHeight = size.height - 10;
|
||||
final sideBarPadding = AdaptiveLayout.of(context).sideBarWidth;
|
||||
return PullToRefresh(
|
||||
onRefresh: () async {
|
||||
await widget.onRefresh?.call();
|
||||
setState(() {
|
||||
if (widget.backDrops?.backDrop?.contains(backgroundImage) == true) {
|
||||
backgroundImage = widget.backDrops?.randomBackDrop;
|
||||
if (context.mounted) {
|
||||
if (widget.backDrops?.backDrop?.contains(backgroundImage) == true) {
|
||||
backgroundImage = widget.backDrops?.randomBackDrop;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -89,7 +92,7 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
|
|||
children: [
|
||||
SizedBox(
|
||||
height: maxHeight,
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
width: size.width,
|
||||
child: FladderImage(
|
||||
image: backgroundImage,
|
||||
blurOnly: true,
|
||||
|
|
@ -120,14 +123,19 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
|
|||
maxHeight: maxHeight.clamp(minHeight, 2500) - 20,
|
||||
),
|
||||
child: FadeInImage(
|
||||
placeholder: backgroundImage!.imageProvider,
|
||||
placeholder: ResizeImage(
|
||||
backgroundImage!.imageProvider,
|
||||
height: maxHeight ~/ 1.5,
|
||||
),
|
||||
placeholderColor: Colors.transparent,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
placeholderFit: BoxFit.cover,
|
||||
excludeFromSemantics: true,
|
||||
placeholderFilterQuality: FilterQuality.low,
|
||||
image: backgroundImage!.imageProvider,
|
||||
image: ResizeImage(
|
||||
backgroundImage!.imageProvider,
|
||||
height: maxHeight ~/ 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -151,8 +159,8 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
|
|||
),
|
||||
),
|
||||
Container(
|
||||
height: MediaQuery.sizeOf(context).height,
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
color: widget.backgroundColor,
|
||||
),
|
||||
Padding(
|
||||
|
|
@ -160,141 +168,148 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
|
|||
bottom: 0,
|
||||
top: MediaQuery.of(context).padding.top,
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: MediaQuery.sizeOf(context).height,
|
||||
maxWidth: MediaQuery.sizeOf(context).width,
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: size.height,
|
||||
maxWidth: size.width,
|
||||
),
|
||||
child: widget.content(
|
||||
padding.copyWith(
|
||||
left: sideBarPadding + 25 + MediaQuery.paddingOf(context).left,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: widget.content(padding.copyWith(
|
||||
left: sideBarPadding + 25 + MediaQuery.paddingOf(context).left,
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
//Top row buttons
|
||||
IconTheme(
|
||||
data: IconThemeData(color: Theme.of(context).colorScheme.onSurface),
|
||||
child: Padding(
|
||||
padding: MediaQuery.paddingOf(context)
|
||||
.copyWith(left: sideBarPadding + MediaQuery.paddingOf(context).left)
|
||||
.add(
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: backGroundColor,
|
||||
if (AdaptiveLayout.of(context).viewSize < ViewSize.desktop)
|
||||
IconTheme(
|
||||
data: IconThemeData(color: Theme.of(context).colorScheme.onSurface),
|
||||
child: Padding(
|
||||
padding: MediaQuery.paddingOf(context)
|
||||
.copyWith(left: sideBarPadding + MediaQuery.paddingOf(context).left)
|
||||
.add(
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
onPressed: () => context.router.popBack(),
|
||||
icon: Padding(
|
||||
padding: EdgeInsets.all(AdaptiveLayout.of(context).inputDevice == InputDevice.pointer ? 0 : 4),
|
||||
child: const BackButtonIcon(),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Container(
|
||||
decoration:
|
||||
BoxDecoration(color: backGroundColor, borderRadius: FladderTheme.defaultShape.borderRadius),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.item != null) ...[
|
||||
ref.watch(syncedItemProvider(widget.item)).when(
|
||||
error: (error, stackTrace) => const SizedBox.shrink(),
|
||||
data: (syncedItem) {
|
||||
if (syncedItem == null &&
|
||||
ref.read(userProvider.select(
|
||||
(value) => value?.canDownload ?? false,
|
||||
)) &&
|
||||
widget.item?.syncAble == true) {
|
||||
return IconButton(
|
||||
onPressed: () =>
|
||||
ref.read(syncProvider.notifier).addSyncItem(context, widget.item!),
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.arrow_down_2,
|
||||
),
|
||||
);
|
||||
} else if (syncedItem != null) {
|
||||
return IconButton(
|
||||
onPressed: () => showSyncItemDetails(context, syncedItem, ref),
|
||||
icon: SyncButton(item: widget.item!, syncedItem: syncedItem),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final newActions = widget.actions?.call(context);
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) {
|
||||
return PopupMenuButton(
|
||||
tooltip: context.localized.moreOptions,
|
||||
enabled: newActions?.isNotEmpty == true,
|
||||
icon: Icon(
|
||||
widget.item!.type.icon,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
itemBuilder: (context) => newActions?.popupMenuItems(useIcons: true) ?? [],
|
||||
);
|
||||
} else {
|
||||
return IconButton(
|
||||
onPressed: () => showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
controller: scrollController,
|
||||
shrinkWrap: true,
|
||||
children: newActions?.listTileItems(context, useIcons: true) ?? [],
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
widget.item!.type.icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer)
|
||||
Builder(
|
||||
builder: (context) => Tooltip(
|
||||
message: context.localized.refresh,
|
||||
child: IconButton(
|
||||
onPressed: () => context.refreshData(),
|
||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single ||
|
||||
AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: const SizedBox(
|
||||
height: 30,
|
||||
width: 30,
|
||||
child: SettingsUserIcon(),
|
||||
),
|
||||
),
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single)
|
||||
Tooltip(
|
||||
message: context.localized.home,
|
||||
child: IconButton(
|
||||
onPressed: () => context.navigateTo(const DashboardRoute()),
|
||||
icon: const Icon(IconsaxPlusLinear.home),
|
||||
)),
|
||||
],
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: backGroundColor,
|
||||
),
|
||||
onPressed: () => context.router.popBack(),
|
||||
icon: Padding(
|
||||
padding:
|
||||
EdgeInsets.all(AdaptiveLayout.of(context).inputDevice == InputDevice.pointer ? 0 : 4),
|
||||
child: const BackButtonIcon(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backGroundColor, borderRadius: FladderTheme.defaultShape.borderRadius),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.item != null) ...[
|
||||
ref.watch(syncedItemProvider(widget.item)).when(
|
||||
error: (error, stackTrace) => const SizedBox.shrink(),
|
||||
data: (syncedItem) {
|
||||
if (syncedItem == null &&
|
||||
ref.read(userProvider.select(
|
||||
(value) => value?.canDownload ?? false,
|
||||
)) &&
|
||||
widget.item?.syncAble == true) {
|
||||
return IconButton(
|
||||
onPressed: () =>
|
||||
ref.read(syncProvider.notifier).addSyncItem(context, widget.item!),
|
||||
icon: const Icon(
|
||||
IconsaxPlusLinear.arrow_down_2,
|
||||
),
|
||||
);
|
||||
} else if (syncedItem != null) {
|
||||
return IconButton(
|
||||
onPressed: () => showSyncItemDetails(context, syncedItem, ref),
|
||||
icon: SyncButton(item: widget.item!, syncedItem: syncedItem),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final newActions = widget.actions?.call(context);
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) {
|
||||
return PopupMenuButton(
|
||||
tooltip: context.localized.moreOptions,
|
||||
enabled: newActions?.isNotEmpty == true,
|
||||
icon: Icon(
|
||||
widget.item!.type.icon,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
itemBuilder: (context) => newActions?.popupMenuItems(useIcons: true) ?? [],
|
||||
);
|
||||
} else {
|
||||
return IconButton(
|
||||
onPressed: () => showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
controller: scrollController,
|
||||
shrinkWrap: true,
|
||||
children: newActions?.listTileItems(context, useIcons: true) ?? [],
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
widget.item!.type.icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer)
|
||||
Builder(
|
||||
builder: (context) => Tooltip(
|
||||
message: context.localized.refresh,
|
||||
child: IconButton(
|
||||
onPressed: () => context.refreshData(),
|
||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single ||
|
||||
AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: const SizedBox(
|
||||
height: 30,
|
||||
width: 30,
|
||||
child: SettingsUserIcon(),
|
||||
),
|
||||
),
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single)
|
||||
Tooltip(
|
||||
message: context.localized.home,
|
||||
child: IconButton(
|
||||
onPressed: () => context.navigateTo(const DashboardRoute()),
|
||||
icon: const Icon(IconsaxPlusLinear.home),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import 'package:fladder/theme.dart';
|
|||
|
||||
class FlatButton extends ConsumerWidget {
|
||||
final Widget? child;
|
||||
final bool autoFocus;
|
||||
final FocusNode? focusNode;
|
||||
final Function(bool value)? onFocusChange;
|
||||
final Function()? onTap;
|
||||
final Function()? onLongPress;
|
||||
final Function()? onDoubleTap;
|
||||
|
|
@ -17,6 +20,9 @@ class FlatButton extends ConsumerWidget {
|
|||
final Clip clipBehavior;
|
||||
const FlatButton({
|
||||
this.child,
|
||||
this.onFocusChange,
|
||||
this.focusNode,
|
||||
this.autoFocus = false,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onDoubleTap,
|
||||
|
|
@ -47,8 +53,11 @@ class FlatButton extends ConsumerWidget {
|
|||
borderRadius: borderRadiusGeometry ?? FladderTheme.defaultShape.borderRadius,
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
autofocus: autoFocus,
|
||||
focusNode: focusNode,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
onFocusChange: onFocusChange,
|
||||
onDoubleTap: onDoubleTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
borderRadius: borderRadiusGeometry ?? BorderRadius.circular(10),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/screens/shared/outlined_text_field.dart';
|
||||
|
||||
class IntInputField extends ConsumerWidget {
|
||||
final int? value;
|
||||
final TextEditingController? controller;
|
||||
|
|
@ -19,25 +22,17 @@ class IntInputField extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Card(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer.withValues(alpha: 0.25),
|
||||
elevation: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: TextField(
|
||||
controller: controller ?? TextEditingController(text: (value ?? 0).toString()),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: false, signed: false),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (value) => onSubmitted?.call(int.tryParse(value)),
|
||||
textAlign: TextAlign.center,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
hintText: placeHolder,
|
||||
suffixText: suffix,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: OutlinedTextField(
|
||||
controller: controller ?? TextEditingController(text: (value ?? 0).toString()),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: false, signed: false),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (value) => onSubmitted?.call(int.tryParse(value)),
|
||||
textAlign: TextAlign.center,
|
||||
suffix: suffix,
|
||||
placeHolder: placeHolder,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,7 +156,9 @@ class _CarouselBannerState extends ConsumerState<CarouselBanner> {
|
|||
);
|
||||
},
|
||||
),
|
||||
BannerPlayButton(item: widget.items[index]),
|
||||
ExcludeFocus(
|
||||
child: BannerPlayButton(item: widget.items[index]),
|
||||
),
|
||||
IgnorePointer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/items/chapters_model.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/disable_keypad_focus.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/humanize_duration.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/horizontal_list.dart';
|
||||
|
|
@ -25,7 +24,7 @@ class ChapterRow extends ConsumerWidget {
|
|||
label: context.localized.chapter(chapters.length),
|
||||
height: AdaptiveLayout.poster(context).size / 1.75,
|
||||
items: chapters,
|
||||
itemBuilder: (context, index) {
|
||||
itemBuilder: (context, index, selected) {
|
||||
final chapter = chapters[index];
|
||||
List<ItemAction> generateActions() {
|
||||
return [
|
||||
|
|
@ -34,16 +33,39 @@ class ChapterRow extends ConsumerWidget {
|
|||
];
|
||||
}
|
||||
|
||||
return AspectRatio(
|
||||
aspectRatio: 1.75,
|
||||
child: Card(
|
||||
return FocusButton(
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position =
|
||||
RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: generateActions().popupMenuItems(),
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) {
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: [
|
||||
...generateActions().listTileItems(context),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.75,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: chapter.imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
CachedNetworkImage(
|
||||
imageUrl: chapter.imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
|
|
@ -64,49 +86,25 @@ class ChapterRow extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
FlatButton(
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position =
|
||||
RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: generateActions().popupMenuItems(),
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) {
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: [
|
||||
...generateActions().listTileItems(context),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (AdaptiveLayout.of(context).isDesktop)
|
||||
DisableFocus(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: PopupMenuButton(
|
||||
tooltip: context.localized.options,
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.white,
|
||||
),
|
||||
itemBuilder: (context) => generateActions().popupMenuItems(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
overlays: [
|
||||
if (AdaptiveLayout.of(context).isDesktop)
|
||||
ExcludeFocus(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: PopupMenuButton(
|
||||
tooltip: context.localized.options,
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.white,
|
||||
),
|
||||
itemBuilder: (context) => generateActions().popupMenuItems(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
contentPadding: contentPadding,
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ class MediaHeader extends ConsumerWidget {
|
|||
final String name;
|
||||
final ImageData? logo;
|
||||
final Function()? onTap;
|
||||
final Alignment alignment;
|
||||
const MediaHeader({
|
||||
required this.name,
|
||||
required this.logo,
|
||||
this.onTap,
|
||||
this.alignment = Alignment.bottomCenter,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -48,7 +50,7 @@ class MediaHeader extends ConsumerWidget {
|
|||
? FladderImage(
|
||||
image: logo,
|
||||
disableBlur: true,
|
||||
alignment: Alignment.bottomCenter,
|
||||
alignment: alignment,
|
||||
imageErrorBuilder: (context, object, stack) => textWidget,
|
||||
placeHolder: const SizedBox(height: 0),
|
||||
fit: BoxFit.contain,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/providers/arguments_provider.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/theme.dart';
|
||||
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
||||
|
||||
class MediaPlayButton extends ConsumerWidget {
|
||||
final ItemBaseModel? item;
|
||||
final Function()? onPressed;
|
||||
final Function()? onLongPressed;
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback? onLongPressed;
|
||||
|
||||
const MediaPlayButton({
|
||||
required this.item,
|
||||
this.onPressed,
|
||||
|
|
@ -19,66 +23,110 @@ class MediaPlayButton extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final resume = (item?.progress ?? 0) > 0;
|
||||
Widget buttonBuilder(bool resume, ButtonStyle? style, Color? textColor) {
|
||||
return ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPressed,
|
||||
style: style,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item?.playButtonLabel(context) ?? "",
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
final progress = (item?.progress ?? 0) / 100.0;
|
||||
final radius = FladderTheme.defaultShape.borderRadius;
|
||||
|
||||
Widget buttonTitle(Color contentColor) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item?.playButtonLabel(context) ?? "",
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.clip,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: contentColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
IconsaxPlusBold.play,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
IconsaxPlusBold.play,
|
||||
color: contentColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AnimatedFadeSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: onPressed != null
|
||||
? Stack(
|
||||
children: [
|
||||
buttonBuilder(resume, null, null),
|
||||
IgnorePointer(
|
||||
child: ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: (item?.progress ?? 0) / 100,
|
||||
child: buttonBuilder(
|
||||
resume,
|
||||
ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.primary),
|
||||
foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onPrimary),
|
||||
iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onPrimary),
|
||||
child: onPressed == null
|
||||
? const SizedBox.shrink(key: ValueKey('empty'))
|
||||
: TextButton(
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPressed,
|
||||
autofocus: ref.read(argumentsStateProvider).htpcMode,
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onFocusChange: (value) {
|
||||
if (value) {
|
||||
context.ensureVisible(
|
||||
alignment: 1.0,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Progress background
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 8.0,
|
||||
offset: const Offset(0, 2),
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
)
|
||||
],
|
||||
borderRadius: radius,
|
||||
),
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Button content
|
||||
buttonTitle(Theme.of(context).colorScheme.primary),
|
||||
Positioned.fill(
|
||||
child: ClipRect(
|
||||
clipper: _ProgressClipper(
|
||||
progress,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: radius,
|
||||
),
|
||||
child: buttonTitle(Theme.of(context).colorScheme.onPrimary),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container(
|
||||
key: UniqueKey(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressClipper extends CustomClipper<Rect> {
|
||||
final double progress;
|
||||
_ProgressClipper(this.progress);
|
||||
|
||||
@override
|
||||
Rect getClip(Size size) {
|
||||
final w = (progress.clamp(0.0, 1.0) * size.width);
|
||||
return Rect.fromLTWH(0, 0, w, size.height);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(covariant _ProgressClipper old) => old.progress != progress;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:fladder/models/items/movie_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
|
@ -7,14 +6,14 @@ import 'package:iconsax_plus/iconsax_plus.dart';
|
|||
import 'package:fladder/models/book_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/models/items/movie_model.dart';
|
||||
import 'package:fladder/models/items/photos_model.dart';
|
||||
import 'package:fladder/models/items/series_model.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/media/components/poster_placeholder.dart';
|
||||
import 'package:fladder/theme.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/disable_keypad_focus.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/humanize_duration.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
|
@ -26,7 +25,6 @@ import 'package:fladder/widgets/shared/status_card.dart';
|
|||
|
||||
class PosterImage extends ConsumerStatefulWidget {
|
||||
final ItemBaseModel poster;
|
||||
final bool heroTag;
|
||||
final bool? selected;
|
||||
final ValueChanged<bool>? playVideo;
|
||||
final bool inlineTitle;
|
||||
|
|
@ -36,9 +34,11 @@ class PosterImage extends ConsumerStatefulWidget {
|
|||
final Function(ItemBaseModel newItem)? onItemUpdated;
|
||||
final Function(ItemBaseModel oldItem)? onItemRemoved;
|
||||
final Function(Function() action, ItemBaseModel item)? onPressed;
|
||||
final bool primaryPosters;
|
||||
final Function(bool focus)? onFocusChanged;
|
||||
|
||||
const PosterImage({
|
||||
required this.poster,
|
||||
this.heroTag = false,
|
||||
this.selected,
|
||||
this.playVideo,
|
||||
this.inlineTitle = false,
|
||||
|
|
@ -48,6 +48,8 @@ class PosterImage extends ConsumerStatefulWidget {
|
|||
this.otherActions = const [],
|
||||
this.onPressed,
|
||||
this.onUserDataChanged,
|
||||
this.primaryPosters = false,
|
||||
this.onFocusChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -56,15 +58,8 @@ class PosterImage extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _PosterImageState extends ConsumerState<PosterImage> {
|
||||
late String currentTag = widget.heroTag == true ? widget.poster.id : UniqueKey().toString();
|
||||
bool hover = false;
|
||||
|
||||
final tag = UniqueKey();
|
||||
void pressedWidget(BuildContext context) async {
|
||||
if (widget.heroTag == false) {
|
||||
setState(() {
|
||||
currentTag = widget.poster.id;
|
||||
});
|
||||
}
|
||||
if (widget.onPressed != null) {
|
||||
widget.onPressed?.call(() async {
|
||||
await navigateToDetails();
|
||||
|
|
@ -78,7 +73,7 @@ class _PosterImageState extends ConsumerState<PosterImage> {
|
|||
}
|
||||
|
||||
Future<void> navigateToDetails() async {
|
||||
await widget.poster.navigateTo(context, ref: ref);
|
||||
await widget.poster.navigateTo(context, ref: ref, tag: tag);
|
||||
}
|
||||
|
||||
final posterRadius = FladderTheme.smallShape.borderRadius;
|
||||
|
|
@ -87,302 +82,268 @@ class _PosterImageState extends ConsumerState<PosterImage> {
|
|||
Widget build(BuildContext context) {
|
||||
final poster = widget.poster;
|
||||
final padding = const EdgeInsets.all(5);
|
||||
|
||||
return Hero(
|
||||
tag: currentTag,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (event) => setState(() => hover = true),
|
||||
onExit: (event) => setState(() => hover = false),
|
||||
child: Card(
|
||||
elevation: 6,
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
width: 1.0,
|
||||
color: Colors.white.withValues(alpha: 0.10),
|
||||
),
|
||||
borderRadius: posterRadius,
|
||||
tag: tag,
|
||||
child: Card(
|
||||
elevation: 6,
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
width: 1.0,
|
||||
color: Colors.white.withValues(alpha: 0.10),
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
FladderImage(
|
||||
image: widget.poster.getPosters?.primary ?? widget.poster.getPosters?.backDrop?.lastOrNull,
|
||||
placeHolder: PosterPlaceholder(item: widget.poster),
|
||||
),
|
||||
if (poster.userData.progress > 0 && widget.poster.type == FladderItemType.book)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.5),
|
||||
child: Text(
|
||||
context.localized.page((widget.poster as BookModel).currentPage),
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
borderRadius: posterRadius,
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
FladderImage(
|
||||
image: widget.primaryPosters
|
||||
? widget.poster.images?.primary
|
||||
: widget.poster.getPosters?.primary ?? widget.poster.getPosters?.backDrop?.lastOrNull,
|
||||
placeHolder: PosterPlaceholder(item: widget.poster),
|
||||
),
|
||||
if (poster.userData.progress > 0 && widget.poster.type == FladderItemType.book)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.5),
|
||||
child: Text(
|
||||
context.localized.page((widget.poster as BookModel).currentPage),
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.selected == true)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary),
|
||||
borderRadius: posterRadius,
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: Text(
|
||||
widget.poster.name,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium
|
||||
?.copyWith(color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.selected == true)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary),
|
||||
borderRadius: posterRadius,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
if (widget.poster.userData.isFavourite)
|
||||
const Row(
|
||||
children: [
|
||||
StatusCard(
|
||||
color: Colors.red,
|
||||
child: Icon(
|
||||
IconsaxPlusBold.heart,
|
||||
size: 21,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if ((poster.userData.progress > 0 && poster.userData.progress < 100) &&
|
||||
widget.poster.type != FladderItemType.book) ...{
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3).copyWith(bottom: 3).add(padding),
|
||||
child: Card(
|
||||
color: Colors.transparent,
|
||||
elevation: 3,
|
||||
shadowColor: Colors.transparent,
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 7.5,
|
||||
backgroundColor: Theme.of(context).colorScheme.onPrimary.withValues(alpha: 0.5),
|
||||
value: poster.userData.progress / 100,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: Text(
|
||||
widget.poster.name,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium
|
||||
?.copyWith(color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.inlineTitle)
|
||||
IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
widget.poster.title.maxLength(limitTo: 25),
|
||||
style:
|
||||
Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.poster.userData.isFavourite)
|
||||
const Row(
|
||||
children: [
|
||||
StatusCard(
|
||||
color: Colors.red,
|
||||
child: Icon(
|
||||
IconsaxPlusBold.heart,
|
||||
size: 21,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if ((poster.userData.progress > 0 && poster.userData.progress < 100) &&
|
||||
widget.poster.type != FladderItemType.book) ...{
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3).copyWith(bottom: 3).add(padding),
|
||||
child: Card(
|
||||
color: Colors.transparent,
|
||||
elevation: 3,
|
||||
shadowColor: Colors.transparent,
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 7.5,
|
||||
backgroundColor: Theme.of(context).colorScheme.onPrimary.withValues(alpha: 0.5),
|
||||
value: poster.userData.progress / 100,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.inlineTitle)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
widget.poster.title.maxLength(limitTo: 25),
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
if ((widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel)
|
||||
|| (widget.poster is MovieModel && !widget.poster.unWatched))
|
||||
IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: StatusCard(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
useFittedBox: widget.poster.unPlayedItemCount != 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: widget.poster.unPlayedItemCount != 0
|
||||
? Container(
|
||||
constraints: const BoxConstraints(minWidth: 16),
|
||||
child: Text(
|
||||
widget.poster.userData.unPlayedItemCount.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.visible,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.check_rounded,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.poster.overview.runTime != null &&
|
||||
((widget.poster is PhotoModel) &&
|
||||
(widget.poster as PhotoModel).internalType == FladderItemType.video)) ...{
|
||||
Align(
|
||||
),
|
||||
if ((widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel) ||
|
||||
(widget.poster is MovieModel && !widget.poster.unWatched))
|
||||
IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Card(
|
||||
elevation: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.poster.overview.runTime.humanizeSmall ?? "",
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
child: StatusCard(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
useFittedBox: widget.poster.unPlayedItemCount != 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: widget.poster.unPlayedItemCount != 0
|
||||
? Container(
|
||||
constraints: const BoxConstraints(minWidth: 16),
|
||||
child: Text(
|
||||
widget.poster.userData.unPlayedItemCount.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.visible,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.check_rounded,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.poster.overview.runTime != null &&
|
||||
((widget.poster is PhotoModel) &&
|
||||
(widget.poster as PhotoModel).internalType == FladderItemType.video)) ...{
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Card(
|
||||
elevation: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.poster.overview.runTime.humanizeSmall ?? "",
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
//Desktop overlay
|
||||
if (AdaptiveLayout.of(context).inputDevice != InputDevice.touch &&
|
||||
widget.poster.type != FladderItemType.person)
|
||||
AnimatedOpacity(
|
||||
opacity: hover ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 125),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
//Hover color overlay
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.55),
|
||||
border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary),
|
||||
borderRadius: posterRadius,
|
||||
)),
|
||||
//Poster Button
|
||||
Focus(
|
||||
onFocusChange: (value) => setState(() => hover = value),
|
||||
child: FlatButton(
|
||||
onTap: () => pressedWidget(context),
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position = RelativeRect.fromLTRB(
|
||||
localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: widget.poster
|
||||
.generateActions(
|
||||
context,
|
||||
ref,
|
||||
exclude: widget.excludeActions,
|
||||
otherActions: widget.otherActions,
|
||||
onUserDataChanged: widget.onUserDataChanged,
|
||||
onDeleteSuccesFully: widget.onItemRemoved,
|
||||
onItemUpdated: widget.onItemUpdated,
|
||||
)
|
||||
.popupMenuItems(useIcons: true),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
//Play Button
|
||||
if (widget.poster.playAble)
|
||||
DisableFocus(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: IconButton.filledTonal(
|
||||
onPressed: () => widget.playVideo?.call(false),
|
||||
icon: const Icon(
|
||||
IconsaxPlusBold.play,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DisableFocus(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PopupMenuButton(
|
||||
tooltip: "Options",
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.white,
|
||||
),
|
||||
itemBuilder: (context) => widget.poster
|
||||
.generateActions(
|
||||
context,
|
||||
ref,
|
||||
exclude: widget.excludeActions,
|
||||
otherActions: widget.otherActions,
|
||||
onUserDataChanged: widget.onUserDataChanged,
|
||||
onDeleteSuccesFully: widget.onItemRemoved,
|
||||
onItemUpdated: widget.onItemUpdated,
|
||||
)
|
||||
.popupMenuItems(useIcons: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
},
|
||||
FocusButton(
|
||||
onTap: () => pressedWidget(context),
|
||||
onFocusChanged: widget.onFocusChanged,
|
||||
onLongPress: () {
|
||||
showBottomSheetPill(
|
||||
context: context,
|
||||
item: widget.poster,
|
||||
content: (scrollContext, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: widget.poster
|
||||
.generateActions(
|
||||
context,
|
||||
ref,
|
||||
exclude: widget.excludeActions,
|
||||
otherActions: widget.otherActions,
|
||||
onUserDataChanged: widget.onUserDataChanged,
|
||||
onDeleteSuccesFully: widget.onItemRemoved,
|
||||
onItemUpdated: widget.onItemUpdated,
|
||||
)
|
||||
.listTileItems(scrollContext, useIcons: true),
|
||||
),
|
||||
)
|
||||
else
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => pressedWidget(context),
|
||||
onLongPress: () {
|
||||
showBottomSheetPill(
|
||||
context: context,
|
||||
item: widget.poster,
|
||||
content: (scrollContext, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: widget.poster
|
||||
);
|
||||
},
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position =
|
||||
RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: widget.poster
|
||||
.generateActions(
|
||||
context,
|
||||
ref,
|
||||
exclude: widget.excludeActions,
|
||||
otherActions: widget.otherActions,
|
||||
onUserDataChanged: widget.onUserDataChanged,
|
||||
onDeleteSuccesFully: widget.onItemRemoved,
|
||||
onItemUpdated: widget.onItemUpdated,
|
||||
)
|
||||
.popupMenuItems(useIcons: true),
|
||||
);
|
||||
},
|
||||
overlays: [
|
||||
//Poster Button
|
||||
if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[
|
||||
// Play Button
|
||||
if (widget.poster.playAble)
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: IconButton.filledTonal(
|
||||
onPressed: () => widget.playVideo?.call(false),
|
||||
icon: const Icon(
|
||||
IconsaxPlusBold.play,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PopupMenuButton(
|
||||
tooltip: "Options",
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.white,
|
||||
),
|
||||
itemBuilder: (context) => widget.poster
|
||||
.generateActions(
|
||||
context,
|
||||
ref,
|
||||
|
|
@ -392,14 +353,15 @@ class _PosterImageState extends ConsumerState<PosterImage> {
|
|||
onDeleteSuccesFully: widget.onItemRemoved,
|
||||
onItemUpdated: widget.onItemUpdated,
|
||||
)
|
||||
.listTileItems(scrollContext, useIcons: true),
|
||||
.popupMenuItems(useIcons: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
151
lib/screens/shared/media/detailed_banner.dart
Normal file
151
lib/screens/shared/media/detailed_banner.dart
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/screens/details_screens/components/overview_header.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_row.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
||||
|
||||
class DetailedBanner extends ConsumerStatefulWidget {
|
||||
final List<ItemBaseModel> posters;
|
||||
final Function(ItemBaseModel selected) onSelect;
|
||||
const DetailedBanner({
|
||||
required this.posters,
|
||||
required this.onSelect,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _DetailedBannerState();
|
||||
}
|
||||
|
||||
class _DetailedBannerState extends ConsumerState<DetailedBanner> {
|
||||
late ItemBaseModel selectedPoster = widget.posters.first;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final color = Theme.of(context).colorScheme.surface;
|
||||
final stops = [0.05, 0.35, 0.65, 0.95];
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: size.height * 0.50,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
color.withValues(alpha: 0.85),
|
||||
color.withValues(alpha: 0.75),
|
||||
color.withValues(alpha: 0),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ExcludeFocus(
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.7,
|
||||
child: ShaderMask(
|
||||
shaderCallback: (Rect bounds) {
|
||||
return LinearGradient(
|
||||
colors: [
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
Colors.white.withAlpha(0),
|
||||
],
|
||||
stops: stops,
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
).createShader(bounds);
|
||||
},
|
||||
child: ShaderMask(
|
||||
shaderCallback: (Rect bounds) {
|
||||
return LinearGradient(
|
||||
colors: [
|
||||
Colors.white.withAlpha(0),
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
],
|
||||
stops: stops,
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
).createShader(bounds);
|
||||
},
|
||||
child: FladderImage(
|
||||
image: selectedPoster.images?.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
spacing: 16,
|
||||
children: [
|
||||
Flexible(
|
||||
child: OverviewHeader(
|
||||
name: selectedPoster.parentBaseModel.name,
|
||||
subTitle: selectedPoster.label(context),
|
||||
image: selectedPoster.getPosters,
|
||||
logoAlignment: Alignment.centerLeft,
|
||||
summary: selectedPoster.overview.summary,
|
||||
productionYear: selectedPoster.overview.productionYear,
|
||||
runTime: selectedPoster.overview.runTime,
|
||||
genres: selectedPoster.overview.genreItems,
|
||||
studios: selectedPoster.overview.studios,
|
||||
officialRating: selectedPoster.overview.parentalRating,
|
||||
communityRating: selectedPoster.overview.communityRating,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: size.height * 0.05,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
FocusProvider(
|
||||
autoFocus: true,
|
||||
child: PosterRow(
|
||||
key: const Key("detailed-banner-row"),
|
||||
primaryPosters: true,
|
||||
label: context.localized.nextUp,
|
||||
posters: widget.posters,
|
||||
onFocused: (poster) {
|
||||
context.ensureVisible(
|
||||
alignment: 1.0,
|
||||
);
|
||||
setState(() {
|
||||
selectedPoster = poster;
|
||||
});
|
||||
widget.onSelect(poster);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,11 +6,10 @@ import 'package:fladder/models/items/episode_model.dart';
|
|||
import 'package:fladder/models/syncing/sync_item.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/syncing/sync_button.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/disable_keypad_focus.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/clickable_text.dart';
|
||||
|
|
@ -64,14 +63,14 @@ class _EpisodePosterState extends ConsumerState<EpisodePosters> {
|
|||
EnumBox(
|
||||
current: selectedSeason != null ? "${context.localized.season(1)} $selectedSeason" : context.localized.all,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Text(context.localized.all),
|
||||
onTap: () => setState(() => selectedSeason = null),
|
||||
ItemActionButton(
|
||||
label: Text(context.localized.all),
|
||||
action: () => setState(() => selectedSeason = null),
|
||||
),
|
||||
...episodesBySeason.entries.map(
|
||||
(e) => PopupMenuItem(
|
||||
child: Text("${context.localized.season(1)} ${e.key}"),
|
||||
onTap: () {
|
||||
(e) => ItemActionButton(
|
||||
label: Text("${context.localized.season(1)} ${e.key}"),
|
||||
action: () {
|
||||
setState(() => selectedSeason = e.key);
|
||||
},
|
||||
),
|
||||
|
|
@ -84,7 +83,7 @@ class _EpisodePosterState extends ConsumerState<EpisodePosters> {
|
|||
contentPadding: widget.contentPadding,
|
||||
startIndex: indexOfCurrent,
|
||||
items: episodes,
|
||||
itemBuilder: (context, index) {
|
||||
itemBuilder: (context, index, selected) {
|
||||
final episode = episodes[index];
|
||||
final isCurrentEpisode = index == indexOfCurrent;
|
||||
return EpisodePoster(
|
||||
|
|
@ -164,14 +163,43 @@ class EpisodePoster extends ConsumerWidget {
|
|||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
FladderImage(
|
||||
image: !episodeAvailable ? episode.parentImages?.primary : episode.images?.primary,
|
||||
placeHolder: placeHolder,
|
||||
blurOnly: !episodeAvailable
|
||||
? true
|
||||
: ref.watch(clientSettingsProvider.select((value) => value.blurUpcomingEpisodes))
|
||||
? blur
|
||||
: false,
|
||||
FocusButton(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position =
|
||||
RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
|
||||
await showMenu(
|
||||
context: context, position: position, items: actions.popupMenuItems(useIcons: true));
|
||||
},
|
||||
child: FladderImage(
|
||||
image: !episodeAvailable ? episode.parentImages?.primary : episode.images?.primary,
|
||||
placeHolder: placeHolder,
|
||||
blurOnly: !episodeAvailable
|
||||
? true
|
||||
: ref.watch(clientSettingsProvider.select((value) => value.blurUpcomingEpisodes))
|
||||
? blur
|
||||
: false,
|
||||
decodeHeight: 250,
|
||||
),
|
||||
overlays: [
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer && actions.isNotEmpty)
|
||||
ExcludeFocus(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: PopupMenuButton(
|
||||
tooltip: context.localized.options,
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.white,
|
||||
),
|
||||
itemBuilder: (context) => actions.popupMenuItems(useIcons: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!episodeAvailable)
|
||||
Align(
|
||||
|
|
@ -236,36 +264,6 @@ class EpisodePoster extends ConsumerWidget {
|
|||
value: episode.userData.progress / 100,
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return FlatButton(
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position = RelativeRect.fromLTRB(
|
||||
localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
|
||||
await showMenu(
|
||||
context: context, position: position, items: actions.popupMenuItems(useIcons: true));
|
||||
},
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer && actions.isNotEmpty)
|
||||
DisableFocus(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: PopupMenuButton(
|
||||
tooltip: context.localized.options,
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: Colors.white,
|
||||
),
|
||||
itemBuilder: (context) => actions.popupMenuItems(useIcons: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/media/banner_play_button.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/themes_data.dart';
|
||||
|
|
@ -99,9 +99,7 @@ class _MediaBannerState extends ConsumerState<MediaBanner> {
|
|||
surfaceTintColor: overlayColor,
|
||||
color: overlayColor,
|
||||
child: MouseRegion(
|
||||
onEnter: (event) => setState(() => showControls = true),
|
||||
onHover: (event) => timer.reset(),
|
||||
onExit: (event) => setState(() => showControls = false),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
|
|
@ -146,56 +144,52 @@ class _MediaBannerState extends ConsumerState<MediaBanner> {
|
|||
],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: FladderImage(
|
||||
fit: BoxFit.cover,
|
||||
image: currentItem.bannerImage,
|
||||
),
|
||||
child: FocusButton(
|
||||
onTap: () => currentItem.navigateTo(context),
|
||||
onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
|
||||
? () async {
|
||||
interacting = true;
|
||||
final poster = currentItem;
|
||||
showBottomSheetPill(
|
||||
context: context,
|
||||
item: poster,
|
||||
content: (scrollContext, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: poster
|
||||
.generateActions(context, ref)
|
||||
.listTileItems(scrollContext, useIcons: true),
|
||||
),
|
||||
);
|
||||
interacting = false;
|
||||
timer.reset();
|
||||
}
|
||||
: null,
|
||||
onSecondaryTapDown: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
|
||||
? null
|
||||
: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position = RelativeRect.fromLTRB(
|
||||
localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
final poster = currentItem;
|
||||
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: poster.generateActions(context, ref).popupMenuItems(useIcons: true),
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: FladderImage(
|
||||
fit: BoxFit.cover,
|
||||
image: currentItem.bannerImage,
|
||||
),
|
||||
),
|
||||
FlatButton(
|
||||
onTap: () => currentItem.navigateTo(context),
|
||||
onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
|
||||
? () async {
|
||||
interacting = true;
|
||||
final poster = currentItem;
|
||||
showBottomSheetPill(
|
||||
context: context,
|
||||
item: poster,
|
||||
content: (scrollContext, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: poster
|
||||
.generateActions(context, ref)
|
||||
.listTileItems(scrollContext, useIcons: true),
|
||||
),
|
||||
);
|
||||
interacting = false;
|
||||
timer.reset();
|
||||
}
|
||||
: null,
|
||||
onSecondaryTapDown: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
|
||||
? null
|
||||
: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position = RelativeRect.fromLTRB(localPosition.dx - 320,
|
||||
localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
final poster = currentItem;
|
||||
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: poster.generateActions(context, ref).popupMenuItems(useIcons: true),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/screens/details_screens/person_detail_screen.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/clickable_text.dart';
|
||||
|
|
@ -47,7 +47,7 @@ class PeopleRow extends ConsumerWidget {
|
|||
height: AdaptiveLayout.poster(context).size * 0.9,
|
||||
contentPadding: contentPadding,
|
||||
items: people,
|
||||
itemBuilder: (context, index) {
|
||||
itemBuilder: (context, index, selected) {
|
||||
final person = people[index];
|
||||
return AspectRatio(
|
||||
aspectRatio: 0.6,
|
||||
|
|
@ -63,19 +63,13 @@ class PeopleRow extends ConsumerWidget {
|
|||
transitionType: ContainerTransitionType.fadeThrough,
|
||||
openColor: Colors.transparent,
|
||||
tappable: false,
|
||||
closedBuilder: (context, action) => Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Card(
|
||||
child: FladderImage(
|
||||
image: person.image,
|
||||
placeHolder: placeHolder(person.name),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
FlatButton(onTap: () => action()),
|
||||
],
|
||||
closedBuilder: (context, action) => FocusButton(
|
||||
onTap: () => action(),
|
||||
child: FladderImage(
|
||||
image: person.image,
|
||||
placeHolder: placeHolder(person.name),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
openBuilder: (context, action) => PersonDetailScreen(
|
||||
person: person,
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ import 'package:fladder/models/items/item_shared_models.dart';
|
|||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/clickable_text.dart';
|
||||
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||
|
||||
|
|
@ -65,8 +67,13 @@ class PosterListItem extends ConsumerWidget {
|
|||
color: Theme.of(context).colorScheme.primary.withValues(alpha: selected == true ? 0.25 : 0),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: InkWell(
|
||||
child: FocusButton(
|
||||
onTap: () => pressedWidget(context),
|
||||
onFocusChanged: (focus) {
|
||||
if (focus) {
|
||||
context.ensureVisible();
|
||||
}
|
||||
},
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position =
|
||||
|
|
|
|||
|
|
@ -3,53 +3,56 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/providers/arguments_provider.dart';
|
||||
import 'package:fladder/screens/shared/media/poster_widget.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
||||
import 'package:fladder/widgets/shared/horizontal_list.dart';
|
||||
|
||||
class PosterRow extends ConsumerStatefulWidget {
|
||||
class PosterRow extends ConsumerWidget {
|
||||
final List<ItemBaseModel> posters;
|
||||
final String label;
|
||||
final double? collectionAspectRatio;
|
||||
final Function()? onLabelClick;
|
||||
final EdgeInsets contentPadding;
|
||||
final Function(ItemBaseModel focused)? onFocused;
|
||||
final bool primaryPosters;
|
||||
const PosterRow({
|
||||
required this.posters,
|
||||
this.contentPadding = const EdgeInsets.symmetric(horizontal: 16),
|
||||
required this.label,
|
||||
this.collectionAspectRatio,
|
||||
this.onLabelClick,
|
||||
this.onFocused,
|
||||
this.primaryPosters = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _PosterRowState();
|
||||
}
|
||||
|
||||
class _PosterRowState extends ConsumerState<PosterRow> {
|
||||
late final controller = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dominantRatio = widget.collectionAspectRatio ?? widget.posters.getMostCommonType.aspectRatio;
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final dominantRatio = primaryPosters ? 1.2 : collectionAspectRatio ?? posters.getMostCommonType.aspectRatio;
|
||||
return HorizontalList(
|
||||
contentPadding: widget.contentPadding,
|
||||
label: widget.label,
|
||||
onLabelClick: widget.onLabelClick,
|
||||
contentPadding: contentPadding,
|
||||
label: label,
|
||||
autoFocus: ref.read(argumentsStateProvider).htpcMode ? FocusProvider.autoFocusOf(context) : false,
|
||||
onLabelClick: onLabelClick,
|
||||
dominantRatio: dominantRatio,
|
||||
items: widget.posters,
|
||||
itemBuilder: (context, index) {
|
||||
final poster = widget.posters[index];
|
||||
items: posters,
|
||||
onFocused: (index) {
|
||||
if (onFocused != null) {
|
||||
onFocused?.call(posters[index]);
|
||||
} else {
|
||||
context.ensureVisible();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context, index, selected) {
|
||||
final poster = posters[index];
|
||||
return PosterWidget(
|
||||
key: Key(poster.id),
|
||||
poster: poster,
|
||||
aspectRatio: dominantRatio,
|
||||
key: Key(poster.id),
|
||||
primaryPosters: primaryPosters,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ class PosterWidget extends ConsumerWidget {
|
|||
final ItemBaseModel poster;
|
||||
final Widget? subTitle;
|
||||
final bool? selected;
|
||||
final bool? heroTag;
|
||||
final int maxLines;
|
||||
final double? aspectRatio;
|
||||
final bool inlineTitle;
|
||||
|
|
@ -26,22 +25,27 @@ class PosterWidget extends ConsumerWidget {
|
|||
final Function(ItemBaseModel newItem)? onItemUpdated;
|
||||
final Function(ItemBaseModel oldItem)? onItemRemoved;
|
||||
final Function(VoidCallback action, ItemBaseModel item)? onPressed;
|
||||
const PosterWidget(
|
||||
{required this.poster,
|
||||
this.subTitle,
|
||||
this.maxLines = 3,
|
||||
this.selected,
|
||||
this.heroTag,
|
||||
this.aspectRatio,
|
||||
this.inlineTitle = false,
|
||||
this.underTitle = true,
|
||||
this.excludeActions = const {},
|
||||
this.otherActions = const [],
|
||||
this.onUserDataChanged,
|
||||
this.onItemUpdated,
|
||||
this.onItemRemoved,
|
||||
this.onPressed,
|
||||
super.key});
|
||||
final bool primaryPosters;
|
||||
final Function(bool focus)? onFocusChanged;
|
||||
|
||||
const PosterWidget({
|
||||
required this.poster,
|
||||
this.subTitle,
|
||||
this.maxLines = 3,
|
||||
this.selected,
|
||||
this.aspectRatio,
|
||||
this.inlineTitle = false,
|
||||
this.underTitle = true,
|
||||
this.excludeActions = const {},
|
||||
this.otherActions = const [],
|
||||
this.onUserDataChanged,
|
||||
this.onItemUpdated,
|
||||
this.onItemRemoved,
|
||||
this.onPressed,
|
||||
this.primaryPosters = false,
|
||||
this.onFocusChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
|
@ -54,7 +58,6 @@ class PosterWidget extends ConsumerWidget {
|
|||
Expanded(
|
||||
child: PosterImage(
|
||||
poster: poster,
|
||||
heroTag: heroTag ?? false,
|
||||
selected: selected,
|
||||
playVideo: (value) async => await poster.play(context, ref),
|
||||
inlineTitle: inlineTitle,
|
||||
|
|
@ -64,67 +67,71 @@ class PosterWidget extends ConsumerWidget {
|
|||
onItemRemoved: onItemRemoved,
|
||||
onItemUpdated: onItemUpdated,
|
||||
onPressed: onPressed,
|
||||
primaryPosters: primaryPosters,
|
||||
onFocusChanged: onFocusChanged,
|
||||
),
|
||||
),
|
||||
if (!inlineTitle && underTitle)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Flexible(
|
||||
child: ClickableText(
|
||||
onTap: AdaptiveLayout.viewSizeOf(context) != ViewSize.phone
|
||||
? () => poster.parentBaseModel.navigateTo(context)
|
||||
: null,
|
||||
text: poster.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
ExcludeFocus(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Flexible(
|
||||
child: ClickableText(
|
||||
onTap: AdaptiveLayout.viewSizeOf(context) != ViewSize.phone
|
||||
? () => poster.parentBaseModel.navigateTo(context)
|
||||
: null,
|
||||
text: poster.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (subTitle != null) ...[
|
||||
Flexible(
|
||||
child: Opacity(
|
||||
opacity: opacity,
|
||||
child: subTitle!,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (subTitle != null) ...[
|
||||
Flexible(
|
||||
child: Opacity(
|
||||
opacity: opacity,
|
||||
child: subTitle!,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (poster.subText?.isNotEmpty ?? false)
|
||||
Flexible(
|
||||
child: ClickableText(
|
||||
opacity: opacity,
|
||||
text: poster.subText ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: ClickableText(
|
||||
opacity: opacity,
|
||||
text: poster.subTextShort(context) ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (poster.subText?.isNotEmpty ?? false)
|
||||
Flexible(
|
||||
child: ClickableText(
|
||||
opacity: opacity,
|
||||
text: poster.subText ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: ClickableText(
|
||||
opacity: opacity,
|
||||
text: poster.subTextShort(context) ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: ClickableText(
|
||||
opacity: opacity,
|
||||
text: poster.subText?.isNotEmpty ?? false ? poster.subTextShort(context) ?? "" : "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
].take(maxLines).toList(),
|
||||
Flexible(
|
||||
child: ClickableText(
|
||||
opacity: opacity,
|
||||
text: poster.subText?.isNotEmpty ?? false ? poster.subTextShort(context) ?? "" : "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
].take(maxLines).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
|
||||
import 'package:fladder/models/items/season_model.dart';
|
||||
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/syncing/sync_button.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/disable_keypad_focus.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/widgets/shared/clickable_text.dart';
|
||||
|
|
@ -39,6 +38,7 @@ class SeasonsRow extends ConsumerWidget {
|
|||
itemBuilder: (
|
||||
context,
|
||||
index,
|
||||
selected,
|
||||
) {
|
||||
final season = (seasons ?? [])[index];
|
||||
return SeasonPoster(
|
||||
|
|
@ -141,46 +141,46 @@ class SeasonPoster extends ConsumerWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return FlatButton(
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position = RelativeRect.fromLTRB(
|
||||
localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: season.generateActions(context, ref).popupMenuItems(useIcons: true));
|
||||
},
|
||||
onTap: () => onSeasonPressed?.call(season),
|
||||
onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
|
||||
? () {
|
||||
showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children:
|
||||
season.generateActions(context, ref).listTileItems(context, useIcons: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer)
|
||||
DisableFocus(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: PopupMenuButton(
|
||||
tooltip: context.localized.options,
|
||||
icon: const Icon(Icons.more_vert, color: Colors.white),
|
||||
itemBuilder: (context) => season.generateActions(context, ref).popupMenuItems(useIcons: true),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: FocusButton(
|
||||
onSecondaryTapDown: (details) async {
|
||||
Offset localPosition = details.globalPosition;
|
||||
RelativeRect position = RelativeRect.fromLTRB(
|
||||
localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy);
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: season.generateActions(context, ref).popupMenuItems(useIcons: true));
|
||||
},
|
||||
onTap: () => onSeasonPressed?.call(season),
|
||||
onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
|
||||
? () {
|
||||
showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: season.generateActions(context, ref).listTileItems(context, useIcons: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
overlays: [
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer)
|
||||
ExcludeFocus(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: PopupMenuButton(
|
||||
tooltip: context.localized.options,
|
||||
icon: const Icon(Icons.more_vert, color: Colors.white),
|
||||
itemBuilder: (context) =>
|
||||
season.generateActions(context, ref).popupMenuItems(useIcons: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ import 'package:flutter/services.dart';
|
|||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/providers/arguments_provider.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/theme.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/widgets/shared/ensure_visible.dart';
|
||||
|
||||
class OutlinedTextField extends ConsumerStatefulWidget {
|
||||
final String? label;
|
||||
|
|
@ -24,6 +27,9 @@ class OutlinedTextField extends ConsumerStatefulWidget {
|
|||
final TextAlign textAlign;
|
||||
final TextInputType? keyboardType;
|
||||
final TextInputAction? textInputAction;
|
||||
final InputDecoration? decoration;
|
||||
final String? placeHolder;
|
||||
final String? suffix;
|
||||
final String? errorText;
|
||||
final bool? enabled;
|
||||
|
||||
|
|
@ -46,6 +52,9 @@ class OutlinedTextField extends ConsumerStatefulWidget {
|
|||
this.keyboardType,
|
||||
this.textInputAction,
|
||||
this.errorText,
|
||||
this.placeHolder,
|
||||
this.decoration,
|
||||
this.suffix,
|
||||
this.enabled,
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -55,7 +64,26 @@ class OutlinedTextField extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _OutlinedTextFieldState extends ConsumerState<OutlinedTextField> {
|
||||
late FocusNode focusNode = widget.focusNode ?? FocusNode();
|
||||
late final FocusNode _textFocus = widget.focusNode ?? FocusNode();
|
||||
late final FocusNode _wrapperFocus = FocusNode()
|
||||
..addListener(() {
|
||||
setState(() {
|
||||
hasFocus = _wrapperFocus.hasFocus;
|
||||
if (hasFocus) {
|
||||
context.ensureVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
bool hasFocus = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textFocus.dispose();
|
||||
_wrapperFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _obscureText = true;
|
||||
void _toggle() {
|
||||
setState(() {
|
||||
|
|
@ -65,101 +93,103 @@ class _OutlinedTextFieldState extends ConsumerState<OutlinedTextField> {
|
|||
|
||||
Color getColor() {
|
||||
if (widget.errorText != null) return Theme.of(context).colorScheme.errorContainer;
|
||||
return Theme.of(context).colorScheme.secondaryContainer.withValues(alpha: 0.25);
|
||||
return Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.35);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPasswordField = widget.keyboardType == TextInputType.visiblePassword;
|
||||
final leanBackMode = ref.watch(argumentsStateProvider).leanBackMode;
|
||||
if (widget.autoFocus) {
|
||||
focusNode.requestFocus();
|
||||
if (leanBackMode) {
|
||||
_wrapperFocus.requestFocus();
|
||||
} else {
|
||||
_textFocus.requestFocus();
|
||||
}
|
||||
}
|
||||
focusNode.addListener(
|
||||
() {},
|
||||
);
|
||||
return Column(
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.fillColor ?? getColor(),
|
||||
borderRadius: FladderTheme.defaultShape.borderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 175),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.decoration == null ? widget.fillColor ?? getColor() : null,
|
||||
borderRadius: FladderTheme.smallShape.borderRadius,
|
||||
border: BoxBorder.all(
|
||||
width: 2,
|
||||
color: hasFocus ? Theme.of(context).colorScheme.primaryFixed : Colors.transparent,
|
||||
),
|
||||
IgnorePointer(
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: IgnorePointer(
|
||||
ignoring: widget.enabled == false,
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
onChanged: widget.onChanged,
|
||||
focusNode: focusNode,
|
||||
onTap: widget.onTap,
|
||||
autofillHints: widget.autoFillHints,
|
||||
keyboardType: widget.keyboardType,
|
||||
autocorrect: widget.autocorrect,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
textInputAction: widget.textInputAction,
|
||||
obscureText: isPasswordField ? _obscureText : false,
|
||||
style: widget.style,
|
||||
maxLines: widget.maxLines,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
textAlign: widget.textAlign,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0),
|
||||
width: widget.borderWidth,
|
||||
),
|
||||
child: KeyboardListener(
|
||||
focusNode: _wrapperFocus,
|
||||
onKeyEvent: (KeyEvent event) {
|
||||
if (event is KeyUpEvent && acceptKeys.contains(event.logicalKey)) {
|
||||
if (_textFocus.hasFocus) {
|
||||
_textFocus.unfocus();
|
||||
_wrapperFocus.requestFocus();
|
||||
} else if (_wrapperFocus.hasFocus) {
|
||||
_textFocus.requestFocus();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: ExcludeFocusTraversal(
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
onChanged: widget.onChanged,
|
||||
focusNode: _textFocus,
|
||||
onTap: widget.onTap,
|
||||
autofillHints: widget.autoFillHints,
|
||||
keyboardType: widget.keyboardType,
|
||||
autocorrect: widget.autocorrect,
|
||||
onSubmitted: widget.onSubmitted != null
|
||||
? (value) {
|
||||
widget.onSubmitted?.call(value);
|
||||
Future.microtask(() async {
|
||||
await Future.delayed(const Duration(milliseconds: 125));
|
||||
_wrapperFocus.requestFocus();
|
||||
});
|
||||
}
|
||||
: null,
|
||||
textInputAction: widget.textInputAction,
|
||||
obscureText: isPasswordField ? _obscureText : false,
|
||||
style: widget.style,
|
||||
maxLines: widget.maxLines,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
textAlign: widget.textAlign,
|
||||
canRequestFocus: true,
|
||||
decoration: widget.decoration ??
|
||||
InputDecoration(
|
||||
border: InputBorder.none,
|
||||
filled: widget.fillColor != null,
|
||||
fillColor: widget.fillColor,
|
||||
labelText: widget.label,
|
||||
suffix: widget.suffix != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: Text(widget.suffix!),
|
||||
)
|
||||
: null,
|
||||
hintText: widget.placeHolder,
|
||||
// errorText: widget.errorText,
|
||||
suffixIcon: isPasswordField
|
||||
? InkWell(
|
||||
onTap: _toggle,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: Icon(
|
||||
_obscureText ? Icons.visibility : Icons.visibility_off,
|
||||
size: 16.0,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0),
|
||||
width: widget.borderWidth,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0),
|
||||
width: widget.borderWidth,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0),
|
||||
width: widget.borderWidth,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0),
|
||||
width: widget.borderWidth,
|
||||
),
|
||||
),
|
||||
filled: widget.fillColor != null,
|
||||
fillColor: widget.fillColor,
|
||||
labelText: widget.label,
|
||||
// errorText: widget.errorText,
|
||||
suffixIcon: isPasswordField
|
||||
? InkWell(
|
||||
onTap: _toggle,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: Icon(
|
||||
_obscureText ? Icons.visibility : Icons.visibility_off,
|
||||
size: 16.0,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedFadeSize(
|
||||
child: widget.errorText != null
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/util/input_handler.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
||||
class PassCodeInput extends ConsumerStatefulWidget {
|
||||
final ValueChanged<String> passCode;
|
||||
|
|
@ -20,6 +18,18 @@ class _PassCodeInputState extends ConsumerState<PassCodeInput> {
|
|||
final passCodeLength = 4;
|
||||
var currentPasscode = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
HardwareKeyboard.instance.addHandler(_onKey);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
HardwareKeyboard.instance.removeHandler(_onKey);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _onKey(KeyEvent value) {
|
||||
if (value is KeyDownEvent) {
|
||||
final keyInt = int.tryParse(value.logicalKey.keyLabel);
|
||||
|
|
@ -37,70 +47,68 @@ class _PassCodeInputState extends ConsumerState<PassCodeInput> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InputHandler(
|
||||
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
|
||||
child: AlertDialog(
|
||||
scrollable: true,
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(
|
||||
passCodeLength,
|
||||
(index) => Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: SizedBox(
|
||||
height: iconSize * 1.2,
|
||||
width: iconSize * 1.2,
|
||||
child: Card(
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, 5),
|
||||
child: AnimatedFadeSize(
|
||||
child: Text(
|
||||
currentPasscode.length > index ? "*" : "",
|
||||
style: Theme.of(context).textTheme.displayLarge?.copyWith(fontSize: 60),
|
||||
),
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(
|
||||
passCodeLength,
|
||||
(index) => Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: SizedBox(
|
||||
height: iconSize * 1.2,
|
||||
width: iconSize * 1.2,
|
||||
child: Card(
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, 5),
|
||||
child: AnimatedFadeSize(
|
||||
child: Text(
|
||||
currentPasscode.length > index ? "*" : "",
|
||||
style: Theme.of(context).textTheme.displayLarge?.copyWith(fontSize: 60),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
).toList(),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.of([1, 2, 3]).map((e) => passCodeNumber(e)).toList(),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.of([4, 5, 6]).map((e) => passCodeNumber(e)).toList(),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.of([7, 8, 9]).map((e) => passCodeNumber(e)).toList(),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
backSpaceButton,
|
||||
passCodeNumber(0),
|
||||
clearAllButton,
|
||||
],
|
||||
)
|
||||
].addPadding(const EdgeInsets.symmetric(vertical: 8)),
|
||||
),
|
||||
),
|
||||
).toList(),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.of([1, 2, 3]).map((e) => passCodeNumber(e)).toList(),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.of([4, 5, 6]).map((e) => passCodeNumber(e)).toList(),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.of([7, 8, 9]).map((e) => passCodeNumber(e)).toList(),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
backSpaceButton,
|
||||
passCodeNumber(0),
|
||||
clearAllButton,
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget passCodeNumber(int value) {
|
||||
return IconButton.filledTonal(
|
||||
onPressed: () async {
|
||||
onPressed: () {
|
||||
addToPassCode(value.toString());
|
||||
},
|
||||
icon: Container(
|
||||
|
|
@ -138,6 +146,7 @@ class _PassCodeInputState extends ConsumerState<PassCodeInput> {
|
|||
|
||||
Widget get clearAllButton {
|
||||
return IconButton.filled(
|
||||
autofocus: true,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer),
|
||||
iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ class UserIcon extends ConsumerWidget {
|
|||
imageUrl: user?.avatar ?? "",
|
||||
progressIndicatorBuilder: (context, url, progress) => placeHolder(),
|
||||
errorWidget: (context, url, error) => placeHolder(),
|
||||
memCacheHeight: 128,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
FlatButton(
|
||||
onTap: onTap,
|
||||
|
|
|
|||
|
|
@ -11,153 +11,144 @@ import 'package:fladder/screens/shared/default_alert_dialog.dart';
|
|||
import 'package:fladder/screens/syncing/sync_item_details.dart';
|
||||
import 'package:fladder/screens/syncing/sync_widgets.dart';
|
||||
import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart';
|
||||
import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/size_formatting.dart';
|
||||
|
||||
class SyncListItem extends ConsumerStatefulWidget {
|
||||
class SyncListItem extends ConsumerWidget {
|
||||
final SyncedItem syncedItem;
|
||||
const SyncListItem({required this.syncedItem, super.key});
|
||||
const SyncListItem({
|
||||
required this.syncedItem,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => SyncListItemState();
|
||||
}
|
||||
|
||||
class SyncListItemState extends ConsumerState<SyncListItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final syncedItem = widget.syncedItem;
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final baseItem = syncedItem.itemModel;
|
||||
print(FocusManager.instance.primaryFocus);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: SyncStatusOverlay(
|
||||
syncedItem: syncedItem,
|
||||
child: Card(
|
||||
elevation: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceDim,
|
||||
shadowColor: Colors.transparent,
|
||||
child: Dismissible(
|
||||
background: Container(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [Icon(IconsaxPlusBold.trash)],
|
||||
),
|
||||
child: Card(
|
||||
elevation: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceDim,
|
||||
shadowColor: Colors.transparent,
|
||||
child: Dismissible(
|
||||
key: Key(syncedItem.id),
|
||||
background: Container(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [Icon(IconsaxPlusBold.trash)],
|
||||
),
|
||||
),
|
||||
key: Key(syncedItem.id),
|
||||
direction: DismissDirection.startToEnd,
|
||||
confirmDismiss: (direction) async {
|
||||
await showDefaultAlertDialog(
|
||||
context,
|
||||
context.localized.deleteItem(baseItem?.detailedName(context) ?? ""),
|
||||
context.localized.syncDeletePopupPermanent,
|
||||
(context) async {
|
||||
ref.read(syncProvider.notifier).removeSync(context, syncedItem);
|
||||
Navigator.of(context).pop();
|
||||
return true;
|
||||
},
|
||||
context.localized.delete,
|
||||
(context) async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
context.localized.cancel);
|
||||
return false;
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return IntrinsicHeight(
|
||||
child: InkWell(
|
||||
onTap: () => baseItem?.navigateTo(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 16,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: 125, maxWidth: constraints.maxWidth * 0.2),
|
||||
child: Card(
|
||||
child: AspectRatio(
|
||||
aspectRatio: baseItem?.primaryRatio ?? 1.0,
|
||||
child: FladderImage(
|
||||
image: baseItem?.getPosters?.primary,
|
||||
fit: BoxFit.cover,
|
||||
)),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: ref.read(syncProvider.notifier).getNestedChildren(syncedItem),
|
||||
builder: (context, asyncSnapshot) {
|
||||
final nestedChildren = asyncSnapshot.data ?? [];
|
||||
return SyncProgressBuilder(
|
||||
item: syncedItem,
|
||||
children: nestedChildren,
|
||||
builder: (context, combinedStream) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
baseItem?.detailedName(context) ?? "",
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: SyncSubtitle(
|
||||
syncItem: syncedItem,
|
||||
children: nestedChildren,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) => SyncLabel(
|
||||
label: context.localized.totalSize(
|
||||
ref.watch(syncSizeProvider(syncedItem, nestedChildren)).byteFormat ??
|
||||
'--'),
|
||||
status: combinedStream?.status ?? TaskStatus.notFound,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (combinedStream != null && combinedStream.hasDownload == true)
|
||||
SyncProgressBar(item: syncedItem, task: combinedStream)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Card(
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: Text(baseItem != null ? baseItem.type.label(context) : ""),
|
||||
)),
|
||||
IconButton(
|
||||
onPressed: () => showSyncItemDetails(context, syncedItem, ref),
|
||||
icon: const Icon(IconsaxPlusLinear.more_square),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
direction: DismissDirection.startToEnd,
|
||||
confirmDismiss: (direction) async {
|
||||
await showDefaultAlertDialog(
|
||||
context,
|
||||
context.localized.deleteItem(baseItem?.detailedName(context) ?? ""),
|
||||
context.localized.syncDeletePopupPermanent,
|
||||
(context) async {
|
||||
ref.read(syncProvider.notifier).removeSync(context, syncedItem);
|
||||
Navigator.of(context).pop();
|
||||
return true;
|
||||
},
|
||||
context.localized.delete,
|
||||
(context) async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
context.localized.cancel);
|
||||
return false;
|
||||
},
|
||||
child: FocusButton(
|
||||
onTap: () => baseItem?.navigateTo(context),
|
||||
onLongPress: () => showSyncItemDetails(context, syncedItem, ref),
|
||||
child: ExcludeFocus(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 16,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 125, maxWidth: 512),
|
||||
child: Card(
|
||||
child: AspectRatio(
|
||||
aspectRatio: baseItem?.primaryRatio ?? 1.0,
|
||||
child: FladderImage(
|
||||
image: baseItem?.getPosters?.primary,
|
||||
fit: BoxFit.cover,
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: ref.read(syncProvider.notifier).getNestedChildren(syncedItem),
|
||||
builder: (context, asyncSnapshot) {
|
||||
final nestedChildren = asyncSnapshot.data ?? [];
|
||||
return SyncProgressBuilder(
|
||||
item: syncedItem,
|
||||
children: nestedChildren,
|
||||
builder: (context, combinedStream) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
baseItem?.detailedName(context) ?? "",
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: SyncSubtitle(
|
||||
syncItem: syncedItem,
|
||||
children: nestedChildren,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) => SyncLabel(
|
||||
label: context.localized.totalSize(
|
||||
ref.watch(syncSizeProvider(syncedItem, nestedChildren)).byteFormat ?? '--'),
|
||||
status: combinedStream?.status ?? TaskStatus.notFound,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (combinedStream != null && combinedStream.hasDownload == true)
|
||||
SyncProgressBar(item: syncedItem, task: combinedStream)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Card(
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: Text(baseItem != null ? baseItem.type.label(context) : ""),
|
||||
)),
|
||||
IconButton(
|
||||
onPressed: () => showSyncItemDetails(context, syncedItem, ref),
|
||||
icon: const Icon(IconsaxPlusLinear.more_square),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:auto_route/auto_route.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/sync_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
import 'package:fladder/screens/home_screen.dart';
|
||||
|
|
@ -13,10 +12,10 @@ import 'package:fladder/screens/shared/nested_scaffold.dart';
|
|||
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
|
||||
import 'package:fladder/screens/syncing/sync_list_item.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/focus_provider.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/sliver_list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart';
|
||||
import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
|
||||
@RoutePage()
|
||||
|
|
@ -38,82 +37,82 @@ class _SyncedScreenState extends ConsumerState<SyncedScreen> {
|
|||
onRefresh: () => ref.read(syncProvider.notifier).refresh(),
|
||||
child: NestedScaffold(
|
||||
background: BackgroundImage(images: items.map((value) => value.images).nonNulls.toList()),
|
||||
body: PinchPosterZoom(
|
||||
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2),
|
||||
child: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: AdaptiveLayout.scrollOf(context, HomeTabs.sync),
|
||||
slivers: [
|
||||
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
||||
NestedSliverAppBar(
|
||||
parent: context,
|
||||
route: LibrarySearchRoute(),
|
||||
)
|
||||
else
|
||||
const DefaultSliverTopBadding(),
|
||||
if (kDebugMode)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
spacing: 12,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.read(syncProvider.notifier).viewDatabase(context),
|
||||
child: const Text("View Database"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.read(syncProvider.notifier).removeAllSyncedData(),
|
||||
child: const Text("Clear drift database"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (items.isNotEmpty) ...[
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Text(
|
||||
context.localized.syncedItems,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
body: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: AdaptiveLayout.scrollOf(context, HomeTabs.sync),
|
||||
slivers: [
|
||||
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
|
||||
NestedSliverAppBar(
|
||||
parent: context,
|
||||
route: LibrarySearchRoute(),
|
||||
)
|
||||
else
|
||||
const DefaultSliverTopBadding(),
|
||||
if (kDebugMode)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return SyncListItem(syncedItem: item);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
SliverFillRemaining(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
spacing: 12,
|
||||
children: [
|
||||
Text(
|
||||
context.localized.noItemsSynced,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.read(syncProvider.notifier).viewDatabase(context),
|
||||
child: const Text("View Database"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.read(syncProvider.notifier).removeAllSyncedData(),
|
||||
child: const Text("Clear drift database"),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Icon(
|
||||
IconsaxPlusLinear.cloud_cross,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
const DefautlSliverBottomPadding(),
|
||||
),
|
||||
),
|
||||
if (items.isNotEmpty) ...[
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Text(
|
||||
context.localized.syncedItems,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: padding,
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return FocusProvider(
|
||||
autoFocus: index == 0,
|
||||
child: SyncListItem(syncedItem: item),
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
SliverFillRemaining(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.localized.noItemsSynced,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Icon(
|
||||
IconsaxPlusLinear.cloud_cross,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const DefautlSliverBottomPadding(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class VideoPlayerChapters extends ConsumerWidget {
|
|||
startIndex: chapters.indexOf(currentChapter ?? chapters.first),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
items: chapters.toList(),
|
||||
itemBuilder: (context, index) {
|
||||
itemBuilder: (context, index, selected) {
|
||||
final chapter = chapters[index];
|
||||
final isCurrent = chapter == currentChapter;
|
||||
return Card(
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/episode_model.dart';
|
||||
|
|
@ -31,6 +31,7 @@ import 'package:fladder/util/refresh_state.dart';
|
|||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||
import 'package:fladder/widgets/shared/fladder_slider.dart';
|
||||
import 'package:fladder/widgets/shared/item_actions.dart';
|
||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||
import 'package:fladder/widgets/shared/spaced_list_tile.dart';
|
||||
|
||||
|
|
@ -153,10 +154,9 @@ class _VideoOptionsMobileState extends ConsumerState<VideoOptions> {
|
|||
label: Text(context.localized.scale),
|
||||
current: videoSettings.videoFit.name.toUpperCaseSplit(),
|
||||
itemBuilder: (context) => BoxFit.values
|
||||
.map((value) => PopupMenuItem(
|
||||
value: value,
|
||||
child: Text(value.name.toUpperCaseSplit()),
|
||||
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(value),
|
||||
.map((value) => ItemActionButton(
|
||||
label: Text(value.name.toUpperCaseSplit()),
|
||||
action: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(value),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
|
|
|
|||
171
lib/src/player_settings_helper.g.dart
Normal file
171
lib/src/player_settings_helper.g.dart
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// Autogenerated from Pigeon (v26.0.1), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
PlatformException _createConnectionError(String channelName) {
|
||||
return PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
}
|
||||
bool _deepEquals(Object? a, Object? b) {
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length &&
|
||||
a.indexed
|
||||
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
|
||||
(b as Map<Object?, Object?>).containsKey(entry.key) &&
|
||||
_deepEquals(entry.value, b[entry.key]));
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
|
||||
|
||||
enum SegmentType {
|
||||
commercial,
|
||||
preview,
|
||||
recap,
|
||||
intro,
|
||||
outro,
|
||||
}
|
||||
|
||||
enum SegmentSkip {
|
||||
ask,
|
||||
skip,
|
||||
none,
|
||||
}
|
||||
|
||||
class PlayerSettings {
|
||||
PlayerSettings({
|
||||
required this.skipTypes,
|
||||
required this.skipForward,
|
||||
required this.skipBackward,
|
||||
});
|
||||
|
||||
Map<SegmentType, SegmentSkip> skipTypes;
|
||||
|
||||
int skipForward;
|
||||
|
||||
int skipBackward;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
skipTypes,
|
||||
skipForward,
|
||||
skipBackward,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList(); }
|
||||
|
||||
static PlayerSettings decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return PlayerSettings(
|
||||
skipTypes: (result[0] as Map<Object?, Object?>?)!.cast<SegmentType, SegmentSkip>(),
|
||||
skipForward: result[1]! as int,
|
||||
skipBackward: result[2]! as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! PlayerSettings || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList())
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
void writeValue(WriteBuffer buffer, Object? value) {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else if (value is SegmentType) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.index);
|
||||
} else if (value is SegmentSkip) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.index);
|
||||
} else if (value is PlayerSettings) {
|
||||
buffer.putUint8(131);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
case 129:
|
||||
final int? value = readValue(buffer) as int?;
|
||||
return value == null ? null : SegmentType.values[value];
|
||||
case 130:
|
||||
final int? value = readValue(buffer) as int?;
|
||||
return value == null ? null : SegmentSkip.values[value];
|
||||
case 131:
|
||||
return PlayerSettings.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PlayerSettingsPigeon {
|
||||
/// Constructor for [PlayerSettingsPigeon]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
PlayerSettingsPigeon({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<void> sendPlayerSettings(PlayerSettings playerSettings) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.PlayerSettingsPigeon.sendPlayerSettings$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[playerSettings]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
1137
lib/src/video_player_helper.g.dart
Normal file
1137
lib/src/video_player_helper.g.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -23,7 +23,7 @@ class LibMDK extends BasePlayer {
|
|||
Future<void> dispose() async {}
|
||||
|
||||
@override
|
||||
Future<void> open(String url, bool play) async {}
|
||||
Future<void> loadVideo(String url, bool play) async {}
|
||||
|
||||
void setState(PlayerState state) {}
|
||||
|
||||
|
|
@ -34,6 +34,10 @@ class LibMDK extends BasePlayer {
|
|||
|
||||
@override
|
||||
Future<void> play() async {}
|
||||
|
||||
@override
|
||||
Future<void> open(BuildContext context) async {}
|
||||
|
||||
@override
|
||||
Future<void> playOrPause() async {}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,15 @@ class FladderTheme {
|
|||
static ThemeData theme(ColorScheme? colorScheme, DynamicSchemeVariant dynamicSchemeVariant) {
|
||||
final ColorScheme? scheme = generateDynamicColourSchemes(colorScheme, dynamicSchemeVariant);
|
||||
|
||||
final buttonState = WidgetStateProperty.resolveWith(
|
||||
(states) {
|
||||
return BorderSide(
|
||||
width: 2,
|
||||
color: states.contains(WidgetState.focused) ? Colors.white.withValues(alpha: 0.65) : Colors.transparent,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final textTheme = FladderFonts.rubikTextTheme(
|
||||
const TextTheme(),
|
||||
);
|
||||
|
|
@ -61,7 +70,6 @@ class FladderTheme {
|
|||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: scheme?.secondaryContainer,
|
||||
foregroundColor: scheme?.onSecondaryContainer,
|
||||
shape: defaultShape,
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: scheme?.secondary,
|
||||
|
|
@ -90,11 +98,6 @@ class FladderTheme {
|
|||
}),
|
||||
trackOutlineWidth: const WidgetStatePropertyAll(1),
|
||||
),
|
||||
iconButtonTheme: IconButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(defaultShape),
|
||||
),
|
||||
),
|
||||
navigationBarTheme: const NavigationBarThemeData(),
|
||||
dialogTheme: DialogThemeData(shape: defaultShape),
|
||||
scrollbarTheme: ScrollbarThemeData(
|
||||
|
|
@ -130,7 +133,7 @@ class FladderTheme {
|
|||
dividerTheme: DividerThemeData(
|
||||
indent: 6,
|
||||
endIndent: 6,
|
||||
color: scheme?.onSurface.withAlpha(125),
|
||||
color: scheme?.onSurface.withAlpha(30),
|
||||
),
|
||||
segmentedButtonTheme: SegmentedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
|
|
@ -145,9 +148,36 @@ class FladderTheme {
|
|||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(style: ButtonStyle(shape: WidgetStatePropertyAll(defaultShape))),
|
||||
filledButtonTheme: FilledButtonThemeData(style: ButtonStyle(shape: WidgetStatePropertyAll(defaultShape))),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(style: ButtonStyle(shape: WidgetStatePropertyAll(defaultShape))),
|
||||
iconButtonTheme: IconButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(smallShape),
|
||||
side: buttonState,
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(smallShape),
|
||||
side: buttonState,
|
||||
),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(smallShape),
|
||||
side: buttonState,
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(smallShape),
|
||||
side: buttonState,
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(smallShape),
|
||||
side: buttonState,
|
||||
),
|
||||
),
|
||||
textTheme: textTheme.copyWith(
|
||||
titleMedium: textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/arguments_provider.dart';
|
||||
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
||||
import 'package:fladder/screens/home_screen.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout_model.dart';
|
||||
|
|
@ -15,12 +16,14 @@ import 'package:fladder/util/resolution_checker.dart';
|
|||
enum InputDevice {
|
||||
touch,
|
||||
pointer,
|
||||
dpad,
|
||||
}
|
||||
|
||||
enum ViewSize {
|
||||
phone,
|
||||
tablet,
|
||||
desktop;
|
||||
desktop,
|
||||
television;
|
||||
|
||||
const ViewSize();
|
||||
|
||||
|
|
@ -28,6 +31,7 @@ enum ViewSize {
|
|||
ViewSize.phone => context.localized.phone,
|
||||
ViewSize.tablet => context.localized.tablet,
|
||||
ViewSize.desktop => context.localized.desktop,
|
||||
ViewSize.television => context.localized.television,
|
||||
};
|
||||
|
||||
bool operator >(ViewSize other) => index > other.index;
|
||||
|
|
@ -174,12 +178,20 @@ class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final acceptedLayouts = ref.watch(homeSettingsProvider.select((value) => value.screenLayouts));
|
||||
final acceptedViewSizes = ref.watch(homeSettingsProvider.select((value) => value.layoutStates));
|
||||
final arguments = ref.watch(argumentsStateProvider);
|
||||
final htpcMode = arguments.htpcMode;
|
||||
final acceptedLayouts =
|
||||
htpcMode ? {LayoutMode.dual} : ref.watch(homeSettingsProvider.select((value) => value.screenLayouts));
|
||||
final acceptedViewSizes =
|
||||
htpcMode ? {ViewSize.television} : ref.watch(homeSettingsProvider.select((value) => value.layoutStates));
|
||||
|
||||
final selectedViewSize = selectAvailableOrSmaller<ViewSize>(viewSize, acceptedViewSizes, ViewSize.values);
|
||||
final selectedLayoutMode = selectAvailableOrSmaller<LayoutMode>(layoutMode, acceptedLayouts, LayoutMode.values);
|
||||
final input = (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch;
|
||||
final input = htpcMode
|
||||
? InputDevice.dpad
|
||||
: (isDesktop || kIsWeb)
|
||||
? InputDevice.pointer
|
||||
: InputDevice.touch;
|
||||
|
||||
final posterDefaults = const PosterDefaults(size: 350, ratio: 0.55);
|
||||
|
||||
|
|
@ -195,8 +207,10 @@ class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
|
|||
posterDefaults: posterDefaults,
|
||||
);
|
||||
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
data: mediaQuery.copyWith(
|
||||
padding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null,
|
||||
viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null,
|
||||
),
|
||||
|
|
@ -211,9 +225,14 @@ class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
|
|||
posterDefaults: posterDefaults,
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) => ResolutionChecker(
|
||||
child: widget.adaptiveLayout == null ? DebugBanner(child: widget.child(context)) : widget.child(context),
|
||||
),
|
||||
builder: (context) => isDesktop
|
||||
? ResolutionChecker(
|
||||
child:
|
||||
widget.adaptiveLayout == null ? DebugBanner(child: widget.child(context)) : widget.child(context),
|
||||
)
|
||||
: widget.adaptiveLayout == null
|
||||
? DebugBanner(child: widget.child(context))
|
||||
: widget.child(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue