mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
more refactors
This commit is contained in:
parent
7ea21d636f
commit
8f47645cdb
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"immich": "Immich",
|
||||||
"tab_controller": {
|
"tab_controller": {
|
||||||
"photos": "Photos",
|
"photos": "Photos",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
@ -34,5 +35,8 @@
|
|||||||
"oauth_button": "OAuth",
|
"oauth_button": "OAuth",
|
||||||
"login_disabled": "Login Disabled"
|
"login_disabled": "Login Disabled"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"title": "Logs"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,4 +11,7 @@ abstract interface class IAlbumRepository {
|
|||||||
|
|
||||||
/// Removes album with the given [id]
|
/// Removes album with the given [id]
|
||||||
FutureOr<void> deleteId(int id);
|
FutureOr<void> deleteId(int id);
|
||||||
|
|
||||||
|
/// Removes all albums
|
||||||
|
FutureOr<void> deleteAll();
|
||||||
}
|
}
|
||||||
|
@ -14,4 +14,7 @@ abstract interface class IAlbumToAssetRepository {
|
|||||||
|
|
||||||
/// Removes album with the given [albumId]
|
/// Removes album with the given [albumId]
|
||||||
FutureOr<void> deleteAlbumId(int albumId);
|
FutureOr<void> deleteAlbumId(int albumId);
|
||||||
|
|
||||||
|
/// Removes all album to asset mappings
|
||||||
|
FutureOr<void> deleteAll();
|
||||||
}
|
}
|
||||||
|
@ -8,4 +8,7 @@ abstract interface class IAlbumETagRepository {
|
|||||||
|
|
||||||
/// Fetches the album etag for the given [albumId]
|
/// Fetches the album etag for the given [albumId]
|
||||||
FutureOr<AlbumETag?> get(int albumId);
|
FutureOr<AlbumETag?> get(int albumId);
|
||||||
|
|
||||||
|
/// Removes all album eTags
|
||||||
|
FutureOr<void> deleteAll();
|
||||||
}
|
}
|
||||||
|
@ -8,4 +8,7 @@ abstract interface class IUserRepository {
|
|||||||
|
|
||||||
/// Fetches user
|
/// Fetches user
|
||||||
FutureOr<User?> getForId(String userId);
|
FutureOr<User?> getForId(String userId);
|
||||||
|
|
||||||
|
/// Removes all users
|
||||||
|
FutureOr<void> deleteAll();
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ class AlbumETag {
|
|||||||
required this.modifiedTime,
|
required this.modifiedTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AlbumETag.empty() {
|
factory AlbumETag.initial() {
|
||||||
return AlbumETag(
|
return AlbumETag(
|
||||||
albumId: -1,
|
albumId: -1,
|
||||||
assetCount: 0,
|
assetCount: 0,
|
||||||
|
@ -18,6 +18,35 @@ sealed class RenderListElement {
|
|||||||
int get hashCode => date.hashCode;
|
int get hashCode => date.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Used to pad the render list elements
|
||||||
|
class RenderListPaddingElement extends RenderListElement {
|
||||||
|
final double topPadding;
|
||||||
|
|
||||||
|
const RenderListPaddingElement({
|
||||||
|
required this.topPadding,
|
||||||
|
required super.date,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RenderListPaddingElement.beforeElement({
|
||||||
|
required double top,
|
||||||
|
RenderListElement? before,
|
||||||
|
}) =>
|
||||||
|
RenderListPaddingElement(
|
||||||
|
topPadding: top,
|
||||||
|
date: before?.date ?? DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant RenderListPaddingElement other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return super == other && other.topPadding == topPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => super.hashCode ^ topPadding.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
class RenderListMonthHeaderElement extends RenderListElement {
|
class RenderListMonthHeaderElement extends RenderListElement {
|
||||||
late final String header;
|
late final String header;
|
||||||
|
|
||||||
|
@ -50,7 +50,12 @@ class AlbumRepository with LogMixin implements IAlbumRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> deleteId(int id) async {
|
FutureOr<void> deleteId(int id) async {
|
||||||
await _db.asset.deleteWhere((row) => row.id.equals(id));
|
await _db.album.deleteWhere((row) => row.id.equals(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> deleteAll() async {
|
||||||
|
await _db.album.deleteAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,4 +75,9 @@ class AlbumToAssetRepository with LogMixin implements IAlbumToAssetRepository {
|
|||||||
FutureOr<void> deleteAlbumId(int albumId) async {
|
FutureOr<void> deleteAlbumId(int albumId) async {
|
||||||
await _db.albumToAsset.deleteWhere((row) => row.albumId.equals(albumId));
|
await _db.albumToAsset.deleteWhere((row) => row.albumId.equals(albumId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> deleteAll() async {
|
||||||
|
await _db.albumToAsset.deleteAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,11 @@ class AlbumETagRepository with LogMixin implements IAlbumETagRepository {
|
|||||||
..where((r) => r.albumId.equals(albumId));
|
..where((r) => r.albumId.equals(albumId));
|
||||||
return await query.map(_toModel).getSingleOrNull();
|
return await query.map(_toModel).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> deleteAll() async {
|
||||||
|
await _db.albumETag.deleteAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumETagCompanion _toEntity(AlbumETag albumETag) {
|
AlbumETagCompanion _toEntity(AlbumETag albumETag) {
|
||||||
|
@ -44,6 +44,11 @@ class UserRepository with LogMixin implements IUserRepository {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> deleteAll() async {
|
||||||
|
await _db.user.deleteAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
User _toModel(UserData user) {
|
User _toModel(UserData user) {
|
||||||
|
@ -22,6 +22,7 @@ class AlbumSyncService with LogMixin {
|
|||||||
|
|
||||||
Future<bool> performFullDeviceSync() async {
|
Future<bool> performFullDeviceSync() async {
|
||||||
try {
|
try {
|
||||||
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
final deviceAlbums = await di<IDeviceAlbumRepository>().getAll();
|
final deviceAlbums = await di<IDeviceAlbumRepository>().getAll();
|
||||||
final dbAlbums = await di<IAlbumRepository>().getAll(localOnly: true);
|
final dbAlbums = await di<IAlbumRepository>().getAll(localOnly: true);
|
||||||
final hasChange = await CollectionUtil.diffLists(
|
final hasChange = await CollectionUtil.diffLists(
|
||||||
@ -34,6 +35,7 @@ class AlbumSyncService with LogMixin {
|
|||||||
onlySecond: _addDeviceAlbum,
|
onlySecond: _addDeviceAlbum,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
log.i("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
|
||||||
return hasChange;
|
return hasChange;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
log.e("Error performing full device sync", e, s);
|
log.e("Error performing full device sync", e, s);
|
||||||
@ -47,8 +49,8 @@ class AlbumSyncService with LogMixin {
|
|||||||
DateTime? modifiedUntil,
|
DateTime? modifiedUntil,
|
||||||
}) async {
|
}) async {
|
||||||
assert(dbAlbum.id != null, "Album ID from DB is null");
|
assert(dbAlbum.id != null, "Album ID from DB is null");
|
||||||
final albumEtag =
|
final albumEtag = await di<IAlbumETagRepository>().get(dbAlbum.id!) ??
|
||||||
await di<IAlbumETagRepository>().get(dbAlbum.id!) ?? AlbumETag.empty();
|
AlbumETag.initial();
|
||||||
final assetCountInDevice =
|
final assetCountInDevice =
|
||||||
await di<IDeviceAlbumRepository>().getAssetCount(deviceAlbum.localId!);
|
await di<IDeviceAlbumRepository>().getAssetCount(deviceAlbum.localId!);
|
||||||
|
|
||||||
@ -66,6 +68,7 @@ class AlbumSyncService with LogMixin {
|
|||||||
|
|
||||||
Future<void> _addDeviceAlbum(Album album, {DateTime? modifiedUntil}) async {
|
Future<void> _addDeviceAlbum(Album album, {DateTime? modifiedUntil}) async {
|
||||||
try {
|
try {
|
||||||
|
log.i("Syncing device album ${album.name}");
|
||||||
final albumId = (await di<IAlbumRepository>().upsert(album))?.id;
|
final albumId = (await di<IAlbumRepository>().upsert(album))?.id;
|
||||||
// break fast if we cannot add an album
|
// break fast if we cannot add an album
|
||||||
if (albumId == null) {
|
if (albumId == null) {
|
||||||
@ -115,6 +118,7 @@ class AlbumSyncService with LogMixin {
|
|||||||
|
|
||||||
Future<void> _removeDeviceAlbum(Album album) async {
|
Future<void> _removeDeviceAlbum(Album album) async {
|
||||||
assert(album.id != null, "Album ID from DB is null");
|
assert(album.id != null, "Album ID from DB is null");
|
||||||
|
log.i("Removing device album ${album.name}");
|
||||||
final albumId = album.id!;
|
final albumId = album.id!;
|
||||||
try {
|
try {
|
||||||
await di<IDatabaseRepository>().txn(() async {
|
await di<IDatabaseRepository>().txn(() async {
|
||||||
|
@ -35,6 +35,7 @@ class AssetSyncService with LogMixin {
|
|||||||
int? limit,
|
int? limit,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
final db = di<IDatabaseRepository>();
|
final db = di<IDatabaseRepository>();
|
||||||
final assetRepo = di<IAssetRepository>();
|
final assetRepo = di<IAssetRepository>();
|
||||||
final syncApiRepo = di<ISyncApiRepository>();
|
final syncApiRepo = di<ISyncApiRepository>();
|
||||||
@ -74,6 +75,7 @@ class AssetSyncService with LogMixin {
|
|||||||
if (assetsFromServer.length != chunkSize) break;
|
if (assetsFromServer.length != chunkSize) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.i("Full remote sync took - ${stopwatch.elapsedMilliseconds}ms");
|
||||||
return true;
|
return true;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
log.e("Error performing full remote sync for user - ${user.name}", e, s);
|
log.e("Error performing full remote sync for user - ${user.name}", e, s);
|
||||||
|
@ -1,14 +1,25 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/album.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/album_asset.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/album_etag.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/api/authentication_api.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/api/authentication_api.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/api/server_api.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/api/server_api.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/api/user_api.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/api/user_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/asset.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/album_sync.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/asset_sync.service.dart';
|
||||||
|
import 'package:immich_mobile/presentation/states/gallery_permission.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/states/server_feature_config.state.dart';
|
||||||
import 'package:immich_mobile/service_locator.dart';
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
import 'package:immich_mobile/utils/immich_api_client.dart';
|
import 'package:immich_mobile/utils/immich_api_client.dart';
|
||||||
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
||||||
@ -99,6 +110,38 @@ class LoginService with LogMixin {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> handlePostUrlResolution(String serverEndpoint) async {
|
||||||
|
await ServiceLocator.registerApiClient(serverEndpoint);
|
||||||
|
ServiceLocator.registerPostGlobalStates();
|
||||||
|
|
||||||
|
// Fetch server features
|
||||||
|
await di<ServerFeatureConfigProvider>().getFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<User?> handlePostLogin() async {
|
||||||
|
final user = await di<IUserApiRepository>().getMyUser().timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
// ignore: function-always-returns-null
|
||||||
|
onTimeout: () {
|
||||||
|
log.w("Timedout while fetching user details using saved credentials");
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (user == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceLocator.registerCurrentUser(user);
|
||||||
|
|
||||||
|
// sync assets in background
|
||||||
|
unawaited(di<AssetSyncService>().performFullRemoteSyncIsolate(user));
|
||||||
|
if (di<GalleryPermissionProvider>().hasPermission) {
|
||||||
|
unawaited(di<AlbumSyncService>().performFullDeviceSyncIsolate());
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> tryAutoLogin() async {
|
Future<bool> tryAutoLogin() async {
|
||||||
final serverEndpoint =
|
final serverEndpoint =
|
||||||
await di<IStoreRepository>().tryGet(StoreKey.serverEndpoint);
|
await di<IStoreRepository>().tryGet(StoreKey.serverEndpoint);
|
||||||
@ -106,8 +149,7 @@ class LoginService with LogMixin {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ServiceLocator.registerApiClient(serverEndpoint);
|
await handlePostUrlResolution(serverEndpoint);
|
||||||
ServiceLocator.registerPostGlobalStates();
|
|
||||||
|
|
||||||
final accessToken =
|
final accessToken =
|
||||||
await di<IStoreRepository>().tryGet(StoreKey.accessToken);
|
await di<IStoreRepository>().tryGet(StoreKey.accessToken);
|
||||||
@ -118,19 +160,20 @@ class LoginService with LogMixin {
|
|||||||
// Set token to interceptor
|
// Set token to interceptor
|
||||||
await di<ImApiClient>().init(accessToken: accessToken);
|
await di<ImApiClient>().init(accessToken: accessToken);
|
||||||
|
|
||||||
final user = await di<IUserApiRepository>().getMyUser().timeout(
|
final user = await handlePostLogin();
|
||||||
const Duration(seconds: 10),
|
|
||||||
// ignore: function-always-returns-null
|
|
||||||
onTimeout: () {
|
|
||||||
log.w("Timedout while fetching user details using saved credentials");
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceLocator.registerCurrentUser(user);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> logout() async {
|
||||||
|
// Remove existing assets
|
||||||
|
await di<IAssetRepository>().deleteAll();
|
||||||
|
await di<IAlbumRepository>().deleteAll();
|
||||||
|
await di<IAlbumToAssetRepository>().deleteAll();
|
||||||
|
await di<IAlbumETagRepository>().deleteAll();
|
||||||
|
await di<IUserRepository>().deleteAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||||
|
|
||||||
|
class ImAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
const ImAppBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppBar(
|
||||||
|
backgroundColor: context.theme.appBarTheme.backgroundColor,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
centerTitle: false,
|
||||||
|
title: Text(context.t.immich),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -75,8 +75,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
this.backgroundColor = Colors.white,
|
this.backgroundColor = Colors.white,
|
||||||
this.foregroundColor = Colors.black,
|
this.foregroundColor = Colors.black,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
|
this.scrollbarAnimationDuration = Durations.medium2,
|
||||||
this.scrollbarTimeToFade = const Duration(milliseconds: 600),
|
this.scrollbarTimeToFade = Durations.long4,
|
||||||
this.labelTextBuilder,
|
this.labelTextBuilder,
|
||||||
this.labelConstraints,
|
this.labelConstraints,
|
||||||
}) : assert(child.scrollDirection == Axis.vertical),
|
}) : assert(child.scrollDirection == Axis.vertical),
|
||||||
@ -219,6 +219,10 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
Timer? _fadeoutTimer;
|
Timer? _fadeoutTimer;
|
||||||
List<FlutterListViewItemPosition> _positions = [];
|
List<FlutterListViewItemPosition> _positions = [];
|
||||||
|
|
||||||
|
/// The controller can have only one active callback
|
||||||
|
/// cache the old one, invoke it in the new callback and restore it on dispose
|
||||||
|
FlutterSliverListControllerOnPaintItemPositionCallback? _oldCallback;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -246,14 +250,19 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
curve: Curves.fastOutSlowIn,
|
curve: Curves.fastOutSlowIn,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_oldCallback =
|
||||||
|
widget.controller.sliverController.onPaintItemPositionsCallback;
|
||||||
widget.controller.sliverController.onPaintItemPositionsCallback =
|
widget.controller.sliverController.onPaintItemPositionsCallback =
|
||||||
(height, pos) {
|
(height, pos) {
|
||||||
_positions = pos;
|
_positions = pos;
|
||||||
|
_oldCallback?.call(height, pos);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
widget.controller.sliverController.onPaintItemPositionsCallback =
|
||||||
|
_oldCallback;
|
||||||
_thumbAnimationController.dispose();
|
_thumbAnimationController.dispose();
|
||||||
_labelAnimationController.dispose();
|
_labelAnimationController.dispose();
|
||||||
_fadeoutTimer?.cancel();
|
_fadeoutTimer?.cancel();
|
||||||
@ -304,7 +313,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
}
|
}
|
||||||
|
|
||||||
double get _barMaxScrollExtent =>
|
double get _barMaxScrollExtent =>
|
||||||
(context.size?.height ?? 0) - widget.heightScrollThumb;
|
(context.size?.height ?? 0) -
|
||||||
|
widget.heightScrollThumb -
|
||||||
|
(widget.padding?.vertical ?? 0);
|
||||||
|
|
||||||
double get _maxScrollRatio =>
|
double get _maxScrollRatio =>
|
||||||
_barMaxScrollExtent / widget.controller.position.maxScrollExtent;
|
_barMaxScrollExtent / widget.controller.position.maxScrollExtent;
|
||||||
@ -414,7 +425,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
widget.scrollStateListener(true);
|
widget.scrollStateListener(true);
|
||||||
|
|
||||||
_dragHaltTimer = Timer(
|
_dragHaltTimer = Timer(
|
||||||
const Duration(milliseconds: 500),
|
Durations.long2,
|
||||||
() => widget.scrollStateListener(false),
|
() => widget.scrollStateListener(false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,27 @@ import 'package:immich_mobile/domain/models/render_list.model.dart';
|
|||||||
import 'package:immich_mobile/domain/utils/renderlist_providers.dart';
|
import 'package:immich_mobile/domain/utils/renderlist_providers.dart';
|
||||||
import 'package:immich_mobile/utils/constants/globals.dart';
|
import 'package:immich_mobile/utils/constants/globals.dart';
|
||||||
|
|
||||||
class AssetGridCubit extends Cubit<RenderList> {
|
class AssetGridState {
|
||||||
|
final bool isDragScrolling;
|
||||||
|
final RenderList renderList;
|
||||||
|
|
||||||
|
const AssetGridState({
|
||||||
|
required this.isDragScrolling,
|
||||||
|
required this.renderList,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AssetGridState.empty() =>
|
||||||
|
AssetGridState(isDragScrolling: false, renderList: RenderList.empty());
|
||||||
|
|
||||||
|
AssetGridState copyWith({bool? isDragScrolling, RenderList? renderList}) {
|
||||||
|
return AssetGridState(
|
||||||
|
isDragScrolling: isDragScrolling ?? this.isDragScrolling,
|
||||||
|
renderList: renderList ?? this.renderList,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetGridCubit extends Cubit<AssetGridState> {
|
||||||
final RenderListProvider _renderListProvider;
|
final RenderListProvider _renderListProvider;
|
||||||
late final StreamSubscription _renderListSubscription;
|
late final StreamSubscription _renderListSubscription;
|
||||||
|
|
||||||
@ -20,21 +40,27 @@ class AssetGridCubit extends Cubit<RenderList> {
|
|||||||
|
|
||||||
AssetGridCubit({required RenderListProvider renderListProvider})
|
AssetGridCubit({required RenderListProvider renderListProvider})
|
||||||
: _renderListProvider = renderListProvider,
|
: _renderListProvider = renderListProvider,
|
||||||
super(RenderList.empty()) {
|
super(AssetGridState.empty()) {
|
||||||
_renderListSubscription =
|
_renderListSubscription =
|
||||||
_renderListProvider.renderStreamProvider().listen((renderList) {
|
_renderListProvider.renderStreamProvider().listen((renderList) {
|
||||||
_bufOffset = 0;
|
_bufOffset = 0;
|
||||||
_buf = [];
|
_buf = [];
|
||||||
emit(renderList);
|
emit(state.copyWith(renderList: renderList));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setDragScrolling(bool isScrolling) {
|
||||||
|
if (state.isDragScrolling != isScrolling) {
|
||||||
|
emit(state.copyWith(isDragScrolling: isScrolling));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Loads the requested assets from the database to an internal buffer if not cached
|
/// Loads the requested assets from the database to an internal buffer if not cached
|
||||||
/// and returns a slice of that buffer
|
/// and returns a slice of that buffer
|
||||||
Future<List<Asset>> loadAssets(int offset, int count) async {
|
Future<List<Asset>> loadAssets(int offset, int count) async {
|
||||||
assert(offset >= 0);
|
assert(offset >= 0);
|
||||||
assert(count > 0);
|
assert(count > 0);
|
||||||
assert(offset + count <= state.totalCount);
|
assert(offset + count <= state.renderList.totalCount);
|
||||||
|
|
||||||
// the requested slice (offset:offset+count) is not contained in the cache buffer `_buf`
|
// 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
|
// thus, fill the buffer with a new batch of assets that at least contains the requested
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_list_view/flutter_list_view.dart';
|
import 'package:flutter_list_view/flutter_list_view.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/models/render_list_element.model.dart';
|
||||||
import 'package:immich_mobile/presentation/components/grid/draggable_scrollbar.dart';
|
import 'package:immich_mobile/presentation/components/grid/draggable_scrollbar.dart';
|
||||||
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.state.dart';
|
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.state.dart';
|
||||||
@ -15,28 +15,33 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
part 'immich_asset_grid_header.widget.dart';
|
part 'immich_asset_grid_header.widget.dart';
|
||||||
|
|
||||||
class ImAssetGrid extends StatefulWidget {
|
class ImAssetGrid extends StatefulWidget {
|
||||||
const ImAssetGrid({super.key});
|
/// The padding for the grid
|
||||||
|
final double? topPadding;
|
||||||
|
|
||||||
|
final FlutterListViewController? controller;
|
||||||
|
|
||||||
|
const ImAssetGrid({this.controller, this.topPadding, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State createState() => _ImAssetGridState();
|
State createState() => _ImAssetGridState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ImAssetGridState extends State<ImAssetGrid> {
|
class _ImAssetGridState extends State<ImAssetGrid> {
|
||||||
bool _isDragScrolling = false;
|
late final FlutterListViewController _controller;
|
||||||
final FlutterListViewController _controller = FlutterListViewController();
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = widget.controller ?? FlutterListViewController();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.dispose();
|
// Dispose controller if it was created here
|
||||||
super.dispose();
|
if (widget.controller == null) {
|
||||||
}
|
_controller.dispose();
|
||||||
|
|
||||||
void _onDragScrolling(bool isScrolling) {
|
|
||||||
if (_isDragScrolling != isScrolling) {
|
|
||||||
setState(() {
|
|
||||||
_isDragScrolling = isScrolling;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Text? _labelBuilder(List<RenderListElement> elements, int currentPosition) {
|
Text? _labelBuilder(List<RenderListElement> elements, int currentPosition) {
|
||||||
@ -55,9 +60,21 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => BlocBuilder<AssetGridCubit, RenderList>(
|
Widget build(BuildContext context) =>
|
||||||
builder: (_, renderList) {
|
BlocBuilder<AssetGridCubit, AssetGridState>(
|
||||||
final elements = renderList.elements;
|
builder: (_, state) {
|
||||||
|
final elements = state.renderList.elements;
|
||||||
|
if (widget.topPadding != null &&
|
||||||
|
elements.firstOrNull is! RenderListPaddingElement) {
|
||||||
|
elements.insert(
|
||||||
|
0,
|
||||||
|
RenderListPaddingElement.beforeElement(
|
||||||
|
top: widget.topPadding!,
|
||||||
|
before: elements.firstOrNull,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final grid = FlutterListView(
|
final grid = FlutterListView(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
delegate: FlutterListViewDelegate(
|
delegate: FlutterListViewDelegate(
|
||||||
@ -66,6 +83,9 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
|||||||
final section = elements[sectionIndex];
|
final section = elements[sectionIndex];
|
||||||
|
|
||||||
return switch (section) {
|
return switch (section) {
|
||||||
|
RenderListPaddingElement() => Padding(
|
||||||
|
padding: EdgeInsets.only(top: section.topPadding),
|
||||||
|
),
|
||||||
RenderListMonthHeaderElement() =>
|
RenderListMonthHeaderElement() =>
|
||||||
_MonthHeader(text: section.header),
|
_MonthHeader(text: section.header),
|
||||||
RenderListDayHeaderElement() => Text(section.header),
|
RenderListDayHeaderElement() => Text(section.header),
|
||||||
@ -95,7 +115,7 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
|||||||
return SizedBox.square(
|
return SizedBox.square(
|
||||||
dimension: 200,
|
dimension: 200,
|
||||||
// Show Placeholder when drag scrolled
|
// Show Placeholder when drag scrolled
|
||||||
child: asset == null || _isDragScrolling
|
child: asset == null || state.isDragScrolling
|
||||||
? const ImImagePlaceholder()
|
? const ImImagePlaceholder()
|
||||||
: ImThumbnail(asset),
|
: ImThumbnail(asset),
|
||||||
);
|
);
|
||||||
@ -111,17 +131,26 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
if (widget.topPadding != null) {
|
||||||
|
padding = EdgeInsets.only(top: widget.topPadding!);
|
||||||
|
} else {
|
||||||
|
padding = null;
|
||||||
|
}
|
||||||
|
|
||||||
return DraggableScrollbar(
|
return DraggableScrollbar(
|
||||||
foregroundColor: context.colorScheme.onSurface,
|
foregroundColor: context.colorScheme.onSurface,
|
||||||
backgroundColor: context.colorScheme.surfaceContainerHighest,
|
backgroundColor: context.colorScheme.surfaceContainerHighest,
|
||||||
scrollStateListener: _onDragScrolling,
|
scrollStateListener:
|
||||||
|
context.read<AssetGridCubit>().setDragScrolling,
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
maxItemCount: elements.length,
|
maxItemCount: elements.length,
|
||||||
labelTextBuilder: (int position) =>
|
labelTextBuilder: (int position) =>
|
||||||
_labelBuilder(elements, position),
|
_labelBuilder(elements, position),
|
||||||
labelConstraints: const BoxConstraints(maxHeight: 36),
|
labelConstraints: const BoxConstraints(maxHeight: 36),
|
||||||
scrollbarAnimationDuration: const Duration(milliseconds: 300),
|
scrollbarAnimationDuration: Durations.medium2,
|
||||||
scrollbarTimeToFade: const Duration(milliseconds: 1000),
|
scrollbarTimeToFade: Durations.extralong4,
|
||||||
|
padding: padding,
|
||||||
child: grid,
|
child: grid,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -64,7 +64,7 @@ class ImImage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return OctoImage(
|
return OctoImage(
|
||||||
fadeInDuration: const Duration(milliseconds: 0),
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
fadeOutDuration: const Duration(milliseconds: 200),
|
fadeOutDuration: Durations.short4,
|
||||||
placeholderBuilder: (_) => placeholder,
|
placeholderBuilder: (_) => placeholder,
|
||||||
image: ImImage.imageProvider(asset: asset),
|
image: ImImage.imageProvider(asset: asset),
|
||||||
width: width,
|
width: width,
|
||||||
|
@ -22,7 +22,7 @@ class ImAdaptiveScaffoldBody extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AdaptiveLayout(
|
return AdaptiveLayout(
|
||||||
internalAnimations: false,
|
internalAnimations: false,
|
||||||
transitionDuration: const Duration(milliseconds: 300),
|
transitionDuration: Durations.medium2,
|
||||||
bodyRatio: bodyRatio,
|
bodyRatio: bodyRatio,
|
||||||
body: SlotLayout(
|
body: SlotLayout(
|
||||||
config: {
|
config: {
|
||||||
|
@ -2,13 +2,28 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:immich_mobile/domain/utils/renderlist_providers.dart';
|
import 'package:immich_mobile/domain/utils/renderlist_providers.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/appbar/immich_app_bar.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.state.dart';
|
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.state.dart';
|
||||||
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.widget.dart';
|
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.widget.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class HomePage extends StatelessWidget {
|
class HomePage extends StatefulWidget {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State createState() => _HomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageState extends State<HomePage> {
|
||||||
|
final _showAppBar = ValueNotifier<bool>(true);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_showAppBar.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -16,7 +31,35 @@ class HomePage extends StatelessWidget {
|
|||||||
create: (_) => AssetGridCubit(
|
create: (_) => AssetGridCubit(
|
||||||
renderListProvider: RenderListProvider.mainTimeline(),
|
renderListProvider: RenderListProvider.mainTimeline(),
|
||||||
),
|
),
|
||||||
child: const ImAssetGrid(),
|
child: Stack(children: [
|
||||||
|
ImAssetGrid(
|
||||||
|
topPadding: kToolbarHeight + context.mediaQueryPadding.top - 8,
|
||||||
|
),
|
||||||
|
ValueListenableBuilder(
|
||||||
|
valueListenable: _showAppBar,
|
||||||
|
builder: (_, shouldShow, appBar) {
|
||||||
|
final Duration duration;
|
||||||
|
if (shouldShow) {
|
||||||
|
// Animate out app bar slower
|
||||||
|
duration = Durations.short3;
|
||||||
|
} else {
|
||||||
|
// Animate in app bar faster
|
||||||
|
duration = Durations.medium2;
|
||||||
|
}
|
||||||
|
return AnimatedPositioned(
|
||||||
|
duration: duration,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: shouldShow
|
||||||
|
? 0
|
||||||
|
: -(kToolbarHeight + context.mediaQueryPadding.top),
|
||||||
|
child: appBar!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const ImAppBar(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/api/user_api.interface.dart';
|
|
||||||
import 'package:immich_mobile/domain/interfaces/asset.interface.dart';
|
|
||||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/album_sync.service.dart';
|
|
||||||
import 'package:immich_mobile/domain/services/asset_sync.service.dart';
|
|
||||||
import 'package:immich_mobile/domain/services/login.service.dart';
|
import 'package:immich_mobile/domain/services/login.service.dart';
|
||||||
import 'package:immich_mobile/i18n/strings.g.dart';
|
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/login/models/login_page.model.dart';
|
import 'package:immich_mobile/presentation/modules/login/models/login_page.model.dart';
|
||||||
import 'package:immich_mobile/presentation/states/gallery_permission.state.dart';
|
|
||||||
import 'package:immich_mobile/presentation/states/server_info/server_feature_config.state.dart';
|
|
||||||
import 'package:immich_mobile/service_locator.dart';
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
import 'package:immich_mobile/utils/immich_api_client.dart';
|
import 'package:immich_mobile/utils/immich_api_client.dart';
|
||||||
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
||||||
@ -68,11 +62,7 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogMixin {
|
|||||||
url = await loginService.resolveEndpoint(uri);
|
url = await loginService.resolveEndpoint(uri);
|
||||||
|
|
||||||
di<IStoreRepository>().upsert(StoreKey.serverEndpoint, url);
|
di<IStoreRepository>().upsert(StoreKey.serverEndpoint, url);
|
||||||
await ServiceLocator.registerApiClient(url);
|
await di<LoginService>().handlePostUrlResolution(url);
|
||||||
ServiceLocator.registerPostGlobalStates();
|
|
||||||
|
|
||||||
// Fetch server features
|
|
||||||
await di<ServerFeatureConfigProvider>().getFeatures();
|
|
||||||
|
|
||||||
emit(state.copyWith(isServerValidated: true));
|
emit(state.copyWith(isServerValidated: true));
|
||||||
} finally {
|
} finally {
|
||||||
@ -129,20 +119,13 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogMixin {
|
|||||||
/// Set token to interceptor
|
/// Set token to interceptor
|
||||||
await di<ImApiClient>().init(accessToken: accessToken);
|
await di<ImApiClient>().init(accessToken: accessToken);
|
||||||
|
|
||||||
final user = await di<IUserApiRepository>().getMyUser();
|
final user = await di<LoginService>().handlePostLogin();
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
SnackbarManager.showError(t.login.error.error_login);
|
SnackbarManager.showError(t.login.error.error_login);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register user
|
|
||||||
ServiceLocator.registerCurrentUser(user);
|
|
||||||
await di<IUserRepository>().upsert(user);
|
await di<IUserRepository>().upsert(user);
|
||||||
// Remove and Sync assets in background
|
|
||||||
await di<IAssetRepository>().deleteAll();
|
|
||||||
await di<GalleryPermissionProvider>().requestPermission();
|
|
||||||
unawaited(di<AssetSyncService>().performFullRemoteSyncIsolate(user));
|
|
||||||
unawaited(di<AlbumSyncService>().performFullDeviceSyncIsolate());
|
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
isValidationInProgress: false,
|
isValidationInProgress: false,
|
||||||
|
@ -11,7 +11,7 @@ import 'package:immich_mobile/presentation/components/input/text_button.widget.d
|
|||||||
import 'package:immich_mobile/presentation/components/input/text_form_field.widget.dart';
|
import 'package:immich_mobile/presentation/components/input/text_form_field.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/login/models/login_page.model.dart';
|
import 'package:immich_mobile/presentation/modules/login/models/login_page.model.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/login/states/login_page.state.dart';
|
import 'package:immich_mobile/presentation/modules/login/states/login_page.state.dart';
|
||||||
import 'package:immich_mobile/presentation/states/server_info/server_feature_config.state.dart';
|
import 'package:immich_mobile/presentation/states/server_feature_config.state.dart';
|
||||||
import 'package:immich_mobile/service_locator.dart';
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/i18n/strings.g.dart';
|
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||||
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_appbar.widget.dart';
|
import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_appbar.widget.dart';
|
||||||
import 'package:immich_mobile/utils/constants/globals.dart';
|
|
||||||
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -19,7 +18,7 @@ class AboutSettingsPage extends StatelessWidget {
|
|||||||
subtitle: Text(context.t.settings.about.third_party_sub_title),
|
subtitle: Text(context.t.settings.about.third_party_sub_title),
|
||||||
onTap: () => showLicensePage(
|
onTap: () => showLicensePage(
|
||||||
context: context,
|
context: context,
|
||||||
applicationName: kImmichAppName,
|
applicationName: context.t.immich,
|
||||||
applicationIcon: const ImLogo(width: SizeConstants.xl),
|
applicationIcon: const ImLogo(width: SizeConstants.xl),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5,6 +5,7 @@ import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_ap
|
|||||||
import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_wrapper.widget.dart';
|
import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_wrapper.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/settings/models/settings_section.model.dart';
|
import 'package:immich_mobile/presentation/modules/settings/models/settings_section.model.dart';
|
||||||
import 'package:immich_mobile/presentation/router/router.dart';
|
import 'package:immich_mobile/presentation/router/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
||||||
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -16,7 +17,7 @@ class SettingsWrapperPage extends StatelessWidget {
|
|||||||
return ImAdaptiveRouteWrapper(
|
return ImAdaptiveRouteWrapper(
|
||||||
primaryBody: (_) => const SettingsPage(),
|
primaryBody: (_) => const SettingsPage(),
|
||||||
primaryRoute: SettingsRoute.name,
|
primaryRoute: SettingsRoute.name,
|
||||||
bodyRatio: 0.3,
|
bodyRatio: BodyRatioConstants.oneThird,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,10 @@ import 'dart:async';
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:immich_mobile/domain/services/album_sync.service.dart';
|
|
||||||
import 'package:immich_mobile/domain/services/asset_sync.service.dart';
|
|
||||||
import 'package:immich_mobile/domain/services/login.service.dart';
|
import 'package:immich_mobile/domain/services/login.service.dart';
|
||||||
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/login/states/login_page.state.dart';
|
import 'package:immich_mobile/presentation/modules/login/states/login_page.state.dart';
|
||||||
import 'package:immich_mobile/presentation/router/router.dart';
|
import 'package:immich_mobile/presentation/router/router.dart';
|
||||||
import 'package:immich_mobile/presentation/states/current_user.state.dart';
|
|
||||||
import 'package:immich_mobile/presentation/states/gallery_permission.state.dart';
|
|
||||||
import 'package:immich_mobile/service_locator.dart';
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
||||||
|
|
||||||
@ -53,11 +49,7 @@ class _SplashScreenState extends State<SplashScreenPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _tryLogin() async {
|
Future<void> _tryLogin() async {
|
||||||
await di<GalleryPermissionProvider>().requestPermission();
|
|
||||||
if (await di<LoginService>().tryAutoLogin() && mounted) {
|
if (await di<LoginService>().tryAutoLogin() && mounted) {
|
||||||
unawaited(di<AssetSyncService>()
|
|
||||||
.performFullRemoteSyncIsolate(di<CurrentUserProvider>().value));
|
|
||||||
unawaited(di<AlbumSyncService>().performFullDeviceSyncIsolate());
|
|
||||||
unawaited(context.replaceRoute(const TabControllerRoute()));
|
unawaited(context.replaceRoute(const TabControllerRoute()));
|
||||||
} else if (mounted) {
|
} else if (mounted) {
|
||||||
unawaited(context.replaceRoute(const LoginRoute()));
|
unawaited(context.replaceRoute(const LoginRoute()));
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/presentation/theme/app_colors.dart';
|
import 'package:immich_mobile/presentation/theme/app_colors.dart';
|
||||||
|
import 'package:immich_mobile/presentation/theme/app_typography.dart';
|
||||||
import 'package:immich_mobile/utils/extensions/material_state.extension.dart';
|
import 'package:immich_mobile/utils/extensions/material_state.extension.dart';
|
||||||
|
|
||||||
enum AppTheme {
|
enum AppTheme {
|
||||||
@ -70,6 +71,23 @@ enum AppTheme {
|
|||||||
Color.alphaBlend(color.primary.withAlpha(80), color.onSurface)
|
Color.alphaBlend(color.primary.withAlpha(80), color.onSurface)
|
||||||
.withAlpha(240),
|
.withAlpha(240),
|
||||||
),
|
),
|
||||||
|
textTheme: TextTheme(
|
||||||
|
titleLarge: AppTypography.titleLarge,
|
||||||
|
titleMedium: AppTypography.titleMedium,
|
||||||
|
titleSmall: AppTypography.titleSmall,
|
||||||
|
displayLarge: AppTypography.displayLarge,
|
||||||
|
displayMedium: AppTypography.displayMedium,
|
||||||
|
displaySmall: AppTypography.displaySmall,
|
||||||
|
headlineLarge: AppTypography.headlineLarge,
|
||||||
|
headlineMedium: AppTypography.headlineMedium,
|
||||||
|
headlineSmall: AppTypography.headlineSmall,
|
||||||
|
bodyLarge: AppTypography.bodyLarge,
|
||||||
|
bodyMedium: AppTypography.bodyMedium,
|
||||||
|
bodySmall: AppTypography.bodySmall,
|
||||||
|
labelLarge: AppTypography.labelLarge,
|
||||||
|
labelMedium: AppTypography.labelMedium,
|
||||||
|
labelSmall: AppTypography.labelSmall,
|
||||||
|
),
|
||||||
snackBarTheme: SnackBarThemeData(
|
snackBarTheme: SnackBarThemeData(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
|
70
mobile-v2/lib/presentation/theme/app_typography.dart
Normal file
70
mobile-v2/lib/presentation/theme/app_typography.dart
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppTypography {
|
||||||
|
const AppTypography();
|
||||||
|
|
||||||
|
static const TextStyle displayLarge = TextStyle(
|
||||||
|
fontSize: 57,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
);
|
||||||
|
static const TextStyle displayMedium = TextStyle(
|
||||||
|
fontSize: 45,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
);
|
||||||
|
static const TextStyle displaySmall = TextStyle(
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const TextStyle headlineLarge = TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
);
|
||||||
|
static const TextStyle headlineMedium = TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
);
|
||||||
|
static const TextStyle headlineSmall = TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const TextStyle titleLarge = TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
);
|
||||||
|
static const TextStyle titleMedium = TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
);
|
||||||
|
static const TextStyle titleSmall = TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const TextStyle bodyLarge = TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
);
|
||||||
|
static const TextStyle bodyMedium = TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
);
|
||||||
|
static const TextStyle bodySmall = TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const TextStyle labelLarge = TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
);
|
||||||
|
static const TextStyle labelMedium = TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
);
|
||||||
|
static const TextStyle labelSmall = TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
);
|
||||||
|
}
|
@ -42,7 +42,7 @@ import 'package:immich_mobile/presentation/router/router.dart';
|
|||||||
import 'package:immich_mobile/presentation/states/app_theme.state.dart';
|
import 'package:immich_mobile/presentation/states/app_theme.state.dart';
|
||||||
import 'package:immich_mobile/presentation/states/current_user.state.dart';
|
import 'package:immich_mobile/presentation/states/current_user.state.dart';
|
||||||
import 'package:immich_mobile/presentation/states/gallery_permission.state.dart';
|
import 'package:immich_mobile/presentation/states/gallery_permission.state.dart';
|
||||||
import 'package:immich_mobile/presentation/states/server_info/server_feature_config.state.dart';
|
import 'package:immich_mobile/presentation/states/server_feature_config.state.dart';
|
||||||
import 'package:immich_mobile/utils/immich_api_client.dart';
|
import 'package:immich_mobile/utils/immich_api_client.dart';
|
||||||
|
|
||||||
final di = GetIt.I;
|
final di = GetIt.I;
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const String kImmichAppName = "Immich";
|
|
||||||
|
|
||||||
/// Log messages stored in the DB
|
/// Log messages stored in the DB
|
||||||
const int kLogMessageLimit = 500;
|
const int kLogMessageLimit = 500;
|
||||||
|
|
||||||
@ -13,6 +11,7 @@ const String kCacheThumbnailsKey = 'ImThumbnailCacheKey';
|
|||||||
const int kCacheMaxNrOfThumbnails = 500;
|
const int kCacheMaxNrOfThumbnails = 500;
|
||||||
|
|
||||||
/// Grid constants
|
/// Grid constants
|
||||||
|
const double kGridAutoHideAppBarOffset = 30;
|
||||||
const int kGridThumbnailSize = 200;
|
const int kGridThumbnailSize = 200;
|
||||||
const int kGridThumbnailQuality = 80;
|
const int kGridThumbnailQuality = 80;
|
||||||
|
|
||||||
|
@ -9,3 +9,9 @@ class SizeConstants {
|
|||||||
static const l = 32.0;
|
static const l = 32.0;
|
||||||
static const xl = 64.0;
|
static const xl = 64.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BodyRatioConstants {
|
||||||
|
const BodyRatioConstants._();
|
||||||
|
|
||||||
|
static const oneThird = 1 / 3;
|
||||||
|
}
|
||||||
|
@ -17,6 +17,9 @@ extension BuildContextHelper on BuildContext {
|
|||||||
/// Get the [Size] of [MediaQuery]
|
/// Get the [Size] of [MediaQuery]
|
||||||
Size get mediaQuerySize => MediaQuery.sizeOf(this);
|
Size get mediaQuerySize => MediaQuery.sizeOf(this);
|
||||||
|
|
||||||
|
/// Get the [Padding] of [MediaQuery]
|
||||||
|
EdgeInsets get mediaQueryPadding => MediaQuery.paddingOf(this);
|
||||||
|
|
||||||
/// Get the [EdgeInsets] of [MediaQuery]
|
/// Get the [EdgeInsets] of [MediaQuery]
|
||||||
EdgeInsets get viewInsets => MediaQuery.viewInsetsOf(this);
|
EdgeInsets get viewInsets => MediaQuery.viewInsetsOf(this);
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import 'package:device_info_plus/device_info_plus.dart';
|
|||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/login.service.dart';
|
||||||
import 'package:immich_mobile/presentation/router/router.dart';
|
import 'package:immich_mobile/presentation/router/router.dart';
|
||||||
import 'package:immich_mobile/service_locator.dart';
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
import 'package:immich_mobile/utils/constants/globals.dart';
|
import 'package:immich_mobile/utils/constants/globals.dart';
|
||||||
@ -57,6 +58,7 @@ class ImApiClient extends ApiClient with LogMixin {
|
|||||||
|
|
||||||
if (res.statusCode == HttpStatus.unauthorized) {
|
if (res.statusCode == HttpStatus.unauthorized) {
|
||||||
log.e("Token invalid. Redirecting to login route");
|
log.e("Token invalid. Redirecting to login route");
|
||||||
|
await di<LoginService>().logout();
|
||||||
await di<AppRouter>().replaceAll([const LoginRoute()]);
|
await di<AppRouter>().replaceAll([const LoginRoute()]);
|
||||||
throw ApiException(res.statusCode, "Unauthorized");
|
throw ApiException(res.statusCode, "Unauthorized");
|
||||||
}
|
}
|
||||||
|
@ -69,12 +69,12 @@ class IsolateHelper {
|
|||||||
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
||||||
DartPluginRegistrant.ensureInitialized();
|
DartPluginRegistrant.ensureInitialized();
|
||||||
// Delay to ensure the isolate is ready
|
// Delay to ensure the isolate is ready
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(Durations.short2);
|
||||||
helper.postIsolateHandling();
|
helper.postIsolateHandling();
|
||||||
try {
|
try {
|
||||||
final result = await computation();
|
final result = await computation();
|
||||||
// Delay to ensure the isolate is not killed prematurely
|
// Delay to ensure the isolate is not killed prematurely
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(Durations.short2);
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
// Always close the new database connection on Isolate end
|
// Always close the new database connection on Isolate end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user