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

feat(mobile): Folder View for mobile #15047

Merged
merged 27 commits into from
Mar 6, 2025
Merged
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e3e63e5
very rough prototype for folder navigation without assets
arnolicious Jan 2, 2025
424e3b6
Merge branch 'main' of github.com:immich-app/immich into feat/mobile-…
alextran1502 Jan 3, 2025
c1c37fb
fix: refactored data model and tried to implement asset loading
arnolicious Jan 4, 2025
538dd32
Merge branch 'main' into feat/mobile-folder-view
arnolicious Jan 4, 2025
766949b
Merge branch 'main' into feat/mobile-folder-view
arnolicious Jan 6, 2025
62fa229
Merge branch 'main' into feat/mobile-folder-view
arnolicious Jan 10, 2025
4e05030
fix: openapi generator shadowing query param in /view/folder
shenlong-tanwen Jan 22, 2025
22b32e5
Merge branch 'main' into feat/mobile-folder-view
arnolicious Jan 25, 2025
f78532e
add simple alphanumeric sorting for folders
arnolicious Jan 25, 2025
9b495e5
basic asset viewing in folders
arnolicious Jan 25, 2025
fd94741
rudimentary switch sorting order
arnolicious Jan 25, 2025
8b9346d
fixed reactivity when toggling sort order
arnolicious Feb 1, 2025
0e56363
Merge branch 'main' into feat/mobile-folder-view
arnolicious Feb 1, 2025
aeadf6f
Merge branch 'main' into feat/mobile-folder-view
arnolicious Feb 7, 2025
617acf9
Merge branch 'main' into feat/mobile-folder-view
arnolicious Mar 3, 2025
01bf2ee
Merge branch 'main' into feat/mobile-folder-view
arnolicious Mar 3, 2025
1286ec7
Fixed trailing comma
arnolicious Mar 3, 2025
618aea0
Merge branch 'feat/mobile-folder-view' of https://github.com/arnolici…
arnolicious Mar 3, 2025
96400a1
Fixed bad merge conflict resolution
arnolicious Mar 3, 2025
4bcda49
Merge branch 'main' into feat/mobile-folder-view
arnolicious Mar 3, 2025
2327f06
Regenerated open-api
arnolicious Mar 3, 2025
7d95888
Added rudimentary breadcrumbs
arnolicious Mar 3, 2025
3ac9740
Merge branch 'main' into feat/mobile-folder-view
arnolicious Mar 3, 2025
7e3f6b9
Fixed linting problems
arnolicious Mar 3, 2025
031e9fe
Merge branch 'feat/mobile-folder-view' of https://github.com/arnolici…
arnolicious Mar 3, 2025
b5108c4
Merge branch 'main' into feat/mobile-folder-view
alextran1502 Mar 6, 2025
6a4b424
feat: cleanup
alextran1502 Mar 6, 2025
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
Prev Previous commit
Next Next commit
fixed reactivity when toggling sort order
arnolicious committed Feb 1, 2025
commit 8b9346d7fc55cab64c8bc74b1a1b29aeea7ef3b1
6 changes: 5 additions & 1 deletion mobile/assets/i18n/en-US.json
Original file line number Diff line number Diff line change
@@ -250,6 +250,7 @@
"edit_date_time_dialog_timezone": "Timezone",
"edit_image_title": "Edit",
"edit_location_dialog_title": "Location",
"empty_folder": "This folder is empty",
"end_date": "End date",
"enqueued": "Enqueued",
"enter_wifi_name": "Enter WiFi name",
@@ -274,6 +275,9 @@
"filename_search": "File name or extension",
"filter": "Filter",
"folders": "Folders",
"failed_to_load_folder": "Failed to load folder",
"failed_to_load_assets": "Failed to load assets",
"folder_not_found": "Folder not found",
"get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
"grant_permission": "Grant permission",
"haptic_feedback_switch": "Enable haptic feedback",
@@ -671,4 +675,4 @@
"viewer_unstack": "Un-Stack",
"wifi_name": "WiFi Name",
"your_wifi_name": "Your WiFi name"
}
}
3 changes: 1 addition & 2 deletions mobile/lib/models/folder/recursive_folder.model.dart
Original file line number Diff line number Diff line change
@@ -2,11 +2,10 @@ import 'package:immich_mobile/models/folder/root_folder.model.dart';

