From e6244edac73f03b90265e26de8e263bbaa133285 Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Fri, 13 Sep 2024 04:00:50 +0530 Subject: [PATCH] chore: style grid --- .../domain/interfaces/asset.interface.dart | 2 +- .../models/render_list_element.model.dart | 68 +++++++++++++------ .../domain/repositories/asset.repository.dart | 8 +-- .../domain/services/app_setting.service.dart | 10 +-- .../render_list.service.dart} | 26 +++---- .../lib/domain/services/sync.service.dart | 13 ++-- .../grid/immich_asset_grid.widget.dart | 21 ++++-- .../grid/immich_asset_grid_header.widget.dart | 43 ++++++++++++ .../modules/home/pages/home.page.dart | 2 +- .../modules/login/pages/login.page.dart | 1 + .../login/states/login_page.state.dart | 3 +- .../login/widgets/login_form.widget.dart | 26 +++---- .../theme/models/app_colors.model.dart | 6 +- .../modules/theme/models/app_theme.model.dart | 21 ++++-- mobile-v2/lib/service_locator.dart | 65 ++++++++++++------ mobile-v2/lib/utils/constants/globals.dart | 4 ++ .../extensions/async_snapshot.extension.dart | 6 ++ .../extensions/build_context.extension.dart | 6 ++ mobile-v2/lib/utils/immich_api_client.dart | 21 ------ mobile-v2/lib/utils/isolate_helper.dart | 46 +++++++++++++ open-api/bin/generate-open-api.sh | 9 ++- open-api/patch/api-v2.dart.patch | 8 +++ .../native/native_class.mustache.patch | 56 +++++++++++++++ 23 files changed, 344 insertions(+), 127 deletions(-) rename mobile-v2/lib/domain/{models/render_list.model.dart => services/render_list.service.dart} (73%) create mode 100644 mobile-v2/lib/presentation/components/grid/immich_asset_grid_header.widget.dart create mode 100644 mobile-v2/lib/utils/extensions/async_snapshot.extension.dart create mode 100644 mobile-v2/lib/utils/isolate_helper.dart create mode 100644 open-api/patch/api-v2.dart.patch create mode 100644 open-api/templates/mobile-v2/serialization/native/native_class.mustache.patch diff --git a/mobile-v2/lib/domain/interfaces/asset.interface.dart b/mobile-v2/lib/domain/interfaces/asset.interface.dart index 5e040bd0ef..e21a14760a 100644 --- a/mobile-v2/lib/domain/interfaces/asset.interface.dart +++ b/mobile-v2/lib/domain/interfaces/asset.interface.dart @@ -1,5 +1,5 @@ import 'package:immich_mobile/domain/models/asset.model.dart'; -import 'package:immich_mobile/domain/models/render_list.model.dart'; +import 'package:immich_mobile/domain/services/render_list.service.dart'; abstract class IAssetRepository { /// Batch insert asset diff --git a/mobile-v2/lib/domain/models/render_list_element.model.dart b/mobile-v2/lib/domain/models/render_list_element.model.dart index 2d90794b31..cb1071fdac 100644 --- a/mobile-v2/lib/domain/models/render_list_element.model.dart +++ b/mobile-v2/lib/domain/models/render_list_element.model.dart @@ -1,42 +1,70 @@ +import 'package:intl/intl.dart'; + +enum RenderListGroupBy { month, day } + sealed class RenderListElement { - const RenderListElement(); + const RenderListElement({required this.date}); + + final DateTime date; + + @override + bool operator ==(covariant RenderListElement other) { + if (identical(this, other)) return true; + + return date == other.date; + } + + @override + int get hashCode => date.hashCode; } class RenderListMonthHeaderElement extends RenderListElement { - final String header; + late final String header; - const RenderListMonthHeaderElement({required this.header}); + RenderListMonthHeaderElement({required super.date}) { + final formatter = DateTime.now().year == date.year + ? DateFormat.MMMM() + : DateFormat.yMMMM(); + header = formatter.format(date); + } + + @override + bool operator ==(covariant RenderListMonthHeaderElement other) { + if (identical(this, other)) return true; + + return super == other && header == other.header; + } + + @override + int get hashCode => super.hashCode ^ date.hashCode; } class RenderListDayHeaderElement extends RenderListElement { final String header; - const RenderListDayHeaderElement({required this.header}); + const RenderListDayHeaderElement({required super.date, required this.header}); + + @override + bool operator ==(covariant RenderListDayHeaderElement other) { + if (identical(this, other)) return true; + + return super == other && header == other.header; + } + + @override + int get hashCode => super.hashCode ^ date.hashCode; } class RenderListAssetElement extends RenderListElement { - final DateTime date; final int assetCount; final int assetOffset; const RenderListAssetElement({ - required this.date, + required super.date, required this.assetCount, required this.assetOffset, }); - RenderListAssetElement copyWith({ - DateTime? date, - int? assetCount, - int? assetOffset, - }) { - return RenderListAssetElement( - date: date ?? this.date, - assetCount: assetCount ?? this.assetCount, - assetOffset: assetOffset ?? this.assetOffset, - ); - } - @override String toString() => 'RenderListAssetElement(date: $date, assetCount: $assetCount, assetOffset: $assetOffset)'; @@ -45,12 +73,12 @@ class RenderListAssetElement extends RenderListElement { bool operator ==(covariant RenderListAssetElement other) { if (identical(this, other)) return true; - return other.date == date && + return super == other && other.assetCount == assetCount && other.assetOffset == assetOffset; } @override int get hashCode => - date.hashCode ^ assetCount.hashCode ^ assetOffset.hashCode; + super.hashCode ^ assetCount.hashCode ^ assetOffset.hashCode; } diff --git a/mobile-v2/lib/domain/repositories/asset.repository.dart b/mobile-v2/lib/domain/repositories/asset.repository.dart index e9cb398314..d64f852e70 100644 --- a/mobile-v2/lib/domain/repositories/asset.repository.dart +++ b/mobile-v2/lib/domain/repositories/asset.repository.dart @@ -4,12 +4,11 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/entities/asset.entity.drift.dart'; import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; import 'package:immich_mobile/domain/models/asset.model.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/domain/repositories/database.repository.dart'; +import 'package:immich_mobile/domain/services/render_list.service.dart'; import 'package:immich_mobile/utils/extensions/drift.extension.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; -import 'package:intl/intl.dart'; class RemoteAssetDriftRepository with LogContext implements IAssetRepository { final DriftDatabaseRepository _db; @@ -66,7 +65,6 @@ class RemoteAssetDriftRepository with LogContext implements IAssetRepository { ..orderBy([OrderingTerm.desc(createdTimeExp)]); int lastAssetOffset = 0; - final monthFormatter = DateFormat.yMMMM(); return query .expand((row) { @@ -76,9 +74,7 @@ class RemoteAssetDriftRepository with LogContext implements IAssetRepository { lastAssetOffset += assetCount; return [ - RenderListMonthHeaderElement( - header: monthFormatter.format(createdTime), - ), + RenderListMonthHeaderElement(date: createdTime), RenderListAssetElement( date: createdTime, assetCount: assetCount, diff --git a/mobile-v2/lib/domain/services/app_setting.service.dart b/mobile-v2/lib/domain/services/app_setting.service.dart index 2213d1cde9..641b75c755 100644 --- a/mobile-v2/lib/domain/services/app_setting.service.dart +++ b/mobile-v2/lib/domain/services/app_setting.service.dart @@ -2,21 +2,21 @@ import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/models/app_setting.model.dart'; class AppSettingService { - final IStoreRepository store; + final IStoreRepository _store; - const AppSettingService(this.store); + const AppSettingService(this._store); Future getSetting(AppSetting setting) async { - final value = await store.tryGet(setting.storeKey); + final value = await _store.tryGet(setting.storeKey); return value ?? setting.defaultValue; } Future setSetting(AppSetting setting, T value) async { - return await store.set(setting.storeKey, value); + return await _store.set(setting.storeKey, value); } Stream watchSetting(AppSetting setting) { - return store + return _store .watch(setting.storeKey) .map((value) => value ?? setting.defaultValue); } diff --git a/mobile-v2/lib/domain/models/render_list.model.dart b/mobile-v2/lib/domain/services/render_list.service.dart similarity index 73% rename from mobile-v2/lib/domain/models/render_list.model.dart rename to mobile-v2/lib/domain/services/render_list.service.dart index fa6f1bd937..009c8855bd 100644 --- a/mobile-v2/lib/domain/models/render_list.model.dart +++ b/mobile-v2/lib/domain/services/render_list.service.dart @@ -5,15 +5,16 @@ import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; import 'package:immich_mobile/domain/models/asset.model.dart'; import 'package:immich_mobile/domain/models/render_list_element.model.dart'; import 'package:immich_mobile/service_locator.dart'; +import 'package:immich_mobile/utils/constants/globals.dart'; class RenderList { final List elements; final int totalCount; - /// global offset of assets in [_buf] + /// offset of the assets from last section in [_buf] int _bufOffset = 0; - /// reference to batch of assets loaded from DB with offset [_bufOffset] + /// assets cache loaded from DB with offset [_bufOffset] List _buf = []; RenderList({required this.elements, required this.totalCount}); @@ -25,21 +26,18 @@ class RenderList { assert(count > 0); assert(offset + count <= totalCount); - // general case: we have the query to load assets via offset from the DB on demand + // 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) { - // 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 - final bool forward = _bufOffset < offset; - // if the requested offset is greater than the cached offset, the user scrolls forward "down" - const batchSize = 256; - const oppositeSize = 64; // 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(batchSize, count + oppositeSize); + 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 @@ -47,9 +45,10 @@ class RenderList { final start = math.max( 0, forward - ? offset - oppositeSize - : (len > batchSize ? offset : offset + count - len), + ? 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 di().fetchAssets(offset: start, limit: len); @@ -58,6 +57,7 @@ class RenderList { 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); } diff --git a/mobile-v2/lib/domain/services/sync.service.dart b/mobile-v2/lib/domain/services/sync.service.dart index aed0986648..6cff50897f 100644 --- a/mobile-v2/lib/domain/services/sync.service.dart +++ b/mobile-v2/lib/domain/services/sync.service.dart @@ -6,32 +6,29 @@ import 'package:immich_mobile/domain/repositories/database.repository.dart'; import 'package:immich_mobile/service_locator.dart'; import 'package:immich_mobile/utils/constants/globals.dart'; import 'package:immich_mobile/utils/immich_api_client.dart'; -import 'package:immich_mobile/utils/log_manager.dart'; +import 'package:immich_mobile/utils/isolate_helper.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; class SyncService with LogContext { - final ImmichApiClient _appClient; final DriftDatabaseRepository _db; - SyncService(this._appClient, this._db); + SyncService(this._db); Future doFullSyncForUserDrift( User user, { DateTime? updatedUtil, int? limit, }) async { - final clientData = _appClient.clientData; + final helper = IsolateHelper()..preIsolateHandling(); try { await _db.computeWithDatabase( connect: (connection) => DriftDatabaseRepository(connection), computation: (database) async { - ServiceLocator.configureServicesForIsolate(database: database); - LogManager.I.init(); + helper.postIsolateHandling(database: database); final logger = Logger("SyncService "); - final syncClient = - ImmichApiClient.clientData(clientData).getSyncApi(); + final syncClient = di().getSyncApi(); final chunkSize = limit ?? kFullSyncChunkSize; final updatedTill = updatedUtil ?? DateTime.now().toUtc(); diff --git a/mobile-v2/lib/presentation/components/grid/immich_asset_grid.widget.dart b/mobile-v2/lib/presentation/components/grid/immich_asset_grid.widget.dart index 4531af9147..36207d0b44 100644 --- a/mobile-v2/lib/presentation/components/grid/immich_asset_grid.widget.dart +++ b/mobile-v2/lib/presentation/components/grid/immich_asset_grid.widget.dart @@ -3,8 +3,13 @@ import 'package:immich_mobile/domain/interfaces/asset.interface.dart'; import 'package:immich_mobile/domain/models/render_list_element.model.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:material_symbols_icons/symbols.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +part 'immich_asset_grid_header.widget.dart'; + class ImAssetGrid extends StatelessWidget { const ImAssetGrid({super.key}); @@ -21,11 +26,14 @@ class ImAssetGrid extends StatelessWidget { final elements = renderList.elements; return ScrollablePositionedList.builder( itemCount: elements.length, + addAutomaticKeepAlives: false, + minCacheExtent: 100, itemBuilder: (_, sectionIndex) { final section = elements[sectionIndex]; return switch (section) { - RenderListMonthHeaderElement() => Text(section.header), + RenderListMonthHeaderElement() => + _MonthHeader(text: section.header), RenderListDayHeaderElement() => Text(section.header), RenderListAssetElement() => FutureBuilder( future: renderList.loadAssets( @@ -34,12 +42,11 @@ class ImAssetGrid extends StatelessWidget { ), builder: (_, assetsSnap) { final assets = assetsSnap.data; - if (assets == null) { - return const SizedBox.shrink(); - } return GridView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, + addAutomaticKeepAlives: false, + cacheExtent: 100, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, @@ -47,8 +54,10 @@ class ImAssetGrid extends StatelessWidget { itemBuilder: (_, i) { return SizedBox.square( dimension: 200, - // ignore: avoid-unsafe-collection-methods - child: ImImage(assets.elementAt(i)), + child: assetsSnap.isWaiting || assets == null + ? Container(color: Colors.grey) + // ignore: avoid-unsafe-collection-methods + : ImImage(assets[i]), ); }, itemCount: section.assetCount, diff --git a/mobile-v2/lib/presentation/components/grid/immich_asset_grid_header.widget.dart b/mobile-v2/lib/presentation/components/grid/immich_asset_grid_header.widget.dart new file mode 100644 index 0000000000..29fe39a4de --- /dev/null +++ b/mobile-v2/lib/presentation/components/grid/immich_asset_grid_header.widget.dart @@ -0,0 +1,43 @@ +part of 'immich_asset_grid.widget.dart'; + +class _HeaderText extends StatelessWidget { + final String text; + final TextStyle? style; + + const _HeaderText({required this.text, this.style}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 32.0, left: 16.0, right: 12.0), + child: Row( + children: [ + Text(text, style: style), + const Spacer(), + IconButton( + // ignore: no-empty-block + onPressed: () {}, + icon: Icon( + Symbols.check_circle_rounded, + color: context.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } +} + +class _MonthHeader extends StatelessWidget { + final String text; + + const _MonthHeader({required this.text}); + + @override + Widget build(BuildContext context) { + return _HeaderText( + text: text, + style: context.textTheme.bodyLarge?.copyWith(fontSize: 24.0), + ); + } +} diff --git a/mobile-v2/lib/presentation/modules/home/pages/home.page.dart b/mobile-v2/lib/presentation/modules/home/pages/home.page.dart index 257d3c0afe..bd33fff298 100644 --- a/mobile-v2/lib/presentation/modules/home/pages/home.page.dart +++ b/mobile-v2/lib/presentation/modules/home/pages/home.page.dart @@ -8,6 +8,6 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return const ImAssetGrid(); + return const Scaffold(body: ImAssetGrid()); } } diff --git a/mobile-v2/lib/presentation/modules/login/pages/login.page.dart b/mobile-v2/lib/presentation/modules/login/pages/login.page.dart index 4d04b83eaf..617db0f0ad 100644 --- a/mobile-v2/lib/presentation/modules/login/pages/login.page.dart +++ b/mobile-v2/lib/presentation/modules/login/pages/login.page.dart @@ -146,6 +146,7 @@ class _LoginPageState extends State child: Column(children: [ Expanded(child: rotatingLogo), serverUrl, + const SizedGap.sh(), Expanded(flex: 2, child: form), bottom, ]), diff --git a/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart b/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart index beab452d86..c2b8e8f7ac 100644 --- a/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart +++ b/mobile-v2/lib/presentation/modules/login/states/login_page.state.dart @@ -66,7 +66,8 @@ class LoginPageCubit extends Cubit with LogContext { url = await loginService.resolveEndpoint(uri); di().set(StoreKey.serverEndpoint, url); - ServiceLocator.registerPostValidationServices(url); + ServiceLocator.registerApiClient(url); + ServiceLocator.registerPostValidationServices(); ServiceLocator.registerPostGlobalStates(); // Fetch server features diff --git a/mobile-v2/lib/presentation/modules/login/widgets/login_form.widget.dart b/mobile-v2/lib/presentation/modules/login/widgets/login_form.widget.dart index 93094bece8..d37e55dabb 100644 --- a/mobile-v2/lib/presentation/modules/login/widgets/login_form.widget.dart +++ b/mobile-v2/lib/presentation/modules/login/widgets/login_form.widget.dart @@ -32,28 +32,28 @@ class LoginForm extends StatelessWidget { Widget build(BuildContext context) { return BlocSelector( selector: (model) => model.isServerValidated, - builder: (_, isServerValidated) => AnimatedSwitcher( - duration: Durations.medium1, - child: SingleChildScrollView( + builder: (_, isServerValidated) => SingleChildScrollView( + child: AnimatedSwitcher( + duration: Durations.medium1, child: isServerValidated - ? _CredentialsPage( + ? _CredentialsForm( emailController: emailController, passwordController: passwordController, ) - : _ServerPage(controller: serverUrlController), + : _ServerForm(controller: serverUrlController), + layoutBuilder: (current, previous) => + current ?? (previous.lastOrNull ?? const SizedBox.shrink()), ), - layoutBuilder: (current, previous) => - current ?? (previous.lastOrNull ?? const SizedBox.shrink()), ), ); } } -class _ServerPage extends StatelessWidget { +class _ServerForm extends StatelessWidget { final TextEditingController controller; final GlobalKey _formKey = GlobalKey(); - _ServerPage({required this.controller}); + _ServerForm({required this.controller}); Future _validateForm(BuildContext context) async { if (_formKey.currentState?.validate() == true) { @@ -96,20 +96,20 @@ class _ServerPage extends StatelessWidget { } } -class _CredentialsPage extends StatefulWidget { +class _CredentialsForm extends StatefulWidget { final TextEditingController emailController; final TextEditingController passwordController; - const _CredentialsPage({ + const _CredentialsForm({ required this.emailController, required this.passwordController, }); @override - State<_CredentialsPage> createState() => _CredentialsPageState(); + State<_CredentialsForm> createState() => _CredentialsFormState(); } -class _CredentialsPageState extends State<_CredentialsPage> { +class _CredentialsFormState extends State<_CredentialsForm> { final passwordFocusNode = FocusNode(); @override diff --git a/mobile-v2/lib/presentation/modules/theme/models/app_colors.model.dart b/mobile-v2/lib/presentation/modules/theme/models/app_colors.model.dart index 7682f9fa1c..1c725101ee 100644 --- a/mobile-v2/lib/presentation/modules/theme/models/app_colors.model.dart +++ b/mobile-v2/lib/presentation/modules/theme/models/app_colors.model.dart @@ -23,9 +23,10 @@ abstract class AppColors { onError: Color(0xfffffbff), errorContainer: Color(0xffffdad6), onErrorContainer: Color(0xff410002), - surface: Color(0xfffefbff), + surface: Color(0xFFF0EFF4), onSurface: Color(0xff1a1b21), onSurfaceVariant: Color(0xff444651), + surfaceContainer: Color(0xfffefbff), surfaceContainerHighest: Color(0xffe0e2ef), outline: Color(0xff747782), outlineVariant: Color(0xffc4c6d3), @@ -55,9 +56,10 @@ abstract class AppColors { onError: Color(0xff410002), errorContainer: Color(0xff93000a), onErrorContainer: Color(0xffffb4ab), - surface: Color(0xff1a1e22), + surface: Color(0xFF15181C), onSurface: Color(0xffe2e2e9), onSurfaceVariant: Color(0xffc2c6d2), + surfaceContainer: Color(0xff1a1e22), surfaceContainerHighest: Color(0xff424852), outline: Color(0xff8c919c), outlineVariant: Color(0xff424751), diff --git a/mobile-v2/lib/presentation/modules/theme/models/app_theme.model.dart b/mobile-v2/lib/presentation/modules/theme/models/app_theme.model.dart index e674ac04ac..bbd26875f5 100644 --- a/mobile-v2/lib/presentation/modules/theme/models/app_theme.model.dart +++ b/mobile-v2/lib/presentation/modules/theme/models/app_theme.model.dart @@ -18,7 +18,7 @@ enum AppTheme { primaryColor: color.primary, iconTheme: const IconThemeData(weight: 500, opticalSize: 24), navigationBarTheme: NavigationBarThemeData( - backgroundColor: color.surface, + backgroundColor: color.surfaceContainer, indicatorColor: color.primary, iconTheme: WidgetStateProperty.resolveWith( (Set states) { @@ -29,8 +29,9 @@ enum AppTheme { }, ), ), + scaffoldBackgroundColor: color.surface, navigationRailTheme: NavigationRailThemeData( - backgroundColor: color.surface, + backgroundColor: color.surfaceContainer, elevation: 3, indicatorColor: color.primary, selectedIconTheme: @@ -41,9 +42,21 @@ enum AppTheme { color: color.onSurface.withAlpha(175), ), ), - inputDecorationTheme: const InputDecorationTheme( - border: OutlineInputBorder(), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: color.primary), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: color.outlineVariant), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + hintStyle: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.normal, + ), ), + textSelectionTheme: TextSelectionThemeData(cursorColor: color.primary), sliderTheme: SliderThemeData( valueIndicatorColor: Color.alphaBlend(color.primary.withAlpha(80), color.onSurface) diff --git a/mobile-v2/lib/service_locator.dart b/mobile-v2/lib/service_locator.dart index 77cc7403e5..705db3a901 100644 --- a/mobile-v2/lib/service_locator.dart +++ b/mobile-v2/lib/service_locator.dart @@ -25,60 +25,83 @@ final di = GetIt.I; class ServiceLocator { const ServiceLocator._internal(); + static void _registerFactory(T Function() factoryFun) { + if (!di.isRegistered()) { + di.registerFactory(factoryFun); + } + } + + static void _registerSingleton(T instance) { + if (!di.isRegistered()) { + di.registerSingleton(instance); + } + } + + static void _registerLazySingleton( + T Function() factoryFun, + ) { + if (!di.isRegistered()) { + di.registerLazySingleton(factoryFun); + } + } + static void configureServices() { - di.registerSingleton(DriftDatabaseRepository()); + _registerSingleton(DriftDatabaseRepository()); _registerRepositories(); _registerPreGlobalStates(); } static void configureServicesForIsolate({ required DriftDatabaseRepository database, + required ImmichApiClient apiClient, }) { - di.registerSingleton(database); + _registerSingleton(database); + _registerSingleton(apiClient); + _registerRepositories(); + registerPostValidationServices(); } static void _registerRepositories() { /// Repositories - di.registerFactory(() => StoreDriftRepository(di())); - di.registerFactory(() => LogDriftRepository(di())); - di.registerFactory(() => AppSettingService(di())); - di.registerFactory(() => UserDriftRepository(di())); - di.registerFactory( + _registerFactory(() => StoreDriftRepository(di())); + _registerFactory(() => LogDriftRepository(di())); + _registerFactory(() => AppSettingService(di())); + _registerFactory(() => UserDriftRepository(di())); + _registerFactory( () => RemoteAssetDriftRepository(di()), ); /// Services - di.registerFactory(() => const LoginService()); + _registerFactory(() => const LoginService()); } static void _registerPreGlobalStates() { - di.registerSingleton(AppRouter()); - di.registerLazySingleton(() => AppThemeCubit(di())); + _registerSingleton(AppRouter()); + _registerLazySingleton(() => AppThemeCubit(di())); } - static void registerPostValidationServices(String endpoint) { - di.registerSingleton(ImmichApiClient(endpoint: endpoint)); - di.registerFactory(() => UserService( + static void registerApiClient(String endpoint) { + _registerSingleton(ImmichApiClient(endpoint: endpoint)); + } + + static void registerPostValidationServices() { + _registerFactory(() => UserService( di().getUsersApi(), )); - di.registerFactory(() => ServerInfoService( + _registerFactory(() => ServerInfoService( di().getServerApi(), )); - di.registerFactory(() => SyncService(di(), di())); + _registerFactory(() => SyncService(di())); } static void registerPostGlobalStates() { - di.registerLazySingleton( + _registerLazySingleton( () => ServerFeatureConfigCubit(di()), ); } static void registerCurrentUser(User user) { - if (di.isRegistered()) { - di().updateUser(user); - } else { - di.registerSingleton(CurrentUserCubit(user)); - } + _registerSingleton(CurrentUserCubit(user)); } } diff --git a/mobile-v2/lib/utils/constants/globals.dart b/mobile-v2/lib/utils/constants/globals.dart index 3da227d922..1742911876 100644 --- a/mobile-v2/lib/utils/constants/globals.dart +++ b/mobile-v2/lib/utils/constants/globals.dart @@ -3,6 +3,10 @@ import 'package:flutter/material.dart'; /// Log messages stored in the DB const int kLogMessageLimit = 500; +/// RenderList constants +const int kRenderListBatchSize = 512; +const int kRenderListOppositeBatchSize = 128; + /// Chunked asset sync size const int kFullSyncChunkSize = 10000; diff --git a/mobile-v2/lib/utils/extensions/async_snapshot.extension.dart b/mobile-v2/lib/utils/extensions/async_snapshot.extension.dart new file mode 100644 index 0000000000..f5f2bb9084 --- /dev/null +++ b/mobile-v2/lib/utils/extensions/async_snapshot.extension.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +extension AsynSnapShotState on AsyncSnapshot { + bool get isWaiting => connectionState == ConnectionState.waiting; + bool get isDone => connectionState == ConnectionState.done; +} diff --git a/mobile-v2/lib/utils/extensions/build_context.extension.dart b/mobile-v2/lib/utils/extensions/build_context.extension.dart index 96174caaed..759a995bb3 100644 --- a/mobile-v2/lib/utils/extensions/build_context.extension.dart +++ b/mobile-v2/lib/utils/extensions/build_context.extension.dart @@ -5,6 +5,12 @@ extension BuildContextHelper on BuildContext { /// Get the current [ThemeData] used ThemeData get theme => Theme.of(this); + /// Get the current [ColorScheme] used + ColorScheme get colorScheme => theme.colorScheme; + + /// Get the current [TextTheme] used + TextTheme get textTheme => theme.textTheme; + /// Get the default [TextStyle] TextStyle get defaultTextStyle => DefaultTextStyle.of(this).style; diff --git a/mobile-v2/lib/utils/immich_api_client.dart b/mobile-v2/lib/utils/immich_api_client.dart index ea16096f64..289129429f 100644 --- a/mobile-v2/lib/utils/immich_api_client.dart +++ b/mobile-v2/lib/utils/immich_api_client.dart @@ -12,21 +12,9 @@ import 'package:immich_mobile/utils/constants/globals.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:openapi/api.dart'; -@immutable -class ImApiClientData { - final String endpoint; - final Map headersMap; - - const ImApiClientData({required this.endpoint, required this.headersMap}); -} - class ImmichApiClient extends ApiClient with LogContext { ImmichApiClient({required String endpoint}) : super(basePath: endpoint); - /// Used to recreate the client in Isolates - ImApiClientData get clientData => - ImApiClientData(endpoint: basePath, headersMap: defaultHeaderMap); - Map get headers => defaultHeaderMap; Future init({String? accessToken}) async { @@ -49,15 +37,6 @@ class ImmichApiClient extends ApiClient with LogContext { addDefaultHeader(kImmichHeaderDeviceType, Platform.operatingSystem); } - factory ImmichApiClient.clientData(ImApiClientData data) { - final client = ImmichApiClient(endpoint: data.endpoint); - - for (final entry in data.headersMap.entries) { - client.addDefaultHeader(entry.key, entry.value); - } - return client; - } - @override Future invokeAPI( String path, diff --git a/mobile-v2/lib/utils/isolate_helper.dart b/mobile-v2/lib/utils/isolate_helper.dart new file mode 100644 index 0000000000..76d42b4f6c --- /dev/null +++ b/mobile-v2/lib/utils/isolate_helper.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/repositories/database.repository.dart'; +import 'package:immich_mobile/service_locator.dart'; +import 'package:immich_mobile/utils/immich_api_client.dart'; +import 'package:immich_mobile/utils/log_manager.dart'; + +@immutable +class _ImApiClientData { + final String endpoint; + final Map headersMap; + + const _ImApiClientData({required this.endpoint, required this.headersMap}); +} + +class IsolateHelper { + // Cache the ApiClient to reconstruct it later after inside the isolate + late final _ImApiClientData? _clientData; + + IsolateHelper(); + + void preIsolateHandling() { + final apiClient = di(); + _clientData = _ImApiClientData( + endpoint: apiClient.basePath, + headersMap: apiClient.defaultHeaderMap, + ); + } + + void postIsolateHandling({required DriftDatabaseRepository database}) { + assert(_clientData != null); + // Reconstruct client from cached data + final client = ImmichApiClient(endpoint: _clientData!.endpoint); + for (final entry in _clientData.headersMap.entries) { + client.addDefaultHeader(entry.key, entry.value); + } + + // Register all services in the isolates memory + ServiceLocator.configureServicesForIsolate( + database: database, + apiClient: client, + ); + + // Init log manager to continue listening to log events + LogManager.I.init(); + } +} diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index 01ebc79652..c186d44e13 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -24,17 +24,16 @@ function dart { function dartDio { rm -rf ../mobile-v2/openapi - cd ./templates/mobile/serialization/native + cd ./templates/mobile-v2/serialization/native wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache - patch --no-backup-if-mismatch -u native_class.mustache (); + +- // Ensure that the map contains the required keys. +- // Note 1: the values aren't checked for validity beyond being non-null. +- // Note 2: this code is stripped in release mode! +- assert(() { +- requiredKeys.forEach((key) { +- assert(json.containsKey(key), 'Required key "{{{classname}}}[$key]" is missing from JSON.'); +- assert(json[key] != null, 'Required key "{{{classname}}}[$key]" has a null value in JSON.'); +- }); +- return true; +- }()); +- + return {{{classname}}}( + {{#vars}} + {{#isDateTime}} +@@ -215,6 +204,10 @@ + ? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}} + : {{{datatypeWithEnum}}}.parse(json[r'{{{baseName}}}'].toString()), + {{/isNumber}} ++ {{#isDouble}} ++ {{{name}}}: (mapValueOfType(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(), ++ {{/isDouble}} ++ {{^isDouble}} + {{^isNumber}} + {{^isEnum}} + {{{name}}}: mapValueOfType<{{{datatypeWithEnum}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, +@@ -223,6 +216,7 @@ + {{{name}}}: {{{enumName}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, + {{/isEnum}} + {{/isNumber}} ++ {{/isDouble}} + {{/isMap}} + {{/isArray}} + {{/complexType}}