refactor: asset grid

This commit is contained in:
shenlong-tanwen
2024-09-14 22:29:51 +05:30
parent 53974e7276
commit 6fce1ebb79
23 changed files with 796 additions and 113 deletions
@@ -0,0 +1,484 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
/// Build the Scroll Thumb and label using the current configuration
typedef ScrollThumbBuilder = Widget Function(
Color backgroundColor,
Color foregroundColor,
Animation<double> thumbAnimation,
Animation<double> labelAnimation,
double height, {
Text? labelText,
BoxConstraints? labelConstraints,
});
/// Build a Text widget using the current scroll offset
typedef LabelTextBuilder = Text? Function(int item);
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
/// for quick navigation of the BoxScrollView.
class DraggableScrollbar extends StatefulWidget {
/// The view that will be scrolled with the scroll thumb
final ScrollablePositionedList child;
final ItemPositionsListener itemPositionsListener;
/// A function that builds a thumb using the current configuration
final ScrollThumbBuilder scrollThumbBuilder;
/// The height of the scroll thumb
final double heightScrollThumb;
/// The background color of the label and thumb
final Color backgroundColor;
/// The background color of the arrows
final Color foregroundColor;
/// The amount of padding that should surround the thumb
final EdgeInsetsGeometry? padding;
/// Determines how quickly the scrollbar will animate in and out
final Duration scrollbarAnimationDuration;
/// How long should the thumb be visible before fading out
final Duration scrollbarTimeToFade;
/// Build a Text widget from the current offset in the BoxScrollView
final LabelTextBuilder? labelTextBuilder;
/// Determines box constraints for Container displaying label
final BoxConstraints? labelConstraints;
/// The ScrollController for the BoxScrollView
final ItemScrollController controller;
/// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder]
final bool alwaysVisibleScrollThumb;
final Function(bool scrolling) scrollStateListener;
DraggableScrollbar({
super.key,
Key? scrollThumbKey,
this.alwaysVisibleScrollThumb = false,
required this.child,
required this.controller,
required this.itemPositionsListener,
required this.scrollStateListener,
this.heightScrollThumb = 48.0,
this.backgroundColor = Colors.white,
this.foregroundColor = Colors.black,
this.padding,
this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
this.scrollbarTimeToFade = const Duration(milliseconds: 600),
this.labelTextBuilder,
this.labelConstraints,
}) : assert(child.scrollDirection == Axis.vertical),
scrollThumbBuilder = _thumbSemicircleBuilder(
heightScrollThumb * 0.6,
scrollThumbKey,
alwaysVisibleScrollThumb,
);
@override
State createState() => _DraggableScrollbarState();
static buildScrollThumbAndLabel({
required Widget scrollThumb,
required Color backgroundColor,
required Animation<double>? thumbAnimation,
required Animation<double>? labelAnimation,
required Text? labelText,
required BoxConstraints? labelConstraints,
required bool alwaysVisibleScrollThumb,
}) {
var scrollThumbAndLabel = labelText == null
? scrollThumb
: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
_ScrollLabel(
animation: labelAnimation,
backgroundColor: backgroundColor,
constraints: labelConstraints,
child: labelText,
),
scrollThumb,
],
);
if (alwaysVisibleScrollThumb) {
return scrollThumbAndLabel;
}
return _SlideFadeTransition(
animation: thumbAnimation!,
child: scrollThumbAndLabel,
);
}
static ScrollThumbBuilder _thumbSemicircleBuilder(
double width,
Key? scrollThumbKey,
bool alwaysVisibleScrollThumb,
) {
return (
Color backgroundColor,
Color foregroundColor,
Animation<double> thumbAnimation,
Animation<double> labelAnimation,
double height, {
Text? labelText,
BoxConstraints? labelConstraints,
}) {
final scrollThumb = CustomPaint(
key: scrollThumbKey,
foregroundPainter: _ArrowCustomPainter(foregroundColor),
child: Material(
elevation: 4.0,
color: backgroundColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(height),
bottomLeft: Radius.circular(height),
topRight: const Radius.circular(4.0),
bottomRight: const Radius.circular(4.0),
),
child: Container(
constraints: BoxConstraints.tight(Size(width, height)),
),
),
);
return buildScrollThumbAndLabel(
scrollThumb: scrollThumb,
backgroundColor: backgroundColor,
thumbAnimation: thumbAnimation,
labelAnimation: labelAnimation,
labelText: labelText,
labelConstraints: labelConstraints,
alwaysVisibleScrollThumb: alwaysVisibleScrollThumb,
);
};
}
}
class _ScrollLabel extends StatelessWidget {
final Animation<double>? animation;
final Color backgroundColor;
final Text child;
final BoxConstraints? constraints;
static const BoxConstraints _defaultConstraints =
BoxConstraints.tightFor(width: 72.0, height: 28.0);
const _ScrollLabel({
required this.child,
required this.animation,
required this.backgroundColor,
this.constraints = _defaultConstraints,
});
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: animation!,
child: Container(
margin: const EdgeInsets.only(right: 12.0),
child: Material(
elevation: 4.0,
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
child: Container(
constraints: constraints ?? _defaultConstraints,
padding: const EdgeInsets.symmetric(horizontal: 15),
alignment: Alignment.center,
child: child,
),
),
),
);
}
}
class _DraggableScrollbarState extends State<DraggableScrollbar>
with TickerProviderStateMixin {
late double _barOffset;
late bool _isDragInProcess;
late int _currentItem;
late AnimationController _thumbAnimationController;
late Animation<double> _thumbAnimation;
late AnimationController _labelAnimationController;
late Animation<double> _labelAnimation;
Timer? _fadeoutTimer;
@override
void initState() {
super.initState();
_barOffset = 0.0;
_isDragInProcess = false;
_currentItem = 0;
_thumbAnimationController = AnimationController(
vsync: this,
duration: widget.scrollbarAnimationDuration,
);
_thumbAnimation = CurvedAnimation(
parent: _thumbAnimationController,
curve: Curves.fastOutSlowIn,
);
_labelAnimationController = AnimationController(
vsync: this,
duration: widget.scrollbarAnimationDuration,
);
_labelAnimation = CurvedAnimation(
parent: _labelAnimationController,
curve: Curves.fastOutSlowIn,
);
}
@override
void dispose() {
_thumbAnimationController.dispose();
_labelAnimationController.dispose();
_fadeoutTimer?.cancel();
_dragHaltTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
Text? labelText;
if (widget.labelTextBuilder != null && _isDragInProcess) {
labelText = widget.labelTextBuilder!(_currentItem);
}
return LayoutBuilder(
builder: (BuildContext _, BoxConstraints constraints) {
return NotificationListener<ScrollNotification>(
onNotification: _onScrollNotification,
child: Stack(
children: [
RepaintBoundary(child: widget.child),
RepaintBoundary(
child: GestureDetector(
onVerticalDragStart: _onVerticalDragStart,
onVerticalDragUpdate: _onVerticalDragUpdate,
onVerticalDragEnd: _onVerticalDragEnd,
child: Container(
alignment: Alignment.topRight,
margin: EdgeInsets.only(top: _barOffset),
padding: widget.padding,
child: widget.scrollThumbBuilder(
widget.backgroundColor,
widget.foregroundColor,
_thumbAnimation,
_labelAnimation,
widget.heightScrollThumb,
labelText: labelText,
labelConstraints: widget.labelConstraints,
),
),
),
),
],
),
);
},
);
}
double get _barMaxScrollExtent =>
(context.size?.height ?? 0) - widget.heightScrollThumb;
double get _barMinScrollExtent => 0;
int get maxItemCount => widget.child.itemCount;
bool _onScrollNotification(ScrollNotification notification) {
_changePosition(notification);
return false;
}
void _onScrollFade() {
_thumbAnimationController.reverse();
_labelAnimationController.reverse();
_fadeoutTimer = null;
}
// scroll bar has received notification that it's view was scrolled
// so it should also changes his position
// but only if it isn't dragged
void _changePosition(ScrollNotification notification) {
if (_isDragInProcess) {
return;
}
setState(() {
try {
if (notification is ScrollUpdateNotification) {
int? firstItemIndex = widget
.itemPositionsListener.itemPositions.value.firstOrNull?.index;
if (firstItemIndex != null) {
_barOffset = (firstItemIndex / maxItemCount) * _barMaxScrollExtent;
}
_barOffset =
clampDouble(_barOffset, _barMinScrollExtent, _barMaxScrollExtent);
}
if (notification is ScrollUpdateNotification ||
notification is OverscrollNotification) {
if (_thumbAnimationController.status != AnimationStatus.forward) {
_thumbAnimationController.forward();
}
if (itemPos < maxItemCount) {
_currentItem = itemPos;
}
_fadeoutTimer?.cancel();
_fadeoutTimer = Timer(widget.scrollbarTimeToFade, _onScrollFade);
}
} catch (_) {}
});
}
void _onVerticalDragStart(DragStartDetails details) {
setState(() {
_isDragInProcess = true;
_labelAnimationController.forward();
_fadeoutTimer?.cancel();
});
widget.scrollStateListener(true);
}
int get itemPos {
int numberOfItems = widget.child.itemCount;
return ((_barOffset / (_barMaxScrollExtent)) * numberOfItems).toInt();
}
void _jumpToBarPos() {
if (itemPos > maxItemCount - 1) {
return;
}
_currentItem = itemPos;
final alignment = (_barOffset / _barMaxScrollExtent);
widget.controller.jumpTo(
index: _currentItem,
// // Align at the top or middle while scrolling, but always align at the top while
// // towards the end.
alignment: alignment > 0.95 ? 0 : clampDouble(alignment - 0.2, 0, 1),
);
}
Timer? _dragHaltTimer;
int lastTimerPos = 0;
void _onVerticalDragUpdate(DragUpdateDetails details) {
setState(() {
if (_thumbAnimationController.status != AnimationStatus.forward) {
_thumbAnimationController.forward();
}
if (_isDragInProcess) {
_barOffset += details.delta.dy;
_barOffset =
clampDouble(_barOffset, _barMinScrollExtent, _barMaxScrollExtent);
if (itemPos != lastTimerPos) {
lastTimerPos = itemPos;
_dragHaltTimer?.cancel();
widget.scrollStateListener(true);
_dragHaltTimer = Timer(
const Duration(milliseconds: 500),
() => widget.scrollStateListener(false),
);
}
_jumpToBarPos();
}
});
}
void _onVerticalDragEnd(DragEndDetails details) {
_fadeoutTimer = Timer(widget.scrollbarTimeToFade, _onScrollFade);
setState(() {
_jumpToBarPos();
_isDragInProcess = false;
});
widget.scrollStateListener(false);
}
}
/// Draws 2 triangles like arrow up and arrow down
class _ArrowCustomPainter extends CustomPainter {
Color color;
_ArrowCustomPainter(this.color);
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color;
const width = 12.0;
const height = 8.0;
final baseX = size.width / 2;
final baseY = size.height / 2;
canvas.drawPath(
_trianglePath(Offset(baseX, baseY - 2.0), width, height, true),
paint,
);
canvas.drawPath(
_trianglePath(Offset(baseX, baseY + 2.0), width, height, false),
paint,
);
}
static Path _trianglePath(Offset o, double width, double height, bool isUp) {
return Path()
..moveTo(o.dx, o.dy)
..lineTo(o.dx + width, o.dy)
..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height)
..close();
}
}
class _SlideFadeTransition extends StatelessWidget {
final Animation<double> animation;
final Widget child;
const _SlideFadeTransition({required this.animation, required this.child});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (_, c) => animation.value == 0.0 ? const SizedBox() : c!,
child: SlideTransition(
position: Tween(
begin: const Offset(0.3, 0.0),
end: const Offset(0.0, 0.0),
).animate(animation),
child: FadeTransition(opacity: animation, child: child),
),
);
}
}
@@ -0,0 +1,87 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:immich_mobile/domain/models/asset.model.dart';
import 'package:immich_mobile/domain/models/render_list.model.dart';
import 'package:immich_mobile/utils/constants/globals.dart';
typedef RenderListProvider = Stream<RenderList> Function();
typedef RenderListAssetProvider = Future<List<Asset>> Function({
int? offset,
int? limit,
});
class ImmichAssetGridCubit extends Cubit<RenderList> {
final Stream<RenderList> _renderStream;
final RenderListAssetProvider _assetProvider;
late final StreamSubscription _renderListSubscription;
/// offset of the assets from last section in [_buf]
int _bufOffset = 0;
/// assets cache loaded from DB with offset [_bufOffset]
List<Asset> _buf = [];
ImmichAssetGridCubit({
required Stream<RenderList> renderStream,
required RenderListAssetProvider assetProvider,
}) : _renderStream = renderStream,
_assetProvider = assetProvider,
super(RenderList.empty()) {
_renderListSubscription = _renderStream.listen((renderList) {
_bufOffset = 0;
_buf = [];
emit(renderList);
});
}
/// Loads the requested assets from the database to an internal buffer if not cached
/// and returns a slice of that buffer
Future<List<Asset>> loadAssets(int offset, int count) async {
assert(offset >= 0);
assert(count > 0);
assert(offset + count <= state.totalCount);
// the requested slice (offset:offset+count) is not contained in the cache buffer `_buf`
// thus, fill the buffer with a new batch of assets that at least contains the requested
// assets and some more
if (offset < _bufOffset || offset + count > _bufOffset + _buf.length) {
final bool forward = _bufOffset < offset;
// make sure to load a meaningful amount of data (and not only the requested slice)
// otherwise, each call to [loadAssets] would result in DB call trashing performance
// fills small requests to [batchSize], adds some legroom into the opposite scroll direction for large requests
final len =
math.max(kRenderListBatchSize, count + kRenderListOppositeBatchSize);
// when scrolling forward, start shortly before the requested offset...
// when scrolling backward, end shortly after the requested offset...
// ... to guard against the user scrolling in the other direction
// a tiny bit resulting in a another required load from the DB
final start = math.max(
0,
forward
? offset - kRenderListOppositeBatchSize
: (len > kRenderListBatchSize ? offset : offset + count - len),
);
// load the calculated batch (start:start+len) from the DB and put it into the buffer
_buf = await _assetProvider(offset: start, limit: len);
_bufOffset = start;
assert(_bufOffset <= offset);
assert(_bufOffset + _buf.length >= offset + count);
}
// return the requested slice from the buffer (we made sure before that the assets are loaded!)
return _buf.slice(offset - _bufOffset, offset - _bufOffset + count);
}
@override
Future<void> close() {
_renderListSubscription.cancel();
return super.close();
}
}
@@ -1,73 +1,124 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/interfaces/asset.interface.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:immich_mobile/domain/models/render_list.model.dart';
import 'package:immich_mobile/domain/models/render_list_element.model.dart';
import 'package:immich_mobile/presentation/components/grid/draggable_scrollbar.dart';
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.state.dart';
import 'package:immich_mobile/presentation/components/image/immich_image.widget.dart';
import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/extensions/async_snapshot.extension.dart';
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
import 'package:immich_mobile/utils/extensions/color.extension.dart';
import 'package:intl/intl.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
part 'immich_asset_grid_header.widget.dart';
part 'immich_grid_asset_placeholder.widget.dart';
class ImAssetGrid extends StatelessWidget {
class ImAssetGrid extends StatefulWidget {
const ImAssetGrid({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: di<IAssetRepository>().getRenderList(),
builder: (_, renderSnap) {
final renderList = renderSnap.data;
if (renderList == null) {
return const SizedBox.shrink();
}
State createState() => _ImAssetGridState();
}
final elements = renderList.elements;
return ScrollablePositionedList.builder(
itemCount: elements.length,
addAutomaticKeepAlives: false,
minCacheExtent: 100,
itemBuilder: (_, sectionIndex) {
final section = elements[sectionIndex];
class _ImAssetGridState extends State<ImAssetGrid> {
bool _isDragScrolling = false;
final ItemScrollController _itemScrollController = ItemScrollController();
final ItemPositionsListener _itemPositionsListener =
ItemPositionsListener.create();
return switch (section) {
RenderListMonthHeaderElement() =>
_MonthHeader(text: section.header),
RenderListDayHeaderElement() => Text(section.header),
RenderListAssetElement() => FutureBuilder(
future: renderList.loadAssets(
section.assetOffset,
section.assetCount,
),
builder: (_, assetsSnap) {
final assets = assetsSnap.data;
return GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
addAutomaticKeepAlives: false,
cacheExtent: 100,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
itemBuilder: (_, i) {
return SizedBox.square(
dimension: 200,
child: assetsSnap.isWaiting || assets == null
? Container(color: Colors.grey)
// ignore: avoid-unsafe-collection-methods
: ImImage(assets[i]),
);
},
itemCount: section.assetCount,
);
},
),
};
},
);
},
void _onDragScrolling(bool isScrolling) {
if (_isDragScrolling != isScrolling) {
setState(() {
_isDragScrolling = isScrolling;
});
}
}
Text? _labelBuilder(List<RenderListElement> elements, int currentPosition) {
final element = elements.elementAtOrNull(currentPosition);
if (element == null) {
return null;
}
return Text(
DateFormat.yMMMM().format(element.date),
style: TextStyle(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
);
}
@override
Widget build(BuildContext context) =>
BlocBuilder<ImmichAssetGridCubit, RenderList>(
builder: (_, renderList) {
final elements = renderList.elements;
final grid = ScrollablePositionedList.builder(
itemCount: elements.length,
addAutomaticKeepAlives: false,
minCacheExtent: 100,
itemPositionsListener: _itemPositionsListener,
itemScrollController: _itemScrollController,
itemBuilder: (_, sectionIndex) {
final section = elements[sectionIndex];
return switch (section) {
RenderListMonthHeaderElement() =>
_MonthHeader(text: section.header),
RenderListDayHeaderElement() => Text(section.header),
RenderListAssetElement() => FutureBuilder(
future: context.read<ImmichAssetGridCubit>().loadAssets(
section.assetOffset,
section.assetCount,
),
builder: (_, assetsSnap) {
final assets = assetsSnap.data;
return GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
addAutomaticKeepAlives: false,
cacheExtent: 100,
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 || _isDragScrolling
? const _ImImagePlaceholder()
: ImImage(asset),
);
},
itemCount: section.assetCount,
);
},
),
};
},
);
return DraggableScrollbar(
foregroundColor: context.colorScheme.onSurface,
backgroundColor: context.colorScheme.surfaceContainerHighest,
scrollStateListener: _onDragScrolling,
itemPositionsListener: _itemPositionsListener,
controller: _itemScrollController,
labelTextBuilder: (int position) =>
_labelBuilder(elements, position),
labelConstraints: const BoxConstraints(maxHeight: 36),
scrollbarAnimationDuration: const Duration(milliseconds: 300),
scrollbarTimeToFade: const Duration(milliseconds: 1000),
child: grid,
);
},
);
}
@@ -9,18 +9,15 @@ class _HeaderText extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 32.0, left: 16.0, right: 12.0),
padding: const EdgeInsets.only(top: 32.0, left: 16.0, right: 24.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(text, style: style),
const Spacer(),
IconButton(
// ignore: no-empty-block
onPressed: () {},
icon: Icon(
Symbols.check_circle_rounded,
color: context.colorScheme.onSurfaceVariant,
),
Icon(
Symbols.check_circle_rounded,
color: context.colorScheme.onSurface,
),
],
),
@@ -37,7 +34,11 @@ class _MonthHeader extends StatelessWidget {
Widget build(BuildContext context) {
return _HeaderText(
text: text,
style: context.textTheme.bodyLarge?.copyWith(fontSize: 24.0),
style: context.textTheme.bodyLarge?.copyWith(
fontSize: 24.0,
fontWeight: FontWeight.w500,
color: context.colorScheme.onSurface,
),
);
}
}
@@ -0,0 +1,25 @@
part of 'immich_asset_grid.widget.dart';
class _ImImagePlaceholder extends StatelessWidget {
const _ImImagePlaceholder();
@override
Widget build(BuildContext context) {
var gradientColors = [
context.colorScheme.surfaceContainer,
context.colorScheme.surfaceContainer.darken(amount: .1),
];
return Container(
width: 200,
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: gradientColors,
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
);
}
}
@@ -27,6 +27,7 @@ class ImLogo extends StatelessWidget {
}
}
// ignore: prefer-single-widget-per-file
class ImLogoText extends StatelessWidget {
const ImLogoText({
super.key,
@@ -17,6 +17,7 @@ class ImAdaptiveRoutePrimaryAppBar extends StatelessWidget
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
// ignore: prefer-single-widget-per-file
class ImAdaptiveRouteSecondaryAppBar extends StatelessWidget
implements PreferredSizeWidget {
const ImAdaptiveRouteSecondaryAppBar({super.key});
@@ -1,6 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:immich_mobile/domain/interfaces/asset.interface.dart';
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.state.dart';
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.widget.dart';
import 'package:immich_mobile/service_locator.dart';
@RoutePage()
class HomePage extends StatelessWidget {
@@ -8,6 +12,14 @@ class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Scaffold(body: ImAssetGrid());
return Scaffold(
body: BlocProvider(
create: (_) => ImmichAssetGridCubit(
renderStream: di<IAssetRepository>().watchRenderList(),
assetProvider: di<IAssetRepository>().fetchAssets,
),
child: const ImAssetGrid(),
),
);
}
}
@@ -53,6 +53,12 @@ class _LoginPageState extends State<LoginPage>
_passwordController.text = 'demo';
}
void _onLoginPageStateChange(BuildContext context, LoginPageState state) {
if (state.isLoginSuccessful) {
context.replaceRoute(const TabControllerRoute());
}
}
@override
Widget build(BuildContext context) {
final PreferredSizeWidget? appBar;
@@ -154,11 +160,7 @@ class _LoginPageState extends State<LoginPage>
}
return BlocListener<LoginPageCubit, LoginPageState>(
listener: (_, loginState) {
if (loginState.isLoginSuccessful) {
context.replaceRoute(const TabControllerRoute());
}
},
listener: _onLoginPageStateChange,
child: Scaffold(
resizeToAvoidBottomInset: false,
appBar: appBar,
@@ -49,15 +49,23 @@ class LoginForm extends StatelessWidget {
}
}
class _ServerForm extends StatelessWidget {
class _ServerForm extends StatefulWidget {
final TextEditingController controller;
final GlobalKey<FormState> _formKey = GlobalKey();
_ServerForm({required this.controller});
const _ServerForm({required this.controller});
@override
State createState() => _ServerFormState();
}
class _ServerFormState extends State<_ServerForm> {
final GlobalKey<FormState> _formKey = GlobalKey();
Future<void> _validateForm(BuildContext context) async {
if (_formKey.currentState?.validate() == true) {
await context.read<LoginPageCubit>().validateServer(controller.text);
await context
.read<LoginPageCubit>()
.validateServer(widget.controller.text);
}
}
@@ -72,7 +80,7 @@ class _ServerForm extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
ImTextFormField(
controller: controller,
controller: widget.controller,
label: context.t.login.label.endpoint,
validator: context.read<LoginPageCubit>().validateServerUrl,
autoFillHints: const [AutofillHints.url],
@@ -22,6 +22,7 @@ class SettingsWrapperPage extends StatelessWidget {
}
@RoutePage()
// ignore: prefer-single-widget-per-file
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@@ -35,9 +36,7 @@ class SettingsPage extends StatelessWidget {
final section = SettingSection.values.elementAt(index);
return ListTile(
title: Text(context.t[section.labelKey]),
onTap: () {
context.navigateRoot(section.destination);
},
onTap: () => context.navigateRoot(section.destination),
leading: Icon(section.icon),
);
},
@@ -7,7 +7,7 @@ import 'package:immich_mobile/presentation/modules/theme/models/app_theme.model.
class AppThemeCubit extends Cubit<AppTheme> {
final AppSettingService _appSettings;
StreamSubscription? _appSettingSubscription;
late final StreamSubscription _appSettingSubscription;
AppThemeCubit(this._appSettings) : super(AppTheme.blue) {
_appSettingSubscription = _appSettings
@@ -17,7 +17,7 @@ class AppThemeCubit extends Cubit<AppTheme> {
@override
Future<void> close() {
_appSettingSubscription?.cancel();
_appSettingSubscription.cancel();
return super.close();
}
}
@@ -18,6 +18,7 @@ class SplashScreenWrapperPage extends AutoRouter implements AutoRouteWrapper {
}
@RoutePage()
// ignore: prefer-single-widget-per-file
class SplashScreenPage extends StatefulWidget {
const SplashScreenPage({super.key});