mirror of
https://github.com/immich-app/immich.git
synced 2025-02-03 01:22:44 +01:00
Compare commits
15 commits
07b5a2b32f
...
57e0bc57d1
Author | SHA1 | Date | |
---|---|---|---|
|
57e0bc57d1 | ||
|
060300de8a | ||
|
c2ba1cc202 | ||
|
08db77db23 | ||
|
fd94741698 | ||
|
9b495e55ff | ||
|
f78532e345 | ||
|
22b32e596b | ||
|
4e050307be | ||
|
62fa2298bc | ||
|
766949bfe2 | ||
|
538dd32f17 | ||
|
c1c37fbd43 | ||
|
424e3b611b | ||
|
e3e63e5d8b |
57 changed files with 868 additions and 39 deletions
docs/src/components
mobile
assets/i18n
lib
interfaces
models/folder
pages/library
providers
repositories
routing
services
openapi/lib/api
activities_api.dartalbums_api.dartapi_keys_api.dartassets_api.dartaudit_api.dartauthentication_api.dartdeprecated_api.dartdownload_api.dartduplicates_api.dartfaces_api.dartfile_reports_api.dartjobs_api.dartlibraries_api.dartmap_api.dartmemories_api.dartnotifications_api.darto_auth_api.dartpartners_api.dartpeople_api.dartsearch_api.dartserver_api.dartsessions_api.dartshared_links_api.dartstacks_api.dartsync_api.dartsystem_config_api.dartsystem_metadata_api.darttags_api.darttimeline_api.darttrash_api.dartusers_admin_api.dartusers_api.dartview_api.dart
pubspec.lockpubspec.yamlopen-api
web
package-lock.jsonpackage.json
src
lib/components/asset-viewer
routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]
|
@ -99,6 +99,11 @@ const projects: CommunityProjectProps[] = [
|
|||
description: 'Downloads a configurable number of random photos based on people or album ID.',
|
||||
url: 'https://github.com/jon6fingrs/immich-dl',
|
||||
},
|
||||
{
|
||||
title: 'Immich Upload Optimizer',
|
||||
description: 'Automatically optimize files uploaded to Immich in order to save storage space',
|
||||
url: 'https://github.com/miguelangel-nubla/immich-upload-optimizer',
|
||||
},
|
||||
];
|
||||
|
||||
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {
|
||||
|
|
|
@ -274,6 +274,7 @@
|
|||
"favorites_page_title": "Favorites",
|
||||
"filename_search": "File name or extension",
|
||||
"filter": "Filter",
|
||||
"folders": "Folders",
|
||||
"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",
|
||||
|
|
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);
|
||||
}
|
12
mobile/lib/models/folder/recursive_folder.model.dart
Normal file
12
mobile/lib/models/folder/recursive_folder.model.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
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.subfolders,
|
||||
});
|
||||
}
|
9
mobile/lib/models/folder/root_folder.model.dart
Normal file
9
mobile/lib/models/folder/root_folder.model.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
import 'package:immich_mobile/models/folder/recursive_folder.model.dart';
|
||||
|
||||
class RootFolder {
|
||||
final List<RecursiveFolder> subfolders;
|
||||
|
||||
RootFolder({
|
||||
required this.subfolders,
|
||||
});
|
||||
}
|
165
mobile/lib/pages/library/folder/folder.page.dart
Normal file
165
mobile/lib/pages/library/folder/folder.page.dart
Normal file
|
@ -0,0 +1,165 @@
|
|||
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/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';
|
||||
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';
|
||||
|
||||
@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);
|
||||
useEffect(
|
||||
() {
|
||||
if (folder == null) {
|
||||
ref.read(folderStructureProvider.notifier).fetchFolders();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
void onToggleSortOrder() {
|
||||
if (folder != null) {
|
||||
ref.read(folderRenderListProvider(folder!).notifier).toggleSortOrder();
|
||||
}
|
||||
ref.read(folderStructureProvider.notifier).toggleSortOrder();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(folder?.name ?? 'Root'),
|
||||
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);
|
||||
} else {
|
||||
return FolderContent(folder: folder!);
|
||||
}
|
||||
},
|
||||
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;
|
||||
|
||||
const FolderContent({super.key, this.folder});
|
||||
|
||||
@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!));
|
||||
useEffect(
|
||||
() {
|
||||
ref.read(folderRenderListProvider(folder!).notifier).fetchAssets();
|
||||
return null;
|
||||
},
|
||||
[folder],
|
||||
);
|
||||
|
||||
return folderRenderlist.when(
|
||||
data: (list) {
|
||||
return ListView(
|
||||
children: [
|
||||
if (folder!.subfolders.isNotEmpty)
|
||||
...folder!.subfolders.map(
|
||||
(subfolder) => ListTile(
|
||||
leading: Icon(Icons.folder, color: context.primaryColor),
|
||||
title: Text(subfolder.name),
|
||||
onTap: () =>
|
||||
context.pushRoute(FolderRoute(folder: subfolder)),
|
||||
),
|
||||
),
|
||||
if (!list.isEmpty &&
|
||||
list.allAssets != null &&
|
||||
list.allAssets!.isNotEmpty)
|
||||
...list.allAssets!.map(
|
||||
(asset) => ListTile(
|
||||
onTap: () => context.pushRoute(
|
||||
GalleryViewerRoute(
|
||||
renderList: list,
|
||||
initialIndex: list.allAssets!.indexOf(asset),
|
||||
),
|
||||
),
|
||||
leading: SizedBox(
|
||||
// height: 100,
|
||||
width: 80,
|
||||
child: ThumbnailImage(
|
||||
asset: asset,
|
||||
showStorageIndicator: false,
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
// Remove the file extension from the file name
|
||||
// Sometimes the file name has multiple dots (.TS.mp4)
|
||||
asset.fileName.split('.').first,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
// Display the file extension(s)
|
||||
".${asset.fileName.substring(asset.fileName.indexOf('.') + 1)}",
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
"${asset.exifInfo?.fileSize != null ? formatBytes(asset.exifInfo?.fileSize ?? 0) : ""} · ${DateFormat.yMMMd().format(asset.fileCreatedAt)}",
|
||||
),
|
||||
),
|
||||
),
|
||||
if (folder!.subfolders.isEmpty && list.isEmpty)
|
||||
Center(child: const Text("No subfolders or assets").tr()),
|
||||
],
|
||||
);
|
||||
},
|
||||
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());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -78,6 +78,7 @@ class LibraryPage extends ConsumerWidget {
|
|||
PeopleCollectionCard(),
|
||||
PlacesCollectionCard(),
|
||||
LocalAlbumsCollectionCard(),
|
||||
FoldersCollectionCard(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
@ -380,6 +381,54 @@ class PlacesCollectionCard extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class FoldersCollectionCard extends StatelessWidget {
|
||||
const FoldersCollectionCard({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isTablet = constraints.maxWidth > 600;
|
||||
final widthFactor = isTablet ? 0.25 : 0.5;
|
||||
final size = context.width * widthFactor - 20.0;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(FolderRoute()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: context.colorScheme.secondaryContainer.withAlpha(100),
|
||||
),
|
||||
child: IgnorePointer(
|
||||
child: Icon(
|
||||
Icons.folder_outlined,
|
||||
color: context.primaryColor,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'folders'.tr(),
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActionButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
final IconData icon;
|
||||
|
|
76
mobile/lib/providers/folder.provider.dart
Normal file
76
mobile/lib/providers/folder.provider.dart
Normal file
|
@ -0,0 +1,76 @@
|
|||
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");
|
||||
|
||||
var sortOrder = SortOrder.asc;
|
||||
|
||||
FolderStructureNotifier(this._folderService) : super(const AsyncLoading());
|
||||
|
||||
Future<void> fetchFolders() async {
|
||||
try {
|
||||
final folders = await _folderService.getFolderStructure(sortOrder);
|
||||
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 =
|
||||
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");
|
||||
|
||||
var sortOrder = SortOrder.asc;
|
||||
|
||||
FolderRenderListNotifier(this._folderService, this._folder)
|
||||
: super(const AsyncLoading());
|
||||
|
||||
Future<void> fetchAssets() async {
|
||||
try {
|
||||
final assets = await _folderService.getFolderAssets(_folder, sortOrder);
|
||||
|
||||
state =
|
||||
AsyncData(await RenderList.fromAssets(assets, GroupAssetsBy.none));
|
||||
} 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<
|
||||
FolderRenderListNotifier,
|
||||
AsyncValue<RenderList>,
|
||||
RootFolder>((ref, folder) {
|
||||
return FolderRenderListNotifier(
|
||||
ref.watch(folderServiceProvider),
|
||||
folder,
|
||||
);
|
||||
});
|
44
mobile/lib/repositories/folder_api.repository.dart
Normal file
44
mobile/lib/repositories/folder_api.repository.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
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 ?? '/');
|
||||
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);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import 'package:immich_mobile/entities/album.entity.dart';
|
|||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/models/folder/recursive_folder.model.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';
|
||||
|
@ -16,6 +17,7 @@ import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
|||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
||||
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/library/folder/folder.page.dart';
|
||||
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
||||
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
||||
import 'package:immich_mobile/pages/library/places/places_collection.page.dart';
|
||||
|
@ -208,6 +210,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;
|
||||
|
||||
ApiService() {
|
||||
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
|
||||
|
@ -64,6 +65,7 @@ class ApiService implements Authentication {
|
|||
downloadApi = DownloadApi(_apiClient);
|
||||
trashApi = TrashApi(_apiClient);
|
||||
stacksApi = StacksApi(_apiClient);
|
||||
viewApi = ViewApi(_apiClient);
|
||||
}
|
||||
|
||||
Future<String> resolveAndSetEndpoint(String serverUrl) async {
|
||||
|
|
129
mobile/lib/services/folder.service.dart
Normal file
129
mobile/lib/services/folder.service.dart
Normal file
|
@ -0,0 +1,129 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
BIN
mobile/openapi/lib/api/activities_api.dart
generated
BIN
mobile/openapi/lib/api/activities_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/albums_api.dart
generated
BIN
mobile/openapi/lib/api/albums_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/api_keys_api.dart
generated
BIN
mobile/openapi/lib/api/api_keys_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/assets_api.dart
generated
BIN
mobile/openapi/lib/api/assets_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/audit_api.dart
generated
BIN
mobile/openapi/lib/api/audit_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/authentication_api.dart
generated
BIN
mobile/openapi/lib/api/authentication_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/deprecated_api.dart
generated
BIN
mobile/openapi/lib/api/deprecated_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/download_api.dart
generated
BIN
mobile/openapi/lib/api/download_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/duplicates_api.dart
generated
BIN
mobile/openapi/lib/api/duplicates_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/faces_api.dart
generated
BIN
mobile/openapi/lib/api/faces_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/file_reports_api.dart
generated
BIN
mobile/openapi/lib/api/file_reports_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/jobs_api.dart
generated
BIN
mobile/openapi/lib/api/jobs_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/libraries_api.dart
generated
BIN
mobile/openapi/lib/api/libraries_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/map_api.dart
generated
BIN
mobile/openapi/lib/api/map_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/memories_api.dart
generated
BIN
mobile/openapi/lib/api/memories_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/notifications_api.dart
generated
BIN
mobile/openapi/lib/api/notifications_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/o_auth_api.dart
generated
BIN
mobile/openapi/lib/api/o_auth_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/partners_api.dart
generated
BIN
mobile/openapi/lib/api/partners_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/people_api.dart
generated
BIN
mobile/openapi/lib/api/people_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/search_api.dart
generated
BIN
mobile/openapi/lib/api/search_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/server_api.dart
generated
BIN
mobile/openapi/lib/api/server_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/sessions_api.dart
generated
BIN
mobile/openapi/lib/api/sessions_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/shared_links_api.dart
generated
BIN
mobile/openapi/lib/api/shared_links_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/stacks_api.dart
generated
BIN
mobile/openapi/lib/api/stacks_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/sync_api.dart
generated
BIN
mobile/openapi/lib/api/sync_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/system_config_api.dart
generated
BIN
mobile/openapi/lib/api/system_config_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/system_metadata_api.dart
generated
BIN
mobile/openapi/lib/api/system_metadata_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/tags_api.dart
generated
BIN
mobile/openapi/lib/api/tags_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/timeline_api.dart
generated
BIN
mobile/openapi/lib/api/timeline_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/trash_api.dart
generated
BIN
mobile/openapi/lib/api/trash_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/users_admin_api.dart
generated
BIN
mobile/openapi/lib/api/users_admin_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/users_api.dart
generated
BIN
mobile/openapi/lib/api/users_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/view_api.dart
generated
BIN
mobile/openapi/lib/api/view_api.dart
generated
Binary file not shown.
|
@ -5,23 +5,23 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "72.0.0"
|
||||
version: "76.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.2"
|
||||
version: "0.3.3"
|
||||
analyzer:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.7.0"
|
||||
version: "6.11.0"
|
||||
analyzer_plugin:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
|
@ -250,10 +250,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.19.0"
|
||||
connectivity_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -900,18 +900,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.5"
|
||||
version: "10.0.7"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.8"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -940,10 +940,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2-main.4"
|
||||
version: "0.1.3-main.0"
|
||||
maplibre_gl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1452,7 +1452,7 @@ packages:
|
|||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
version: "0.0.0"
|
||||
socket_io_client:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1513,10 +1513,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.0"
|
||||
state_notifier:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1545,10 +1545,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1577,10 +1577,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
version: "0.7.3"
|
||||
thumbhash:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1737,10 +1737,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.5"
|
||||
version: "14.3.0"
|
||||
wakelock_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1793,10 +1793,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
|
||||
sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.4"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -83,7 +83,7 @@ dependencies:
|
|||
# Taken from https://github.com/Myzel394/locus/blob/445013d22ec1d759027d4303bd65b30c5c8588c8/pubspec.yaml#L105
|
||||
dependency_overrides:
|
||||
# TODO: remove once Isar is updated
|
||||
analyzer: ^6.3.0
|
||||
analyzer: ^6.0.0
|
||||
# TODO: remove once analyzer override is removed
|
||||
meta: ^1.11.0
|
||||
# TODO: remove once analyzer override is removed
|
||||
|
|
|
@ -9,7 +9,11 @@ function dart {
|
|||
wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
|
||||
patch --no-backup-if-mismatch -u native_class.mustache <native_class.mustache.patch
|
||||
|
||||
cd ../../../../
|
||||
cd ../../
|
||||
wget -O api.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/api.mustache
|
||||
patch --no-backup-if-mismatch -u api.mustache <api.mustache.patch
|
||||
|
||||
cd ../../
|
||||
npx --yes @openapitools/openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./templates/mobile
|
||||
|
||||
# Post generate patches
|
||||
|
|
194
open-api/templates/mobile/api.mustache
Normal file
194
open-api/templates/mobile/api.mustache
Normal file
|
@ -0,0 +1,194 @@
|
|||
{{>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<Response> {{{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 = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
{{#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 = <String>[{{#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}}
|
29
open-api/templates/mobile/api.mustache.patch
Normal file
29
open-api/templates/mobile/api.mustache.patch
Normal file
|
@ -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<Response> {{{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,
|
21
web/package-lock.json
generated
21
web/package-lock.json
generated
|
@ -16,6 +16,8 @@
|
|||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.11.5",
|
||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5",
|
||||
"@photo-sphere-viewer/resolution-plugin": "^5.11.5",
|
||||
"@photo-sphere-viewer/settings-plugin": "^5.11.5",
|
||||
"@photo-sphere-viewer/video-plugin": "^5.11.5",
|
||||
"@zoom-image/svelte": "^0.3.0",
|
||||
"dom-to-image": "^2.6.0",
|
||||
|
@ -1669,6 +1671,25 @@
|
|||
"@photo-sphere-viewer/video-plugin": "5.11.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@photo-sphere-viewer/resolution-plugin": {
|
||||
"version": "5.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/resolution-plugin/-/resolution-plugin-5.11.5.tgz",
|
||||
"integrity": "sha512-Dbvp5bBtozD3IWt1Q0wORVaZBcB1bV9xUeoOS9A7F7b3EkQ2pkC5/jot/1AyM4wtU5wJ63NWHskQ1d7m6WWazQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@photo-sphere-viewer/core": "5.11.5",
|
||||
"@photo-sphere-viewer/settings-plugin": "5.11.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@photo-sphere-viewer/settings-plugin": {
|
||||
"version": "5.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/settings-plugin/-/settings-plugin-5.11.5.tgz",
|
||||
"integrity": "sha512-ZgYaWjiBMhsoRH5ddW3h+v4J4LPmofsT7BBRq5UCssWw2Fsrvv7mFFRi4UbZ1qzeKmvNUOr8BaFQgX1ZLvUWfQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@photo-sphere-viewer/core": "5.11.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@photo-sphere-viewer/video-plugin": {
|
||||
"version": "5.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.11.5.tgz",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.11.5",
|
||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.11.5",
|
||||
"@photo-sphere-viewer/resolution-plugin": "^5.11.5",
|
||||
"@photo-sphere-viewer/settings-plugin": "^5.11.5",
|
||||
"@photo-sphere-viewer/video-plugin": "^5.11.5",
|
||||
"@zoom-image/svelte": "^0.3.0",
|
||||
"dom-to-image": "^2.6.0",
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
{:then [data, { default: PhotoSphereViewer }]}
|
||||
<PhotoSphereViewer
|
||||
panorama={data}
|
||||
originalImageUrl={isWebCompatibleImage(asset) ? getAssetOriginalUrl(asset.id) : undefined}
|
||||
originalPanorama={isWebCompatibleImage(asset) ? getAssetOriginalUrl(asset.id) : undefined}
|
||||
/>
|
||||
{:catch}
|
||||
{$t('errors.failed_to_load_asset')}
|
||||
|
|
|
@ -7,18 +7,21 @@
|
|||
type AdapterConstructor,
|
||||
type PluginConstructor,
|
||||
} from '@photo-sphere-viewer/core';
|
||||
import { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin';
|
||||
import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin';
|
||||
import '@photo-sphere-viewer/core/index.css';
|
||||
import '@photo-sphere-viewer/settings-plugin/index.css';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
panorama: string | { source: string };
|
||||
originalImageUrl?: string;
|
||||
originalPanorama?: string | { source: string };
|
||||
adapter?: AdapterConstructor | [AdapterConstructor, unknown];
|
||||
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
||||
navbar?: boolean;
|
||||
}
|
||||
|
||||
let { panorama, originalImageUrl, adapter = EquirectangularAdapter, plugins = [], navbar = false }: Props = $props();
|
||||
let { panorama, originalPanorama, adapter = EquirectangularAdapter, plugins = [], navbar = false }: Props = $props();
|
||||
|
||||
let container: HTMLDivElement | undefined = $state();
|
||||
let viewer: Viewer;
|
||||
|
@ -30,9 +33,33 @@
|
|||
|
||||
viewer = new Viewer({
|
||||
adapter,
|
||||
plugins,
|
||||
plugins: [
|
||||
SettingsPlugin,
|
||||
[
|
||||
ResolutionPlugin,
|
||||
{
|
||||
defaultResolution: $alwaysLoadOriginalFile && originalPanorama ? 'original' : 'default',
|
||||
resolutions: [
|
||||
{
|
||||
id: 'default',
|
||||
label: 'Default',
|
||||
panorama,
|
||||
},
|
||||
...(originalPanorama
|
||||
? [
|
||||
{
|
||||
id: 'original',
|
||||
label: 'Original',
|
||||
panorama: originalPanorama,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
...plugins,
|
||||
],
|
||||
container,
|
||||
panorama,
|
||||
touchmoveTwoFingers: false,
|
||||
mousewheelCtrlKey: false,
|
||||
navbar,
|
||||
|
@ -40,15 +67,14 @@
|
|||
maxFov: 120,
|
||||
fisheye: false,
|
||||
});
|
||||
const resolutionPlugin = viewer.getPlugin(ResolutionPlugin) as ResolutionPlugin;
|
||||
|
||||
if (originalImageUrl && !$alwaysLoadOriginalFile) {
|
||||
if (originalPanorama && !$alwaysLoadOriginalFile) {
|
||||
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
|
||||
// zoomLevel range: [0, 100]
|
||||
if (Math.round(zoomLevel) >= 75) {
|
||||
// Replace the preview with the original
|
||||
viewer.setPanorama(originalImageUrl, { showLoader: false, speed: 150 }).catch(() => {
|
||||
viewer.setPanorama(panorama, { showLoader: false, speed: 0 }).catch(() => {});
|
||||
});
|
||||
void resolutionPlugin.setResolution('original');
|
||||
viewer.removeEventListener(events.ZoomUpdatedEvent.type, zoomHandler);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { getAssetOriginalUrl } from '$lib/utils';
|
||||
import { getAssetPlaybackUrl, getAssetOriginalUrl } from '$lib/utils';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
@ -22,7 +22,13 @@
|
|||
{#await modules}
|
||||
<LoadingSpinner />
|
||||
{:then [PhotoSphereViewer, adapter, videoPlugin]}
|
||||
<PhotoSphereViewer panorama={{ source: getAssetOriginalUrl(assetId) }} plugins={[videoPlugin]} {adapter} navbar />
|
||||
<PhotoSphereViewer
|
||||
panorama={{ source: getAssetPlaybackUrl(assetId) }}
|
||||
originalPanorama={{ source: getAssetOriginalUrl(assetId) }}
|
||||
plugins={[videoPlugin]}
|
||||
{adapter}
|
||||
navbar
|
||||
/>
|
||||
{:catch}
|
||||
{$t('errors.failed_to_load_asset')}
|
||||
{/await}
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
let personMerge1: PersonResponseDto | undefined = $state();
|
||||
let personMerge2: PersonResponseDto | undefined = $state();
|
||||
let potentialMergePeople: PersonResponseDto[] = $state([]);
|
||||
let isSuggestionSelectedByUser = $state(false);
|
||||
|
||||
let personName = '';
|
||||
let suggestedPeople: PersonResponseDto[] = $state([]);
|
||||
|
@ -233,15 +234,22 @@
|
|||
personName = person.name;
|
||||
personMerge1 = person;
|
||||
personMerge2 = person2;
|
||||
isSuggestionSelectedByUser = true;
|
||||
viewMode = PersonPageViewMode.SUGGEST_MERGE;
|
||||
};
|
||||
|
||||
const changeName = async () => {
|
||||
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
||||
person.name = personName;
|
||||
try {
|
||||
isEditingName = false;
|
||||
isEditingName = false;
|
||||
|
||||
if (isSuggestionSelectedByUser) {
|
||||
// User canceled the merge
|
||||
isSuggestionSelectedByUser = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
person = await updatePerson({ id: person.id, personUpdateDto: { name: personName } });
|
||||
|
||||
notificationController.show({
|
||||
|
|
Loading…
Reference in a new issue