mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(mobile): Folder View for mobile (#15047)
* very rough prototype for folder navigation without assets * fix: refactored data model and tried to implement asset loading * fix: openapi generator shadowing query param in /view/folder * add simple alphanumeric sorting for folders * basic asset viewing in folders * rudimentary switch sorting order * fixed reactivity when toggling sort order * Fixed trailing comma * Fixed bad merge conflict resolution * Regenerated open-api * Added rudimentary breadcrumbs * Fixed linting problems * feat: cleanup --------- Co-authored-by: Alex <alex.tran1502@gmail.com> Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
deb399ea15
commit
4ebc25c754
49 changed files with 1238 additions and 371 deletions
6
mobile/lib/interfaces/folder_api.interface.dart
Normal file
6
mobile/lib/interfaces/folder_api.interface.dart
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
|
||||
abstract interface class IFolderApiRepository {
|
||||
Future<List<String>> getAllUniquePaths();
|
||||
Future<List<Asset>> getAssetsForPath(String? path);
|
||||
}
|
||||
11
mobile/lib/models/folder/recursive_folder.model.dart
Normal file
11
mobile/lib/models/folder/recursive_folder.model.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:immich_mobile/models/folder/root_folder.model.dart';
|
||||
|
||||
class RecursiveFolder extends RootFolder {
|
||||
final String name;
|
||||
|
||||
RecursiveFolder({
|
||||
required this.name,
|
||||
required super.path,
|
||||
required super.subfolders,
|
||||
});
|
||||
}
|
||||
11
mobile/lib/models/folder/root_folder.model.dart
Normal file
11
mobile/lib/models/folder/root_folder.model.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
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,
|
||||
});
|
||||
}
|
||||
320
mobile/lib/pages/library/folder/folder.page.dart
Normal file
320
mobile/lib/pages/library/folder/folder.page.dart
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
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/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/models/folder/recursive_folder.model.dart';
|
||||
import 'package:immich_mobile/models/folder/root_folder.model.dart';
|
||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||
import 'package:immich_mobile/providers/folder.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
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 (final folder in rootFolder.subfolders) {
|
||||
if (targetFolder.path == '/' &&
|
||||
folder.path.isEmpty &&
|
||||
folder.name == targetFolder.name) {
|
||||
return folder;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const FolderPage({super.key, this.folder});
|
||||
|
||||
@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(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() {
|
||||
final newOrder =
|
||||
sortOrder.value == SortOrder.asc ? SortOrder.desc : SortOrder.asc;
|
||||
|
||||
ref.read(folderStructureProvider.notifier).fetchFolders(newOrder);
|
||||
|
||||
sortOrder.value = newOrder;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(currentFolder.value?.name ?? tr("folders")),
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.swap_vert),
|
||||
onPressed: onToggleSortOrder,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: folderState.when(
|
||||
data: (rootFolder) {
|
||||
if (folder == null) {
|
||||
return FolderContent(
|
||||
folder: rootFolder,
|
||||
root: rootFolder,
|
||||
sortOrder: sortOrder.value,
|
||||
);
|
||||
} else {
|
||||
return FolderContent(
|
||||
folder: currentFolder.value!,
|
||||
root: rootFolder,
|
||||
sortOrder: sortOrder.value,
|
||||
);
|
||||
}
|
||||
},
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, stack) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "failed_to_load_folder".tr(),
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
return Center(child: const Text("failed_to_load_folder").tr());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FolderContent extends HookConsumerWidget {
|
||||
final RootFolder? folder;
|
||||
final RootFolder root;
|
||||
final SortOrder sortOrder;
|
||||
|
||||
const FolderContent({
|
||||
super.key,
|
||||
this.folder,
|
||||
required this.root,
|
||||
this.sortOrder = SortOrder.asc,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final folderRenderlist = ref.watch(folderRenderListProvider(folder!));
|
||||
|
||||
// Initial asset fetch
|
||||
useEffect(
|
||||
() {
|
||||
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());
|
||||
}
|
||||
|
||||
getSubtitle(int subFolderCount) {
|
||||
if (subFolderCount > 0) {
|
||||
return "$subFolderCount ${tr("folders")}".toLowerCase();
|
||||
}
|
||||
|
||||
if (subFolderCount == 1) {
|
||||
return "1 ${tr("folder")}".toLowerCase();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
FolderPath(currentFolder: folder!, root: root),
|
||||
Expanded(
|
||||
child: folderRenderlist.when(
|
||||
data: (list) {
|
||||
if (folder!.subfolders.isEmpty && list.isEmpty) {
|
||||
return Center(child: const Text("empty_folder").tr());
|
||||
}
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
if (folder!.subfolders.isNotEmpty)
|
||||
...folder!.subfolders.map(
|
||||
(subfolder) => LargeLeadingTile(
|
||||
leading: Icon(
|
||||
Icons.folder,
|
||||
color: context.primaryColor,
|
||||
size: 48,
|
||||
),
|
||||
title: Text(
|
||||
subfolder.name,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: subfolder.subfolders.isNotEmpty
|
||||
? Text(
|
||||
getSubtitle(subfolder.subfolders.length),
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onTap: () =>
|
||||
context.pushRoute(FolderRoute(folder: subfolder)),
|
||||
),
|
||||
),
|
||||
if (!list.isEmpty &&
|
||||
list.allAssets != null &&
|
||||
list.allAssets!.isNotEmpty)
|
||||
...list.allAssets!.map(
|
||||
(asset) => LargeLeadingTile(
|
||||
onTap: () => context.pushRoute(
|
||||
GalleryViewerRoute(
|
||||
renderList: list,
|
||||
initialIndex: list.allAssets!.indexOf(asset),
|
||||
),
|
||||
),
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(15),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: ThumbnailImage(
|
||||
asset: asset,
|
||||
showStorageIndicator: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
asset.fileName,
|
||||
maxLines: 2,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
"${asset.exifInfo?.fileSize != null ? formatBytes(asset.exifInfo?.fileSize ?? 0) : ""} • ${DateFormat.yMMMd().format(asset.fileCreatedAt)}",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
error: (error, stack) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "failed_to_load_assets".tr(),
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
return Center(child: const Text("failed_to_load_assets").tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FolderPath extends StatelessWidget {
|
||||
final RootFolder currentFolder;
|
||||
final RootFolder root;
|
||||
|
||||
const FolderPath({
|
||||
super.key,
|
||||
required this.currentFolder,
|
||||
required this.root,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (currentFolder.path.isEmpty || currentFolder.path == '/') {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
currentFolder.path,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inconsolata',
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: context.colorScheme.onSurface.withAlpha(175),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -128,6 +128,19 @@ class QuickAccessButtons extends ConsumerWidget {
|
|||
bottomRight: Radius.circular(partners.isEmpty ? 20 : 0),
|
||||
),
|
||||
),
|
||||
leading: const Icon(
|
||||
Icons.folder_outlined,
|
||||
size: 26,
|
||||
),
|
||||
title: Text(
|
||||
'folders'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
onTap: () => context.pushRoute(FolderRoute()),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.group_outlined,
|
||||
size: 26,
|
||||
|
|
|
|||
62
mobile/lib/providers/folder.provider.dart
Normal file
62
mobile/lib/providers/folder.provider.dart
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/models/folder/root_folder.model.dart';
|
||||
import 'package:immich_mobile/services/folder.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class FolderStructureNotifier extends StateNotifier<AsyncValue<RootFolder>> {
|
||||
final FolderService _folderService;
|
||||
final Logger _log = Logger("FolderStructureNotifier");
|
||||
|
||||
FolderStructureNotifier(this._folderService) : super(const AsyncLoading());
|
||||
|
||||
Future<void> fetchFolders(SortOrder order) async {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final folderStructureProvider =
|
||||
StateNotifierProvider<FolderStructureNotifier, AsyncValue<RootFolder>>(
|
||||
(ref) {
|
||||
return FolderStructureNotifier(
|
||||
ref.watch(folderServiceProvider),
|
||||
);
|
||||
});
|
||||
|
||||
class FolderRenderListNotifier extends StateNotifier<AsyncValue<RenderList>> {
|
||||
final FolderService _folderService;
|
||||
final RootFolder _folder;
|
||||
final Logger _log = Logger("FolderAssetsNotifier");
|
||||
|
||||
FolderRenderListNotifier(this._folderService, this._folder)
|
||||
: super(const AsyncLoading());
|
||||
|
||||
Future<void> fetchAssets(SortOrder order) async {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final folderRenderListProvider = StateNotifierProvider.family<
|
||||
FolderRenderListNotifier,
|
||||
AsyncValue<RenderList>,
|
||||
RootFolder>((ref, folder) {
|
||||
return FolderRenderListNotifier(
|
||||
ref.watch(folderServiceProvider),
|
||||
folder,
|
||||
);
|
||||
});
|
||||
43
mobile/lib/repositories/folder_api.repository.dart
Normal file
43
mobile/lib/repositories/folder_api.repository.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/folder_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final folderApiRepositoryProvider = Provider(
|
||||
(ref) => FolderApiRepository(
|
||||
ref.watch(apiServiceProvider).viewApi,
|
||||
),
|
||||
);
|
||||
|
||||
class FolderApiRepository extends ApiRepository
|
||||
implements IFolderApiRepository {
|
||||
final ViewApi _api;
|
||||
final Logger _log = Logger("FolderApiRepository");
|
||||
|
||||
FolderApiRepository(this._api);
|
||||
|
||||
@override
|
||||
Future<List<String>> getAllUniquePaths() async {
|
||||
try {
|
||||
final list = await _api.getUniqueOriginalPaths();
|
||||
return list ?? [];
|
||||
} catch (e, stack) {
|
||||
_log.severe("Failed to fetch unique original links", e, stack);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Asset>> getAssetsForPath(String? path) async {
|
||||
try {
|
||||
final list = await _api.getAssetsByOriginalPath(path ?? '/');
|
||||
return list != null ? list.map(Asset.remote).toList() : [];
|
||||
} catch (e, stack) {
|
||||
_log.severe("Failed to fetch Assets by original path", e, stack);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import 'package:immich_mobile/domain/models/log.model.dart';
|
|||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/models/folder/recursive_folder.model.dart';
|
||||
import 'package:immich_mobile/pages/library/folder/folder.page.dart';
|
||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||
|
|
@ -207,6 +209,11 @@ class AppRouter extends RootStackRouter {
|
|||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: TransitionsBuilders.slideLeft,
|
||||
),
|
||||
CustomRoute(
|
||||
page: FolderRoute.page,
|
||||
guards: [_authGuard],
|
||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||
),
|
||||
AutoRoute(
|
||||
page: PartnerDetailRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
|
|
|
|||
|
|
@ -1175,6 +1175,40 @@ class PartnerRoute extends PageRouteInfo<void> {
|
|||
);
|
||||
}
|
||||
|
||||
/// manually written (with love) route for
|
||||
/// [FolderPage]
|
||||
class FolderRoute extends PageRouteInfo<FolderRouteArgs> {
|
||||
FolderRoute({
|
||||
RecursiveFolder? folder,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
FolderRoute.name,
|
||||
args: FolderRouteArgs(folder: folder),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'FolderRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<FolderRouteArgs>();
|
||||
return FolderPage(folder: args.folder);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class FolderRouteArgs {
|
||||
const FolderRouteArgs({this.folder});
|
||||
|
||||
final RecursiveFolder? folder;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FolderRouteArgs{folder: $folder}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [PeopleCollectionPage]
|
||||
class PeopleCollectionRoute extends PageRouteInfo<void> {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class ApiService implements Authentication {
|
|||
late DownloadApi downloadApi;
|
||||
late TrashApi trashApi;
|
||||
late StacksApi stacksApi;
|
||||
late ViewApi viewApi;
|
||||
late MemoriesApi memoriesApi;
|
||||
|
||||
ApiService() {
|
||||
|
|
@ -64,6 +65,7 @@ class ApiService implements Authentication {
|
|||
downloadApi = DownloadApi(_apiClient);
|
||||
trashApi = TrashApi(_apiClient);
|
||||
stacksApi = StacksApi(_apiClient);
|
||||
viewApi = ViewApi(_apiClient);
|
||||
memoriesApi = MemoriesApi(_apiClient);
|
||||
}
|
||||
|
||||
|
|
|
|||
132
mobile/lib/services/folder.service.dart
Normal file
132
mobile/lib/services/folder.service.dart
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/models/folder/recursive_folder.model.dart';
|
||||
import 'package:immich_mobile/models/folder/root_folder.model.dart';
|
||||
import 'package:immich_mobile/repositories/folder_api.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final folderServiceProvider = Provider(
|
||||
(ref) => FolderService(ref.watch(folderApiRepositoryProvider)),
|
||||
);
|
||||
|
||||
class FolderService {
|
||||
final FolderApiRepository _folderApiRepository;
|
||||
final Logger _log = Logger("FolderService");
|
||||
|
||||
FolderService(this._folderApiRepository);
|
||||
|
||||
Future<RootFolder> getFolderStructure(SortOrder order) async {
|
||||
final paths = await _folderApiRepository.getAllUniquePaths();
|
||||
|
||||
// Create folder structure
|
||||
Map<String, List<RecursiveFolder>> folderMap = {};
|
||||
|
||||
for (String fullPath in paths) {
|
||||
if (fullPath == '/') continue;
|
||||
|
||||
// Ensure the path starts with a slash
|
||||
if (!fullPath.startsWith('/')) {
|
||||
fullPath = '/$fullPath';
|
||||
}
|
||||
|
||||
List<String> segments = fullPath.split('/')
|
||||
..removeWhere((s) => s.isEmpty);
|
||||
|
||||
String currentPath = '';
|
||||
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
String parentPath = currentPath.isEmpty ? '_root_' : currentPath;
|
||||
currentPath =
|
||||
i == 0 ? '/${segments[i]}' : '$currentPath/${segments[i]}';
|
||||
|
||||
if (!folderMap.containsKey(parentPath)) {
|
||||
folderMap[parentPath] = [];
|
||||
}
|
||||
|
||||
if (!folderMap[parentPath]!.any((f) => f.name == segments[i])) {
|
||||
folderMap[parentPath]!.add(
|
||||
RecursiveFolder(
|
||||
path: parentPath == '_root_' ? '' : parentPath,
|
||||
name: segments[i],
|
||||
subfolders: [],
|
||||
),
|
||||
);
|
||||
// Sort folders based on order parameter
|
||||
folderMap[parentPath]!.sort(
|
||||
(a, b) => order == SortOrder.desc
|
||||
? b.name.compareTo(a.name)
|
||||
: a.name.compareTo(b.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void attachSubfolders(RecursiveFolder folder) {
|
||||
String fullPath = folder.path.isEmpty
|
||||
? '/${folder.name}'
|
||||
: '${folder.path}/${folder.name}';
|
||||
|
||||
if (folderMap.containsKey(fullPath)) {
|
||||
folder.subfolders.addAll(folderMap[fullPath]!);
|
||||
// Sort subfolders based on order parameter
|
||||
folder.subfolders.sort(
|
||||
(a, b) => order == SortOrder.desc
|
||||
? b.name.compareTo(a.name)
|
||||
: a.name.compareTo(b.name),
|
||||
);
|
||||
for (var subfolder in folder.subfolders) {
|
||||
attachSubfolders(subfolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<RecursiveFolder> rootSubfolders = folderMap['_root_'] ?? [];
|
||||
// Sort root subfolders based on order parameter
|
||||
rootSubfolders.sort(
|
||||
(a, b) => order == SortOrder.desc
|
||||
? b.name.compareTo(a.name)
|
||||
: a.name.compareTo(b.name),
|
||||
);
|
||||
|
||||
for (var folder in rootSubfolders) {
|
||||
attachSubfolders(folder);
|
||||
}
|
||||
|
||||
return RootFolder(
|
||||
subfolders: rootSubfolders,
|
||||
path: '/',
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Asset>> getFolderAssets(
|
||||
RootFolder folder,
|
||||
SortOrder order,
|
||||
) async {
|
||||
try {
|
||||
if (folder is RecursiveFolder) {
|
||||
String fullPath =
|
||||
folder.path.isEmpty ? folder.name : '${folder.path}/${folder.name}';
|
||||
fullPath = fullPath[0] == '/' ? fullPath.substring(1) : fullPath;
|
||||
var result = await _folderApiRepository.getAssetsForPath(fullPath);
|
||||
|
||||
if (order == SortOrder.desc) {
|
||||
result.sort((a, b) => b.fileCreatedAt.compareTo(a.fileCreatedAt));
|
||||
} else {
|
||||
result.sort((a, b) => a.fileCreatedAt.compareTo(b.fileCreatedAt));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
final result = await _folderApiRepository.getAssetsForPath('/');
|
||||
return result;
|
||||
} catch (e, stack) {
|
||||
_log.severe(
|
||||
"Failed to fetch assets for folder ${folder is RecursiveFolder ? folder.name : "root"}",
|
||||
e,
|
||||
stack,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
|||
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/immich_logo_provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/sync_stream.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart';
|
||||
|
|
@ -186,12 +185,6 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
ref.read(syncStreamServiceProvider).syncUsers();
|
||||
},
|
||||
icon: const Icon(Icons.sync),
|
||||
),
|
||||
if (actions != null)
|
||||
...actions!.map(
|
||||
(action) => Padding(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue