chore: Improved custom keyboard logic

This commit is contained in:
PartyDonut 2025-10-11 18:46:25 +02:00
parent 07972ea5ee
commit 117d873683
10 changed files with 203 additions and 202 deletions

View file

@ -33,19 +33,21 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
ImageType.banner,
}.toList();
final fieldsToFetch = [
ItemFields.parentid,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.primaryimageaspectratio,
ItemFields.overview,
ItemFields.genres,
];
if (viewTypes.containsAny([CollectionType.movies, CollectionType.tvshows])) {
final resumeVideoResponse = await api.usersUserIdItemsResumeGet(
enableImageTypes: imagesToFetch,
fields: [
ItemFields.parentid,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.primaryimageaspectratio,
ItemFields.overview,
ItemFields.genres,
],
fields: fieldsToFetch,
mediaTypes: [MediaType.video],
enableTotalRecordCount: false,
);
@ -58,16 +60,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
if (viewTypes.contains(CollectionType.music)) {
final resumeAudioResponse = await api.usersUserIdItemsResumeGet(
enableImageTypes: imagesToFetch,
fields: [
ItemFields.parentid,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.primaryimageaspectratio,
ItemFields.overview,
ItemFields.genres,
],
fields: fieldsToFetch,
mediaTypes: [MediaType.audio],
enableTotalRecordCount: false,
);
@ -80,16 +73,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
if (viewTypes.contains(CollectionType.books)) {
final resumeBookResponse = await api.usersUserIdItemsResumeGet(
enableImageTypes: imagesToFetch,
fields: [
ItemFields.parentid,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.primaryimageaspectratio,
ItemFields.overview,
ItemFields.genres,
],
fields: fieldsToFetch,
mediaTypes: [MediaType.book],
enableTotalRecordCount: false,
);
@ -102,16 +86,7 @@ class DashboardNotifier extends StateNotifier<HomeModel> {
final nextResponse = await api.showsNextUpGet(
nextUpDateCutoff: DateTime.now().subtract(
ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))),
fields: [
ItemFields.parentid,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.primaryimageaspectratio,
ItemFields.overview,
ItemFields.genres,
],
fields: fieldsToFetch,
);
final next = nextResponse.body?.items

View file

@ -105,7 +105,16 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
valueListenable: selectedPoster,
builder: (_, value, __) {
return BackgroundImage(
items: value != null ? [value] : [...homeCarouselItems, ...dashboardData.nextUp, ...allResume],
images: (value != null
? [value]
: [
...homeCarouselItems,
...dashboardData.nextUp,
...allResume,
])
.map((e) => e.images)
.nonNulls
.toList(),
);
},
),

View file

@ -14,7 +14,7 @@ import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:fladder/widgets/keyboard/custom_keyboard.dart';
import 'package:fladder/widgets/keyboard/slide_in_keyboard.dart';
import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart';
import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart';
import 'package:fladder/widgets/navigation_scaffold/navigation_scaffold.dart';

View file

@ -13,7 +13,7 @@ import 'package:fladder/screens/login/login_user_grid.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/screens/shared/fladder_logo.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/widgets/keyboard/custom_keyboard.dart';
import 'package:fladder/widgets/keyboard/slide_in_keyboard.dart';
import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart';
@RoutePage()

View file

@ -251,7 +251,7 @@ class _PosterImageState extends ConsumerState<PosterImage> {
),
),
if ((widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel) ||
(widget.poster.playAble && !widget.poster.unWatched))
(widget.poster.playAble && !widget.poster.unWatched && widget.poster is! PhotoAlbumModel))
IgnorePointer(
child: Align(
alignment: Alignment.topRight,

View file

@ -11,7 +11,7 @@ import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/theme.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/focus_provider.dart';
import 'package:fladder/widgets/keyboard/custom_keyboard.dart';
import 'package:fladder/widgets/keyboard/slide_in_keyboard.dart';
import 'package:fladder/widgets/shared/ensure_visible.dart';
class OutlinedTextField extends ConsumerStatefulWidget {
@ -71,6 +71,7 @@ class OutlinedTextField extends ConsumerStatefulWidget {
}
class _OutlinedTextFieldState extends ConsumerState<OutlinedTextField> {
late final controller = widget.controller ?? TextEditingController();
late final FocusNode _textFocus = widget.focusNode ?? FocusNode();
late final FocusNode _wrapperFocus = FocusNode()
..addListener(() {
@ -130,7 +131,7 @@ class _OutlinedTextFieldState extends ConsumerState<OutlinedTextField> {
ref.watch(clientSettingsProvider.select((value) => !value.useSystemIME));
final textField = TextField(
controller: widget.controller,
controller: controller,
onChanged: widget.onChanged,
focusNode: _textFocus,
onTap: widget.onTap,
@ -199,26 +200,28 @@ class _OutlinedTextFieldState extends ConsumerState<OutlinedTextField> {
ignoring: widget.enabled == false,
child: KeyboardListener(
focusNode: _wrapperFocus,
onKeyEvent: (KeyEvent event) {
onKeyEvent: (KeyEvent event) async {
if (keyboardFocus || AdaptiveLayout.inputDeviceOf(context) != InputDevice.dPad) return;
if (event is KeyDownEvent && acceptKeys.contains(event.logicalKey)) {
if (_textFocus.hasFocus) {
_wrapperFocus.requestFocus();
} else if (_wrapperFocus.hasFocus) {
if (useCustomKeyboard) {
CustomKeyboard.of(context).openKeyboard(
textField,
onClosed: () {
setState(() {
keyboardFocus = false;
});
_wrapperFocus.requestFocus();
await openKeyboard(
context,
controller,
inputType: widget.keyboardType,
inputAction: widget.textInputAction,
searchQuery: widget.searchQuery,
onChanged: () {
widget.onChanged?.call(controller.text);
},
query: widget.searchQuery,
);
widget.onSubmitted?.call(controller.text);
setState(() {
keyboardFocus = true;
keyboardFocus = false;
});
_wrapperFocus.requestFocus();
} else {
_textFocus.requestFocus();
}

View file

@ -12,6 +12,7 @@ import 'package:fladder/util/debug_banner.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/poster_defaults.dart';
import 'package:fladder/util/resolution_checker.dart';
import 'package:fladder/widgets/keyboard/slide_in_keyboard.dart';
enum InputDevice {
touch,
@ -209,32 +210,40 @@ class _AdaptiveLayoutBuilderState extends ConsumerState<AdaptiveLayoutBuilder> {
final mediaQuery = MediaQuery.of(context);
return MediaQuery(
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,
),
child: AdaptiveLayout(
data: currentLayout.copyWith(
viewSize: selectedViewSize,
layoutMode: selectedLayoutMode,
inputDevice: input,
platform: currentPlatform,
isDesktop: isDesktop,
controller: scrollControllers,
posterDefaults: posterDefaults,
),
child: Builder(
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),
),
),
return ValueListenableBuilder(
valueListenable: isKeyboardOpen,
builder: (context, value, child) {
return MediaQuery(
data: mediaQuery.copyWith(
padding: (isDesktop || kIsWeb
? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16)
: mediaQuery.padding),
viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null,
),
child: AdaptiveLayout(
data: currentLayout.copyWith(
viewSize: selectedViewSize,
layoutMode: selectedLayoutMode,
inputDevice: input,
platform: currentPlatform,
isDesktop: isDesktop,
controller: scrollControllers,
posterDefaults: posterDefaults,
),
child: Builder(
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),
),
),
);
},
);
}
}

View file

@ -141,7 +141,6 @@ class FocusButtonState extends State<FocusButton> {
cursor: SystemMouseCursors.click,
onEnter: (event) => onHover.value = true,
onExit: (event) => onHover.value = false,
hitTestBehavior: HitTestBehavior.translucent,
child: Focus(
focusNode: focusNode,
autofocus: widget.autoFocus,
@ -160,7 +159,13 @@ class FocusButtonState extends State<FocusButton> {
onTap: widget.onTap,
onSecondaryTapDown: widget.onSecondaryTapDown,
onLongPress: widget.onLongPress,
child: widget.child,
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: widget.borderRadius ?? FladderTheme.smallShape.borderRadius,
),
child: widget.child,
),
),
Positioned.fill(
child: ValueListenableBuilder(

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart';
@ -9,128 +10,124 @@ import 'package:fladder/util/focus_provider.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/keyboard/alpha_numeric_keyboard.dart';
class CustomKeyboard extends InheritedWidget {
final CustomKeyboardState state;
ValueNotifier<bool> isKeyboardOpen = ValueNotifier<bool>(false);
const CustomKeyboard({
required this.state,
required super.child,
double keyboardWidthFactor = 0.2;
class CustomKeyboardWrapper extends StatelessWidget {
final Widget child;
const CustomKeyboardWrapper({
required this.child,
super.key,
});
static CustomKeyboardState of(BuildContext context) {
final inherited = context.dependOnInheritedWidgetOfExactType<CustomKeyboard>();
assert(inherited != null, 'No CustomKeyboard found in context');
return inherited!.state;
}
@override
bool updateShouldNotify(CustomKeyboard oldWidget) => state != oldWidget.state;
}
class CustomKeyboardWrapper extends StatefulWidget {
final Widget child;
const CustomKeyboardWrapper({super.key, required this.child});
@override
CustomKeyboardState createState() => CustomKeyboardState();
}
class CustomKeyboardState extends State<CustomKeyboardWrapper> {
bool _isOpen = false;
TextEditingController? _controller;
TextField? _textField;
bool get isOpen => _isOpen;
VoidCallback? onCloseCall;
FutureOr<List<String>> Function(String query)? searchQuery;
void openKeyboard(
TextField textField, {
VoidCallback? onClosed,
FutureOr<List<String>> Function(String query)? query,
}) {
onCloseCall = onClosed;
searchQuery = query;
setState(() {
_isOpen = true;
_textField = textField;
_controller = textField.controller;
});
_controller?.addListener(() {
_textField?.onChanged?.call(_controller?.text ?? "");
});
}
void closeKeyboard() {
setState(() {
_isOpen = false;
});
onCloseCall?.call();
onCloseCall = null;
if (_controller != null) {
_textField?.onSubmitted?.call(_controller?.text ?? "");
_textField?.onEditingComplete?.call();
_controller?.removeListener(() {
_textField?.onChanged?.call(_controller?.text ?? "");
});
}
}
@override
void dispose() {
super.dispose();
_controller?.dispose();
}
@override
Widget build(BuildContext context) {
final mq = MediaQuery.of(context);
return BackButtonListener(
onBackButtonPressed: () async {
if (!context.mounted) return false;
if (_isOpen && context.mounted) {
closeKeyboard();
return true;
} else {
return false;
}
},
child: CustomKeyboard(
state: this,
return Container(
color: Theme.of(context).colorScheme.surface,
child: ValueListenableBuilder(
valueListenable: isKeyboardOpen,
builder: (context, value, _) {
return AnimatedFractionallySizedBox(
duration: const Duration(milliseconds: 175),
widthFactor: value ? 1.0 - keyboardWidthFactor : 1.0,
heightFactor: 1.0,
alignment: Alignment.centerRight,
child: child,
);
},
),
);
}
}
Future<T?> openKeyboard<T>(
BuildContext context,
TextEditingController controller, {
TextInputType? inputType,
TextInputAction? inputAction,
FutureOr<List<String>> Function(String query)? searchQuery,
VoidCallback? onChanged,
}) async {
isKeyboardOpen.value = true;
await showGeneralDialog(
context: context,
transitionDuration: const Duration(milliseconds: 175),
barrierDismissible: true,
barrierColor: Colors.transparent,
barrierLabel: 'Custom keyboard',
useRootNavigator: true,
fullscreenDialog: true,
transitionBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween(begin: const Offset(-1, 0), end: const Offset(0, 0)).animate(
animation,
),
child: child,
);
},
pageBuilder: (context, animation1, animation2) {
return Align(
alignment: Alignment.centerLeft,
child: _SlideInKeyboard(
controller: controller,
onChanged: onChanged ?? () {},
onClose: () {
context.router.pop();
isKeyboardOpen.value = false;
return null;
},
inputType: inputType,
inputAction: inputAction,
searchQuery: searchQuery,
),
);
},
);
isKeyboardOpen.value = false;
return null;
}
class _SlideInKeyboard extends StatefulWidget {
final TextEditingController controller;
final Function() onChanged;
final Function() onClose;
final TextInputType? inputType;
final TextInputAction? inputAction;
final FutureOr<List<String>> Function(String query)? searchQuery;
const _SlideInKeyboard({
required this.controller,
required this.onChanged,
required this.onClose,
this.inputType,
this.inputAction,
this.searchQuery,
});
@override
State<_SlideInKeyboard> createState() => __SlideInKeyboardState();
}
class __SlideInKeyboardState extends State<_SlideInKeyboard> {
@override
Widget build(BuildContext context) {
final padding = MediaQuery.paddingOf(context);
final width = MediaQuery.sizeOf(context).width * keyboardWidthFactor;
return FractionallySizedBox(
widthFactor: keyboardWidthFactor,
heightFactor: 1.0,
child: Padding(
padding: padding.copyWith(left: (padding.left - width).clamp(0, padding.left)),
child: Container(
height: double.infinity,
color: Theme.of(context).colorScheme.surface,
alignment: Alignment.center,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AnimatedSize(
duration: const Duration(milliseconds: 125),
child: _isOpen
? SizedBox(
width: mq.size.width * 0.20,
height: double.infinity,
child: Material(
color: Theme.of(context).colorScheme.surface,
child: _CustomKeyboardView(
controller: _controller!,
keyboardType: _textField?.keyboardType,
keyboardActionType: _textField?.textInputAction,
onClose: closeKeyboard,
onChanged: () => _textField?.onChanged?.call(_controller?.text ?? ""),
searchQuery: searchQuery,
),
),
)
: const SizedBox.shrink(),
),
Expanded(child: widget.child),
],
child: _CustomKeyboardView(
controller: widget.controller,
onChanged: widget.onChanged,
onClose: widget.onClose,
keyboardType: widget.inputType,
keyboardActionType: widget.inputAction,
searchQuery: widget.searchQuery,
),
),
),
@ -187,7 +184,7 @@ class _CustomKeyboardViewState extends State<_CustomKeyboardView> {
node: scope,
autofocus: true,
child: Padding(
padding: const EdgeInsets.all(12.0).add(MediaQuery.paddingOf(context)),
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 16,
@ -301,7 +298,10 @@ class _SearchResults extends StatelessWidget {
spacing: 8,
children: [
const Icon(IconsaxPlusLinear.search_status_1),
Text(context.localized.noResults),
Text(
context.localized.noResults,
style: Theme.of(context).textTheme.titleLarge,
),
],
),
),

View file

@ -31,7 +31,7 @@ class _BackgroundImageState extends ConsumerState<BackgroundImage> {
@override
void didUpdateWidget(covariant BackgroundImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (!oldWidget.items.equals(widget.items)) {
if (!oldWidget.items.equals(widget.items) || !oldWidget.images.equals(widget.images)) {
updateItems();
}
}
@ -46,7 +46,7 @@ class _BackgroundImageState extends ConsumerState<BackgroundImage> {
ImageData? newImage;
if (widget.images.isNotEmpty) {
newImage = widget.images.shuffled().firstOrNull?.primary;
newImage = widget.images.shuffled().firstOrNull?.randomBackDrop;
} else if (widget.items.isNotEmpty) {
final randomItem = widget.items.shuffled().firstOrNull;
final itemId = switch (randomItem?.type) {