mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-08 23:18:16 -07: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
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue