Compare commits

..

8 Commits

Author SHA1 Message Date
github-actions
f15376a107 chore: version v1.140.1 2025-08-30 19:13:06 +00:00
Brandon Wees
32955915dd fix: show "preparing" when sharing in beta timeline (#21390)
* fix: show "preparing" when sharing in beta timeline

* embed dialog inside of share_action_button

* dont await the share sheet so "preparing" dialog disappears once share sheet presents

this mimics old timeline behavior

* chore: lint
2025-08-30 13:51:32 -05:00
Alex
aacb27ea5f fix: network criteria for upload LivePhotos (#21386) 2025-08-30 18:45:42 +00:00
Alex
d6b8c0926f chore: post release tasks (#21385) 2025-08-30 13:45:29 -05:00
Snowknight26
225af973c1 fix(web): Prevent changing asset location triggering keyboard shortcuts (#21451)
fix(web): Prevent changing asset location triggering asset keyboard shortcuts
2025-08-30 13:39:25 -05:00
Brandon Wees
b3372064e0 fix: default zoom level when location is not set (#21428) 2025-08-30 13:33:11 -05:00
Mert
303307e1ac fix(mobile): memory lane query (#21422) 2025-08-29 19:33:58 -05:00
Aaron Liu
f75c9dfe37 fix(devcontainer): logging typo (#21415) 2025-08-29 20:54:42 +00:00
30 changed files with 167 additions and 178 deletions

View File

@@ -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
)

View File

@@ -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",

View File

@@ -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"

View File

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

View File

@@ -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",

View File

@@ -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')

View File

@@ -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;

View File

@@ -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>

View File

@@ -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,

View File

@@ -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;

View File

@@ -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() &

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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

View File

@@ -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());

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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(

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.140.0
- API version: 1.140.1
- 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.140.0+3010
version: 1.140.1+3011
environment:
sdk: '>=3.8.0 <4.0.0'

View File

@@ -9789,7 +9789,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.140.0",
"version": "1.140.1",
"contact": {}
},
"tags": [],

View File

@@ -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",

View File

@@ -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
*/

View File

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

View File

@@ -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": {

View File

@@ -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();
});
});

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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}
/>