Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(mobile): add offline support #1506

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions mobile/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
.pub/
/build/

# Generated code
/lib/shared/models/*.g.dart

# Web related
lib/generated_plugin_registrant.dart

Expand Down
1 change: 1 addition & 0 deletions mobile/assets/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"library_page_albums": "Albums",
"library_page_new_album": "New album",
"library_page_device_albums": "Device Albums",
"login_form_button_text": "Login",
"login_form_email_hint": "[email protected]",
"login_form_endpoint_hint": "http://your-server-ip:port/api",
Expand Down
2 changes: 1 addition & 1 deletion mobile/integration_test/test_utils/general_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class ImmichTestHelper {
await Hive.deleteFromDisk();
await app.openBoxes();
// Load main Widget
await tester.pumpWidget(app.getMainWidget());
await tester.pumpWidget(app.getMainWidget(await app.loadDb()));
// Post run tasks
await tester.pumpAndSettle();
await EasyLocalization.ensureInitialized();
Expand Down
24 changes: 21 additions & 3 deletions mobile/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,28 @@ import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.d
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/immich_logger_message.model.dart';
import 'package:immich_mobile/shared/models/value.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/providers/release_info.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/services/immich_logger.service.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import 'constants/hive_box.dart';

void main() async {
await initApp();
runApp(getMainWidget());
runApp(getMainWidget(await loadDb()));
}

Future<void> openBoxes() async {
Expand Down Expand Up @@ -69,13 +76,24 @@ Future<void> initApp() async {
ImmichLogger().init();
}

Widget getMainWidget() {
Future<Isar> loadDb() async {
final dir = await getApplicationDocumentsDirectory();
return Isar.open(
[UserSchema, AssetSchema, AlbumSchema, ValueSchema],
directory: dir.path,
);
}

Widget getMainWidget(Isar db) {
return EasyLocalization(
supportedLocales: locales,
path: translationsPath,
useFallbackTranslations: true,
fallbackLocale: locales.first,
child: const ProviderScope(child: ImmichApp()),
child: ProviderScope(
overrides: [dbProvider.overrideWithValue(db)],
child: const ImmichApp(),
),
);
}

Expand Down
60 changes: 31 additions & 29 deletions mobile/lib/modules/album/providers/album.provider.dart
Original file line number Diff line number Diff line change
@@ -1,58 +1,60 @@
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/user.service.dart';
import 'package:isar/isar.dart';

class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
AlbumNotifier(this._albumService, this._albumCacheService) : super([]);
class AlbumNotifier extends StateNotifier<List<Album>> {
AlbumNotifier(this._albumService, this._userService, this._db) : super([]);
final AlbumService _albumService;
final AlbumCacheService _albumCacheService;
final UserService _userService;
final Isar _db;

_cacheState() {
_albumCacheService.put(state);
}

getAllAlbums() async {
if (await _albumCacheService.isValid() && state.isEmpty) {
state = await _albumCacheService.get();
Future<void> getAllAlbums() async {
if (state.isEmpty && 0 < await _db.albums.count()) {
state = await _db.albums.where().findAll();
}

List<AlbumResponseDto>? albums =
await _albumService.getAlbums(isShared: false);

if (albums != null) {
await Future.wait([
_albumService.refreshDeviceAlbums(),
_albumService.refreshRemoteAlbums(isShared: false),
]);
final User? me = await _userService.getLoggedInUser();
final albums = await _db.albums
.filter()
.owner((q) => q.isarIdEqualTo(me!.isarId))
.findAll();
if (!const ListEquality().equals(albums, state)) {
state = albums;
_cacheState();
}
}

deleteAlbum(String albumId) {
state = state.where((album) => album.id != albumId).toList();
_cacheState();
Future<void> deleteAlbum(Album album) async {
_albumService.deleteAlbum(album);
state = state.where((album) => album.id != album.id).toList();
}

Future<AlbumResponseDto?> createAlbum(
Future<Album?> createAlbum(
String albumTitle,
Set<Asset> assets,
) async {
AlbumResponseDto? album =
await _albumService.createAlbum(albumTitle, assets, []);
Album? album = await _albumService.createAlbum(albumTitle, assets, []);

if (album != null) {
state = [...state, album];
_cacheState();

return album;
}
return null;
}
}

final albumProvider =
StateNotifierProvider<AlbumNotifier, List<AlbumResponseDto>>((ref) {
final albumProvider = StateNotifierProvider<AlbumNotifier, List<Album>>((ref) {
return AlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(albumCacheServiceProvider),
ref.watch(userServiceProvider),
ref.watch(dbProvider),
);
});
7 changes: 3 additions & 4 deletions mobile/lib/modules/album/providers/album_viewer.provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/models/album_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/shared/models/album.dart';

class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
AlbumViewerNotifier(this.ref)
Expand Down Expand Up @@ -30,14 +31,12 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
}

Future<bool> changeAlbumTitle(
String albumId,
String ownerId,
Album album,
String newAlbumTitle,
) async {
AlbumService service = ref.watch(albumServiceProvider);

bool isSuccess =
await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);
bool isSuccess = await service.changeTitleAlbum(album, newAlbumTitle);

if (isSuccess) {
state = state.copyWith(editTitleText: "", isEditAlbum: false);
Expand Down
86 changes: 38 additions & 48 deletions mobile/lib/modules/album/providers/shared_album.provider.dart
Original file line number Diff line number Diff line change
@@ -1,101 +1,91 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:isar/isar.dart';

class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
SharedAlbumNotifier(this._albumService, this._sharedAlbumCacheService)
: super([]);
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
SharedAlbumNotifier(
this._albumService,
this._db,
) : super([]);

final AlbumService _albumService;
final SharedAlbumCacheService _sharedAlbumCacheService;
final Isar _db;

_cacheState() {
_sharedAlbumCacheService.put(state);
}

Future<AlbumResponseDto?> createSharedAlbum(
Future<Album?> createSharedAlbum(
String albumName,
Set<Asset> assets,
List<String> sharedUserIds,
Iterable<User> sharedUsers,
) async {
try {
var newAlbum = await _albumService.createAlbum(
Album? newAlbum = await _albumService.createAlbum(
albumName,
assets,
sharedUserIds,
sharedUsers,
);

if (newAlbum != null) {
state = [...state, newAlbum];
_cacheState();
return newAlbum;
}

return newAlbum;
} catch (e) {
debugPrint("Error createSharedAlbum ${e.toString()}");

return null;
}
return null;
}

getAllSharedAlbums() async {
if (await _sharedAlbumCacheService.isValid() && state.isEmpty) {
state = await _sharedAlbumCacheService.get();
Future<void> getAllSharedAlbums() async {
if (0 < await _db.albums.filter().sharedEqualTo(true).count() &&
state.isEmpty) {
state = await _db.albums.filter().sharedEqualTo(true).findAll();
}
await _albumService.refreshRemoteAlbums(isShared: true);

List<AlbumResponseDto>? sharedAlbums =
await _albumService.getAlbums(isShared: true);

if (sharedAlbums != null) {
state = sharedAlbums;
_cacheState();
final albums = await _db.albums.filter().sharedEqualTo(true).findAll();
if (!const ListEquality().equals(albums, state)) {
state = albums;
}
}

deleteAlbum(String albumId) async {
state = state.where((album) => album.id != albumId).toList();
_cacheState();
Future<bool> deleteAlbum(Album albumId) {
state = state.where((album) => album.id != albumId.id).toList();
return _albumService.deleteAlbum(albumId);
}

Future<bool> leaveAlbum(String albumId) async {
var res = await _albumService.leaveAlbum(albumId);
Future<bool> leaveAlbum(Album album) async {
var res = await _albumService.leaveAlbum(album);

if (res) {
state = state.where((album) => album.id != albumId).toList();
_cacheState();
await deleteAlbum(album);
return true;
} else {
return false;
}
}

Future<bool> removeAssetFromAlbum(
String albumId,
List<String> assetIds,
Album album,
Set<Asset> assets,
) async {
var res = await _albumService.removeAssetFromAlbum(albumId, assetIds);

if (res) {
return true;
} else {
return false;
}
return _albumService.removeAssetFromAlbum(album, assets);
}
}

final sharedAlbumProvider =
StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
StateNotifierProvider<SharedAlbumNotifier, List<Album>>((ref) {
return SharedAlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(sharedAlbumCacheServiceProvider),
ref.watch(dbProvider),
);
});

final sharedAlbumDetailProvider = FutureProvider.autoDispose
.family<AlbumResponseDto?, String>((ref, albumId) async {
final sharedAlbumDetailProvider =
FutureProvider.autoDispose.family<Album?, int>((ref, albumId) async {
final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);

return await sharedAlbumService.getAlbumDetail(albumId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/services/user.service.dart';
import 'package:openapi/api.dart';

final suggestedSharedUsersProvider =
FutureProvider.autoDispose<List<UserResponseDto>>((ref) async {
FutureProvider.autoDispose<List<User>>((ref) {
UserService userService = ref.watch(userServiceProvider);

return await userService.getAllUsersInfo(isAll: false) ?? [];
return userService.getAllUsersInDb(all: false);
});
Loading