Compare commits

...

7 Commits

Author SHA1 Message Date
midzelis
61689258be chore: update devcontainers for trixie, devenv changes 2025-09-19 02:39:36 +00:00
Jason Rasmussen
68f3ed89c5 chore: remove unused init service (#22188) 2025-09-19 01:11:45 +00:00
Sergey Katsubo
78516a97b3 chore(server): proper log context formatting (#22173)
* Fix log formatting for logger.error(..., error)

Rewrite it to avoid printing error msg in [context]

* Fix log formatting for logger.warn(..., error?.stack)

Rewrite it to avoid printing stack in [context]

* Fix log formatting for logger.debug(..., error.message);

Rewrite it to avoid printing error msg in [context]

* Print error msg instead of literal "Error"
2025-09-18 19:56:05 -04:00
Alex
b8a17c3c26 fix: disable scrubbing mode on drag ended (#22186) 2025-09-18 16:42:33 -05:00
Alex
e42886b767 fix: display thumbnail while scrubbing paused (#22164)
* fix: display thumbnail while scrubbing paused

* pr feedback

* pr feedback

* tune timeout
2025-09-18 20:59:58 +00:00
Alex
d36c26bf97 chore: refresh backup stats when entering backup page (#21977)
* chore: refresh backup stats when entering backup page

* check for success status

* remove logs

* remove sync remote when toggle the button

* show status immediately after navigating to screen

* pr feedback
2025-09-18 15:36:43 -05:00
Brandon Wees
dcbc266b83 chore: disable mise lockfile (#22182) 2025-09-18 15:44:33 -04:00
22 changed files with 155 additions and 109 deletions

View File

@@ -5,8 +5,7 @@
"immich-server",
"redis",
"database",
"immich-machine-learning",
"init"
"immich-machine-learning"
],
"dockerComposeFile": [
"../docker/docker-compose.dev.yml",

View File

@@ -6,28 +6,35 @@ services:
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
volumes: !override # bind mount host to /workspaces/immich
- ..:/workspaces/immich
- cli_node_modules:/workspaces/immich/cli/node_modules
- e2e_node_modules:/workspaces/immich/e2e/node_modules
- open_api_node_modules:/workspaces/immich/open-api/typescript-sdk/node_modules
- server_node_modules:/workspaces/immich/server/node_modules
- web_node_modules:/workspaces/immich/web/node_modules
- ${UPLOAD_LOCATION}/photos:/data
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
- pnpm-store:/usr/src/app/.pnpm-store
- server-node_modules:/usr/src/app/server/node_modules
- web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules
- docs-node_modules:/usr/src/app/docs/node_modules
- e2e-node_modules:/usr/src/app/e2e/node_modules
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
- app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
- /etc/localtime:/etc/localtime:ro
immich-web:
env_file: !reset []
immich-machine-learning:
env_file: !reset []
database:
env_file: !reset []
environment: !override
POSTGRES_PASSWORD: ${DB_PASSWORD-postgres}
POSTGRES_USER: ${DB_USERNAME-postgres}
POSTGRES_DB: ${DB_DATABASE_NAME-immich}
POSTGRES_INITDB_ARGS: '--data-checksums'
POSTGRES_HOST_AUTH_METHOD: md5
volumes:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
- ${UPLOAD_LOCATION:-postgres-devcontainer-volume}${UPLOAD_LOCATION:+/postgres}:/var/lib/postgresql/data
redis:
env_file: !reset []
volumes:
# Node modules for each service to avoid conflicts and ensure consistent dependencies
cli_node_modules:
e2e_node_modules:
open_api_node_modules:
server_node_modules:
web_node_modules:
# UPLOAD_LOCATION must be set to a absolute path or vol-upload
vol-upload:
# DB_DATA_LOCATION must be set to a absolute path or vol-database
vol-database:
upload-devcontainer-volume:
postgres-devcontainer-volume:

View File

@@ -40,7 +40,7 @@
"userEnvProbe": "loginInteractiveShell",
"remoteEnv": {
// The location where your uploaded files are stored
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./Library}",
"UPLOAD_LOCATION": "${localEnv:UPLOAD_LOCATION:./library}",
// Connection secret for postgres. You should change it to a random password
// Please use only the characters `A-Za-z0-9`, without special characters or spaces
"DB_PASSWORD": "${localEnv:DB_PASSWORD:postgres}",

View File

@@ -1,34 +0,0 @@
[tools.dart]
version = "3.8.2"
backend = "asdf:dart"
[tools.flutter]
version = "3.35.3-stable"
backend = "asdf:flutter"
[tools."github:CQLabs/homebrew-dcm"]
version = "1.31.4"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm".platforms.linux-x64]
checksum = "blake3:e9df5b765df327e1248fccf2c6165a89d632a065667f99c01765bf3047b94955"
size = 8821083
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.31.4/dcm-linux-x64-release.zip"
[tools.node]
version = "22.18.0"
backend = "core:node"
[tools.node.platforms.linux-x64]
checksum = "sha256:a2e703725d8683be86bb5da967bf8272f4518bdaf10f21389e2b2c9eaeae8c8a"
size = 54824343
url = "https://nodejs.org/dist/v22.18.0/node-v22.18.0-linux-x64.tar.gz"
[tools.pnpm]
version = "10.14.0"
backend = "aqua:pnpm/pnpm"
[tools.pnpm.platforms.linux-x64]
checksum = "blake3:13dfa46b7173d3cad3bad60a756a492ecf0bce48b23eb9f793e7ccec5a09b46d"
size = 66231525
url = "https://github.com/pnpm/pnpm/releases/download/v10.14.0/pnpm-linux-x64"

View File

@@ -11,7 +11,6 @@ postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
[settings]
experimental = true
lockfile = true
pin = true
# .github

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -133,6 +133,8 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync;
sourceTree = "<group>";
};
@@ -519,14 +521,10 @@
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";
@@ -555,14 +553,10 @@
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";

View File

@@ -12,6 +12,7 @@ import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.w
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
@@ -33,7 +34,14 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
return;
}
ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
WidgetsBinding.instance.addPostFrameCallback((_) async {
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
await ref.read(backgroundSyncProvider).syncRemote();
if (mounted) {
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
}
});
}
@override
@@ -44,7 +52,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
.toList();
final backupNotifier = ref.read(driftBackupProvider.notifier);
final backgroundManager = ref.read(backgroundSyncProvider);
Future<void> startBackup() async {
final currentUser = Store.tryGet(StoreKey.currentUser);
@@ -52,7 +59,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
return;
}
await backgroundManager.syncRemote();
await backupNotifier.getBackupStatus(currentUser.id);
await backupNotifier.startBackup(currentUser.id);
}
@@ -235,11 +241,13 @@ class _BackupCard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final backupCount = ref.watch(driftBackupProvider.select((p) => p.backupCount));
final syncStatus = ref.watch(syncStatusProvider);
return BackupInfoCard(
title: "backup_controller_page_backup".tr(),
subtitle: "backup_controller_page_backup_sub".tr(),
info: backupCount.toString(),
isLoading: syncStatus.isRemoteSyncing,
);
}
}
@@ -250,10 +258,13 @@ class _RemainderCard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final remainderCount = ref.watch(driftBackupProvider.select((p) => p.remainderCount));
final syncStatus = ref.watch(syncStatusProvider);
return BackupInfoCard(
title: "backup_controller_page_remainder".tr(),
subtitle: "backup_controller_page_remainder_sub".tr(),
info: remainderCount.toString(),
isLoading: syncStatus.isRemoteSyncing,
onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()),
);
}

View File

@@ -101,7 +101,6 @@ class _FixedSegmentRow extends ConsumerWidget {
if (isScrubbing) {
return _buildPlaceholder(context);
}
if (timelineService.hasRange(assetIndex, assetCount)) {
return _buildAssetRow(context, timelineService.getAssets(assetIndex, assetCount), timelineService);
}

View File

@@ -11,6 +11,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/utils/debounce.dart';
import 'package:intl/intl.dart' hide TextDirection;
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
@@ -81,6 +82,8 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
bool _isDragging = false;
List<_Segment> _segments = [];
int _monthCount = 0;
DateTime? _currentScrubberDate;
Debouncer? _scrubberDebouncer;
late AnimationController _thumbAnimationController;
Timer? _fadeOutTimer;
@@ -133,6 +136,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
_thumbAnimationController.dispose();
_labelAnimationController.dispose();
_fadeOutTimer?.cancel();
_scrubberDebouncer?.dispose();
super.dispose();
}
@@ -176,11 +180,25 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
return false;
}
void _onDragStart(DragStartDetails _) {
if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
void _onScrubberDateChanged(DateTime date) {
if (_currentScrubberDate != date) {
// Date changed, immediately set scrubbing to true
_currentScrubberDate = date;
ref.read(timelineStateProvider.notifier).setScrubbing(true);
}
// Initialize debouncer if needed
_scrubberDebouncer ??= Debouncer(interval: const Duration(milliseconds: 50));
// Debounce setting scrubbing to false
_scrubberDebouncer!.run(() {
if (_currentScrubberDate == date) {
ref.read(timelineStateProvider.notifier).setScrubbing(false);
}
});
}
}
void _onDragStart(DragStartDetails _) {
setState(() {
_isDragging = true;
_labelAnimationController.forward();
@@ -206,6 +224,11 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
if (_lastLabel != label) {
ref.read(hapticFeedbackProvider.notifier).selectionClick();
_lastLabel = label;
// Notify timeline state of the new scrubber date position
if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
_onScrubberDateChanged(nearestMonthSegment.date);
}
}
}
@@ -294,12 +317,18 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
}
void _onDragEnd(DragEndDetails _) {
ref.read(timelineStateProvider.notifier).setScrubbing(false);
_labelAnimationController.reverse();
setState(() {
_isDragging = false;
});
ref.read(timelineStateProvider.notifier).setScrubbing(false);
// Reset scrubber tracking when drag ends
_currentScrubberDate = null;
_scrubberDebouncer?.dispose();
_scrubberDebouncer = null;
_resetThumbTimer();
}

View File

@@ -72,8 +72,6 @@ class TimelineState {
}
class TimelineStateNotifier extends Notifier<TimelineState> {
TimelineStateNotifier();
void setScrubbing(bool isScrubbing) {
state = state.copyWith(isScrubbing: isScrubbing);
}

View File

@@ -235,7 +235,9 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
switch (update.status) {
case TaskStatus.complete:
if (update.task.group == kBackupGroup) {
state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1);
if (update.responseStatusCode == 201) {
state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1);
}
}
// Remove the completed task from the upload items

View File

@@ -8,8 +8,17 @@ class BackupInfoCard extends StatelessWidget {
final String title;
final String subtitle;
final String info;
final VoidCallback? onTap;
const BackupInfoCard({super.key, required this.title, required this.subtitle, required this.info, this.onTap});
final bool isLoading;
const BackupInfoCard({
super.key,
required this.title,
required this.subtitle,
required this.info,
this.onTap,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
@@ -38,8 +47,36 @@ class BackupInfoCard extends StatelessWidget {
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(info, style: context.textTheme.titleLarge),
Text("backup_info_card_assets", style: context.textTheme.labelLarge).tr(),
Stack(
children: [
Text(
info,
style: context.textTheme.titleLarge?.copyWith(
color: context.colorScheme.onSurface.withAlpha(isLoading ? 50 : 255),
),
),
if (isLoading)
Positioned.fill(
child: Align(
alignment: Alignment.center,
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: context.colorScheme.onSurface.withAlpha(150),
),
),
),
),
],
),
Text(
"backup_info_card_assets",
style: context.textTheme.labelLarge?.copyWith(
color: context.colorScheme.onSurface.withAlpha(isLoading ? 50 : 255),
),
).tr(),
],
),
),

View File

@@ -27,8 +27,8 @@ ENTRYPOINT ["tini", "--", "/bin/bash", "-c"]
FROM dev AS dev-container-server
RUN apt-get update --allow-releaseinfo-change && \
apt-get install sudo inetutils-ping openjdk-11-jre-headless \
vim nano \
apt-get install sudo inetutils-ping openjdk-21-jre-headless \
vim nano curl \
-y --no-install-recommends --fix-missing
RUN usermod -aG sudo node && \
@@ -44,13 +44,18 @@ FROM dev-container-server AS dev-container-mobile
USER root
# Enable multiarch for arm64 if necessary
RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
dpkg --add-architecture amd64 && \
apt-get update && \
apt-get install -y --no-install-recommends \
qemu-user-static \
libc6:amd64 \
libstdc++6:amd64 \
libgcc1:amd64; \
dpkg --add-architecture amd64 && \
apt-get update && \
apt-get install -y --no-install-recommends \
gnupg \
qemu-user-static \
libc6:amd64 \
libstdc++6:amd64 \
libgcc1:amd64; \
else \
apt-get update && \
apt-get install -y --no-install-recommends \
gnupg; \
fi
# Flutter SDK
@@ -65,11 +70,11 @@ RUN mkdir -p ${FLUTTER_HOME} \
&& curl -C - --output flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz \
&& tar -xf flutter.tar.xz --strip-components=1 -C ${FLUTTER_HOME} \
&& rm flutter.tar.xz \
&& chown -R node ${FLUTTER_HOME}
&& chown -R node ${FLUTTER_HOME} \
&& git config --global --add safe.directory ${FLUTTER_HOME}
RUN apt-get update \
&& wget -qO- https://dcm.dev/pgp-key.public | gpg --dearmor -o /usr/share/keyrings/dcm.gpg \
RUN wget -qO- https://dcm.dev/pgp-key.public | gpg --dearmor -o /usr/share/keyrings/dcm.gpg \
&& echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | tee /etc/apt/sources.list.d/dart_stable.list \
&& apt-get update \
&& apt-get install dcm -y

View File

