From 4ebc25c754a4747282ddb70d25c4adf897cc9b4b Mon Sep 17 00:00:00 2001 From: Arno <46051866+arnolicious@users.noreply.github.com> Date: Thu, 6 Mar 2025 18:27:43 +0100 Subject: [PATCH] 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 Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/assets/i18n/en-US.json | 8 +- .../lib/interfaces/folder_api.interface.dart | 6 + .../models/folder/recursive_folder.model.dart | 11 + .../lib/models/folder/root_folder.model.dart | 11 + .../lib/pages/library/folder/folder.page.dart | 320 ++++++++++++++++++ mobile/lib/pages/library/library.page.dart | 13 + mobile/lib/providers/folder.provider.dart | 62 ++++ .../repositories/folder_api.repository.dart | 43 +++ mobile/lib/routing/router.dart | 7 + mobile/lib/routing/router.gr.dart | 34 ++ mobile/lib/services/api.service.dart | 2 + mobile/lib/services/folder.service.dart | 132 ++++++++ mobile/lib/widgets/common/immich_app_bar.dart | 7 - mobile/openapi/lib/api/activities_api.dart | Bin 7622 -> 7646 bytes mobile/openapi/lib/api/albums_api.dart | Bin 18794 -> 18860 bytes mobile/openapi/lib/api/api_keys_api.dart | Bin 7888 -> 7918 bytes mobile/openapi/lib/api/assets_api.dart | Bin 33399 -> 33501 bytes .../openapi/lib/api/authentication_api.dart | Bin 8171 -> 8201 bytes mobile/openapi/lib/api/deprecated_api.dart | Bin 2203 -> 2209 bytes mobile/openapi/lib/api/download_api.dart | Bin 4069 -> 4081 bytes mobile/openapi/lib/api/duplicates_api.dart | Bin 1926 -> 1932 bytes mobile/openapi/lib/api/faces_api.dart | Bin 6238 -> 6262 bytes mobile/openapi/lib/api/file_reports_api.dart | Bin 4729 -> 4747 bytes mobile/openapi/lib/api/jobs_api.dart | Bin 4649 -> 4667 bytes mobile/openapi/lib/api/libraries_api.dart | Bin 12442 -> 12490 bytes mobile/openapi/lib/api/map_api.dart | Bin 5443 -> 5455 bytes mobile/openapi/lib/api/memories_api.dart | Bin 12472 -> 12514 bytes mobile/openapi/lib/api/notifications_api.dart | Bin 3978 -> 3990 bytes mobile/openapi/lib/api/o_auth_api.dart | Bin 7717 -> 7747 bytes mobile/openapi/lib/api/partners_api.dart | Bin 6558 -> 6582 bytes mobile/openapi/lib/api/people_api.dart | Bin 16725 -> 16779 bytes mobile/openapi/lib/api/search_api.dart | Bin 14921 -> 14969 bytes mobile/openapi/lib/api/server_api.dart | Bin 18418 -> 18496 bytes mobile/openapi/lib/api/sessions_api.dart | Bin 3834 -> 3852 bytes mobile/openapi/lib/api/shared_links_api.dart | Bin 14571 -> 14619 bytes mobile/openapi/lib/api/stacks_api.dart | Bin 9223 -> 9259 bytes mobile/openapi/lib/api/sync_api.dart | Bin 8738 -> 8774 bytes mobile/openapi/lib/api/system_config_api.dart | Bin 6341 -> 6365 bytes .../openapi/lib/api/system_metadata_api.dart | Bin 4697 -> 4715 bytes mobile/openapi/lib/api/tags_api.dart | Bin 14728 -> 14782 bytes mobile/openapi/lib/api/timeline_api.dart | Bin 8909 -> 8921 bytes mobile/openapi/lib/api/trash_api.dart | Bin 4758 -> 4776 bytes mobile/openapi/lib/api/users_admin_api.dart | Bin 14282 -> 14330 bytes mobile/openapi/lib/api/users_api.dart | Bin 17887 -> 17962 bytes mobile/openapi/lib/api/view_api.dart | Bin 3618 -> 3630 bytes mobile/pubspec.yaml | 2 +- open-api/bin/generate-open-api.sh | 6 +- open-api/templates/mobile/api.mustache | 194 +++++++++++ open-api/templates/mobile/api.mustache.patch | 29 ++ 49 files changed, 877 insertions(+), 10 deletions(-) create mode 100644 mobile/lib/interfaces/folder_api.interface.dart create mode 100644 mobile/lib/models/folder/recursive_folder.model.dart create mode 100644 mobile/lib/models/folder/root_folder.model.dart create mode 100644 mobile/lib/pages/library/folder/folder.page.dart create mode 100644 mobile/lib/providers/folder.provider.dart create mode 100644 mobile/lib/repositories/folder_api.repository.dart create mode 100644 mobile/lib/services/folder.service.dart create mode 100644 open-api/templates/mobile/api.mustache create mode 100644 open-api/templates/mobile/api.mustache.patch diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index fd628d469..3854c0c51 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -252,6 +252,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", @@ -275,6 +276,11 @@ "favorites_page_title": "Favorites", "filename_search": "File name or extension", "filter": "Filter", + "folders": "Folders", + "folder": "Folder", + "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", @@ -678,4 +684,4 @@ "viewer_unstack": "Un-Stack", "wifi_name": "WiFi Name", "your_wifi_name": "Your WiFi name" -} \ No newline at end of file +} diff --git a/mobile/lib/interfaces/folder_api.interface.dart b/mobile/lib/interfaces/folder_api.interface.dart new file mode 100644 index 000000000..68c1652e2 --- /dev/null +++ b/mobile/lib/interfaces/folder_api.interface.dart @@ -0,0 +1,6 @@ +import 'package:immich_mobile/entities/asset.entity.dart'; + +abstract interface class IFolderApiRepository { + Future> getAllUniquePaths(); + Future> getAssetsForPath(String? path); +} diff --git a/mobile/lib/models/folder/recursive_folder.model.dart b/mobile/lib/models/folder/recursive_folder.model.dart new file mode 100644 index 000000000..5b54a2e1b --- /dev/null +++ b/mobile/lib/models/folder/recursive_folder.model.dart @@ -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, + }); +} diff --git a/mobile/lib/models/folder/root_folder.model.dart b/mobile/lib/models/folder/root_folder.model.dart new file mode 100644 index 000000000..8f72a539c --- /dev/null +++ b/mobile/lib/models/folder/root_folder.model.dart @@ -0,0 +1,11 @@ +import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; + +class RootFolder { + final List subfolders; + final String path; + + RootFolder({ + required this.subfolders, + required this.path, + }); +} diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart new file mode 100644 index 000000000..af6f29597 --- /dev/null +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -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(folder); + final sortOrder = useState(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), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 92fe8cec1..31b465ead 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -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, diff --git a/mobile/lib/providers/folder.provider.dart b/mobile/lib/providers/folder.provider.dart new file mode 100644 index 000000000..810c2cea7 --- /dev/null +++ b/mobile/lib/providers/folder.provider.dart @@ -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> { + final FolderService _folderService; + final Logger _log = Logger("FolderStructureNotifier"); + + FolderStructureNotifier(this._folderService) : super(const AsyncLoading()); + + Future 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>( + (ref) { + return FolderStructureNotifier( + ref.watch(folderServiceProvider), + ); +}); + +class FolderRenderListNotifier extends StateNotifier> { + final FolderService _folderService; + final RootFolder _folder; + final Logger _log = Logger("FolderAssetsNotifier"); + + FolderRenderListNotifier(this._folderService, this._folder) + : super(const AsyncLoading()); + + Future 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, + RootFolder>((ref, folder) { + return FolderRenderListNotifier( + ref.watch(folderServiceProvider), + folder, + ); +}); diff --git a/mobile/lib/repositories/folder_api.repository.dart b/mobile/lib/repositories/folder_api.repository.dart new file mode 100644 index 000000000..bd7b03515 --- /dev/null +++ b/mobile/lib/repositories/folder_api.repository.dart @@ -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> 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> 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 []; + } + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index ae5419b71..cd7a6f6b9 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -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], diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 299c8a602..e120f512a 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1175,6 +1175,40 @@ class PartnerRoute extends PageRouteInfo { ); } +/// manually written (with love) route for +/// [FolderPage] +class FolderRoute extends PageRouteInfo { + FolderRoute({ + RecursiveFolder? folder, + List? 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(); + 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 { diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index b87e10f02..0ef68e1c4 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -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); } diff --git a/mobile/lib/services/folder.service.dart b/mobile/lib/services/folder.service.dart new file mode 100644 index 000000000..5b97b475b --- /dev/null +++ b/mobile/lib/services/folder.service.dart @@ -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 getFolderStructure(SortOrder order) async { + final paths = await _folderApiRepository.getAllUniquePaths(); + + // Create folder structure + Map> folderMap = {}; + + for (String fullPath in paths) { + if (fullPath == '/') continue; + + // Ensure the path starts with a slash + if (!fullPath.startsWith('/')) { + fullPath = '/$fullPath'; + } + + List 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 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> 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 []; + } + } +} diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 7f97944cd..7a4260679 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -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( diff --git a/mobile/openapi/lib/api/activities_api.dart b/mobile/openapi/lib/api/activities_api.dart index e5075eee160b7838e88b35445806508059ad365a..5c83ba7db98cdeba9435a404ce07dab95b094dc8 100644 GIT binary patch delta 99 zcmX?Reb0KsT_%>qg3N%;_n2H6AuJUZVO9vsg3}2qR?W4Y87wyW9+ThXm7E=uyCmjr ZUdA5}m1h%N#0Jr`Qv3{5tV}Y11pwFRB7Xn? delta 75 zcmca-eaw2pT_(nY%@3Je8G)1*i!dvYa^!RZ@!GkzGcy)Ue#qpvc>-SxP}$@@$$y*0 Xgq8r+Om^fH*?dWS2S{C?WB>~Q;prKW diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index eb2bb7c0bd9cbe839c2ab56d0b569226144d9db3..a8c518ace26775bce82894c702ae975541f7d0fd 100644 GIT binary patch delta 327 zcmaDgiE+(j#tjP0EQtk~0h<+>cQJxllP|bEo6KXPIyt~Xbn_q9k1P;TMP|{>%-sG6 zHJdGY!ys~-CkWa>#5bQ8dIwi8GMURRaMTiA|CCnjKO;%)vNp)=2k`;n#wUrZQ zg{TZvR)bn&E4Og7r0PVdkrM>HfW~!fuGQj%%b(cnuagJWr^w97I9X3cZt^@9TZp6o zO56c@gLgBJ$yd0uL??T@_Q0Jr8KT5+@&ie>%^lXcOfcVyZ054_g*q+5{w~A{pwmS+ Od%Lzk#V@$cVgdj;{B2PH delta 270 zcmZ28neo*m#tjP0j0KYeWJD(aH~czTmt`Z67tix-vW*f#%@^kV`l z3y@(4Qj<4opP2kWQV?u@mB8SId)%w0#oeo a0v!zYCrH^0*D{a+AdkxPa%}Qg!w3LXgJ1su diff --git a/mobile/openapi/lib/api/api_keys_api.dart b/mobile/openapi/lib/api/api_keys_api.dart index 2e7757f20a15a6eee92296c6be25a0547b276a1c..cf54ac5c04cccf2ca84ca510c4ca9741c7ce4941 100644 GIT binary patch delta 117 zcmca$`_6X5X(pD$g3N%)4~2XtOE7;0b2js_@UlW!2D}c85LPVLC5Y0^2(d!Gd=`l8 hLm^p+y2*un3pXzinFSG_Y`~j8d6gv8*acDsEC5$jC@KH| delta 103 zcmaE7d%vgdUG@iMtCf#e{($>JglC&x=$Oy0o3KKYik u$mVkXYG%fQ$>*7TCa>eY2vjF7Vhz+h*`7ClvNSX2=OJYH0z-Be(NJcPgvLVmv&DyLxSRvw1xO^bun+I@&ADx$OzAmo?F@N$$FOkUs zF_DuK=W5H%k=14}yg30p5%QowqCQnG_nY>z%bMn&U$3XS7go8i^eGr=q)V6t|L;}c=ud?AFLncZro!l-{ z05n8-vTup_=J^T@AVWTgiA)Y&*=sco9w`HaC1BN8)k@j3BLkde9>le k!KYAhSy5NG_`%8bB3hgM#1BH{1fpLg3N%;`X diff --git a/mobile/openapi/lib/api/duplicates_api.dart b/mobile/openapi/lib/api/duplicates_api.dart index b82290e47b9d2f8da0cf79e63042d07114d4382b..715c6d6112f7b71d5d0c336f007252d27e4d376b 100644 GIT binary patch delta 27 ecmZqU@8RE&#l(_WkQuN!n@OG#!s=%J%?to|wg~6| delta 21 bcmeC-Z{y#P#l%>!Igd%65lBsB{>=;kNTvq2 diff --git a/mobile/openapi/lib/api/faces_api.dart b/mobile/openapi/lib/api/faces_api.dart index e92ee93e42927d0d394cdb5ddee89f2ac9ebe9d9..44e3d53f8e20591eebf38784795f7636c02dd140 100644 GIT binary patch delta 94 zcmca-@XcVuIVP6Gg3N%;=b0QCAuK_bI%WuK0(%Nn>??;8L~QbTCeO`{JRDH*F5Yxj Vu-eHJ*z+eR3crGA^brwa0RS|=AU6O2 delta 69 zcmexnaL-`FIVQ$}$?iO!lckw|FcxeUXQ^WbQZw087=aWcrxS?h&JzgY&EZXF1uCA& QoRA8- CED>4& diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 78afc15c93580aed4c8f5c471498cd115904f348..182bb14e4fcdd1eee8fc752c225043c51adc58fc 100644 GIT binary patch delta 70 zcmZ3fvRh@tCMK4|g3N%)^VvKm|7Q9G=4^h)ya6J-c|MyYBSdT``wJEbYY|T-RE&pD GmIVOF!Wl;Z delta 52 zcmdn3vQlNkCML#$$t&1ACjVpl#8|NT3-bn`z~&Wfl8iv=DEkW*AhnSv6U39_lVt$_ DDGUHusb2dw{u(Lvhk1H2UJ|KS)BAmiC1)_TMPaZi& zh?oT5D;7+plO^~TPCl<{G1-BeeX@_5=;SzQuFWo@mzW_+z2zl08%P>Mj6hbq*+rTS zV%%nLc{ixM6wATM?^Gp$X7O&WRP=>9;ka@d)Cqd3%OI}Zd{wIuMUUEMJ>48;0CmAn A(*OVf delta 168 zcmX?=I4g0(RVK!Q%{Q5x8G)1n3p*=g!Q^+!1(VlGUIYpha7_V9Prk|I3lW$cE3$C& zX+BRD6jiYz7a;=Tn>{5>fd&DUJ>Q%u{ht}EOnh^*ygo=&f#u-j`*I4Krz-}7EPbb( g2C_6lbs5A`@yUBNxi%|n2f)<1>V7UbOAD|m<*0Da;Rl>h($ diff --git a/mobile/openapi/lib/api/memories_api.dart b/mobile/openapi/lib/api/memories_api.dart index c5b04a7c7cf5f1959ecbf8e198f62aabf73e782e..88897d30380a05e78e607fc622332d34577a62d8 100644 GIT binary patch delta 193 zcmdmy_$YD14G@0A*=+JRjd#eH<@h-tj z5dCQ4^F#%qX28vz+$DHt@_R|k%{sDd5V^_RTt1uYl{}#)%o9DZxl?r}R1Rp*Tc{aH~k!^CimcisYF_FbPbeREy!9zR% delta 147 zcmaEqxFd1H4<^Qf&3~BE7=ct8%PLkNCBo$h;ze@*W??Lt?4_MQd4W>LWPd?Vpx_+A zOF#{%f|K8>9GDz0q%b*vM`ZIVF4C$hx;Gk!(Q9 XOFIQ<&*U_gXOsIB**4G9bzueoi~=;5 diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart index 323fbcc3d6bc025b2d06b6923ffc93cdb57d99f1..518a1baa4a85ab2054490db2528bfd355bc0bf43 100644 GIT binary patch delta 62 zcmeB@pC-RSmYF57ATwaI9CIlnm^C?{`_N={M_8n{e&4PQqHdhl3yxt0;IZ7 Z*c~E&d~>rX3ky)BTT&4uzd>p;GXT908%h8G diff --git a/mobile/openapi/lib/api/partners_api.dart b/mobile/openapi/lib/api/partners_api.dart index ac0d03054ae5a25f46c6cb402057148aeb872554..9f10ea4d1e5bc16f97259619dd39537afd1f8baa 100644 GIT binary patch delta 93 zcmbPdyv=ySawe9M}V!Z diff --git a/mobile/openapi/lib/api/people_api.dart b/mobile/openapi/lib/api/people_api.dart index 92bd0fdeeacbfe8df5f4973ed6e4e3f911272258..1cdb878852075b635ada9e2ac91481dd3d9e4075 100644 GIT binary patch delta 243 zcmccG#Ms@;xZyYxOJYH0z+^ryugLo_iNzQ#A31tMQ0Y{v*;0p+1$Rg#_%vB?`no^4(wEd&+cEAIgpf3W$P zqBT@pNi7{BzPL-B4PscgZXVRwUwWq@PJuf1E5tRw^bSnsH!<7%$LuK7ltb1@P}6_u RJ)2~0Hu*n`$l_2tW&jd%SXBT3 delta 208 zcmeBfX1v?zX(q49{t6wNUvdS5BprBdfufrmh3$Z9 zCOhz+nEX#jVsg2i$mZ>$SD1l9jlv?E7fYHV$!^{wEx`hnc_VLvEW@H?4U-913kJ$8 zUZ)PS6lBOT-6)XKXxnF#AL!U@b~9KBQhJ)nf3lP52e8C}$(AN&n{~{OLd?jWoMiJA K=(cEE0~P?`fk+Fl%xk=i14w1|pl?S)MXOr{`$sz!Knr2)_8bDG6abxwM25OtHJ4^HmZHQU@^&akGK&1*Dwf diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index 7a832ad61a1582d02e6a4db00943948584b20e0d..629949db32efbeaf25e62fbb8c410eaa3aeaff47 100644 GIT binary patch delta 319 zcmey=&v;-0ldYa8GdE=H2YGnIS5| zxs{N_HFzgL#div*K*TrS6Le$2Gwf$>v6TWD8f}%_+tC^JE2`k2@!zY=%bEG6Rwk(JuKR!s}2Z}SGl`)DG! k)$c4Rf_~V;X6>{X_EjJ0QCwzX#fBK diff --git a/mobile/openapi/lib/api/stacks_api.dart b/mobile/openapi/lib/api/stacks_api.dart index aa1d9b341615d514a500e2463c49b6bf72afae0a..84f23ec55dd7ee1d826aa46dee0dae37144b51ab 100644 GIT binary patch delta 156 zcmZqoSnaXl5EDycL1w^YM{%#oyv$#~oXt!u?^qzLj~sT45S9Vgb0nonydNO?47d(& z7US1}NNt8IpL|8)#N>PqiOIhegg2K8pJs*_;wU}|YTOlxRjd#bK62y&ZRVK#MeYqm J(L;GP769_~H}C)e delta 111 zcmZ4O(eAO~5EEm; diff --git a/mobile/openapi/lib/api/sync_api.dart b/mobile/openapi/lib/api/sync_api.dart index 49a4963bffd39776a42e077515ab3adc9b27a229..fe2876ddd8a70fcf5d0f50a044af873a2e85bcf2 100644 GIT binary patch delta 153 zcmZ4Fa?EAJ877v*g3N%)_vFGRD>J_Vb2baG_%K6QvFu)q5Cw7y2Pe;Cv)a6vGn@q? z_nX%PNzRJbVzaG)E<_G!oX_O#A{QYFJ4N3@wUkRa!L=OSTrU#`v3>JBIeUosW;q2G FCICirG^_vs delta 106 zcmX@+vdCq_879Vp%@>%Q7=e@+iw`r9N@Mo|@+RMqKDfD*b0P~+gp1DuCL%773KD4$ nkp<~kDEbz}nXyv5#o~_Sok+@<($XN1JaXN mlA&O$P^3P&k4Iy33okcB-R22=KUg4oJB8h#Vi!cdG6MiwRwge1 delta 90 zcmca>c+_x11`}h!kmOBpj`!RGVq+KfQzA4eh!Sef|d iDqah;$_xMxLm;;R diff --git a/mobile/openapi/lib/api/system_metadata_api.dart b/mobile/openapi/lib/api/system_metadata_api.dart index 822a54b14f4ef210a65607c08826f99b63cdf4bf..3bd8bddcac52867f6d4d38209e269a4b1a422da6 100644 GIT binary patch delta 82 zcmcbq@>*p>2NO$TL1w^YI}XXo2l%`vzh$}z7TCO>xsCOYL1w__vrMjxVAkY^dIvUNX31cI@c4v1Cco2Vo7~0u2BNT>>l#FA zvmd_=M9b!GfhMRie8Q4YF%6OP=;|hGh%B6}FK9nGgIjR3nlvX=-atMHZuPUtt5tO- zU(k@+ykBt!y8Pzd>c?3j&hq2W20H5##7PhJl%c{9*K$k_HF*V52lUJ)vkqneRU1>) delta 197 zcmdm2+)=#Y3=?C)<_k=&jEn`71@#YXzR8lo0%Tg4_)VV7`35N0%ykVUut7I_a-HNw zpuk*#CZHam3J<7&gUERl6_XuA7Ea#9B0o7nP+_vVf#7BrX-=Sm&4Kbs5L=#2KCh}X z`Im;&=EsUNkYwfx+?hNdee1-5-Uq$L1w^aW!BeF7C-kfE{NDQ(Pa>^$^6_uHhTz%GHyO4%ghP@LwFOM delta 43 wcmccVde(J=5-VfDf}1*j>%`aSoM-GYdUO#aPey}3@=9wd5DKW{8=IVzN->_2OL+(>HU;ctfqO z7hgDe4y*j+8fk^iW^xyxdf1h0vB;~cOoYnAoiMpxe8uKg&0c0fke3omG8AkTiq!Q> zi&Kj>3G%|tSh4wSx`R`B$8+B p4iwqE(header}} +{{>part_of}} +{{#operations}} + +class {{{classname}}} { + {{{classname}}}([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + {{#operation}} + + {{#summary}} + /// {{{.}}} + {{/summary}} + {{#notes}} + {{#summary}} + /// + {{/summary}} + /// {{{notes}}} + /// + /// Note: This method returns the HTTP [Response]. + {{/notes}} + {{^notes}} + {{#summary}} + /// + /// Note: This method returns the HTTP [Response]. + {{/summary}} + {{^summary}} + /// Performs an HTTP '{{{httpMethod}}} {{{path}}}' operation and returns the [Response]. + {{/summary}} + {{/notes}} + {{#hasParams}} + {{#summary}} + /// + {{/summary}} + {{^summary}} + {{#notes}} + /// + {{/notes}} + {{/summary}} + /// Parameters: + /// + {{/hasParams}} + {{#allParams}} + /// * [{{{dataType}}}] {{{paramName}}}{{#required}} (required){{/required}}{{#optional}} (optional){{/optional}}: + {{#description}} + /// {{{.}}} + {{/description}} + {{^-last}} + /// + {{/-last}} + {{/allParams}} + Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { + // ignore: prefer_const_declarations + final apiPath = r'{{{path}}}'{{#pathParams}} + .replaceAll({{=<% %>=}}'{<% baseName %>}'<%={{ }}=%>, {{{paramName}}}{{^isString}}.toString(){{/isString}}){{/pathParams}}; + + // ignore: prefer_final_locals + Object? postBody{{#bodyParam}} = {{{paramName}}}{{/bodyParam}}; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + {{#hasQueryParams}} + + {{#queryParams}} + {{^required}} + if ({{{paramName}}} != null) { + {{/required}} + queryParams.addAll(_queryParams('{{{collectionFormat}}}', '{{{baseName}}}', {{{paramName}}})); + {{^required}} + } + {{/required}} + {{/queryParams}} + {{/hasQueryParams}} + {{#hasHeaderParams}} + + {{#headerParams}} + {{#required}} + headerParams[r'{{{baseName}}}'] = parameterToString({{{paramName}}}); + {{/required}} + {{^required}} + if ({{{paramName}}} != null) { + headerParams[r'{{{baseName}}}'] = parameterToString({{{paramName}}}); + } + {{/required}} + {{/headerParams}} + {{/hasHeaderParams}} + + const contentTypes = [{{#prioritizedContentTypes}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/prioritizedContentTypes}}]; + + {{#isMultipart}} + bool hasFields = false; + final mp = MultipartRequest('{{{httpMethod}}}', Uri.parse(apiPath)); + {{#formParams}} + {{^isFile}} + if ({{{paramName}}} != null) { + hasFields = true; + mp.fields[r'{{{baseName}}}'] = parameterToString({{{paramName}}}); + } + {{/isFile}} + {{#isFile}} + if ({{{paramName}}} != null) { + hasFields = true; + mp.fields[r'{{{baseName}}}'] = {{{paramName}}}.field; + mp.files.add({{{paramName}}}); + } + {{/isFile}} + {{/formParams}} + if (hasFields) { + postBody = mp; + } + {{/isMultipart}} + {{^isMultipart}} + {{#formParams}} + {{^isFile}} + if ({{{paramName}}} != null) { + formParams[r'{{{baseName}}}'] = parameterToString({{{paramName}}}); + } + {{/isFile}} + {{/formParams}} + {{/isMultipart}} + + return apiClient.invokeAPI( + apiPath, + '{{{httpMethod}}}', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + {{#summary}} + /// {{{.}}} + {{/summary}} + {{#notes}} + {{#summary}} + /// + {{/summary}} + /// {{{notes}}} + {{/notes}} + {{#hasParams}} + {{#summary}} + /// + {{/summary}} + {{^summary}} + {{#notes}} + /// + {{/notes}} + {{/summary}} + /// Parameters: + /// + {{/hasParams}} + {{#allParams}} + /// * [{{{dataType}}}] {{{paramName}}}{{#required}} (required){{/required}}{{#optional}} (optional){{/optional}}: + {{#description}} + /// {{{.}}} + {{/description}} + {{^-last}} + /// + {{/-last}} + {{/allParams}} + Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { + final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}} {{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} {{/hasOptionalParams}}); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + {{#returnType}} + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + {{#native_serialization}} + {{#isArray}} + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, '{{{returnType}}}') as List) + .cast<{{{returnBaseType}}}>() + .{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}}; + {{/isArray}} + {{^isArray}} + {{#isMap}} + return {{{returnType}}}.from(await apiClient.deserializeAsync(await _decodeBodyBytes(response), '{{{returnType}}}'),); + {{/isMap}} + {{^isMap}} + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), '{{{returnType}}}',) as {{{returnType}}}; + {{/isMap}}{{/isArray}}{{/native_serialization}} + } + return null; + {{/returnType}} + } + {{/operation}} +} +{{/operations}} diff --git a/open-api/templates/mobile/api.mustache.patch b/open-api/templates/mobile/api.mustache.patch new file mode 100644 index 000000000..e3f888d6d --- /dev/null +++ b/open-api/templates/mobile/api.mustache.patch @@ -0,0 +1,29 @@ +--- api.mustache 2025-01-22 05:50:25 ++++ api.mustache.modified 2025-01-22 05:52:23 +@@ -51,7 +51,7 @@ + {{/allParams}} + Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { + // ignore: prefer_const_declarations +- final path = r'{{{path}}}'{{#pathParams}} ++ final apiPath = r'{{{path}}}'{{#pathParams}} + .replaceAll({{=<% %>=}}'{<% baseName %>}'<%={{ }}=%>, {{{paramName}}}{{^isString}}.toString(){{/isString}}){{/pathParams}}; + + // ignore: prefer_final_locals +@@ -90,7 +90,7 @@ + + {{#isMultipart}} + bool hasFields = false; +- final mp = MultipartRequest('{{{httpMethod}}}', Uri.parse(path)); ++ final mp = MultipartRequest('{{{httpMethod}}}', Uri.parse(apiPath)); + {{#formParams}} + {{^isFile}} + if ({{{paramName}}} != null) { +@@ -121,7 +121,7 @@ + {{/isMultipart}} + + return apiClient.invokeAPI( +- path, ++ apiPath, + '{{{httpMethod}}}', + queryParams, + postBody,