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