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