@@ -57,28 +57,28 @@ export class MediaRepository {
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input);
return { buffer, format: RawExtractedFormat.Jpeg };
} catch (error: any) {
this.logger.debug('Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next', error.message);
this.logger.debug(`Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next: ${error}`);
}
try {
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input);
return { buffer, format: RawExtractedFormat.Jpeg };
} catch (error: any) {
this.logger.debug('Could not extract JPEG buffer from image, trying PreviewJXL next', error.message);
this.logger.debug(`Could not extract JPEG buffer from image, trying PreviewJXL next: ${error}`);
}
try {
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input);
return { buffer, format: RawExtractedFormat.Jxl };
} catch (error: any) {
this.logger.debug('Could not extract PreviewJXL buffer from image, trying PreviewImage next', error.message);
this.logger.debug(`Could not extract PreviewJXL buffer from image, trying PreviewImage next: ${error}`);
}
try {
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input);
return { buffer, format: RawExtractedFormat.Jpeg };
} catch (error: any) {
this.logger.debug('Could not extract preview buffer from image', error.message);
this.logger.debug(`Could not extract preview buffer from image: ${error}`);
return null;
}
}

View File

@@ -103,7 +103,7 @@ export class MetadataRepository {
readTags(path: string): Promise<ImmichTags> {
return this.exiftool.read(path).catch((error) => {
this.logger.warn(`Error reading exif data (${path}): ${error}`, error?.stack);
this.logger.warn(`Error reading exif data (${path}): ${error}\n${error?.stack}`);
return {};
}) as Promise<ImmichTags>;
}

View File

@@ -344,7 +344,7 @@ export class AuthService extends BaseService {
await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [oldPath] } });
}
} catch (error: Error | any) {
this.logger.warn(`Unable to sync oauth profile picture: ${error}`, error?.stack);
this.logger.warn(`Unable to sync oauth profile picture: ${error}\n${error?.stack}`);
}
}

View File

@@ -132,12 +132,12 @@ export class BackupService extends BaseService {
gzip.stdout.pipe(fileStream);
pgdump.on('error', (err) => {
this.logger.error('Backup failed with error', err);
this.logger.error(`Backup failed with error: ${err}`);
reject(err);
});
gzip.on('error', (err) => {
this.logger.error('Gzip failed with error', err);
this.logger.error(`Gzip failed with error: ${err}`);
reject(err);
});
@@ -175,10 +175,10 @@ export class BackupService extends BaseService {
});
await this.storageRepository.rename(backupFilePath, backupFilePath.replace('.tmp', ''));
} catch (error) {
this.logger.error('Database Backup Failure', error);
this.logger.error(`Database Backup Failure: ${error}`);
await this.storageRepository
.unlink(backupFilePath)
.catch((error) => this.logger.error('Failed to delete failed backup file', error));
.catch((error) => this.logger.error(`Failed to delete failed backup file: ${error}`));
throw error;
}

View File

@@ -245,7 +245,7 @@ export class LibraryService extends BaseService {
job.paths.map((path) =>
this.processEntity(path, library.ownerId, job.libraryId)
.then((asset) => assetImports.push(asset))
.catch((error: any) => this.logger.error(`Error processing ${path} for library ${job.libraryId}`, error)),
.catch((error: any) => this.logger.error(`Error processing ${path} for library ${job.libraryId}: ${error}`)),
),
);

View File

@@ -40,7 +40,7 @@ export class MemoryService extends BaseService {
try {
await Promise.all(users.map((owner, i) => this.createOnThisDayMemories(owner.id, usersIds[i], target)));
} catch (error) {
this.logger.error(`Failed to create memories for ${target.toISO()}`, error);
this.logger.error(`Failed to create memories for ${target.toISO()}: ${error}`);
}
// update system metadata even when there is an error to minimize the chance of duplicates
await this.systemMetadataRepository.set(SystemMetadataKey.MemoriesState, {

View File

@@ -338,7 +338,7 @@ export class StorageTemplateService extends BaseService {
return destination;
} catch (error: any) {
this.logger.error(`Unable to get template path for ${filename}`, error);
this.logger.error(`Unable to get template path for ${filename}: ${error}`);
return asset.originalPath;
}
}

View File

@@ -95,7 +95,7 @@ export class VersionService extends BaseService {
this.eventRepository.clientBroadcast('on_new_release', asNotification(metadata));
}
} catch (error: Error | any) {
this.logger.warn(`Unable to run version check: ${error}`, error?.stack);
this.logger.warn(`Unable to run version check: ${error}\n${error?.stack}`);
return JobStatus.Failed;
}

View File

@@ -73,7 +73,7 @@ export const sendFile = async (
// log non-http errors
if (error instanceof HttpException === false) {
logger.error(`Unable to send file: ${error.name}`, error.stack);
logger.error(`Unable to send file: ${error}`, error.stack);
}
res.header('Cache-Control', 'none');