Compare commits

..

1 Commits

Author SHA1 Message Date
Jason Rasmussen 002c82b8fc fix(web): do not notify on patch releases 2025-10-02 12:22:18 -04:00
13 changed files with 85 additions and 106 deletions
+3 -3
View File
@@ -5,7 +5,7 @@ sidebar_position: 65
# One-Click [Cloud Service] # One-Click [Cloud Service]
:::note :::note
This version of Immich is provided via cloud service providers' one-click marketplaces. Hosting costs are set by the cloud service providers. This version of Immich is provided via cloud service provider's one-click marketplaces. Hosting costs are set by the cloud service providers.
Support for these are provided by the individual cloud service providers. Support for these are provided by the individual cloud service providers.
**Please report issues to the corresponding [Github Repository][github].** **Please report issues to the corresponding [Github Repository][github].**
@@ -13,7 +13,7 @@ Support for these are provided by the individual cloud service providers.
## Installation ## Installation
Go to the provider's marketplace and choose Immich, then follow the provided instructions. Simply goto the providers marketplace and choose Immich, then follow the provided instructions.
## One-Click Immich marketplace providers ## One-Click Immich marketplace providers
@@ -29,4 +29,4 @@ https://www.vultr.com/marketplace/apps/immich
For issues, open an issue on the associated [GitHub Repository][github]. For issues, open an issue on the associated [GitHub Repository][github].
[github]: https://github.com/immich-app/immich/ [github]: https://github.com/imagegenius/docker-immich/
@@ -56,8 +56,6 @@ sealed class BaseAsset {
// Overridden in subclasses // Overridden in subclasses
AssetState get storage; AssetState get storage;
String? get localId;
String? get remoteId;
String get heroTag; String get heroTag;
@override @override
@@ -2,12 +2,12 @@ part of 'base_asset.model.dart';
class LocalAsset extends BaseAsset { class LocalAsset extends BaseAsset {
final String id; final String id;
final String? remoteAssetId; final String? remoteId;
final int orientation; final int orientation;
const LocalAsset({ const LocalAsset({
required this.id, required this.id,
String? remoteId, this.remoteId,
required super.name, required super.name,
super.checksum, super.checksum,
required super.type, required super.type,
@@ -19,13 +19,7 @@ class LocalAsset extends BaseAsset {
super.isFavorite = false, super.isFavorite = false,
super.livePhotoVideoId, super.livePhotoVideoId,
this.orientation = 0, this.orientation = 0,
}) : remoteAssetId = remoteId; });
@override
String? get localId => id;
@override
String? get remoteId => remoteAssetId;
@override @override
AssetState get storage => remoteId == null ? AssetState.local : AssetState.merged; AssetState get storage => remoteId == null ? AssetState.local : AssetState.merged;
@@ -5,7 +5,7 @@ enum AssetVisibility { timeline, hidden, archive, locked }
// Model for an asset stored in the server // Model for an asset stored in the server
class RemoteAsset extends BaseAsset { class RemoteAsset extends BaseAsset {
final String id; final String id;
final String? localAssetId; final String? localId;
final String? thumbHash; final String? thumbHash;
final AssetVisibility visibility; final AssetVisibility visibility;
final String ownerId; final String ownerId;
@@ -13,7 +13,7 @@ class RemoteAsset extends BaseAsset {
const RemoteAsset({ const RemoteAsset({
required this.id, required this.id,
String? localId, this.localId,
required super.name, required super.name,
required this.ownerId, required this.ownerId,
required super.checksum, required super.checksum,
@@ -28,13 +28,7 @@ class RemoteAsset extends BaseAsset {
this.visibility = AssetVisibility.timeline, this.visibility = AssetVisibility.timeline,
super.livePhotoVideoId, super.livePhotoVideoId,
this.stackId, this.stackId,
}) : localAssetId = localId; });
@override
String? get localId => localAssetId;
@override
String? get remoteId => id;
@override @override
AssetState get storage => localId == null ? AssetState.remote : AssetState.merged; AssetState get storage => localId == null ? AssetState.remote : AssetState.merged;
@@ -62,7 +62,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
.toDto( .toDto(
assetCount: row.read(assetCount) ?? 0, assetCount: row.read(assetCount) ?? 0,
ownerName: row.read(_db.userEntity.name)!, ownerName: row.read(_db.userEntity.name)!,
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0, isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2,
), ),
) )
.get(); .get();
@@ -107,7 +107,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
.toDto( .toDto(
assetCount: row.read(assetCount) ?? 0, assetCount: row.read(assetCount) ?? 0,
ownerName: row.read(_db.userEntity.name)!, ownerName: row.read(_db.userEntity.name)!,
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0, isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2,
), ),
) )
.getSingleOrNull(); .getSingleOrNull();
@@ -305,9 +305,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
.readTable(_db.remoteAlbumEntity) .readTable(_db.remoteAlbumEntity)
.toDto( .toDto(
ownerName: row.read(_db.userEntity.name)!, ownerName: row.read(_db.userEntity.name)!,
isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 0, isShared: row.read(_db.remoteAlbumUserEntity.userId.count(distinct: true))! > 2,
); );
return album; return album;
}).watchSingleOrNull(); }).watchSingleOrNull();
} }
@@ -8,7 +8,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.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/asset_grid/delete_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
/// This delete action has the following behavior: /// This delete action has the following behavior:
@@ -23,17 +22,7 @@ class DeleteLocalActionButton extends ConsumerWidget {
return; return;
} }
bool? backedUpOnly = await showDialog<bool>( final result = await ref.read(actionProvider.notifier).deleteLocal(source);
context: context,
builder: (BuildContext context) => DeleteLocalOnlyDialog(onDeleteLocal: (_) {}),
);
if (backedUpOnly == null) {
// User cancelled the dialog
return;
}
final result = await ref.read(actionProvider.notifier).deleteLocal(source, backedUpOnly);
ref.read(multiSelectProvider.notifier).reset(); ref.read(multiSelectProvider.notifier).reset();
if (source == ActionSource.viewer) { if (source == ActionSource.viewer) {
@@ -257,15 +257,8 @@ class ActionNotifier extends Notifier<void> {
} }
} }
Future<ActionResult> deleteLocal(ActionSource source, bool backedUpOnly) async { Future<ActionResult> deleteLocal(ActionSource source) async {
final List<String> ids; final ids = _getLocalIdsForSource(source);
if (backedUpOnly) {
final assets = _getAssets(source);
ids = assets.where((asset) => asset.storage == AssetState.merged).map((asset) => asset.localId!).toList();
} else {
ids = _getLocalIdsForSource(source);
}
try { try {
final deletedCount = await _service.deleteLocal(ids); final deletedCount = await _service.deleteLocal(ids);
return ActionResult(count: deletedCount, success: true); return ActionResult(count: deletedCount, success: true);
@@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -22,26 +21,11 @@ final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.
class AssetMediaRepository { class AssetMediaRepository {
final AssetApiRepository _assetApiRepository; final AssetApiRepository _assetApiRepository;
static final Logger _log = Logger("AssetMediaRepository"); static final Logger _log = Logger("AssetMediaRepository");
const AssetMediaRepository(this._assetApiRepository); const AssetMediaRepository(this._assetApiRepository);
Future<List<String>> deleteAll(List<String> ids) async { Future<List<String>> deleteAll(List<String> ids) => PhotoManager.editor.deleteWithIds(ids);
if (CurrentPlatform.isIOS) {
return PhotoManager.editor.deleteWithIds(ids);
} else if (CurrentPlatform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt < 30) {
return PhotoManager.editor.deleteWithIds(ids);
}
return PhotoManager.editor.android.moveToTrash(
// Only the id is needed
ids.map((id) => AssetEntity(id: id, width: 1, height: 1, typeInt: 0)).toList(),
);
}
return [];
}
Future<asset_entity.Asset?> get(String id) async { Future<asset_entity.Asset?> get(String id) async {
final entity = await AssetEntity.fromId(id); final entity = await AssetEntity.fromId(id);
@@ -22,12 +22,12 @@ class DeleteLocalOnlyDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
void onDeleteBackedUpOnly() { void onDeleteBackedUpOnly() {
context.pop(true); context.pop();
onDeleteLocal(true); onDeleteLocal(true);
} }
void onForceDelete() { void onForceDelete() {
context.pop(false); context.pop();
onDeleteLocal(false); onDeleteLocal(false);
} }
@@ -36,44 +36,26 @@ class DeleteLocalOnlyDialog extends StatelessWidget {
title: const Text("delete_dialog_title").tr(), title: const Text("delete_dialog_title").tr(),
content: const Text("delete_dialog_alert_local_non_backed_up").tr(), content: const Text("delete_dialog_alert_local_non_backed_up").tr(),
actions: [ actions: [
SizedBox( TextButton(
width: double.infinity, onPressed: () => context.pop(),
height: 48, child: Text(
child: FilledButton( "cancel",
onPressed: () => context.pop(), style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
style: FilledButton.styleFrom( ).tr(),
backgroundColor: context.colorScheme.surfaceDim,
foregroundColor: context.primaryColor,
),
child: const Text("cancel", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
),
), ),
const SizedBox(height: 8), TextButton(
SizedBox( onPressed: onDeleteBackedUpOnly,
width: double.infinity, child: Text(
height: 48, "delete_local_dialog_ok_backed_up_only",
style: TextStyle(color: context.colorScheme.tertiary, fontWeight: FontWeight.bold),
child: FilledButton( ).tr(),
onPressed: onDeleteBackedUpOnly,
style: FilledButton.styleFrom(
backgroundColor: context.colorScheme.errorContainer,
foregroundColor: context.colorScheme.onErrorContainer,
),
child: const Text(
"delete_local_dialog_ok_backed_up_only",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
), ),
const SizedBox(height: 8), TextButton(
SizedBox( onPressed: onForceDelete,
width: double.infinity, child: Text(
height: 48, "delete_local_dialog_ok_force",
child: FilledButton( style: TextStyle(color: Colors.red[400], fontWeight: FontWeight.bold),
onPressed: onForceDelete, ).tr(),
style: FilledButton.styleFrom(backgroundColor: Colors.red[400], foregroundColor: Colors.white),
child: const Text("delete_local_dialog_ok_force", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
),
), ),
], ],
); );
+1 -1
View File
@@ -25,7 +25,7 @@ export interface Events {
on_person_thumbnail: (personId: string) => void; on_person_thumbnail: (personId: string) => void;
on_server_version: (serverVersion: ServerVersionResponseDto) => void; on_server_version: (serverVersion: ServerVersionResponseDto) => void;
on_config_update: () => void; on_config_update: () => void;
on_new_release: (newRelase: ReleaseEvent) => void; on_new_release: (newRelease: ReleaseEvent) => void;
on_session_delete: (sessionId: string) => void; on_session_delete: (sessionId: string) => void;
on_notification: (notification: NotificationDto) => void; on_notification: (notification: NotificationDto) => void;
} }
+25
View File
@@ -0,0 +1,25 @@
import { getReleaseType } from '$lib/utils';
describe('utils', () => {
describe(getReleaseType.name, () => {
it('should return "major" for major version changes', () => {
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 2, minor: 0, patch: 0 })).toBe('major');
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 3, minor: 2, patch: 1 })).toBe('major');
});
it('should return "minor" for minor version changes', () => {
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 1, patch: 0 })).toBe('minor');
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 2, patch: 1 })).toBe('minor');
});
it('should return "patch" for patch version changes', () => {
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 1 })).toBe('patch');
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 5 })).toBe('patch');
});
it('should return "none" for matching versions', () => {
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 0 })).toBe('none');
expect(getReleaseType({ major: 1, minor: 2, patch: 3 }, { major: 1, minor: 2, patch: 3 })).toBe('none');
});
});
});
+20
View File
@@ -21,6 +21,7 @@ import {
unlinkOAuthAccount, unlinkOAuthAccount,
type MemoryResponseDto, type MemoryResponseDto,
type PersonResponseDto, type PersonResponseDto,
type ServerVersionResponseDto,
type SharedLinkResponseDto, type SharedLinkResponseDto,
type UserResponseDto, type UserResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
@@ -385,3 +386,22 @@ export function createDateFormatter(localeCode: string | undefined): DateFormatt
}, },
}; };
} }
export const getReleaseType = (
current: ServerVersionResponseDto,
newVersion: ServerVersionResponseDto,
): 'major' | 'minor' | 'patch' | 'none' => {
if (current.major !== newVersion.major) {
return 'major';
}
if (current.minor !== newVersion.minor) {
return 'minor';
}
if (current.patch !== newVersion.patch) {
return 'patch';
}
return 'none';
};
+3 -2
View File
@@ -18,7 +18,7 @@
websocketStore, websocketStore,
type ReleaseEvent, type ReleaseEvent,
} from '$lib/stores/websocket'; } from '$lib/stores/websocket';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard, getReleaseType } from '$lib/utils';
import { isAssetViewerRoute } from '$lib/utils/navigation'; import { isAssetViewerRoute } from '$lib/utils/navigation';
import type { ServerVersionResponseDto } from '@immich/sdk'; import type { ServerVersionResponseDto } from '@immich/sdk';
import { modalManager, setTranslations } from '@immich/ui'; import { modalManager, setTranslations } from '@immich/ui';
@@ -85,8 +85,9 @@
const releaseVersion = semverToName(release.releaseVersion); const releaseVersion = semverToName(release.releaseVersion);
const serverVersion = semverToName(release.serverVersion); const serverVersion = semverToName(release.serverVersion);
const type = getReleaseType(release.serverVersion, release.releaseVersion);
if (localStorage.getItem('appVersion') === releaseVersion) { if (type === 'none' || type === 'patch' || localStorage.getItem('appVersion') === releaseVersion) {
return; return;
} }