From fb6253d2d117f08471e482efd1263d951533b6bc Mon Sep 17 00:00:00 2001
From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Date: Fri, 10 May 2024 02:00:00 +0530
Subject: [PATCH] replace bloc with watch_it
---
mobile-v2/analysis_options.yaml | 7 +-
mobile-v2/assets/i18n/strings.i18n.json | 8 +
mobile-v2/build.yaml | 9 +-
mobile-v2/ios/Runner/Info.plist | 6 +
.../domain/interfaces/store.interface.dart | 2 +-
.../lib/domain/models/app_setting.model.dart | 10 ++
mobile-v2/lib/domain/models/store.model.dart | 9 +-
.../domain/repositories/store.repository.dart | 2 +-
mobile-v2/lib/domain/service_locator.dart | 29 ----
.../domain/services/app_setting.service.dart | 22 +++
mobile-v2/lib/domain/store_manager.dart | 10 +-
mobile-v2/lib/immich_app.dart | 35 ++++
mobile-v2/lib/main.dart | 62 ++-----
.../home_page/cubit/home_cubit.dart | 15 --
.../home_page/cubit/home_state.dart | 41 -----
.../modules/home/pages/home.page.dart | 12 ++
.../modules/library/pages/library.page.dart | 12 ++
.../modules/search/pages/search.page.dart | 12 ++
.../modules/settings/pages/settings.page.dart | 12 ++
.../modules/sharing/pages/sharing.page.dart | 12 ++
mobile-v2/lib/presentation/router.dart | 9 -
.../router/pages/tab_controller.page.dart | 132 ++++++++++++++
mobile-v2/lib/presentation/router/router.dart | 25 +++
.../theme/states/app_theme.state.dart | 30 ++++
.../lib/presentation/theme/utils/colors.dart | 92 ++++++++++
.../theme/widgets/app_theme_builder.dart | 39 +++++
mobile-v2/lib/service_locator.dart | 40 +++++
mobile-v2/pubspec.lock | 164 ++++++++++++------
mobile-v2/pubspec.yaml | 44 ++---
29 files changed, 663 insertions(+), 239 deletions(-)
create mode 100644 mobile-v2/assets/i18n/strings.i18n.json
create mode 100644 mobile-v2/lib/domain/models/app_setting.model.dart
delete mode 100644 mobile-v2/lib/domain/service_locator.dart
create mode 100644 mobile-v2/lib/domain/services/app_setting.service.dart
create mode 100644 mobile-v2/lib/immich_app.dart
delete mode 100644 mobile-v2/lib/presentation/home_page/cubit/home_cubit.dart
delete mode 100644 mobile-v2/lib/presentation/home_page/cubit/home_state.dart
create mode 100644 mobile-v2/lib/presentation/modules/home/pages/home.page.dart
create mode 100644 mobile-v2/lib/presentation/modules/library/pages/library.page.dart
create mode 100644 mobile-v2/lib/presentation/modules/search/pages/search.page.dart
create mode 100644 mobile-v2/lib/presentation/modules/settings/pages/settings.page.dart
create mode 100644 mobile-v2/lib/presentation/modules/sharing/pages/sharing.page.dart
delete mode 100644 mobile-v2/lib/presentation/router.dart
create mode 100644 mobile-v2/lib/presentation/router/pages/tab_controller.page.dart
create mode 100644 mobile-v2/lib/presentation/router/router.dart
create mode 100644 mobile-v2/lib/presentation/theme/states/app_theme.state.dart
create mode 100644 mobile-v2/lib/presentation/theme/utils/colors.dart
create mode 100644 mobile-v2/lib/presentation/theme/widgets/app_theme_builder.dart
create mode 100644 mobile-v2/lib/service_locator.dart
diff --git a/mobile-v2/analysis_options.yaml b/mobile-v2/analysis_options.yaml
index d7485e6d50..20b96f7e00 100644
--- a/mobile-v2/analysis_options.yaml
+++ b/mobile-v2/analysis_options.yaml
@@ -15,4 +15,9 @@ dart_code_metrics:
- recommended
rules:
- prefer-match-file-name: false
- - avoid-passing-self-as-argument: false
+ - avoid-passing-self-as-argument:
+ exclude:
+ - lib/domain/repositories/**
+ - prefer-single-widget-per-file:
+ ignore-private-widgets: true
+ - prefer-correct-callback-field-name: false
diff --git a/mobile-v2/assets/i18n/strings.i18n.json b/mobile-v2/assets/i18n/strings.i18n.json
new file mode 100644
index 0000000000..b61d5cd17d
--- /dev/null
+++ b/mobile-v2/assets/i18n/strings.i18n.json
@@ -0,0 +1,8 @@
+{
+ "tab_controller": {
+ "photos": "Photos",
+ "search": "Search",
+ "sharing": "Sharing",
+ "library": "Library"
+ }
+ }
\ No newline at end of file
diff --git a/mobile-v2/build.yaml b/mobile-v2/build.yaml
index 7d88c010c0..33ed84b736 100644
--- a/mobile-v2/build.yaml
+++ b/mobile-v2/build.yaml
@@ -27,4 +27,11 @@ targets:
#autoroute @AutoRouterConfig()
auto_route_generator:auto_router_generator:
generate_for:
- - lib/presentation/router.dart
+ - lib/presentation/router/router.dart
+ #localization
+ slang_build_runner:
+ options:
+ fallback_strategy: base_locale_empty_string
+ input_directory: assets/i18n
+ output_directory: lib/i18n
+ timestamp: false
diff --git a/mobile-v2/ios/Runner/Info.plist b/mobile-v2/ios/Runner/Info.plist
index 73c6c04d66..46292be13c 100644
--- a/mobile-v2/ios/Runner/Info.plist
+++ b/mobile-v2/ios/Runner/Info.plist
@@ -45,5 +45,11 @@
UIApplicationSupportsIndirectInputEvents
+
+ CFBundleLocalizations
+
+ en
+
+
diff --git a/mobile-v2/lib/domain/interfaces/store.interface.dart b/mobile-v2/lib/domain/interfaces/store.interface.dart
index 9c37b8e4d7..e283826151 100644
--- a/mobile-v2/lib/domain/interfaces/store.interface.dart
+++ b/mobile-v2/lib/domain/interfaces/store.interface.dart
@@ -5,7 +5,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
abstract class IStoreRepository {
FutureOr getValue(StoreKey key);
- FutureOr setValue(StoreKey key, T value);
+ FutureOr setValue(StoreKey key, T value);
FutureOr deleteValue(StoreKey key);
diff --git a/mobile-v2/lib/domain/models/app_setting.model.dart b/mobile-v2/lib/domain/models/app_setting.model.dart
new file mode 100644
index 0000000000..65972f55db
--- /dev/null
+++ b/mobile-v2/lib/domain/models/app_setting.model.dart
@@ -0,0 +1,10 @@
+import 'package:immich_mobile/domain/models/store.model.dart';
+
+enum AppSettings {
+ appTheme(StoreKey.appTheme, 10);
+
+ const AppSettings(this.storeKey, this.defaultValue);
+
+ final StoreKey storeKey;
+ final T defaultValue;
+}
diff --git a/mobile-v2/lib/domain/models/store.model.dart b/mobile-v2/lib/domain/models/store.model.dart
index 872a8559a1..5f456fffec 100644
--- a/mobile-v2/lib/domain/models/store.model.dart
+++ b/mobile-v2/lib/domain/models/store.model.dart
@@ -1,10 +1,7 @@
/// Key for each possible value in the `Store`.
/// Defines the data type for each value
-enum StoreKey {
- // Server endpoint related stores
- accessToken(0, type: String),
- serverEndpoint(1, type: String),
- ;
+enum StoreKey {
+ appTheme(1000, type: int);
const StoreKey(this.id, {required this.type});
final int id;
@@ -45,7 +42,7 @@ class StoreValue {
}
}
- static StoreValue of(StoreKey key, T? value) {
+ static StoreValue of(StoreKey key, T? value) {
int? i;
String? s;
diff --git a/mobile-v2/lib/domain/repositories/store.repository.dart b/mobile-v2/lib/domain/repositories/store.repository.dart
index 39ad10273e..1987a2afc9 100644
--- a/mobile-v2/lib/domain/repositories/store.repository.dart
+++ b/mobile-v2/lib/domain/repositories/store.repository.dart
@@ -20,7 +20,7 @@ class StoreDriftRepository implements IStoreRepository {
}
@override
- FutureOr setValue(StoreKey key, T value) {
+ FutureOr setValue(StoreKey key, T value) {
return db.transaction(() async {
final storeValue = StoreValue.of(key, value);
await db.into(db.store).insertOnConflictUpdate(StoreCompanion.insert(
diff --git a/mobile-v2/lib/domain/service_locator.dart b/mobile-v2/lib/domain/service_locator.dart
deleted file mode 100644
index e5c42c54f2..0000000000
--- a/mobile-v2/lib/domain/service_locator.dart
+++ /dev/null
@@ -1,29 +0,0 @@
-import 'package:get_it/get_it.dart';
-import 'package:immich_mobile/domain/interfaces/log.interface.dart';
-import 'package:immich_mobile/domain/interfaces/store.interface.dart';
-import 'package:immich_mobile/domain/repositories/database.repository.dart';
-import 'package:immich_mobile/domain/repositories/log.repository.dart';
-import 'package:immich_mobile/domain/repositories/store.repository.dart';
-import 'package:immich_mobile/domain/store_manager.dart';
-
-/// Ambient instance
-final getIt = GetIt.instance;
-
-class ServiceLocator {
- const ServiceLocator._internal();
-
- static void configureServices() {
- // Register DB
- getIt.registerSingleton(DriftDatabaseRepository());
- _registerCoreServices();
- }
-
- static void _registerCoreServices() {
- // Init store
- getIt
- .registerFactory(() => StoreDriftRepository(getIt()));
- getIt.registerSingleton(StoreManager(getIt()));
- // Logs
- getIt.registerFactory(() => LogDriftRepository(getIt()));
- }
-}
diff --git a/mobile-v2/lib/domain/services/app_setting.service.dart b/mobile-v2/lib/domain/services/app_setting.service.dart
new file mode 100644
index 0000000000..c1e5ba5eff
--- /dev/null
+++ b/mobile-v2/lib/domain/services/app_setting.service.dart
@@ -0,0 +1,22 @@
+import 'package:immich_mobile/domain/models/app_setting.model.dart';
+import 'package:immich_mobile/domain/store_manager.dart';
+
+class AppSettingsService {
+ final StoreManager store;
+
+ const AppSettingsService(this.store);
+
+ T getSetting(AppSettings setting) {
+ return store.get(setting.storeKey, setting.defaultValue);
+ }
+
+ void setSetting(AppSettings setting, T value) {
+ store.put(setting.storeKey, value);
+ }
+
+ Stream watchSetting(AppSettings setting) {
+ return store
+ .watch(setting.storeKey)
+ .map((value) => value ?? setting.defaultValue);
+ }
+}
diff --git a/mobile-v2/lib/domain/store_manager.dart b/mobile-v2/lib/domain/store_manager.dart
index d25f75b17c..3960be6198 100644
--- a/mobile-v2/lib/domain/store_manager.dart
+++ b/mobile-v2/lib/domain/store_manager.dart
@@ -55,11 +55,11 @@ class StoreManager with LogContext {
}
/// Returns the stored value for the given key (possibly null)
- T? tryGet(StoreKey key) => _cache[key.id] as T?;
+ T? tryGet(StoreKey key) => _cache[key.id] as T?;
/// Returns the stored value for the given key or if null the [defaultValue]
/// Throws a [StoreKeyNotFoundException] if both are null
- T get(StoreKey key, [T? defaultValue]) {
+ T get(StoreKey key, [T? defaultValue]) {
final value = _cache[key.id] ?? defaultValue;
if (value == null) {
throw StoreKeyNotFoundException(key);
@@ -68,17 +68,17 @@ class StoreManager with LogContext {
}
/// Watches a specific key for changes
- Stream watch(StoreKey key) => _db.watchValue(key);
+ Stream watch(StoreKey key) => _db.watchValue(key);
/// Stores the value synchronously in the cache and asynchronously in the DB
- FutureOr put(StoreKey key, T value) async {
+ FutureOr put(StoreKey key, T value) async {
if (_cache[key.id] == value) return Future.value();
_cache[key.id] = value;
return await _db.setValue(key, value);
}
/// Removes the value synchronously from the cache and asynchronously from the DB
- Future delete(StoreKey key) async {
+ Future delete(StoreKey key) async {
if (_cache[key.id] == null) return Future.value();
_cache.remove(key.id);
return await _db.deleteValue(key);
diff --git a/mobile-v2/lib/immich_app.dart b/mobile-v2/lib/immich_app.dart
new file mode 100644
index 0000000000..14be0e8790
--- /dev/null
+++ b/mobile-v2/lib/immich_app.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:immich_mobile/i18n/strings.g.dart';
+import 'package:immich_mobile/presentation/router/router.dart';
+import 'package:watch_it/watch_it.dart';
+
+class ImmichApp extends StatefulWidget {
+ final ThemeData lightTheme;
+ final ThemeData darkTheme;
+
+ const ImmichApp({
+ required this.lightTheme,
+ required this.darkTheme,
+ super.key,
+ });
+
+ @override
+ State createState() => _ImmichAppState();
+}
+
+class _ImmichAppState extends State with WidgetsBindingObserver {
+ @override
+ Widget build(BuildContext context) {
+ final router = di();
+
+ return MaterialApp.router(
+ locale: TranslationProvider.of(context).flutterLocale,
+ supportedLocales: AppLocaleUtils.supportedLocales,
+ localizationsDelegates: GlobalMaterialLocalizations.delegates,
+ theme: widget.lightTheme,
+ darkTheme: widget.darkTheme,
+ routerConfig: router.config(),
+ );
+ }
+}
diff --git a/mobile-v2/lib/main.dart b/mobile-v2/lib/main.dart
index 6701f48118..65c7a2474a 100644
--- a/mobile-v2/lib/main.dart
+++ b/mobile-v2/lib/main.dart
@@ -1,63 +1,33 @@
import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:immich_mobile/domain/service_locator.dart';
-import 'package:immich_mobile/presentation/home_page/cubit/home_cubit.dart';
+import 'package:immich_mobile/i18n/strings.g.dart';
+import 'package:immich_mobile/immich_app.dart';
+import 'package:immich_mobile/presentation/theme/states/app_theme.state.dart';
+import 'package:immich_mobile/presentation/theme/widgets/app_theme_builder.dart';
+import 'package:immich_mobile/service_locator.dart';
+import 'package:watch_it/watch_it.dart';
void main() {
// Ensure the bindings are initialized
WidgetsFlutterBinding.ensureInitialized();
-
// DI Injection
ServiceLocator.configureServices();
-
+ // Init localization
+ LocaleSettings.useDeviceLocale();
runApp(const MainWidget());
}
-class MainWidget extends StatelessWidget {
+class MainWidget extends StatelessWidget with WatchItMixin {
const MainWidget({super.key});
@override
Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Flutter Demo',
- theme: ThemeData(
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
- useMaterial3: true,
- ),
- home: MultiBlocProvider(
- providers: [BlocProvider(create: (context) => HomeCubit())],
- child: Scaffold(
- appBar: AppBar(
- title: const Text("Immich v2"),
- ),
- body: BlocConsumer(
- listener: (context, state) {
- print(state);
- },
- builder: (context, state) {
- return Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text("Album count: ${state.albumCount}"),
- ElevatedButton(
- onPressed: () {
- context.read().increaseAlbumCount();
- },
- child: const Text("Increase"),
- ),
- ElevatedButton(
- onPressed: () {
- context.read().decreaseAlbumCount();
- },
- child: const Text("Decrease"),
- ),
- ],
- ),
- );
- },
- ),
- ),
+ final appTheme = watchIt().value;
+
+ return TranslationProvider(
+ child: AppThemeBuilder(
+ theme: appTheme,
+ builder: (lightTheme, darkTheme) =>
+ ImmichApp(lightTheme: lightTheme, darkTheme: darkTheme),
),
);
}
diff --git a/mobile-v2/lib/presentation/home_page/cubit/home_cubit.dart b/mobile-v2/lib/presentation/home_page/cubit/home_cubit.dart
deleted file mode 100644
index c67ea80c64..0000000000
--- a/mobile-v2/lib/presentation/home_page/cubit/home_cubit.dart
+++ /dev/null
@@ -1,15 +0,0 @@
-import 'package:bloc/bloc.dart';
-
-part 'home_state.dart';
-
-class HomeCubit extends Cubit {
- HomeCubit() : super(HomeState(albumCount: 0));
-
- void increaseAlbumCount() {
- emit(state.copyWith(albumCount: state.albumCount + 1));
- }
-
- void decreaseAlbumCount() {
- emit(state.copyWith(albumCount: state.albumCount - 1));
- }
-}
diff --git a/mobile-v2/lib/presentation/home_page/cubit/home_state.dart b/mobile-v2/lib/presentation/home_page/cubit/home_state.dart
deleted file mode 100644
index 64d15c3663..0000000000
--- a/mobile-v2/lib/presentation/home_page/cubit/home_state.dart
+++ /dev/null
@@ -1,41 +0,0 @@
-part of 'home_cubit.dart';
-
-class HomeState {
- final int albumCount;
- HomeState({
- required this.albumCount,
- });
-
- HomeState copyWith({
- int? albumCount,
- }) {
- return HomeState(
- albumCount: albumCount ?? this.albumCount,
- );
- }
-
- Map toMap() {
- return {
- 'albumCount': albumCount,
- };
- }
-
- factory HomeState.fromMap(Map map) {
- return HomeState(
- albumCount: map['albumCount']?.toInt() ?? 0,
- );
- }
-
- @override
- String toString() => 'HomeState(albumCount: $albumCount)';
-
- @override
- bool operator ==(Object other) {
- if (identical(this, other)) return true;
-
- return other is HomeState && other.albumCount == albumCount;
- }
-
- @override
- int get hashCode => albumCount.hashCode;
-}
diff --git a/mobile-v2/lib/presentation/modules/home/pages/home.page.dart b/mobile-v2/lib/presentation/modules/home/pages/home.page.dart
new file mode 100644
index 0000000000..687031f170
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/home/pages/home.page.dart
@@ -0,0 +1,12 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+
+@RoutePage()
+class HomePage extends StatelessWidget {
+ const HomePage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/library/pages/library.page.dart b/mobile-v2/lib/presentation/modules/library/pages/library.page.dart
new file mode 100644
index 0000000000..6d03fb70db
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/library/pages/library.page.dart
@@ -0,0 +1,12 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+
+@RoutePage()
+class LibraryPage extends StatelessWidget {
+ const LibraryPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/search/pages/search.page.dart b/mobile-v2/lib/presentation/modules/search/pages/search.page.dart
new file mode 100644
index 0000000000..c89f9e8742
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/search/pages/search.page.dart
@@ -0,0 +1,12 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+
+@RoutePage()
+class SearchPage extends StatelessWidget {
+ const SearchPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/settings/pages/settings.page.dart b/mobile-v2/lib/presentation/modules/settings/pages/settings.page.dart
new file mode 100644
index 0000000000..e6cb37ac1c
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/settings/pages/settings.page.dart
@@ -0,0 +1,12 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+
+@RoutePage()
+class SettingsPage extends StatelessWidget {
+ const SettingsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
diff --git a/mobile-v2/lib/presentation/modules/sharing/pages/sharing.page.dart b/mobile-v2/lib/presentation/modules/sharing/pages/sharing.page.dart
new file mode 100644
index 0000000000..8bd52e8aff
--- /dev/null
+++ b/mobile-v2/lib/presentation/modules/sharing/pages/sharing.page.dart
@@ -0,0 +1,12 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+
+@RoutePage()
+class SharingPage extends StatelessWidget {
+ const SharingPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
diff --git a/mobile-v2/lib/presentation/router.dart b/mobile-v2/lib/presentation/router.dart
deleted file mode 100644
index 2c9c639adf..0000000000
--- a/mobile-v2/lib/presentation/router.dart
+++ /dev/null
@@ -1,9 +0,0 @@
-import 'package:auto_route/auto_route.dart';
-
-part 'router.gr.dart';
-
-@AutoRouterConfig(replaceInRouteName: 'Page,Route')
-class AppRouter extends _$AppRouter {
- @override
- List get routes => [];
-}
diff --git a/mobile-v2/lib/presentation/router/pages/tab_controller.page.dart b/mobile-v2/lib/presentation/router/pages/tab_controller.page.dart
new file mode 100644
index 0000000000..802b573921
--- /dev/null
+++ b/mobile-v2/lib/presentation/router/pages/tab_controller.page.dart
@@ -0,0 +1,132 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
+import 'package:immich_mobile/i18n/strings.g.dart';
+import 'package:immich_mobile/presentation/router/router.dart';
+import 'package:material_symbols_icons/symbols.dart';
+
+@RoutePage()
+class TabControllerPage extends StatelessWidget {
+ const TabControllerPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return AutoTabsRouter(
+ routes: const [
+ HomeRoute(),
+ SearchRoute(),
+ SharingRoute(),
+ LibraryRoute(),
+ ],
+ builder: (ctx, child) {
+ final tabsRouter = AutoTabsRouter.of(ctx);
+ // Pop-back to photos tab or if already in photos tab, close the app
+ return PopScope(
+ canPop: tabsRouter.activeIndex == 0,
+ onPopInvoked: (didPop) =>
+ !didPop ? tabsRouter.setActiveIndex(0) : null,
+ child: _TabControllerAdaptiveScaffold(
+ body: (ctxx) => child,
+ selectedIndex: tabsRouter.activeIndex,
+ onSelectedIndexChange: (index) => tabsRouter.setActiveIndex(index),
+ destinations: [
+ NavigationDestination(
+ icon: const Icon(Symbols.photo_rounded),
+ selectedIcon: const Icon(Symbols.photo_rounded, fill: 1.0),
+ label: context.t.tab_controller.photos,
+ ),
+ NavigationDestination(
+ icon: const Icon(Symbols.search_rounded),
+ selectedIcon: const Icon(Symbols.search_rounded, fill: 1.0),
+ label: context.t.tab_controller.search,
+ ),
+ NavigationDestination(
+ icon: const Icon(Symbols.group_rounded),
+ selectedIcon: const Icon(Symbols.group_rounded, fill: 1.0),
+ label: context.t.tab_controller.sharing,
+ ),
+ NavigationDestination(
+ icon: const Icon(Symbols.newsstand_rounded),
+ selectedIcon: const Icon(Symbols.newsstand_rounded, fill: 1.0),
+ label: context.t.tab_controller.library,
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+}
+
+/// Adaptive scaffold to layout bottom navigation bar and navigation rail for the main
+/// tab controller layout. This is not used elsewhere so is private to this widget
+class _TabControllerAdaptiveScaffold extends StatelessWidget {
+ const _TabControllerAdaptiveScaffold({
+ required this.body,
+ required this.selectedIndex,
+ required this.onSelectedIndexChange,
+ required this.destinations,
+ });
+
+ final WidgetBuilder body;
+ final List destinations;
+ final int selectedIndex;
+ final void Function(int) onSelectedIndexChange;
+
+ @override
+ Widget build(BuildContext context) {
+ final NavigationRailThemeData navRailTheme =
+ Theme.of(context).navigationRailTheme;
+
+ return Scaffold(
+ body: AdaptiveLayout(
+ // No animation on layout change
+ transitionDuration: Duration.zero,
+ primaryNavigation: SlotLayout(
+ config: {
+ Breakpoints.mediumAndUp: SlotLayout.from(
+ key: const Key(
+ '_TabControllerAdaptiveScaffold Primary Navigation Medium',
+ ),
+ builder: (_) => AdaptiveScaffold.standardNavigationRail(
+ selectedIndex: selectedIndex,
+ destinations: destinations
+ .map((NavigationDestination destination) =>
+ AdaptiveScaffold.toRailDestination(destination))
+ .toList(),
+ onDestinationSelected: onSelectedIndexChange,
+ backgroundColor: navRailTheme.backgroundColor,
+ selectedIconTheme: navRailTheme.selectedIconTheme,
+ unselectedIconTheme: navRailTheme.unselectedIconTheme,
+ selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle,
+ unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle,
+ ),
+ ),
+ },
+ ),
+ body: SlotLayout(
+ config: {
+ Breakpoints.standard: SlotLayout.from(
+ key: const Key('_TabControllerAdaptiveScaffold Body'),
+ builder: body,
+ ),
+ },
+ ),
+ ),
+ bottomNavigationBar: SlotLayout(
+ config: {
+ Breakpoints.small: SlotLayout.from(
+ key: const Key(
+ '_TabControllerAdaptiveScaffold Bottom Navigation Small',
+ ),
+ builder: (_) => AdaptiveScaffold.standardBottomNavigationBar(
+ currentIndex: selectedIndex,
+ destinations: destinations,
+ onDestinationSelected: onSelectedIndexChange,
+ ),
+ ),
+ },
+ ),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/router/router.dart b/mobile-v2/lib/presentation/router/router.dart
new file mode 100644
index 0000000000..f310e823bd
--- /dev/null
+++ b/mobile-v2/lib/presentation/router/router.dart
@@ -0,0 +1,25 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:immich_mobile/presentation/modules/home/pages/home.page.dart';
+import 'package:immich_mobile/presentation/modules/library/pages/library.page.dart';
+import 'package:immich_mobile/presentation/modules/search/pages/search.page.dart';
+import 'package:immich_mobile/presentation/modules/settings/pages/settings.page.dart';
+import 'package:immich_mobile/presentation/modules/sharing/pages/sharing.page.dart';
+import 'package:immich_mobile/presentation/router/pages/tab_controller.page.dart';
+
+part 'router.gr.dart';
+
+@AutoRouterConfig(replaceInRouteName: 'Page,Route')
+class AppRouter extends _$AppRouter {
+ AppRouter();
+
+ @override
+ List get routes => [
+ AutoRoute(page: TabControllerRoute.page, initial: true, children: [
+ AutoRoute(page: HomeRoute.page),
+ AutoRoute(page: SearchRoute.page),
+ AutoRoute(page: SharingRoute.page),
+ AutoRoute(page: LibraryRoute.page),
+ ]),
+ AutoRoute(page: SettingsRoute.page),
+ ];
+}
diff --git a/mobile-v2/lib/presentation/theme/states/app_theme.state.dart b/mobile-v2/lib/presentation/theme/states/app_theme.state.dart
new file mode 100644
index 0000000000..1cc3c4f990
--- /dev/null
+++ b/mobile-v2/lib/presentation/theme/states/app_theme.state.dart
@@ -0,0 +1,30 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:immich_mobile/domain/models/app_setting.model.dart';
+import 'package:immich_mobile/domain/services/app_setting.service.dart';
+import 'package:immich_mobile/presentation/theme/utils/colors.dart';
+
+class AppThemeState extends ValueNotifier {
+ final AppSettingsService _appSettings;
+ StreamSubscription? _appSettingSubscription;
+
+ AppThemeState({required AppSettingsService appSettings})
+ : _appSettings = appSettings,
+ super(AppTheme.blue);
+
+ void init() {
+ _appSettingSubscription =
+ _appSettings.watchSetting(AppSettings.appTheme).listen((themeIndex) {
+ final theme =
+ AppTheme.values.elementAtOrNull(themeIndex) ?? AppTheme.blue;
+ value = theme;
+ });
+ }
+
+ @override
+ void dispose() {
+ _appSettingSubscription?.cancel();
+ return super.dispose();
+ }
+}
diff --git a/mobile-v2/lib/presentation/theme/utils/colors.dart b/mobile-v2/lib/presentation/theme/utils/colors.dart
new file mode 100644
index 0000000000..f365c4fc9c
--- /dev/null
+++ b/mobile-v2/lib/presentation/theme/utils/colors.dart
@@ -0,0 +1,92 @@
+import 'package:flutter/material.dart';
+
+enum AppTheme {
+ blue(AppColors._blueLight, AppColors._blueDark),
+ // Fallback color for dynamic theme for non-supported platforms
+ dynamic(AppColors._blueLight, AppColors._blueDark);
+
+ final ColorScheme lightSchema;
+ final ColorScheme darkSchema;
+
+ const AppTheme(this.lightSchema, this.darkSchema);
+}
+
+class AppColors {
+ const AppColors();
+
+ /// Blue color
+ static const ColorScheme _blueLight = ColorScheme(
+ brightness: Brightness.light,
+ primary: Color(0xff1565c0),
+ onPrimary: Color(0xffffffff),
+ primaryContainer: Color(0xffd6e3ff),
+ onPrimaryContainer: Color(0xff001b3d),
+ secondary: Color(0xff3277d2),
+ onSecondary: Color(0xfffdfbff),
+ secondaryContainer: Color(0xffecf0ff),
+ onSecondaryContainer: Color(0xff001b3d),
+ tertiary: Color(0xff7b4d88),
+ onTertiary: Color(0xfffffbff),
+ tertiaryContainer: Color(0xfffad7ff),
+ onTertiaryContainer: Color(0xff310540),
+ error: Color(0xffba1a1a),
+ onError: Color(0xfffffbff),
+ errorContainer: Color(0xffffdad6),
+ onErrorContainer: Color(0xff410002),
+ background: Color(0xfffcfafe),
+ onBackground: Color(0xff191c20),
+ surface: Color(0xfffdfbff),
+ onSurface: Color(0xff191c20),
+ surfaceVariant: Color(0xffdfe2ef),
+ onSurfaceVariant: Color(0xff424751),
+ outline: Color(0xff737782),
+ outlineVariant: Color(0xffc2c6d2),
+ shadow: Color(0xff000000),
+ scrim: Color(0xff000000),
+ inverseSurface: Color(0xff2e3036),
+ onInverseSurface: Color(0xfff0f0f7),
+ inversePrimary: Color(0xffa9c7ff),
+ surfaceTint: Color(0xff00468c),
+ );
+
+ static const ColorScheme _blueDark = ColorScheme(
+ brightness: Brightness.dark,
+ primary: Color(0xffa9c7ff),
+ onPrimary: Color(0xff001b3d),
+ primaryContainer: Color(0xff00468c),
+ onPrimaryContainer: Color(0xffd6e3ff),
+ secondary: Color(0xffd6e3ff),
+ onSecondary: Color(0xff001b3d),
+ secondaryContainer: Color(0xff003063),
+ onSecondaryContainer: Color(0xffd6e3ff),
+ tertiary: Color(0xffeab4f6),
+ onTertiary: Color(0xff310540),
+ tertiaryContainer: Color(0xff61356e),
+ onTertiaryContainer: Color(0xfffad7ff),
+ error: Color(0xffffb4ab),
+ onError: Color(0xff410002),
+ errorContainer: Color(0xff93000a),
+ onErrorContainer: Color(0xffffb4ab),
+ background: Color(0xff1a1d21),
+ onBackground: Color(0xffe2e2e9),
+ surface: Color(0xff1a1e22),
+ onSurface: Color(0xffe2e2e9),
+ surfaceVariant: Color(0xff424852),
+ onSurfaceVariant: Color(0xffc2c6d2),
+ outline: Color(0xff8c919c),
+ outlineVariant: Color(0xff424751),
+ shadow: Color(0xff000000),
+ scrim: Color(0xff000000),
+ inverseSurface: Color(0xffe1e1e9),
+ onInverseSurface: Color(0xff2e3036),
+ inversePrimary: Color(0xff005db7),
+ surfaceTint: Color(0xffa9c7ff),
+ );
+
+ static ThemeData getThemeForColorScheme(ColorScheme color) {
+ return ThemeData(
+ primaryColor: color.primary,
+ iconTheme: const IconThemeData(weight: 400),
+ );
+ }
+}
diff --git a/mobile-v2/lib/presentation/theme/widgets/app_theme_builder.dart b/mobile-v2/lib/presentation/theme/widgets/app_theme_builder.dart
new file mode 100644
index 0000000000..29acaa457b
--- /dev/null
+++ b/mobile-v2/lib/presentation/theme/widgets/app_theme_builder.dart
@@ -0,0 +1,39 @@
+import 'package:dynamic_color/dynamic_color.dart';
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/presentation/theme/utils/colors.dart';
+
+class AppThemeBuilder extends StatelessWidget {
+ const AppThemeBuilder({
+ super.key,
+ required this.theme,
+ required this.builder,
+ });
+
+ /// Current app theme to switch the theme data used
+ final AppTheme theme;
+
+ /// Builds the child widget of this widget, providing a light and dark [ThemeData] based on the
+ /// [theme] passed.
+ final Widget Function(ThemeData lightTheme, ThemeData darkTheme) builder;
+
+ @override
+ Widget build(BuildContext context) {
+ // Static colors
+ if (theme != AppTheme.dynamic) {
+ final lightTheme = AppColors.getThemeForColorScheme(theme.lightSchema);
+ final darkTheme = AppColors.getThemeForColorScheme(theme.darkSchema);
+
+ return builder(lightTheme, darkTheme);
+ }
+
+ // Dynamic color builder
+ return DynamicColorBuilder(builder: (lightDynamic, darkDynamic) {
+ final lightTheme =
+ AppColors.getThemeForColorScheme(lightDynamic ?? theme.lightSchema);
+ final darkTheme =
+ AppColors.getThemeForColorScheme(darkDynamic ?? theme.darkSchema);
+
+ return builder(lightTheme, darkTheme);
+ });
+ }
+}
diff --git a/mobile-v2/lib/service_locator.dart b/mobile-v2/lib/service_locator.dart
new file mode 100644
index 0000000000..532f99f4e8
--- /dev/null
+++ b/mobile-v2/lib/service_locator.dart
@@ -0,0 +1,40 @@
+import 'package:immich_mobile/domain/interfaces/log.interface.dart';
+import 'package:immich_mobile/domain/interfaces/store.interface.dart';
+import 'package:immich_mobile/domain/repositories/database.repository.dart';
+import 'package:immich_mobile/domain/repositories/log.repository.dart';
+import 'package:immich_mobile/domain/repositories/store.repository.dart';
+import 'package:immich_mobile/domain/services/app_setting.service.dart';
+import 'package:immich_mobile/domain/store_manager.dart';
+import 'package:immich_mobile/presentation/router/router.dart';
+import 'package:immich_mobile/presentation/theme/states/app_theme.state.dart';
+import 'package:watch_it/watch_it.dart';
+
+class ServiceLocator {
+ const ServiceLocator._internal();
+
+ static void configureServices() {
+ // Register DB
+ di.registerSingleton(DriftDatabaseRepository());
+ _registerDomainServices();
+ _registerPresentationService();
+ }
+
+ static void _registerDomainServices() {
+ // Init store
+ di.registerFactory(() => StoreDriftRepository(di()));
+ di.registerSingleton(StoreManager(di()));
+ // Logs
+ di.registerFactory(() => LogDriftRepository(di()));
+ // App Settings
+ di.registerFactory(() => AppSettingsService(di()));
+ }
+
+ static void _registerPresentationService() {
+ // App router
+ di.registerSingleton(AppRouter());
+ // Global states
+ di.registerLazySingleton(
+ () => AppThemeState(appSettings: di())..init(),
+ );
+ }
+}
diff --git a/mobile-v2/pubspec.lock b/mobile-v2/pubspec.lock
index 3bc6db29bb..b28b934eb8 100644
--- a/mobile-v2/pubspec.lock
+++ b/mobile-v2/pubspec.lock
@@ -57,14 +57,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.0.0"
- bloc:
- dependency: transitive
- description:
- name: bloc
- sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
- url: "https://pub.dev"
- source: hosted
- version: "8.1.4"
boolean_selector:
dependency: transitive
description:
@@ -209,6 +201,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
+ csv:
+ dependency: transitive
+ description:
+ name: csv
+ sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.0"
dart_style:
dependency: transitive
description:
@@ -220,21 +220,27 @@ packages:
drift:
dependency: "direct main"
description:
- path: drift
- ref: develop
- resolved-ref: a1af6f6114960caaee6a9d7699e27f92cc8c93dc
- url: "https://github.com/simolus3/drift.git"
- source: git
- version: "2.18.0-dev"
+ name: drift
+ sha256: "6acedc562ffeed308049f78fb1906abad3d65714580b6745441ee6d50ec564cd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.18.0"
drift_dev:
dependency: "direct dev"
description:
- path: drift_dev
- ref: develop
- resolved-ref: a1af6f6114960caaee6a9d7699e27f92cc8c93dc
- url: "https://github.com/simolus3/drift.git"
- source: git
- version: "2.18.0-dev"
+ name: drift_dev
+ sha256: d9b020736ea85fff1568699ce18b89fabb3f0f042e8a7a05e84a3ec20d39acde
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.18.0"
+ dynamic_color:
+ dependency: "direct main"
+ description:
+ name: dynamic_color
+ sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.7.0"
fake_async:
dependency: transitive
description:
@@ -272,22 +278,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
- flutter_bloc:
+ flutter_adaptive_scaffold:
dependency: "direct main"
description:
- name: flutter_bloc
- sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2
+ name: flutter_adaptive_scaffold
+ sha256: "9a1d5e9f728815e27b7b612883db19107ba8a35a46a97c757ea00896cb027451"
url: "https://pub.dev"
source: hosted
- version: "8.1.5"
- flutter_hooks:
- dependency: "direct main"
- description:
- name: flutter_hooks
- sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70
- url: "https://pub.dev"
- source: hosted
- version: "0.20.5"
+ version: "0.1.10+2"
flutter_lints:
dependency: "direct dev"
description:
@@ -296,6 +294,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
+ flutter_localizations:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -309,8 +312,16 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
+ functional_listener:
+ dependency: transitive
+ description:
+ name: functional_listener
+ sha256: "026d1bd4f66367f11d9ec9f1f1ddb42b89e4484b356972c76d983266cf82f33f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.1"
get_it:
- dependency: "direct main"
+ dependency: transitive
description:
name: get_it
sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1
@@ -349,6 +360,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
+ intl:
+ dependency: transitive
+ description:
+ name: intl
+ sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.18.1"
io:
dependency: transitive
description:
@@ -365,6 +384,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.1"
+ json2yaml:
+ dependency: transitive
+ description:
+ name: json2yaml
+ sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
json_annotation:
dependency: transitive
description:
@@ -429,6 +456,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.8.0"
+ material_symbols_icons:
+ dependency: "direct main"
+ description:
+ name: material_symbols_icons
+ sha256: "4410e4bb5c6e16d811340f94532c0b3161d2a0ba60b41d0fa8a603186857cabe"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2719.3"
meta:
dependency: transitive
description:
@@ -445,14 +480,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
- nested:
- dependency: transitive
- description:
- name: nested
- sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
- url: "https://pub.dev"
- source: hosted
- version: "1.0.0"
package_config:
dependency: transitive
description:
@@ -565,14 +592,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.1"
- provider:
- dependency: transitive
- description:
- name: provider
- sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
- url: "https://pub.dev"
- source: hosted
- version: "6.1.2"
pub_semver:
dependency: transitive
description:
@@ -618,8 +637,32 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
- source_gen:
+ slang:
+ dependency: "direct main"
+ description:
+ name: slang
+ sha256: ad2a3974fa705017d40e59f9fce5ba738ce78a40c13247bf655d1760d3af018f
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.30.2"
+ slang_build_runner:
dependency: "direct dev"
+ description:
+ name: slang_build_runner
+ sha256: "2daff2deb2ab8d557a2e7de5405c0ee1376afba5d0231570c2d2c3c56da8a692"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.30.0"
+ slang_flutter:
+ dependency: "direct main"
+ description:
+ name: slang_flutter
+ sha256: "9ee040b0d364d3a4d692e4af536acff6ef513870689403494ebc6d59b0dccea6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.30.0"
+ source_gen:
+ dependency: transitive
description:
name: source_gen
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
@@ -651,14 +694,13 @@ packages:
source: hosted
version: "0.5.21"
sqlparser:
- dependency: "direct overridden"
+ dependency: transitive
description:
- path: sqlparser
- ref: develop
- resolved-ref: a1af6f6114960caaee6a9d7699e27f92cc8c93dc
- url: "https://github.com/simolus3/drift.git"
- source: git
- version: "0.36.0-dev"
+ name: sqlparser
+ sha256: ade9a67fd70d0369329ed3373208de7ebd8662470e8c396fc8d0d60f9acdfc9f
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.36.0"
stack_trace:
dependency: transitive
description:
@@ -739,6 +781,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "13.0.0"
+ watch_it:
+ dependency: "direct main"
+ description:
+ name: watch_it
+ sha256: "9dc3f552d31f6ae121b0de794ab3cdea5d93627fe69337876ebe4b41bfc3729d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.1"
watcher:
dependency: transitive
description:
diff --git a/mobile-v2/pubspec.yaml b/mobile-v2/pubspec.yaml
index c231838ce3..e36d641184 100644
--- a/mobile-v2/pubspec.yaml
+++ b/mobile-v2/pubspec.yaml
@@ -10,47 +10,36 @@ environment:
dependencies:
flutter:
sdk: flutter
+ flutter_localizations:
+ sdk: flutter
# OS specific path
path_provider: ^2.0.0
path: ^1.9.0
# Database
- drift: ^2.17.0
+ drift: ^2.18.0
sqlite3: ^2.4.2
sqlite3_flutter_libs: ^0.5.0
# Route handling
auto_route: ^8.1.0
# Logging
logging: ^1.2.0
- # Hooks
- flutter_hooks: ^0.20.5
# Collection Utils
collection: ^1.18.0
- # BLOC
- flutter_bloc: ^8.1.5
- # get_it
- get_it: ^7.7.0
+ # get_it / watch_it
+ watch_it: ^1.4.1
# Photo Manager
photo_manager: ^3.0.0
photo_manager_image_provider: ^2.1.0
-
-dependency_overrides:
- # TODO: Remove the drift related overrides once the manager PR change version is released to pub
- drift:
- git:
- url: https://github.com/simolus3/drift.git
- ref: develop
- path: drift
- drift_dev:
- git:
- url: https://github.com/simolus3/drift.git
- ref: develop
- path: drift_dev
- sqlparser:
- git:
- url: https://github.com/simolus3/drift.git
- ref: develop
- path: sqlparser
+ # Dynamic colors - Android
+ dynamic_color: ^1.7.0
+ # Material symbols
+ material_symbols_icons: ^4.2719.3
+ # Localization
+ slang: ^3.30.2
+ slang_flutter: ^3.30.0
+ # Adaptive scaffold
+ flutter_adaptive_scaffold: ^0.1.10+2
dev_dependencies:
flutter_test:
@@ -60,11 +49,12 @@ dev_dependencies:
flutter_lints: ^3.0.0
# Code generator
build_runner: ^2.4.9
- source_gen: ^1.5.0
# Database helper
- drift_dev: ^2.17.0
+ drift_dev: ^2.18.0
# Route helper
auto_route_generator: ^8.0.0
+ # Localization generator
+ slang_build_runner: ^3.30.0
flutter:
uses-material-design: true