more refactors and logs page handling

This commit is contained in:
shenlong-tanwen
2024-10-23 02:30:46 +05:30
parent 8f47645cdb
commit a0afea04d8
90 changed files with 2386 additions and 584 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/utils/extensions/async_snapshot.extension.dart';
import 'package:skeletonizer/skeletonizer.dart';
class SkeletonizedFutureBuilder<T> extends StatelessWidget {
const SkeletonizedFutureBuilder({
super.key,
required this.future,
required this.builder,
required this.loadingBuilder,
required this.errorBuilder,
this.emptyBuilder,
this.emptyWhen,
}) : assert(
(emptyBuilder == null && emptyWhen == null) ||
(emptyBuilder != null && emptyWhen != null),
"Both emptyBuilder and emptyWhen should be provided");
/// Future to listen to
final Future<T> future;
/// Callback when data is available
final Widget Function(BuildContext context, T? snapshot) builder;
/// Callback when future is loading. Expected a skeletonizer to be returned
final Widget Function(BuildContext context) loadingBuilder;
/// Callback when future resulted in an error
final Widget Function(BuildContext context, Object? error) errorBuilder;
/// Callback when data is available but is empty. Emptiness is determined based on the [emptyWhen] callback
final Widget Function(BuildContext context)? emptyBuilder;
/// Predicate to call [emptyBuilder] when the [data] passes the filter
final bool Function(T? data)? emptyWhen;
@override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: future,
builder: (ctx, snap) {
final Widget child;
if (snap.isWaiting) {
child = loadingBuilder(ctx);
} else {
if (snap.hasError) {
child = errorBuilder(ctx, snap.error);
} else {
final isEmpty = emptyWhen?.call(snap.data) ?? false;
child = isEmpty ? emptyBuilder!(ctx) : builder(ctx, snap.data);
}
}
return Skeletonizer.zone(
enabled: snap.isWaiting,
enableSwitchAnimation: true,
child: child,
);
},
);
}
}
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/presentation/components/image/immich_cached_network_image.widget.dart';
import 'package:immich_mobile/presentation/components/image/transparent_image.dart';
import 'package:immich_mobile/utils/immich_image_url_helper.dart';
class ImUserAvatar extends StatelessWidget {
final User user;
final double? dimension;
final double? radius;
const ImUserAvatar({
super.key,
required this.user,
this.dimension,
this.radius,
});
@override
Widget build(BuildContext context) {
bool isDarkTheme = Theme.of(context).brightness == Brightness.dark;
final textIcon = Text(
user.name[0].toUpperCase(),
style: TextStyle(
color: isDarkTheme && user.avatarColor == UserAvatarColor.primary
? Colors.black
: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
);
return CircleAvatar(
backgroundColor: user.avatarColor.toColor(),
radius: radius,
child: user.profileImagePath.isEmpty
? textIcon
: ClipOval(
child: ImCachedNetworkImage(
imageUrl: ImImageUrlHelper.getUserAvatarUrl(user),
cacheKey: user.profileImagePath,
height: dimension,
width: dimension,
fit: BoxFit.cover,
placeholder: (_, __) => Image.memory(
kTransparentImage,
semanticLabel: 'Transparent',
),
fadeInDuration: const Duration(milliseconds: 300),
errorWidget: (_, error, stackTrace) => SizedBox.square(),
),
),
);
}
}
@@ -98,11 +98,11 @@ class DraggableScrollbar extends StatefulWidget {
required BoxConstraints? labelConstraints,
required bool alwaysVisibleScrollThumb,
}) {
var scrollThumbAndLabel = labelText == null
Widget scrollThumbAndLabel = labelText == null
? scrollThumb
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
_ScrollLabel(
animation: labelAnimation,
@@ -145,8 +145,8 @@ class DraggableScrollbar extends StatefulWidget {
color: backgroundColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(height),
bottomLeft: Radius.circular(height),
topRight: const Radius.circular(4.0),
bottomLeft: Radius.circular(height),
bottomRight: const Radius.circular(4.0),
),
child: Container(
@@ -195,9 +195,9 @@ class _ScrollLabel extends StatelessWidget {
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
child: Container(
constraints: constraints ?? _defaultConstraints,
padding: const EdgeInsets.symmetric(horizontal: 15),
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 15),
constraints: constraints ?? _defaultConstraints,
child: child,
),
),
@@ -231,8 +231,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
_currentItem = 0;
_thumbAnimationController = AnimationController(
vsync: this,
duration: widget.scrollbarAnimationDuration,
vsync: this,
);
_thumbAnimation = CurvedAnimation(
@@ -241,8 +241,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
);
_labelAnimationController = AnimationController(
vsync: this,
duration: widget.scrollbarAnimationDuration,
vsync: this,
);
_labelAnimation = CurvedAnimation(
@@ -291,16 +291,16 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
onVerticalDragEnd: _onVerticalDragEnd,
child: Container(
alignment: Alignment.topRight,
margin: EdgeInsets.only(top: _barOffset),
padding: widget.padding,
margin: EdgeInsets.only(top: _barOffset),
child: widget.scrollThumbBuilder(
widget.backgroundColor,
widget.foregroundColor,
_thumbAnimation,
_labelAnimation,
widget.heightScrollThumb,
labelText: labelText,
labelConstraints: widget.labelConstraints,
labelText: labelText,
),
),
),
@@ -356,7 +356,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
_thumbAnimationController.forward();
}
final lastItemPos = itemPos;
final lastItemPos = _itemPos;
if (lastItemPos < widget.maxItemCount) {
_currentItem = lastItemPos;
}
@@ -378,7 +378,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
widget.scrollStateListener(true);
}
int get itemIndex {
int get _itemIndex {
int index = 0;
double minDiff = 1000;
for (final pos in _positions) {
@@ -391,21 +391,21 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
return index;
}
int get itemPos =>
int get _itemPos =>
((_barOffset / (_barMaxScrollExtent)) * widget.maxItemCount).toInt();
void _jumpToBarPos() {
final lastItemPos = itemPos;
final lastItemPos = _itemPos;
if (lastItemPos > widget.maxItemCount - 1) {
return;
}
_currentItem = itemIndex;
_currentItem = _itemIndex;
widget.controller.sliverController.jumpToIndex(lastItemPos);
}
Timer? _dragHaltTimer;
int lastTimerPos = 0;
int _lastTimerPos = 0;
void _onVerticalDragUpdate(DragUpdateDetails details) {
setState(() {
@@ -418,9 +418,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
_barOffset =
clampDouble(_barOffset, _barMinScrollExtent, _barMaxScrollExtent);
final lastItemPos = itemPos;
if (lastItemPos != lastTimerPos) {
lastTimerPos = lastItemPos;
final lastItemPos = _itemPos;
if (lastItemPos != _lastTimerPos) {
_lastTimerPos = lastItemPos;
_dragHaltTimer?.cancel();
widget.scrollStateListener(true);
@@ -26,6 +26,17 @@ class AssetGridState {
renderList: renderList ?? this.renderList,
);
}
@override
bool operator ==(covariant AssetGridState other) {
if (identical(this, other)) return true;
return other.renderList == renderList &&
other.isDragScrolling == isDragScrolling;
}
@override
int get hashCode => renderList.hashCode ^ isDragScrolling.hashCode;
}
class AssetGridCubit extends Cubit<AssetGridState> {
@@ -43,6 +54,9 @@ class AssetGridCubit extends Cubit<AssetGridState> {
super(AssetGridState.empty()) {
_renderListSubscription =
_renderListProvider.renderStreamProvider().listen((renderList) {
if (renderList == state.renderList) {
return;
}
_bufOffset = 0;
_buf = [];
emit(state.copyWith(renderList: renderList));
@@ -87,8 +101,8 @@ class AssetGridCubit extends Cubit<AssetGridState> {
// load the calculated batch (start:start+len) from the DB and put it into the buffer
_buf = await _renderListProvider.renderAssetProvider(
offset: start,
limit: len,
offset: start,
);
_bufOffset = start;
@@ -13,6 +13,7 @@ import 'package:intl/intl.dart';
import 'package:material_symbols_icons/symbols.dart';
part 'immich_asset_grid_header.widget.dart';
part 'immich_asset_render_grid.widget.dart';
class ImAssetGrid extends StatefulWidget {
/// The padding for the grid
@@ -76,7 +77,6 @@ class _ImAssetGridState extends State<ImAssetGrid> {
}
final grid = FlutterListView(
controller: _controller,
delegate: FlutterListViewDelegate(
(_, sectionIndex) {
// ignore: avoid-unsafe-collection-methods
@@ -89,70 +89,46 @@ class _ImAssetGridState extends State<ImAssetGrid> {
RenderListMonthHeaderElement() =>
_MonthHeader(text: section.header),
RenderListDayHeaderElement() => Text(section.header),
RenderListAssetElement() => FutureBuilder(
future: context.read<AssetGridCubit>().loadAssets(
section.assetOffset,
section.assetCount,
),
builder: (_, assetsSnap) {
final assets = assetsSnap.data;
return GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
addAutomaticKeepAlives: false,
cacheExtent: 100,
padding: const EdgeInsets.all(0),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 3,
crossAxisSpacing: 3,
),
itemBuilder: (_, i) {
final asset = assetsSnap.isWaiting || assets == null
? null
: assets.elementAtOrNull(i);
return SizedBox.square(
dimension: 200,
// Show Placeholder when drag scrolled
child: asset == null || state.isDragScrolling
? const ImImagePlaceholder()
: ImThumbnail(asset),
);
},
itemCount: section.assetCount,
);
},
RenderListAssetElement() => _StaticGrid(
section: section,
isDragging: state.isDragScrolling,
),
};
},
childCount: elements.length,
addAutomaticKeepAlives: false,
),
controller: _controller,
);
final EdgeInsetsGeometry? padding;
if (widget.topPadding != null) {
padding = EdgeInsets.only(top: widget.topPadding!);
} else {
if (widget.topPadding == null) {
padding = null;
} else {
padding = EdgeInsets.only(top: widget.topPadding!);
}
return DraggableScrollbar(
foregroundColor: context.colorScheme.onSurface,
backgroundColor: context.colorScheme.surfaceContainerHighest,
scrollStateListener:
context.read<AssetGridCubit>().setDragScrolling,
controller: _controller,
maxItemCount: elements.length,
scrollStateListener:
context.read<AssetGridCubit>().setDragScrolling,
backgroundColor: context.colorScheme.surfaceContainerHighest,
foregroundColor: context.colorScheme.onSurface,
padding: padding,
scrollbarAnimationDuration: Durations.medium2,
scrollbarTimeToFade: Durations.extralong4,
labelTextBuilder: (int position) =>
_labelBuilder(elements, position),
labelConstraints: const BoxConstraints(maxHeight: 36),
scrollbarAnimationDuration: Durations.medium2,
scrollbarTimeToFade: Durations.extralong4,
padding: padding,
child: grid,
);
},
// no.of elements are not equal or is modified
buildWhen: (previous, current) =>
(previous.renderList.elements.length !=
current.renderList.elements.length) ||
!previous.renderList.modifiedTime
.isAtSameMomentAs(current.renderList.modifiedTime),
);
}
@@ -10,8 +10,8 @@ class _HeaderText extends StatelessWidget {
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
top: 32.0,
left: 16.0,
top: 32.0,
right: 24.0,
bottom: 16.0,
),
@@ -40,9 +40,9 @@ class _MonthHeader extends StatelessWidget {
return _HeaderText(
text: text,
style: context.textTheme.bodyLarge?.copyWith(
color: context.colorScheme.onSurface,
fontSize: 24.0,
fontWeight: FontWeight.w500,
color: context.colorScheme.onSurface,
),
);
}
@@ -0,0 +1,46 @@
part of 'immich_asset_grid.widget.dart';
class _StaticGrid extends StatelessWidget {
final RenderListAssetElement section;
final bool isDragging;
const _StaticGrid({required this.section, required this.isDragging});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: context.read<AssetGridCubit>().loadAssets(
section.assetOffset,
section.assetCount,
),
builder: (_, assetsSnap) {
final assets = assetsSnap.data;
return GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsets.all(0),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 3,
crossAxisSpacing: 3,
),
itemBuilder: (_, i) {
final asset = assetsSnap.isWaiting || assets == null
? null
: assets.elementAtOrNull(i);
return SizedBox.square(
dimension: 200,
// Show Placeholder when drag scrolled
child: asset == null || isDragging
? const ImImagePlaceholder()
: ImThumbnail(asset),
);
},
itemCount: section.assetCount,
addAutomaticKeepAlives: false,
cacheExtent: 100,
);
},
);
}
}
@@ -14,8 +14,8 @@ class ImRemoteThumbnailCacheManager extends CacheManager {
: super(
Config(
kCacheThumbnailsKey,
maxNrOfCacheObjects: kCacheMaxNrOfThumbnails,
stalePeriod: const Duration(days: kCacheStalePeriod),
maxNrOfCacheObjects: kCacheMaxNrOfThumbnails,
),
);
}
@@ -33,8 +33,8 @@ class ImRemoteImageCacheManager extends CacheManager {
: super(
Config(
kCacheFullImagesKey,
maxNrOfCacheObjects: kCacheMaxNrOfFullImages,
stalePeriod: const Duration(days: kCacheStalePeriod),
maxNrOfCacheObjects: kCacheMaxNrOfFullImages,
),
);
}
@@ -19,7 +19,7 @@ class ImageLoadingException implements Exception {
///
/// Credit to [flutter_cached_network_image](https://github.com/Baseflow/flutter_cached_network_image/blob/develop/cached_network_image/lib/src/image_provider/_image_loader.dart)
/// for this wonderful implementation of their image loader
class ImageLoader {
abstract final class ImageLoader {
static Future<ui.Codec> loadImageFromCache(
String uri, {
required CacheManager cache,
@@ -28,8 +28,8 @@ class ImageLoader {
}) async {
final stream = cache.getFileStream(
uri,
withProgress: chunkEvents != null,
headers: di<ImApiClient>().headers,
withProgress: chunkEvents != null,
);
await for (final result in stream) {
@@ -44,8 +44,7 @@ class ImageLoader {
} else if (result is FileInfo) {
// We have the file
final buffer = await ui.ImmutableBuffer.fromFilePath(result.file.path);
final decoded = await decode(buffer);
return decoded;
return await decode(buffer);
}
}
@@ -0,0 +1,17 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/immich_api_client.dart';
class ImCachedNetworkImage extends CachedNetworkImage {
ImCachedNetworkImage({
super.key,
required super.imageUrl,
super.cacheKey,
super.height,
super.width,
super.fit,
super.placeholder,
super.fadeInDuration,
super.errorWidget,
}) : super(httpHeaders: di<ImApiClient>().headers);
}
@@ -12,9 +12,9 @@ class ImImagePlaceholder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: context.colorScheme.surfaceContainerHighest,
width: 200,
height: 200,
color: context.colorScheme.surfaceContainerHighest,
);
}
}
@@ -63,13 +63,8 @@ class ImImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return OctoImage(
fadeInDuration: const Duration(milliseconds: 0),
fadeOutDuration: Durations.short4,
placeholderBuilder: (_) => placeholder,
image: ImImage.imageProvider(asset: asset),
width: width,
height: height,
fit: BoxFit.cover,
placeholderBuilder: (_) => placeholder,
errorBuilder: (_, error, stackTrace) {
if (error is PlatformException &&
error.code == "The asset not found!") {
@@ -86,6 +81,11 @@ class ImImage extends StatelessWidget {
color: context.colorScheme.primary,
);
},
fadeOutDuration: Durations.short4,
fadeInDuration: Duration.zero,
width: width,
height: height,
fit: BoxFit.cover,
);
}
}
@@ -4,13 +4,13 @@ import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
class ImLogo extends StatelessWidget {
const ImLogo({
this.width,
this.dimension,
this.filterQuality = FilterQuality.high,
super.key,
});
/// The width of the image.
final double? width;
/// The dimension of the image.
final double? dimension;
/// The rendering quality
final FilterQuality filterQuality;
@@ -18,11 +18,12 @@ class ImLogo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Image(
width: width,
filterQuality: filterQuality,
semanticLabel: 'Immich Logo',
image: Assets.images.immichLogo.provider(),
semanticLabel: 'Immich Logo',
width: dimension,
height: dimension,
isAntiAlias: true,
filterQuality: filterQuality,
);
}
}
@@ -43,10 +44,10 @@ class ImLogoText extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Image(
semanticLabel: 'Immich Logo Text',
image: (context.isDarkTheme
? Assets.images.immichTextDark.provider
: Assets.images.immichTextLight.provider)(),
semanticLabel: 'Immich Logo Text',
width: fontSize * 4,
filterQuality: FilterQuality.high,
);
@@ -77,9 +77,9 @@ class _PadAlignedIcon extends StatelessWidget {
alignment: alignment,
child: Icon(
icon,
color: Colors.white,
size: 20,
fill: (filled != null && filled!) ? 1 : null,
color: Colors.white,
),
),
);
@@ -49,12 +49,12 @@ class ImLocalImageProvider extends ImageProvider<ImLocalImageProvider> {
// Load a small thumbnail
final thumbBytes =
await di<IDeviceAssetRepository>().getThumbnail(a.localId!);
if (thumbBytes != null) {
if (thumbBytes == null) {
debugPrint("Loading thumb for ${a.name} failed");
} else {
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
final codec = await decode(buffer);
yield codec;
} else {
debugPrint("Loading thumb for ${a.name} failed");
}
if (asset.isImage) {
@@ -56,12 +56,12 @@ class ImLocalThumbnailProvider extends ImageProvider<ImLocalThumbnailProvider> {
// Load a small thumbnail
final thumbBytes = await di<IDeviceAssetRepository>()
.getThumbnail(a.localId!, width: 32, height: 32, quality: 75);
if (thumbBytes != null) {
if (thumbBytes == null) {
debugPrint("Loading thumb for ${a.name} failed");
} else {
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
final codec = await decode(buffer);
yield codec;
} else {
debugPrint("Loading thumb for ${a.name} failed");
}
final normalThumbBytes = await di<IDeviceAssetRepository>()
@@ -0,0 +1,68 @@
import 'dart:typed_data';
final Uint8List kTransparentImage = Uint8List.fromList([
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
]);
@@ -40,33 +40,33 @@ class ImPasswordFormField extends StatefulWidget {
}
class _ImPasswordFormFieldState extends State<ImPasswordFormField> {
final showPassword = ValueNotifier(false);
final _showPassword = ValueNotifier(false);
@override
void dispose() {
showPassword.dispose();
_showPassword.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: showPassword,
valueListenable: _showPassword,
builder: (_, showPass, child) => ImTextFormField(
controller: widget.controller,
focusNode: widget.focusNode,
onChanged: widget.onChanged,
shouldObscure: !showPass,
hint: widget.hint,
label: widget.label,
focusNode: widget.focusNode,
suffixIcon: IconButton(
onPressed: () => showPassword.value = !showPassword.value,
onPressed: () => _showPassword.value = !_showPassword.value,
icon: Icon(
showPassword.value
_showPassword.value
? Symbols.visibility_off_rounded
: Symbols.visibility_rounded,
),
),
label: widget.label,
hint: widget.hint,
autoFillHints: const [AutofillHints.password],
keyboardType: TextInputType.visiblePassword,
textInputAction: widget.textInputAction,
@@ -29,18 +29,27 @@ class ImSwitchListTile<T> extends StatefulWidget {
class _ImSwitchListTileState<T> extends State<ImSwitchListTile<T>> {
// Actual switch list state
late bool isEnabled;
late bool _isEnabled;
final AppSettingService _appSettingService = di();
Future<void> set(bool enabled) async {
if (isEnabled == enabled) return;
Future<void> _set(bool enabled) async {
if (_isEnabled == enabled) return;
final value = T != bool ? widget.toAppSetting!(enabled) : enabled as T;
final value = T == bool ? enabled as T : widget.toAppSetting!(enabled);
if (value != null &&
await _appSettingService.upsert(widget.setting, value) &&
context.mounted) {
setState(() {
isEnabled = enabled;
_isEnabled = enabled;
});
}
}
Future<void> _initSetting() async {
final value = await _appSettingService.get(widget.setting);
if (context.mounted) {
setState(() {
_isEnabled = T == bool ? value as bool : widget.fromAppSetting!(value);
});
}
}
@@ -48,20 +57,14 @@ class _ImSwitchListTileState<T> extends State<ImSwitchListTile<T>> {
@override
void initState() {
super.initState();
_appSettingService.get(widget.setting).then((value) {
if (context.mounted) {
setState(() {
isEnabled = T != bool ? widget.fromAppSetting!(value) : value as bool;
});
}
});
_initSetting().ignore();
}
@override
Widget build(BuildContext context) {
return SwitchListTile(
value: isEnabled,
onChanged: (value) => set(value),
value: _isEnabled,
onChanged: (value) => unawaited(_set(value)),
);
}
}
@@ -66,21 +66,21 @@ class ImTextFormField extends StatelessWidget {
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
onChanged: onChanged,
focusNode: focusNode,
obscureText: shouldObscure,
validator: validator,
decoration: InputDecoration(
labelText: label,
hintText: hint,
suffixIcon: suffixIcon,
),
autofillHints: autoFillHints,
keyboardType: keyboardType,
textInputAction: textInputAction,
readOnly: isDisabled,
obscureText: shouldObscure,
onChanged: onChanged,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onFieldSubmitted: onSubmitted,
validator: validator,
autofillHints: autoFillHints,
);
}
}
@@ -1,36 +1,44 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
class ImAdaptiveRoutePrimaryAppBar extends StatelessWidget
class ImAdaptiveRouteAppBar extends StatelessWidget
implements PreferredSizeWidget {
const ImAdaptiveRoutePrimaryAppBar({super.key});
final String? title;
final bool isPrimary;
@override
Widget build(BuildContext context) {
return AppBar(
leading: BackButton(onPressed: () => context.router.root.maybePop()),
);
}
/// Passed to [AppBar] actions
final List<Widget>? actions;
const ImAdaptiveRouteAppBar({
super.key,
this.title,
this.isPrimary = true,
this.actions,
});
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
// ignore: prefer-single-widget-per-file
class ImAdaptiveRouteSecondaryAppBar extends StatelessWidget
implements PreferredSizeWidget {
const ImAdaptiveRouteSecondaryAppBar({super.key});
@override
Widget build(BuildContext context) {
final Widget leading;
if (isPrimary) {
leading = BackButton(
onPressed: () => unawaited(context.router.root.maybePop()),
);
} else {
leading = context.isTablet
? CloseButton(onPressed: () => unawaited(context.maybePop()))
: BackButton(onPressed: () => unawaited(context.maybePop()));
}
return AppBar(
leading: context.isTablet
? CloseButton(onPressed: () => context.maybePop())
: BackButton(onPressed: () => context.maybePop()),
leading: leading,
title: title == null ? null : Text(title!),
actions: actions,
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
@@ -27,7 +27,7 @@ class ImAdaptiveRouteWrapper extends StatelessWidget {
return ImAdaptiveScaffoldBody(
primaryBody: primaryBody,
secondaryBody:
ctx.topRoute.name != primaryRoute ? (_) => child : null,
ctx.topRoute.name == primaryRoute ? null : (_) => child,
bodyRatio: bodyRatio,
);
}
@@ -21,14 +21,11 @@ class ImAdaptiveScaffoldBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AdaptiveLayout(
internalAnimations: false,
transitionDuration: Durations.medium2,
bodyRatio: bodyRatio,
body: SlotLayout(
config: {
Breakpoints.standard: SlotLayout.from(
key: const Key('ImAdaptiveScaffold Body Standard'),
builder: primaryBody,
key: const Key('ImAdaptiveScaffold Body Standard'),
),
},
),
@@ -37,11 +34,14 @@ class ImAdaptiveScaffoldBody extends StatelessWidget {
/// No secondary body in mobile layouts
Breakpoints.small: SlotLayoutConfig.empty(),
Breakpoints.mediumAndUp: SlotLayout.from(
key: const Key('ImAdaptiveScaffold Secondary Body Medium'),
builder: secondaryBody,
key: const Key('ImAdaptiveScaffold Secondary Body Medium'),
),
},
),
bodyRatio: bodyRatio,
transitionDuration: Durations.medium2,
internalAnimations: false,
);
}
}