class RecursiveFolder extends RootFolder {
final String name;
final String path;

RecursiveFolder({
required this.path,
required this.name,
required super.path,
required super.subfolders,
});
}
2 changes: 2 additions & 0 deletions mobile/lib/models/folder/root_folder.model.dart
Original file line number Diff line number Diff line change
@@ -2,8 +2,10 @@ import 'package:immich_mobile/models/folder/recursive_folder.model.dart';

class RootFolder {
final List<RecursiveFolder> subfolders;
final String path;

RootFolder({
required this.subfolders,
required this.path,
});
}
98 changes: 77 additions & 21 deletions mobile/lib/pages/library/folder/folder.page.dart
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/folder/recursive_folder.model.dart';
import 'package:immich_mobile/models/folder/root_folder.model.dart';
@@ -12,6 +13,23 @@ import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';

RecursiveFolder? _findFolderInStructure(
RootFolder rootFolder,
RecursiveFolder targetFolder,
) {
for (var folder in rootFolder.subfolders) {
if (folder.path == targetFolder.path && folder.name == targetFolder.name) {
return folder;
}

if (folder.subfolders.isNotEmpty) {
final found = _findFolderInStructure(folder, targetFolder);
if (found != null) return found;
}
}
return null;
}

@RoutePage()
class FolderPage extends HookConsumerWidget {
final RecursiveFolder? folder;
@@ -21,26 +39,48 @@ class FolderPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final folderState = ref.watch(folderStructureProvider);
final currentFolder = useState<RecursiveFolder?>(folder);
final sortOrder = useState<SortOrder>(SortOrder.asc);

useEffect(
() {
if (folder == null) {
ref.read(folderStructureProvider.notifier).fetchFolders();
ref
.read(folderStructureProvider.notifier)
.fetchFolders(sortOrder.value);
}
return null;
},
[],
);

// Update current folder when root structure changes
useEffect(
() {
if (folder != null && folderState.hasValue) {
final updatedFolder =
_findFolderInStructure(folderState.value!, folder!);
if (updatedFolder != null) {
currentFolder.value = updatedFolder;
}
}
return null;
},
[folderState],
);

void onToggleSortOrder() {
if (folder != null) {
ref.read(folderRenderListProvider(folder!).notifier).toggleSortOrder();
}
ref.read(folderStructureProvider.notifier).toggleSortOrder();
var newOrder =
sortOrder.value == SortOrder.asc ? SortOrder.desc : SortOrder.asc;

ref.read(folderStructureProvider.notifier).fetchFolders(newOrder);

sortOrder.value = newOrder;
}

return Scaffold(
appBar: AppBar(
title: Text(folder?.name ?? 'Root'),
title: Text(currentFolder.value?.name ?? tr("folders")),
elevation: 0,
centerTitle: false,
actions: [
@@ -53,19 +93,27 @@ class FolderPage extends HookConsumerWidget {
body: folderState.when(
data: (rootFolder) {
if (folder == null) {
return FolderContent(folder: rootFolder);
return FolderContent(
folder: rootFolder,
sortOrder: sortOrder.value,
);
} else {
return FolderContent(folder: folder!);
return FolderContent(
folder: currentFolder.value!,
sortOrder: sortOrder.value,
);
}
},
loading: () => const Center(child: CircularProgressIndicator()),
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stack) {
ImmichToast.show(
context: context,
msg: "Failed to load folder".tr(),
msg: "failed_to_load_folder".tr(),
toastType: ToastType.error,
);
return Center(child: const Text("Failed to load folder").tr());
return Center(child: const Text("failed_to_load_folder").tr());
},
),
);
@@ -74,24 +122,30 @@ class FolderPage extends HookConsumerWidget {

class FolderContent extends HookConsumerWidget {
final RootFolder? folder;
final SortOrder sortOrder;

const FolderContent({super.key, this.folder});
const FolderContent({super.key, this.folder, this.sortOrder = SortOrder.asc});

@override
Widget build(BuildContext context, WidgetRef ref) {
if (folder == null) {
return Center(child: const Text("Folder not found").tr());
}

final folderRenderlist = ref.watch(folderRenderListProvider(folder!));

// Initial asset fetch
useEffect(
() {
ref.read(folderRenderListProvider(folder!).notifier).fetchAssets();
if (folder == null) return;
ref
.read(folderRenderListProvider(folder!).notifier)
.fetchAssets(sortOrder);
return null;
},
[folder],
);

if (folder == null) {
return Center(child: const Text("folder_not_found").tr());
}

return folderRenderlist.when(
data: (list) {
return ListView(
@@ -147,18 +201,20 @@ class FolderContent extends HookConsumerWidget {
),
),
if (folder!.subfolders.isEmpty && list.isEmpty)
Center(child: const Text("No subfolders or assets").tr()),
Center(child: const Text("empty_folder").tr()),
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stack) {
ImmichToast.show(
context: context,
msg: "Failed to load assets".tr(),
msg: "failed_to_load_assets".tr(),
toastType: ToastType.error,
);
return Center(child: const Text("Failed to load assets").tr());
return Center(child: const Text("failed_to_load_assets").tr());
},
);
}
28 changes: 7 additions & 21 deletions mobile/lib/providers/folder.provider.dart
Original file line number Diff line number Diff line change
@@ -9,24 +9,17 @@ class FolderStructureNotifier extends StateNotifier<AsyncValue<RootFolder>> {
final FolderService _folderService;
final Logger _log = Logger("FolderStructureNotifier");

var sortOrder = SortOrder.asc;

FolderStructureNotifier(this._folderService) : super(const AsyncLoading());

Future<void> fetchFolders() async {
Future<void> fetchFolders(SortOrder order) async {
try {
final folders = await _folderService.getFolderStructure(sortOrder);
final folders = await _folderService.getFolderStructure(order);
state = AsyncData(folders);
} catch (e, stack) {
_log.severe("Failed to build folder structure", e, stack);
state = AsyncError(e, stack);
}
}

Future<void> toggleSortOrder() {
sortOrder = sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc;
return fetchFolders();
}
}

final folderStructureProvider =
@@ -42,27 +35,20 @@ class FolderRenderListNotifier extends StateNotifier<AsyncValue<RenderList>> {
final RootFolder _folder;
final Logger _log = Logger("FolderAssetsNotifier");

var sortOrder = SortOrder.asc;

FolderRenderListNotifier(this._folderService, this._folder)
: super(const AsyncLoading());

Future<void> fetchAssets() async {
Future<void> fetchAssets(SortOrder order) async {
try {
final assets = await _folderService.getFolderAssets(_folder, sortOrder);

state =
AsyncData(await RenderList.fromAssets(assets, GroupAssetsBy.none));
final assets = await _folderService.getFolderAssets(_folder, order);
final renderList =
await RenderList.fromAssets(assets, GroupAssetsBy.none);
state = AsyncData(renderList);
} catch (e, stack) {
_log.severe("Failed to fetch folder assets", e, stack);
state = AsyncError(e, stack);
}
}

Future<void> toggleSortOrder() {
sortOrder = sortOrder == SortOrder.asc ? SortOrder.desc : SortOrder.asc;
return fetchAssets();
}
}

final folderRenderListProvider = StateNotifierProvider.family<
1 change: 0 additions & 1 deletion mobile/lib/repositories/folder_api.repository.dart
Original file line number Diff line number Diff line change
@@ -34,7 +34,6 @@ class FolderApiRepository extends ApiRepository
Future<List<Asset>> getAssetsForPath(String? path) async {
try {
final list = await _api.getAssetsByOriginalPath(path ?? '/');
print("Assets for path: $path -> $list");
return list != null ? list.map(Asset.remote).toList() : [];
} catch (e, stack) {
_log.severe("Failed to fetch Assets by original path", e, stack);
1 change: 1 addition & 0 deletions mobile/lib/services/folder.service.dart
Original file line number Diff line number Diff line change
@@ -95,6 +95,7 @@ class FolderService {

return RootFolder(
subfolders: rootSubfolders,
path: '/',
);
}