Compare commits

..

1 Commits

Author SHA1 Message Date
Alex f3af54c6c9 chore: more logs and feedback 2025-08-29 16:18:52 -05:00
30 changed files with 179 additions and 168 deletions
@@ -11,7 +11,7 @@ run_cmd pnpm --filter immich install
log "Starting Nest API Server" log "Starting Nest API Server"
log "" log ""
cd "${IMMICH_WORKSPACE}/server" || ( cd "${IMMICH_WORKSPACE}/server" || (
log "Immich workspace not found" log "Immich workspace not found"jj
exit 1 exit 1
) )
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.86", "version": "2.2.85",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",
-4
View File
@@ -1,8 +1,4 @@
[ [
{
"label": "v1.140.1",
"url": "https://v1.140.1.archive.immich.app"
},
{ {
"label": "v1.140.0", "label": "v1.140.0",
"url": "https://v1.140.0.archive.immich.app" "url": "https://v1.140.0.archive.immich.app"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.140.1", "version": "1.140.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
+2
View File
@@ -639,6 +639,8 @@
"cannot_update_the_description": "Cannot update the description", "cannot_update_the_description": "Cannot update the description",
"cast": "Cast", "cast": "Cast",
"cast_description": "Configure available cast destinations", "cast_description": "Configure available cast destinations",
"cellular_data_for_photos": "Cellular data for photos",
"cellular_data_for_videos": "Cellular data for videos",
"change_date": "Change date", "change_date": "Change date",
"change_description": "Change description", "change_description": "Change description",
"change_display_order": "Change display order", "change_display_order": "Change display order",
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 3011, "android.injected.version.code" => 3010,
"android.injected.version.name" => "1.140.1", "android.injected.version.name" => "1.140.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') 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')
+10 -18
View File
@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 77; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -507,14 +507,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -543,14 +539,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -697,7 +689,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219; CURRENT_PROJECT_VERSION = 218;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -841,7 +833,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219; CURRENT_PROJECT_VERSION = 218;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -871,7 +863,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219; CURRENT_PROJECT_VERSION = 218;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -905,7 +897,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219; CURRENT_PROJECT_VERSION = 218;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -948,7 +940,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219; CURRENT_PROJECT_VERSION = 218;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -988,7 +980,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219; CURRENT_PROJECT_VERSION = 218;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1027,7 +1019,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219; CURRENT_PROJECT_VERSION = 218;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1071,7 +1063,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219; CURRENT_PROJECT_VERSION = 218;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1112,7 +1104,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 219; CURRENT_PROJECT_VERSION = 218;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
+2 -2
View File
@@ -81,7 +81,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.140.0</string> <string>1.139.3</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@@ -108,7 +108,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>219</string> <string>217</string>
<key>FLTEnableImpeller</key> <key>FLTEnableImpeller</key>
<true /> <true />
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
+1 -1
View File
@@ -22,7 +22,7 @@ platform :ios do
path: "./Runner.xcodeproj", path: "./Runner.xcodeproj",
) )
increment_version_number( increment_version_number(
version_number: "1.140.1" version_number: "1.140.0"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,
+2 -2
View File
@@ -75,8 +75,8 @@ enum StoreKey<T> {
betaPromptShown<bool>._(1001), betaPromptShown<bool>._(1001),
betaTimeline<bool>._(1002), betaTimeline<bool>._(1002),
enableBackup<bool>._(1003), enableBackup<bool>._(1003),
useWifiForUploadVideos<bool>._(1004), useCellularForUploadVideos<bool>._(1004),
useWifiForUploadPhotos<bool>._(1005); useCellularForUploadPhotos<bool>._(1005);
const StoreKey._(this.id); const StoreKey._(this.id);
final int id; final int id;
@@ -15,8 +15,8 @@ class DriftMemoryRepository extends DriftDatabaseRepository {
final query = final query =
_db.select(_db.memoryEntity).join([ _db.select(_db.memoryEntity).join([
innerJoin(_db.memoryAssetEntity, _db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id)), leftOuterJoin(_db.memoryAssetEntity, _db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id)),
innerJoin( leftOuterJoin(
_db.remoteAssetEntity, _db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.memoryAssetEntity.assetId) & _db.remoteAssetEntity.id.equalsExp(_db.memoryAssetEntity.assetId) &
_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.deletedAt.isNull() &
@@ -3,6 +3,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
@@ -93,7 +95,11 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
const _BackupCard(), const _BackupCard(),
const _RemainderCard(), const _RemainderCard(),
const Divider(), const Divider(),
const SizedBox(height: 4),
const _CellularBackupStatus(),
const SizedBox(height: 4),
BackupToggleButton(onStart: () async => await startBackup(), onStop: () async => await stopBackup()), BackupToggleButton(onStart: () async => await startBackup(), onStop: () async => await stopBackup()),
TextButton.icon( TextButton.icon(
icon: const Icon(Icons.info_outline_rounded), icon: const Icon(Icons.info_outline_rounded),
onPressed: () => context.pushRoute(const DriftUploadDetailRoute()), onPressed: () => context.pushRoute(const DriftUploadDetailRoute()),
@@ -109,6 +115,64 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
} }
} }
class _CellularBackupStatus extends ConsumerWidget {
const _CellularBackupStatus();
@override
Widget build(BuildContext context, WidgetRef ref) {
final cellularReqForVideos = Store.watch(StoreKey.useCellularForUploadVideos);
final cellularReqForPhotos = Store.watch(StoreKey.useCellularForUploadPhotos);
return GestureDetector(
onTap: () => context.pushRoute(const DriftBackupOptionsRoute()),
child: Row(
children: [
StreamBuilder(
stream: cellularReqForVideos,
initialData: Store.tryGet(StoreKey.useCellularForUploadVideos) ?? false,
builder: (context, snapshot) {
return Expanded(
child: ListTile(
visualDensity: VisualDensity.compact,
leading: Icon(
snapshot.data ?? false ? Icons.check_circle : Icons.cancel_outlined,
size: 16,
color: snapshot.data ?? false ? Colors.green : context.colorScheme.onSurfaceVariant,
),
title: Text(
"cellular_data_for_videos".t(context: context),
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceVariant),
),
),
);
},
),
StreamBuilder(
stream: cellularReqForPhotos,
initialData: Store.tryGet(StoreKey.useCellularForUploadPhotos) ?? false,
builder: (context, snapshot) {
return Expanded(
child: ListTile(
visualDensity: VisualDensity.compact,
leading: Icon(
snapshot.data ?? false ? Icons.check_circle : Icons.cancel_outlined,
size: 16,
color: snapshot.data ?? false ? Colors.green : context.colorScheme.onSurfaceVariant,
),
title: Text(
"cellular_data_for_photos".t(context: context),
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceVariant),
),
),
);
},
),
],
),
);
}
}
class _BackupAlbumSelectionCard extends ConsumerWidget { class _BackupAlbumSelectionCard extends ConsumerWidget {
const _BackupAlbumSelectionCard(); const _BackupAlbumSelectionCard();
@@ -17,15 +17,15 @@ class DriftBackupOptionsPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
bool hasPopped = false; bool hasPopped = false;
final previousWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false; final previousWifiReqForVideos = Store.tryGet(StoreKey.useCellularForUploadVideos) ?? false;
final previousWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false; final previousWifiReqForPhotos = Store.tryGet(StoreKey.useCellularForUploadPhotos) ?? false;
return PopScope( return PopScope(
onPopInvokedWithResult: (didPop, result) async { onPopInvokedWithResult: (didPop, result) async {
// There is an issue with Flutter where the pop event // There is an issue with Flutter where the pop event
// can be triggered multiple times, so we guard it with _hasPopped // can be triggered multiple times, so we guard it with _hasPopped
final currentWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false; final currentWifiReqForVideos = Store.tryGet(StoreKey.useCellularForUploadVideos) ?? false;
final currentWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false; final currentWifiReqForPhotos = Store.tryGet(StoreKey.useCellularForUploadPhotos) ?? false;
if (currentWifiReqForVideos == previousWifiReqForVideos && if (currentWifiReqForVideos == previousWifiReqForVideos &&
currentWifiReqForPhotos == previousWifiReqForPhotos) { currentWifiReqForPhotos == previousWifiReqForPhotos) {
@@ -1,34 +1,15 @@
import 'dart:io'; import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
class _SharePreparingDialog extends StatelessWidget {
const _SharePreparingDialog();
@override
Widget build(BuildContext context) {
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
Container(margin: const EdgeInsets.only(top: 12), child: const Text('share_dialog_preparing').tr()),
],
),
);
}
}
class ShareActionButton extends ConsumerWidget { class ShareActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;
@@ -39,34 +20,28 @@ class ShareActionButton extends ConsumerWidget {
return; return;
} }
showDialog( final result = await ref.read(actionProvider.notifier).shareAssets(source);
context: context, ref.read(multiSelectProvider.notifier).reset();
builder: (BuildContext buildContext) {
ref.read(actionProvider.notifier).shareAssets(source).then((ActionResult result) {
ref.read(multiSelectProvider.notifier).reset();
if (!context.mounted) { if (!context.mounted) {
return; return;
} }
if (!result.success) { if (!result.success) {
ImmichToast.show( ImmichToast.show(
context: context, context: context,
msg: 'scaffold_body_error_occurred'.t(context: context), msg: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
toastType: ToastType.error, toastType: ToastType.error,
); );
} } else if (result.count > 0) {
ImmichToast.show(
buildContext.pop(); context: context,
}); msg: 'share_action_prompt'.t(context: context, args: {'count': result.count.toString()}),
gravity: ToastGravity.BOTTOM,
// show a loading spinner with a "Preparing" message toastType: ToastType.success,
return const _SharePreparingDialog(); );
}, }
barrierDismissible: false,
useRootNavigator: false,
);
} }
@override @override
@@ -334,8 +334,8 @@ class ActionNotifier extends Notifier<void> {
final ids = _getAssets(source).toList(growable: false); final ids = _getAssets(source).toList(growable: false);
try { try {
await _service.shareAssets(ids); final count = await _service.shareAssets(ids);
return ActionResult(count: ids.length, success: true); return ActionResult(count: count, success: true);
} catch (error, stack) { } catch (error, stack) {
_logger.severe('Failed to share assets', error, stack); _logger.severe('Failed to share assets', error, stack);
return ActionResult(count: ids.length, success: false, error: error.toString()); return ActionResult(count: ids.length, success: false, error: error.toString());
@@ -104,18 +104,15 @@ class AssetMediaRepository {
return 0; return 0;
} }
// we dont want to await the share result since the final result = await Share.shareXFiles(downloadedXFiles);
// "preparing" dialog will not disappear unti
Share.shareXFiles(downloadedXFiles).then((result) async {
for (var file in downloadedXFiles) {
try {
await File(file.path).delete();
} catch (e) {
_log.warning("Failed to delete temporary file: ${file.path}", e);
}
}
});
return downloadedXFiles.length; for (var file in downloadedXFiles) {
try {
await File(file.path).delete();
} catch (e) {
_log.warning("Failed to delete temporary file: ${file.path}", e);
}
}
return result.status == ShareResultStatus.success ? downloadedXFiles.length : 0;
} }
} }
@@ -48,8 +48,8 @@ enum AppSettingsEnum<T> {
photoManagerCustomFilter<bool>(StoreKey.photoManagerCustomFilter, null, true), photoManagerCustomFilter<bool>(StoreKey.photoManagerCustomFilter, null, true),
betaTimeline<bool>(StoreKey.betaTimeline, null, false), betaTimeline<bool>(StoreKey.betaTimeline, null, false),
enableBackup<bool>(StoreKey.enableBackup, null, false), enableBackup<bool>(StoreKey.enableBackup, null, false),
useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false), useCellularForUploadVideos<bool>(StoreKey.useCellularForUploadVideos, null, false),
useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false), useCellularForUploadPhotos<bool>(StoreKey.useCellularForUploadPhotos, null, false),
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false); readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false);
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
+38 -20
View File
@@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
final uploadServiceProvider = Provider((ref) { final uploadServiceProvider = Provider((ref) {
@@ -57,6 +58,7 @@ class UploadService {
Stream<TaskStatusUpdate> get taskStatusStream => _taskStatusController.stream; Stream<TaskStatusUpdate> get taskStatusStream => _taskStatusController.stream;
Stream<TaskProgressUpdate> get taskProgressStream => _taskProgressController.stream; Stream<TaskProgressUpdate> get taskProgressStream => _taskProgressController.stream;
final Logger _log = Logger('UploadService');
bool shouldAbortQueuingTasks = false; bool shouldAbortQueuingTasks = false;
@@ -127,11 +129,16 @@ class UploadService {
final candidates = await _backupRepository.getCandidates(userId); final candidates = await _backupRepository.getCandidates(userId);
if (candidates.isEmpty) { if (candidates.isEmpty) {
_log.info("No backup candidates found for user $userId");
return; return;
} }
_log.info("Starting backup for ${candidates.length} candidates");
onEnqueueTasks(EnqueueStatus(enqueueCount: 0, totalCount: candidates.length));
const batchSize = 100; const batchSize = 100;
int count = 0; int count = 0;
int skippedAssets = 0;
for (int i = 0; i < candidates.length; i += batchSize) { for (int i = 0; i < candidates.length; i += batchSize) {
if (shouldAbortQueuingTasks) { if (shouldAbortQueuingTasks) {
break; break;
@@ -144,16 +151,22 @@ class UploadService {
final task = await _getUploadTask(asset); final task = await _getUploadTask(asset);
if (task != null) { if (task != null) {
tasks.add(task); tasks.add(task);
} else {
skippedAssets++;
_log.warning("Skipped asset ${asset.id} (${asset.name}) - unable to create upload task");
} }
} }
if (tasks.isNotEmpty && !shouldAbortQueuingTasks) { count += tasks.length;
count += tasks.length; onEnqueueTasks(EnqueueStatus(enqueueCount: count, totalCount: candidates.length));
await enqueueTasks(tasks);
onEnqueueTasks(EnqueueStatus(enqueueCount: count, totalCount: candidates.length)); if (tasks.isNotEmpty && !shouldAbortQueuingTasks) {
_log.info("Enqueuing ${tasks.length} upload tasks");
await enqueueTasks(tasks);
} }
} }
_log.info("Upload queueing completed: $count tasks enqueued, $skippedAssets assets skipped");
} }
// Enqueue All does not work from the background on Android yet. This method is a temporary workaround // Enqueue All does not work from the background on Android yet. This method is a temporary workaround
@@ -165,9 +178,14 @@ class UploadService {
final candidates = await _backupRepository.getCandidates(userId); final candidates = await _backupRepository.getCandidates(userId);
if (candidates.isEmpty) { if (candidates.isEmpty) {
debugPrint("No backup candidates found for serial backup");
return; return;
} }
debugPrint("Starting serial backup for ${candidates.length} candidates");
int skippedAssets = 0;
int enqueuedTasks = 0;
for (final asset in candidates) { for (final asset in candidates) {
if (shouldAbortQueuingTasks) { if (shouldAbortQueuingTasks) {
break; break;
@@ -176,8 +194,13 @@ class UploadService {
final task = await _getUploadTask(asset); final task = await _getUploadTask(asset);
if (task != null) { if (task != null) {
await _uploadRepository.enqueueBackground(task); await _uploadRepository.enqueueBackground(task);
enqueuedTasks++;
} else {
skippedAssets++;
} }
} }
debugPrint("Serial backup completed: $enqueuedTasks tasks enqueued, $skippedAssets assets skipped");
} }
/// Cancel all ongoing uploads and reset the upload queue /// Cancel all ongoing uploads and reset the upload queue
@@ -245,6 +268,7 @@ class UploadService {
Future<UploadTask?> _getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async { Future<UploadTask?> _getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async {
final entity = await _storageRepository.getAssetEntityForAsset(asset); final entity = await _storageRepository.getAssetEntityForAsset(asset);
if (entity == null) { if (entity == null) {
_log.warning("Cannot get AssetEntity for asset ${asset.id} (${asset.name}) created on ${asset.createdAt}");
return null; return null;
} }
@@ -267,6 +291,9 @@ class UploadService {
} }
if (file == null) { if (file == null) {
_log.warning(
"Cannot get file for asset ${asset.id} (${asset.name}) created on ${asset.createdAt} - file may have been deleted or moved",
);
return null; return null;
} }
@@ -278,7 +305,13 @@ class UploadService {
livePhotoVideoId: '', livePhotoVideoId: '',
).toJson(); ).toJson();
final requiresWiFi = _shouldRequireWiFi(asset); bool requiresWiFi = true;
if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) {
requiresWiFi = false;
} else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) {
requiresWiFi = false;
}
return buildUploadTask( return buildUploadTask(
file, file,
@@ -305,8 +338,6 @@ class UploadService {
final fields = {'livePhotoVideoId': livePhotoVideoId}; final fields = {'livePhotoVideoId': livePhotoVideoId};
final requiresWiFi = _shouldRequireWiFi(asset);
return buildUploadTask( return buildUploadTask(
file, file,
originalFileName: asset.name, originalFileName: asset.name,
@@ -315,22 +346,9 @@ class UploadService {
group: kBackupLivePhotoGroup, group: kBackupLivePhotoGroup,
priority: 0, // Highest priority to get upload immediately priority: 0, // Highest priority to get upload immediately
isFavorite: asset.isFavorite, isFavorite: asset.isFavorite,
requiresWiFi: requiresWiFi,
); );
} }
bool _shouldRequireWiFi(LocalAsset asset) {
bool requiresWiFi = true;
if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) {
requiresWiFi = false;
} else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) {
requiresWiFi = false;
}
return requiresWiFi;
}
Future<UploadTask> buildUploadTask( Future<UploadTask> buildUploadTask(
File file, { File file, {
required String group, required String group,
@@ -22,7 +22,7 @@ class _UseWifiForUploadVideosButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final valueStream = Store.watch(StoreKey.useWifiForUploadVideos); final valueStream = Store.watch(StoreKey.useCellularForUploadVideos);
return ListTile( return ListTile(
title: Text( title: Text(
@@ -32,7 +32,7 @@ class _UseWifiForUploadVideosButton extends ConsumerWidget {
subtitle: Text("network_requirement_videos_upload".t(context: context), style: context.textTheme.labelLarge), subtitle: Text("network_requirement_videos_upload".t(context: context), style: context.textTheme.labelLarge),
trailing: StreamBuilder( trailing: StreamBuilder(
stream: valueStream, stream: valueStream,
initialData: Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false, initialData: Store.tryGet(StoreKey.useCellularForUploadVideos) ?? false,
builder: (context, snapshot) { builder: (context, snapshot) {
final value = snapshot.data ?? false; final value = snapshot.data ?? false;
return Switch( return Switch(
@@ -54,7 +54,7 @@ class _UseWifiForUploadPhotosButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final valueStream = Store.watch(StoreKey.useWifiForUploadPhotos); final valueStream = Store.watch(StoreKey.useCellularForUploadPhotos);
return ListTile( return ListTile(
title: Text( title: Text(
@@ -64,7 +64,7 @@ class _UseWifiForUploadPhotosButton extends ConsumerWidget {
subtitle: Text("network_requirement_photos_upload".t(context: context), style: context.textTheme.labelLarge), subtitle: Text("network_requirement_photos_upload".t(context: context), style: context.textTheme.labelLarge),
trailing: StreamBuilder( trailing: StreamBuilder(
stream: valueStream, stream: valueStream,
initialData: Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false, initialData: Store.tryGet(StoreKey.useCellularForUploadPhotos) ?? false,
builder: (context, snapshot) { builder: (context, snapshot) {
final value = snapshot.data ?? false; final value = snapshot.data ?? false;
return Switch( return Switch(
+1 -1
View File
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.140.1 - API version: 1.140.0
- Generator version: 7.8.0 - Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen
+1 -1
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none' publish_to: 'none'
version: 1.140.1+3011 version: 1.140.0+3010
environment: environment:
sdk: '>=3.8.0 <4.0.0' sdk: '>=3.8.0 <4.0.0'
+1 -1
View File
@@ -9789,7 +9789,7 @@
"info": { "info": {
"title": "Immich", "title": "Immich",
"description": "Immich API", "description": "Immich API",
"version": "1.140.1", "version": "1.140.0",
"contact": {} "contact": {}
}, },
"tags": [], "tags": [],
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.140.1", "version": "1.140.0",
"description": "Auto-generated TypeScript SDK for the Immich API", "description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module", "type": "module",
"main": "./build/index.js", "main": "./build/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
/** /**
* Immich * Immich
* 1.140.1 * 1.140.0
* DO NOT MODIFY - This file has been generated using oazapfts. * DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts * See https://www.npmjs.com/package/oazapfts
*/ */
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich", "name": "immich",
"version": "1.140.1", "version": "1.140.0",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-web", "name": "immich-web",
"version": "1.140.1", "version": "1.140.0",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -1,24 +1,18 @@
import NumberRangeInput from '$lib/components/shared-components/number-range-input.svelte'; import NumberRangeInput from '$lib/components/shared-components/number-range-input.svelte';
import { render, type RenderResult } from '@testing-library/svelte'; import { render, type RenderResult } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import type { Mock } from 'vitest';
describe('NumberRangeInput component', () => { describe('NumberRangeInput component', () => {
const user = userEvent.setup(); const user = userEvent.setup();
let sut: RenderResult<NumberRangeInput>; let sut: RenderResult<NumberRangeInput>;
let input: HTMLInputElement; let input: HTMLInputElement;
let onInput: Mock;
let onKeyDown: Mock;
beforeEach(() => { beforeEach(() => {
onInput = vi.fn();
onKeyDown = vi.fn();
sut = render(NumberRangeInput, { sut = render(NumberRangeInput, {
id: '', id: '',
min: -90, min: -90,
max: 90, max: 90,
onInput, onInput: () => {},
onKeyDown,
}); });
input = sut.getByRole('spinbutton') as HTMLInputElement; input = sut.getByRole('spinbutton') as HTMLInputElement;
}); });
@@ -27,55 +21,35 @@ describe('NumberRangeInput component', () => {
expect(input.value).toBe(''); expect(input.value).toBe('');
await sut.rerender({ value: 10 }); await sut.rerender({ value: 10 });
expect(input.value).toBe('10'); expect(input.value).toBe('10');
expect(onInput).not.toHaveBeenCalled();
expect(onKeyDown).not.toHaveBeenCalled();
}); });
it('restricts minimum value', async () => { it('restricts minimum value', async () => {
await user.type(input, '-91'); await user.type(input, '-91');
expect(input.value).toBe('-90'); expect(input.value).toBe('-90');
expect(onInput).toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalled();
}); });
it('restricts maximum value', async () => { it('restricts maximum value', async () => {
await user.type(input, '09990'); await user.type(input, '09990');
expect(input.value).toBe('90'); expect(input.value).toBe('90');
expect(onInput).toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalled();
}); });
it('allows entering negative numbers', async () => { it('allows entering negative numbers', async () => {
await user.type(input, '-10'); await user.type(input, '-10');
expect(input.value).toBe('-10'); expect(input.value).toBe('-10');
expect(onInput).toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalled();
}); });
it('allows entering zero', async () => { it('allows entering zero', async () => {
await user.type(input, '0'); await user.type(input, '0');
expect(input.value).toBe('0'); expect(input.value).toBe('0');
expect(onInput).toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalled();
}); });
it('allows entering decimal numbers', async () => { it('allows entering decimal numbers', async () => {
await user.type(input, '-0.09001'); await user.type(input, '-0.09001');
expect(input.value).toBe('-0.09001'); expect(input.value).toBe('-0.09001');
expect(onInput).toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalled();
}); });
it('ignores text input', async () => { it('ignores text input', async () => {
await user.type(input, 'test'); await user.type(input, 'test');
expect(input.value).toBe(''); expect(input.value).toBe('');
expect(onInput).toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalled();
});
it('test', async () => {
await user.type(input, 'd');
expect(onInput).not.toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalled();
}); });
}); });
@@ -44,7 +44,7 @@
let mapLat = $derived(assetLat ?? previousLocation?.lat ?? undefined); let mapLat = $derived(assetLat ?? previousLocation?.lat ?? undefined);
let mapLng = $derived(assetLng ?? previousLocation?.lng ?? undefined); let mapLng = $derived(assetLng ?? previousLocation?.lng ?? undefined);
let zoom = $derived(mapLat && mapLng ? 12.5 : 1); let zoom = $derived(mapLat !== undefined && mapLng !== undefined ? 12.5 : 1);
$effect(() => { $effect(() => {
if (mapElement && initialPoint) { if (mapElement && initialPoint) {
@@ -20,10 +20,6 @@
} }
}; };
const onKeyDown = (event: KeyboardEvent) => {
event.stopPropagation();
};
const onPaste = (event: ClipboardEvent) => { const onPaste = (event: ClipboardEvent) => {
const pastedText = event.clipboardData?.getData('text/plain'); const pastedText = event.clipboardData?.getData('text/plain');
if (!pastedText) { if (!pastedText) {
@@ -46,10 +42,10 @@
<div> <div>
<label class="immich-form-label" for="latitude-input-{id}">{$t('latitude')}</label> <label class="immich-form-label" for="latitude-input-{id}">{$t('latitude')}</label>
<NumberRangeInput id="latitude-input-{id}" min={-90} max={90} {onKeyDown} {onInput} {onPaste} bind:value={lat} /> <NumberRangeInput id="latitude-input-{id}" min={-90} max={90} {onInput} {onPaste} bind:value={lat} />
</div> </div>
<div> <div>
<label class="immich-form-label" for="longitude-input-{id}">{$t('longitude')}</label> <label class="immich-form-label" for="longitude-input-{id}">{$t('longitude')}</label>
<NumberRangeInput id="longitude-input-{id}" min={-180} max={180} {onKeyDown} {onInput} {onPaste} bind:value={lng} /> <NumberRangeInput id="longitude-input-{id}" min={-180} max={180} {onInput} {onPaste} bind:value={lng} />
</div> </div>
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import type { ClipboardEventHandler, KeyboardEventHandler } from 'svelte/elements'; import type { ClipboardEventHandler } from 'svelte/elements';
interface Props { interface Props {
id: string; id: string;
@@ -11,7 +11,6 @@
value?: number; value?: number;
onInput: (value: number | null) => void; onInput: (value: number | null) => void;
onPaste?: ClipboardEventHandler<HTMLInputElement>; onPaste?: ClipboardEventHandler<HTMLInputElement>;
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
} }
let { let {
@@ -23,7 +22,6 @@
value = $bindable(), value = $bindable(),
onInput, onInput,
onPaste = undefined, onPaste = undefined,
onKeyDown = undefined,
}: Props = $props(); }: Props = $props();
const oninput = () => { const oninput = () => {
@@ -50,5 +48,4 @@
bind:value bind:value
{oninput} {oninput}
onpaste={onPaste} onpaste={onPaste}
onkeydown={onKeyDown}
/> />