Compare commits

..

2 Commits

Author SHA1 Message Date
github-actions
c3263e50fc chore: version v1.137.0 2025-07-31 20:19:26 +00:00
Alex
7391ea6ff9 chore: large file size grid view styling (#20472)
* chore: large file grid styles

* chore: large file grid styles
2025-07-31 12:52:19 -04:00
41 changed files with 108 additions and 652 deletions

6
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.73",
"version": "2.2.74",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.73",
"version": "2.2.74",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"chokidar": "^4.0.3",
@@ -54,7 +54,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.136.0",
"version": "1.137.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.73",
"version": "2.2.74",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -1,4 +1,8 @@
[
{
"label": "v1.137.0",
"url": "https://v1.137.0.archive.immich.app"
},
{
"label": "v1.136.0",
"url": "https://v1.136.0.archive.immich.app"

8
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.136.0",
"version": "1.137.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.136.0",
"version": "1.137.0",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -46,7 +46,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.73",
"version": "2.2.74",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -95,7 +95,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.136.0",
"version": "1.137.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.136.0",
"version": "1.137.0",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -1788,15 +1788,9 @@
"shared_link_expires_seconds": "Expires in {count} seconds",
"shared_link_individual_shared": "Individual shared",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_invalid_password": "Invalid password",
"shared_link_manage_links": "Manage Shared links",
"shared_link_options": "Shared link options",
"shared_link_password_description": "Require a password to access this shared link",
"shared_link_password_dialog_content": "Enter the password for the shared link.",
"shared_link_password_dialog_title": "Shared Link Password",
"shared_link": "Shared link",
"shared_link_upload": "Uploaded to shared link",
"shared_link_download": "Downloaded from shared link",
"shared_links": "Shared links",
"shared_links_description": "Share photos and videos with a link",
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",

View File

@@ -36,7 +36,7 @@ platform :android do
build_type: 'Release',
properties: {
"android.injected.version.code" => 205,
"android.injected.version.name" => "1.136.0",
"android.injected.version.name" => "1.137.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -22,7 +22,7 @@ platform :ios do
path: "./Runner.xcodeproj",
)
increment_version_number(
version_number: "1.136.0"
version_number: "1.137.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -1,21 +0,0 @@
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
class SharedRemoteAlbum extends RemoteAlbum {
final List<RemoteAsset> assets;
const SharedRemoteAlbum({
required super.id,
required this.assets,
required super.name,
required super.ownerId,
required super.description,
required super.createdAt,
required super.updatedAt,
super.thumbnailAssetId,
required super.isActivityEnabled,
required super.order,
required super.assetCount,
required super.ownerName,
});
}

View File

@@ -1,34 +0,0 @@
import 'dart:async';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/domain/models/album/shared_album.model.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
class RemoteSharedAlbumService {
final DriftAlbumApiRepository _albumApiRepository;
final AssetApiRepository _assetApiRepository;
const RemoteSharedAlbumService(this._albumApiRepository, this._assetApiRepository);
Future<SharedRemoteAlbum?> getSharedAlbum(String albumId) {
return _albumApiRepository.getShared(albumId);
}
Future<int> uploadAssets(String albumId, List<XFile> files) async {
// Start all uploads concurrently
final uploadFutures = files.map((file) => _assetApiRepository.uploadAsset(file)).toList();
// Wait for all uploads to complete
final assetIds = await Future.wait(uploadFutures);
// Filter out null assetIds
final completedUploads = assetIds.whereType<String>().toList();
if (completedUploads.isNotEmpty) {
await _albumApiRepository.addAssets(albumId, completedUploads);
}
return completedUploads.length;
}
}

View File

@@ -1,9 +1,10 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/search_result.model.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
class SearchService {
@@ -51,3 +52,41 @@ class SearchService {
return null;
}
}
extension on AssetResponseDto {
RemoteAsset toDto() {
return RemoteAsset(
id: id,
name: originalFileName,
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
ownerId: ownerId,
visibility: switch (visibility) {
api.AssetVisibility.timeline => AssetVisibility.timeline,
api.AssetVisibility.hidden => AssetVisibility.hidden,
api.AssetVisibility.archive => AssetVisibility.archive,
api.AssetVisibility.locked => AssetVisibility.locked,
_ => AssetVisibility.timeline,
},
durationInSeconds: duration.toDuration()?.inSeconds ?? 0,
height: exifInfo?.exifImageHeight?.toInt(),
width: exifInfo?.exifImageWidth?.toInt(),
isFavorite: isFavorite,
livePhotoVideoId: livePhotoVideoId,
thumbHash: thumbhash,
localId: null,
type: type.toAssetType(),
);
}
}
extension on AssetTypeEnum {
AssetType toAssetType() => switch (this) {
AssetTypeEnum.IMAGE => AssetType.image,
AssetTypeEnum.VIDEO => AssetType.video,
AssetTypeEnum.AUDIO => AssetType.audio,
AssetTypeEnum.OTHER => AssetType.other,
_ => throw Exception('Unknown AssetType value: $this'),
};
}

View File

@@ -1,5 +1,3 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:openapi/api.dart';
enum SharedLinkSource { album, individual }
@@ -16,8 +14,6 @@ class SharedLink {
final String key;
final bool showMetadata;
final SharedLinkSource type;
final List<RemoteAsset> assets;
final String? albumId;
const SharedLink({
required this.id,
@@ -31,8 +27,6 @@ class SharedLink {
required this.key,
required this.showMetadata,
required this.type,
this.assets = const [],
this.albumId,
});
SharedLink copyWith({
@@ -80,9 +74,7 @@ class SharedLink {
? dto.album?.albumThumbnailAssetId
: dto.assets.isNotEmpty
? dto.assets[0].id
: null,
assets = dto.assets.map((asset) => asset.toDto()).toList(),
albumId = dto.album?.id;
: null;
@override
String toString() =>

View File

@@ -1,218 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/domain/models/album/shared_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/remote_shared_album.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/shared_link.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/shared_link_password_dialog.dart';
// ignore: import_rule_openapi
import 'package:openapi/api.dart';
@RoutePage()
class RemoteSharedLinkPage extends ConsumerStatefulWidget {
final String shareKey;
final String endpoint;
const RemoteSharedLinkPage({super.key, required this.shareKey, required this.endpoint});
@override
ConsumerState<RemoteSharedLinkPage> createState() => _RemoteSharedLinkPageState();
}
class _RemoteSharedLinkPageState extends ConsumerState<RemoteSharedLinkPage> {
late final ApiService _apiService;
SharedLink? sharedLink;
List<RemoteAsset>? assets;
SharedRemoteAlbum? sharedAlbum;
late final RemoteSharedAlbumService sharedAlbumService;
@override
void initState() {
super.initState();
String endpoint = widget.endpoint;
if (!endpoint.endsWith('/api')) {
endpoint += '/api';
}
ImageUrlBuilder.setHost(endpoint);
ImageUrlBuilder.setParameter('key', widget.shareKey);
_apiService = ApiService.shared(endpoint, widget.shareKey);
final assetApiRepository = AssetApiRepository(
_apiService.assetsApi,
_apiService.searchApi,
_apiService.stacksApi,
_apiService.trashApi,
);
final driftApiRepository = DriftAlbumApiRepository(_apiService.albumsApi);
sharedAlbumService = RemoteSharedAlbumService(driftApiRepository, assetApiRepository);
retrieveSharedLink();
}
@override
void dispose() {
ImageUrlBuilder.clear();
super.dispose();
}
Future<void> retrieveSharedLink() async {
try {
sharedLink = await SharedLinkService(_apiService).getMySharedLink();
} on ApiException catch (error, _) {
if (error.code == 401 && error.message != null && error.message!.contains("Invalid password")) {
final password = await showDialog<String>(
context: context,
builder: (context) => const SharedLinkPasswordDialog(),
);
if (password == null) {
context.pop();
}
try {
sharedLink = await SharedLinkService(_apiService).getMySharedLink(password: password);
} catch (e) {
ImmichToast.show(
context: context,
msg: "errors.shared_link_invalid_password".t(context: context),
toastType: ToastType.error,
);
context.pop();
return;
}
}
}
if (sharedLink == null) {
ImmichToast.show(
context: context,
msg: "errors.unable_to_get_shared_link".t(context: context),
toastType: ToastType.error,
);
context.pop();
return;
}
_refreshAssets();
setState(() {});
}
Future<List<RemoteAsset>> retrieveSharedAlbumAssets() async {
try {
sharedAlbum = await sharedAlbumService.getSharedAlbum(sharedLink!.albumId!);
return sharedAlbum?.assets ?? [];
} catch (e) {
ImmichToast.show(
context: context,
msg: "errors.failed_to_load_assets".t(context: context),
toastType: ToastType.error,
);
return [];
}
}
Future<void> _refreshAssets() async {
// Retrieve assets from the shared link
switch (sharedLink!.type) {
case SharedLinkSource.album:
assets = await retrieveSharedAlbumAssets();
break;
case SharedLinkSource.individual:
assets = sharedLink!.assets;
if (!(sharedLink!.allowUpload)) {
context.replaceRoute(
AssetViewerRoute(
initialIndex: 0,
timelineService: ref.read(timelineFactoryProvider).fromAssets(sharedLink!.assets),
),
);
}
break;
}
setState(() {});
}
@override
Widget build(BuildContext context) {
Future<void> addAssets() async {
final List<XFile> uploadAssets = await ImagePicker().pickMultipleMedia();
if (uploadAssets.isEmpty) {
return;
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("uploading".t(context: context)),
content: const SizedBox(height: 48, child: Center(child: CircularProgressIndicator())),
),
);
await sharedAlbumService.uploadAssets(sharedAlbum!.id, uploadAssets);
sharedAlbum = await sharedAlbumService.getSharedAlbum(sharedAlbum!.id);
_refreshAssets();
// close the dialog
context.pop();
}
if (assets == null) {
return const Center(child: CircularProgressIndicator());
}
return ProviderScope(
key: ValueKey(assets?.length),
overrides: [
apiServiceProvider.overrideWith((ref) => _apiService),
timelineServiceProvider.overrideWith((ref) {
final timelineService = ref.watch(timelineFactoryProvider).fromAssets(assets!);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(
topSliverWidgetHeight: 0,
topSliverWidget: const SliverToBoxAdapter(child: SizedBox.shrink()),
groupBy: GroupAssetsBy.none,
bottomSheet: null,
appBar: SliverToBoxAdapter(
child: AppBar(
title: Text(sharedAlbum?.name ?? "shared_link".t(context: context)),
actions: [
if (sharedLink!.allowUpload)
IconButton(
icon: const Icon(Icons.cloud_upload),
onPressed: () => addAssets(),
tooltip: "shared_link_upload".t(context: context),
),
],
),
),
),
);
}
}

View File

@@ -4,5 +4,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'api.provider.g.dart';
@Riverpod(keepAlive: true, dependencies: [])
@Riverpod(keepAlive: true)
ApiService apiService(Ref _) => ApiService();

View File

@@ -6,7 +6,7 @@ part of 'api.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$apiServiceHash() => r'46d8a043f41b85f36f56d81e5261c842cb5c0c06';
String _$apiServiceHash() => r'187a7de59b064fab1104c23717f18ce0ae3e426c';
/// See also [apiService].
@ProviderFor(apiService)
@@ -16,8 +16,8 @@ final apiServiceProvider = Provider<ApiService>.internal(
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$apiServiceHash,
dependencies: const <ProviderOrFamily>[],
allTransitiveDependencies: const <ProviderOrFamily>{},
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')

View File

@@ -28,7 +28,7 @@ final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
ref.watch(secureStorageServiceProvider),
ref.watch(widgetServiceProvider),
);
}, dependencies: [apiServiceProvider]);
});
class AuthNotifier extends StateNotifier<AuthState> {
final AuthService _authService;

View File

@@ -2,12 +2,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart' as old_asset_entity;
import 'package:immich_mobile/models/cast/cast_manager_state.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/services/gcast.service.dart';
final castProvider = StateNotifierProvider<CastNotifier, CastManagerState>(
(ref) => CastNotifier(ref.watch(gCastServiceProvider)),
dependencies: [apiServiceProvider],
);
class CastNotifier extends StateNotifier<CastManagerState> {

View File

@@ -5,7 +5,6 @@ import 'package:immich_mobile/domain/services/local_album.service.dart';
import 'package:immich_mobile/domain/services/remote_album.service.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
@@ -32,7 +31,7 @@ final remoteAlbumRepository = Provider<DriftRemoteAlbumRepository>(
final remoteAlbumServiceProvider = Provider<RemoteAlbumService>(
(ref) => RemoteAlbumService(ref.watch(remoteAlbumRepository), ref.watch(driftAlbumApiRepositoryProvider)),
dependencies: [remoteAlbumRepository, apiServiceProvider],
dependencies: [remoteAlbumRepository],
);
final remoteAlbumProvider = NotifierProvider<RemoteAlbumNotifier, RemoteAlbumState>(

View File

@@ -1,16 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/stack.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart' hide AssetType;
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
import 'package:openapi/api.dart';
final assetApiRepositoryProvider = Provider(
(ref) => AssetApiRepository(
@@ -105,20 +101,6 @@ class AssetApiRepository extends ApiRepository {
Future<void> updateDescription(String assetId, String description) {
return _api.updateAsset(assetId, UpdateAssetDto(description: description));
}
Future<String?> uploadAsset(XFile file) async {
final lastModified = await file.lastModified();
final deviceAssetId = "MOBILE-${file.name}-${lastModified.millisecondsSinceEpoch}";
final multipart = MultipartFile.fromBytes(
'assetData', // field should be 'assetData' to match the backend API
await file.readAsBytes(),
filename: file.name,
);
final asset = await _api.uploadAsset(multipart, deviceAssetId, "MOBILE", lastModified, lastModified);
return asset?.id;
}
}
extension on StackResponseDto {
@@ -126,41 +108,3 @@ extension on StackResponseDto {
return StackResponse(id: id, primaryAssetId: primaryAssetId, assetIds: assets.map((asset) => asset.id).toList());
}
}
extension RemoteAssetDtoExt on AssetResponseDto {
RemoteAsset toDto() {
return RemoteAsset(
id: id,
name: originalFileName,
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
ownerId: ownerId,
visibility: switch (visibility) {
api.AssetVisibility.timeline => AssetVisibility.timeline,
api.AssetVisibility.hidden => AssetVisibility.hidden,
api.AssetVisibility.archive => AssetVisibility.archive,
api.AssetVisibility.locked => AssetVisibility.locked,
_ => AssetVisibility.timeline,
},
durationInSeconds: duration.toDuration()?.inSeconds ?? 0,
height: exifInfo?.exifImageHeight?.toInt(),
width: exifInfo?.exifImageWidth?.toInt(),
isFavorite: isFavorite,
livePhotoVideoId: livePhotoVideoId,
thumbHash: thumbhash,
localId: null,
type: type.toType(),
);
}
}
extension on AssetTypeEnum {
AssetType toType() => switch (this) {
AssetTypeEnum.IMAGE => AssetType.image,
AssetTypeEnum.VIDEO => AssetType.video,
AssetTypeEnum.AUDIO => AssetType.audio,
AssetTypeEnum.OTHER => AssetType.other,
_ => throw Exception('Unknown AssetType value: $this'),
};
}

View File

@@ -1,9 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/album/shared_album.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
// ignore: import_rule_openapi
import 'package:openapi/api.dart';
@@ -89,11 +87,6 @@ class DriftAlbumApiRepository extends ApiRepository {
final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers)));
return response.toRemoteAlbum();
}
Future<SharedRemoteAlbum?> getShared(String albumId) async {
final responseDto = await checkNull(_api.getAlbumInfo(albumId));
return responseDto.toSharedRemoteAlbum();
}
}
extension on AlbumResponseDto {
@@ -112,21 +105,4 @@ extension on AlbumResponseDto {
ownerName: owner.name,
);
}
SharedRemoteAlbum toSharedRemoteAlbum() {
return SharedRemoteAlbum(
id: id,
name: albumName,
ownerId: owner.id,
description: description,
createdAt: createdAt,
updatedAt: updatedAt,
thumbnailAssetId: albumThumbnailAssetId,
isActivityEnabled: isActivityEnabled,
order: order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc,
assetCount: assetCount,
ownerName: owner.name,
assets: assets.map((e) => e.toDto()).toList(),
);
}
}

View File

@@ -6,7 +6,6 @@ import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -18,11 +17,6 @@ class AuthGuard extends AutoRouteGuard {
void onNavigation(NavigationResolver resolver, StackRouter router) async {
resolver.next(true);
if (ImageUrlBuilder.isSharedLink()) {
// If the URL is a shared link, we don't need to validate the access token
return;
}
try {
// Look in the store for an access token
Store.get(StoreKey.accessToken);

View File

@@ -23,11 +23,11 @@ import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart'
import 'package:immich_mobile/pages/album/album_viewer.page.dart';
import 'package:immich_mobile/pages/albums/albums.page.dart';
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart';
import 'package:immich_mobile/pages/backup/drift_backup.page.dart';
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
import 'package:immich_mobile/pages/backup/drift_backup.page.dart';
import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart';
import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart';
import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart';
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
@@ -95,12 +95,11 @@ import 'package:immich_mobile/presentation/pages/drift_person.page.dart';
import 'package:immich_mobile/presentation/pages/drift_place.page.dart';
import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart';
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
import 'package:immich_mobile/presentation/pages/local_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/remote_shared_link.dart';
import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
import 'package:immich_mobile/providers/api.provider.dart';
@@ -330,7 +329,6 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]),
AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: RemoteSharedLinkRoute.page, guards: [_authGuard, _duplicateGuard]),
// required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'),

View File

@@ -2167,58 +2167,6 @@ class RemoteMediaSummaryRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [RemoteSharedLinkPage]
class RemoteSharedLinkRoute extends PageRouteInfo<RemoteSharedLinkRouteArgs> {
RemoteSharedLinkRoute({
Key? key,
required String shareKey,
required String endpoint,
List<PageRouteInfo>? children,
}) : super(
RemoteSharedLinkRoute.name,
args: RemoteSharedLinkRouteArgs(
key: key,
shareKey: shareKey,
endpoint: endpoint,
),
initialChildren: children,
);
static const String name = 'RemoteSharedLinkRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<RemoteSharedLinkRouteArgs>();
return RemoteSharedLinkPage(
key: args.key,
shareKey: args.shareKey,
endpoint: args.endpoint,
);
},
);
}
class RemoteSharedLinkRouteArgs {
const RemoteSharedLinkRouteArgs({
this.key,
required this.shareKey,
required this.endpoint,
});
final Key? key;
final String shareKey;
final String endpoint;
@override
String toString() {
return 'RemoteSharedLinkRouteArgs{key: $key, shareKey: $shareKey, endpoint: $endpoint}';
}
}
/// generated route for
/// [SearchPage]
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {

View File

@@ -8,9 +8,9 @@ import 'package:http/http.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/utils/user_agent.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/user_agent.dart';
class ApiService implements Authentication {
late ApiClient _apiClient;
@@ -45,14 +45,7 @@ class ApiService implements Authentication {
setEndpoint(endpoint);
}
}
ApiService.shared(String endpoint, String sharedKey) {
setEndpoint(endpoint);
_queryParams = {'key': sharedKey};
}
String? _accessToken;
Map<String, String>? _queryParams;
final _log = Logger("ApiService");
setEndpoint(String endpoint) {
@@ -215,8 +208,6 @@ class ApiService implements Authentication {
return Future<void>(() {
var headers = ApiService.getRequestHeaders();
headerParams.addAll(headers);
queryParams.addAll(_queryParams?.entries.map((e) => QueryParam(e.key, e.value)) ?? []);
});
}

View File

@@ -6,8 +6,8 @@ import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider;
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
@@ -80,7 +80,6 @@ class DeepLinkService {
"memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''),
"asset" => await _buildAssetDeepLink(queryParams['id'] ?? ''),
"album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''),
"sharedlink" => await _buildSharedLinkDeepLink(queryParams['key'] ?? '', queryParams['instanceUrl'] ?? ''),
_ => null,
};
@@ -100,24 +99,18 @@ class DeepLinkService {
final path = link.uri.path;
const uuidRegex = r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}';
const b64Regex = r'^[A-Za-z0-9_-]+$';
final assetRegex = RegExp('/photos/($uuidRegex)');
final albumRegex = RegExp('/albums/($uuidRegex)');
final sharedLinkRegex = RegExp('/share/($b64Regex)');
PageRouteInfo<dynamic>? deepLinkRoute;
if (assetRegex.hasMatch(path)) {
final assetId = assetRegex.firstMatch(path)?.group(1) ?? '';
deepLinkRoute = await _buildAssetDeepLink(assetId);
} else if (albumRegex.hasMatch(path)) {
final albumId = albumRegex.firstMatch(path)?.group(1) ?? '';
deepLinkRoute = await _buildAlbumDeepLink(albumId);
} else if (sharedLinkRegex.hasMatch(path)) {
final shareKey = sharedLinkRegex.firstMatch(path)?.group(1) ?? '';
final instanceUrl = link.uri.queryParameters['instanceUrl'] ?? '';
deepLinkRoute = await _buildSharedLinkDeepLink(shareKey, instanceUrl);
}
// Deep link resolution failed, safely handle it based on the app state
if (deepLinkRoute == null) {
if (isColdStart) return DeepLink.defaultPath;
@@ -192,8 +185,4 @@ class DeepLinkService {
return AlbumViewerRoute(albumId: album.id);
}
}
Future<PageRouteInfo?> _buildSharedLinkDeepLink(String shareKey, String instanceUrl) async {
return RemoteSharedLinkRoute(shareKey: shareKey, endpoint: instanceUrl);
}
}

View File

@@ -111,13 +111,4 @@ class SharedLinkService {
}
return null;
}
Future<SharedLink?> getMySharedLink({String? password}) async {
final responseDto = await _apiService.sharedLinksApi.getMySharedLink(password: password);
if (responseDto != null) {
return SharedLink.fromDto(responseDto);
}
return null;
}
}

View File

@@ -36,7 +36,7 @@ String getAlbumThumbNailCacheKey(final Album album, {AssetMediaSize type = Asset
}
String getOriginalUrlForRemoteId(final String id) {
return ImageUrlBuilder.build('/assets/$id/original');
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original';
}
String getImageCacheKey(final Asset asset) {
@@ -46,51 +46,16 @@ String getImageCacheKey(final Asset asset) {
}
String getThumbnailUrlForRemoteId(final String id, {AssetMediaSize type = AssetMediaSize.thumbnail}) {
return ImageUrlBuilder.build('/assets/$id/thumbnail?size=${type.value}');
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}';
}
String getPreviewUrlForRemoteId(final String id) {
return ImageUrlBuilder.build('/assets/$id/thumbnail?size=${AssetMediaSize.preview}');
}
String getPreviewUrlForRemoteId(final String id) =>
'${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${AssetMediaSize.preview}';
String getPlaybackUrlForRemoteId(final String id) {
return ImageUrlBuilder.build('/assets/$id/video/playback?');
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?';
}
String getFaceThumbnailUrl(final String personId) {
return ImageUrlBuilder.build('/people/$personId/thumbnail');
}
class ImageUrlBuilder {
static String? host;
static Map<String, String>? queryParams;
static void setHost(String? host) {
ImageUrlBuilder.host = host;
}
static bool isSharedLink() {
return ImageUrlBuilder.host != null && ImageUrlBuilder.queryParams!.containsKey('key');
}
static void setParameter(String key, String value) {
ImageUrlBuilder.queryParams ??= {};
ImageUrlBuilder.queryParams![key] = value;
}
static String build(String path) {
final endpoint = host ?? Store.get(StoreKey.serverEndpoint);
final uri = Uri.parse('$endpoint$path');
if (queryParams == null || queryParams!.isEmpty) {
return uri.toString();
}
final updatedUri = uri.replace(queryParameters: {...uri.queryParameters, ...queryParams!});
return updatedUri.toString();
}
static void clear() {
ImageUrlBuilder.host = null;
ImageUrlBuilder.queryParams = null;
}
return '${Store.get(StoreKey.serverEndpoint)}/people/$personId/thumbnail';
}

View File

@@ -1,72 +0,0 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
class SharedLinkPasswordDialog extends StatefulWidget {
const SharedLinkPasswordDialog({super.key});
@override
State<SharedLinkPasswordDialog> createState() => _SharedLinkPasswordDialogState();
}
class _SharedLinkPasswordDialogState extends State<SharedLinkPasswordDialog> {
final TextEditingController controller = TextEditingController();
bool isNotEmpty = false;
@override
void initState() {
super.initState();
controller.addListener(() {
setState(() {
isNotEmpty = controller.text.isNotEmpty;
});
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
title: const Text("shared_link_password_dialog_title").t(context: context),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("shared_link_password_dialog_content").t(context: context),
const SizedBox(height: 16),
TextField(
controller: controller,
decoration: InputDecoration(hintText: "password".t(context: context)),
obscureText: true,
autofocus: true,
onSubmitted: (value) {
Navigator.pop(context, value);
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, null),
style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.secondary),
child: const Text("cancel").t(context: context),
),
TextButton(
onPressed: isNotEmpty
? () {
Navigator.pop(context, controller.text);
}
: null,
child: Text(
"submit".t(context: context),
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
],
);
}
}

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.136.0
- API version: 1.137.0
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.136.0+3000
version: 1.137.0+3001
environment:
sdk: '>=3.8.0 <4.0.0'

View File

@@ -9469,7 +9469,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.136.0",
"version": "1.137.0",
"contact": {}
},
"tags": [],

View File

@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
"version": "1.136.0",
"version": "1.137.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.136.0",
"version": "1.137.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.136.0",
"version": "1.137.0",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.136.0
* 1.137.0
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.136.0",
"version": "1.137.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.136.0",
"version": "1.137.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@nestjs/bullmq": "^11.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.136.0",
"version": "1.137.0",
"description": "",
"author": "",
"private": true,

6
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-web",
"version": "1.136.0",
"version": "1.137.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.136.0",
"version": "1.137.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
@@ -94,7 +94,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.136.0",
"version": "1.137.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.136.0",
"version": "1.137.0",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {

View File

@@ -1,12 +1,8 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { getAssetThumbnailUrl } from '$lib/utils';
import { getAssetResolution, getFileSize } from '$lib/utils/asset-utils';
import { getAltText } from '$lib/utils/thumbnail-util';
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
import { getFileSize } from '$lib/utils/asset-utils';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { type AssetResponseDto } from '@immich/sdk';
import { mdiHeart } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
asset: AssetResponseDto;
@@ -16,43 +12,26 @@
let { asset, onViewAsset }: Props = $props();
let assetData = $derived(JSON.stringify(asset, null, 2));
let boxWidth = $state(300);
</script>
<div
class="max-w-60 rounded-xl border-4 transition-colors font-semibold text-xs bg-gray-200 dark:bg-gray-800 border-gray-200 dark:border-gray-800"
class="w-full aspect-square rounded-xl border-4 transition-colors font-semibold text-xs bg-gray-200 dark:bg-gray-800 border-gray-200 dark:border-gray-800"
bind:clientWidth={boxWidth}
title={assetData}
>
<div class="relative w-full">
<button type="button" onclick={() => onViewAsset(asset)} class="block relative w-full" aria-label={$t('keep')}>
<!-- THUMBNAIL-->
<img
src={getAssetThumbnailUrl(asset.id)}
alt={$getAltText(toTimelineAsset(asset))}
title={assetData}
class="h-60 object-cover rounded-t-xl w-full"
draggable="false"
/>
<div class="relative w-full h-full overflow-hidden rounded-lg">
<Thumbnail asset={toTimelineAsset(asset)} readonly onClick={() => onViewAsset(asset)} thumbnailSize={boxWidth} />
<!-- OVERLAY CHIP -->
{#if !!asset.libraryId}
<div class="absolute bottom-1 end-3 px-4 py-1 rounded-xl text-xs transition-colors bg-red-300/90">External</div>
{/if}
<!-- FAVORITE ICON -->
{#if asset.isFavorite}
<div class="absolute bottom-2 start-2">
<Icon path={mdiHeart} size="24" class="text-white" />
</div>
{/if}
</button>
{#if !!asset.libraryId}
<div class="absolute bottom-1 end-3 px-4 py-1 rounded-xl text-xs transition-colors bg-red-500">External</div>
{/if}
</div>
<div class="flex justify-between items-center pl-2 pr-4 gap-2">
<div class="grid gap-y-2 py-2 text-xs transition-colors dark:text-white">
<div class="text-left text-ellipsis truncate">{asset.originalFileName}</div>
<span>{getAssetResolution(asset)}</span>
</div>
<div class="dark:text-white text-lg font-bold whitespace-nowrap w-max">
{getFileSize(asset, 1)}
</div>
<div class="text-center mt-4 px-4 text-sm font-normal truncate" title={asset.originalFileName}>
{asset.originalFileName}
</div>
<div class="text-center">
<p class="text-primary text-xl font-semibold py-3">{getFileSize(asset, 1)}</p>
</div>
</div>

View File

@@ -56,7 +56,7 @@
</script>
<UserPageLayout title={data.meta.title} scrollbar={true}>
<div class="flex gap-2 flex-wrap">
<div class="grid gap-2 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
{#if assets && data.assets.length > 0}
{#each assets as asset (asset.id)}
<LargeAssetData {asset} onViewAsset={(asset) => setAsset(asset)} />