refactor: server capabilities (#30663)

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong 2026-08-14 18:23:12 +05:30 committed by GitHub
parent 447cc40a50
commit 652ef8a427
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 41 additions and 31 deletions

View file

@ -0,0 +1,23 @@
import 'package:immich_mobile/utils/semver.dart';
enum ServerCapability {
// Feature Support
cloudIdMetadata(SemVer(major: 2, minor: 4, patch: 0)),
bulkCloudIdMetadata(SemVer(major: 2, minor: 5, patch: 0)),
assetEdits(SemVer(major: 2, minor: 6, patch: 0)),
assetFacesV2(SemVer(major: 2, minor: 6, patch: 0)),
syncV2(SemVer(major: 3, minor: 0, patch: 0)),
assetOcr(SemVer(major: 3, minor: 0, patch: 0)),
// Migrations
assetPayloadChange20260128(SemVer(major: 2, minor: 5, patch: 0)),
assetPayloadChange20260597(SemVer(major: 2, minor: 7, patch: 6));
const ServerCapability(this.minVersion);
final SemVer minVersion;
}
extension ServerCapabilitySupport on SemVer {
bool supports(ServerCapability capability) => this >= capability.minVersion;
}

View file

@ -4,6 +4,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/server_capability.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
@ -122,7 +123,7 @@ class SyncStreamService {
} }
if (!migrations.contains(SyncMigrationTask.v20260128_ResetAssetV1.name) && if (!migrations.contains(SyncMigrationTask.v20260128_ResetAssetV1.name) &&
semVer >= const SemVer(major: 2, minor: 5, patch: 0)) { semVer.supports(.assetPayloadChange20260128)) {
_logger.info("Running pre-sync task: v20260128_ResetAssetV1"); _logger.info("Running pre-sync task: v20260128_ResetAssetV1");
await _syncApiRepository.deleteSyncAck([ await _syncApiRepository.deleteSyncAck([
SyncEntityType.assetV1, SyncEntityType.assetV1,
@ -139,7 +140,7 @@ class SyncStreamService {
} }
if (!migrations.contains(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name) && if (!migrations.contains(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name) &&
semVer > const SemVer(major: 2, minor: 7, patch: 5)) { semVer.supports(.assetPayloadChange20260597)) {
_logger.info("Running pre-sync task: v20260597_ResetAssetV1AssetV2"); _logger.info("Running pre-sync task: v20260597_ResetAssetV1AssetV2");
await _syncApiRepository.deleteSyncAck([SyncEntityType.assetV1, SyncEntityType.assetV2]); await _syncApiRepository.deleteSyncAck([SyncEntityType.assetV1, SyncEntityType.assetV2]);
migrations.add(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name); migrations.add(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name);

View file

@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart'; import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/server_capability.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
@ -31,12 +32,12 @@ Future<void> syncCloudIds(ProviderContainer ref) async {
await _populateCloudIds(db); await _populateCloudIds(db);
final serverInfo = await ref.read(serverInfoProvider.notifier).getServerInfo(); final serverInfo = await ref.read(serverInfoProvider.notifier).getServerInfo();
final canUpdateMetadata = serverInfo.serverVersion.isAtLeast(major: 2, minor: 4); final canUpdateMetadata = serverInfo.serverVersion.supports(.cloudIdMetadata);
if (!canUpdateMetadata) { if (!canUpdateMetadata) {
logger.fine('Server version does not support asset metadata updates. Skipping cloudId migration.'); logger.fine('Server version does not support asset metadata updates. Skipping cloudId migration.');
return; return;
} }
final canBulkUpdateMetadata = serverInfo.serverVersion.isAtLeast(major: 2, minor: 5); final canBulkUpdateMetadata = serverInfo.serverVersion.supports(.bulkCloudIdMetadata);
// Wait for remote sync to complete, so we have up-to-date asset metadata entries // Wait for remote sync to complete, so we have up-to-date asset metadata entries
try { try {

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/server_capability.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
@ -44,25 +45,16 @@ class SyncApiRepository {
types: [ types: [
SyncRequestType.authUsersV1, SyncRequestType.authUsersV1,
SyncRequestType.usersV1, SyncRequestType.usersV1,
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0) serverVersion.supports(.syncV2) ? SyncRequestType.assetsV2 : SyncRequestType.assetsV1,
? SyncRequestType.assetsV2
: SyncRequestType.assetsV1,
SyncRequestType.assetExifsV1, SyncRequestType.assetExifsV1,
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetEditsV1, if (serverVersion.supports(.assetEdits)) SyncRequestType.assetEditsV1,
SyncRequestType.assetMetadataV1, SyncRequestType.assetMetadataV1,
SyncRequestType.partnersV1, SyncRequestType.partnersV1,
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0) serverVersion.supports(.syncV2) ? SyncRequestType.partnerAssetsV2 : SyncRequestType.partnerAssetsV1,
? SyncRequestType.partnerAssetsV2
: SyncRequestType.partnerAssetsV1,
SyncRequestType.partnerAssetExifsV1, SyncRequestType.partnerAssetExifsV1,
if (serverVersion < const SemVer(major: 3, minor: 0, patch: 0)) serverVersion.supports(.syncV2) ? SyncRequestType.albumsV2 : SyncRequestType.albumsV1,
SyncRequestType.albumsV1
else
SyncRequestType.albumsV2,
SyncRequestType.albumUsersV1, SyncRequestType.albumUsersV1,
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0) serverVersion.supports(.syncV2) ? SyncRequestType.albumAssetsV2 : SyncRequestType.albumAssetsV1,
? SyncRequestType.albumAssetsV2
: SyncRequestType.albumAssetsV1,
SyncRequestType.albumAssetExifsV1, SyncRequestType.albumAssetExifsV1,
SyncRequestType.albumToAssetsV1, SyncRequestType.albumToAssetsV1,
SyncRequestType.memoriesV1, SyncRequestType.memoriesV1,
@ -71,10 +63,8 @@ class SyncApiRepository {
SyncRequestType.partnerStacksV1, SyncRequestType.partnerStacksV1,
SyncRequestType.userMetadataV1, SyncRequestType.userMetadataV1,
SyncRequestType.peopleV1, SyncRequestType.peopleV1,
serverVersion >= const SemVer(major: 2, minor: 6, patch: 0) serverVersion.supports(.assetFacesV2) ? SyncRequestType.assetFacesV2 : SyncRequestType.assetFacesV1,
? SyncRequestType.assetFacesV2 if (serverVersion.supports(.assetOcr)) SyncRequestType.assetOcrV1,
: SyncRequestType.assetFacesV1,
if (serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)) SyncRequestType.assetOcrV1,
], ],
).toJson(), ).toJson(),
); );

View file

@ -6,8 +6,4 @@ class ServerVersion extends SemVer {
ServerVersion.fromDto(ServerVersionResponseDto dto) ServerVersion.fromDto(ServerVersionResponseDto dto)
: super(major: dto.major, minor: dto.minor, patch: dto.patch_, prerelease: dto.prerelease); : super(major: dto.major, minor: dto.minor, patch: dto.patch_, prerelease: dto.prerelease);
bool isAtLeast({int major = 0, int minor = 0, int patch = 0, int? prerelease}) {
return this >= SemVer(major: major, minor: minor, patch: patch, prerelease: prerelease);
}
} }

View file

@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart';
import 'package:immich_mobile/domain/models/server_capability.model.dart';
import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/action.dart';
import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart';
@ -16,12 +17,9 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/error_handler.dart'; import 'package:immich_mobile/utils/error_handler.dart';
import 'package:immich_mobile/utils/semver.dart';
const _minimumServerVersion = SemVer(major: 2, minor: 6, patch: 0);
final _stateProvider = Provider.family.autoDispose<RemoteAsset?, ActionSource>((ref, source) { final _stateProvider = Provider.family.autoDispose<RemoteAsset?, ActionSource>((ref, source) {
final isSupported = ref.watch(serverInfoProvider.select((state) => state.serverVersion >= _minimumServerVersion)); final isSupported = ref.watch(serverInfoProvider.select((state) => state.serverVersion.supports(.assetEdits)));
if (!isSupported) { if (!isSupported) {
return null; return null;
} }

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/server_capability.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/generated/translations.g.dart';
@ -150,7 +151,7 @@ class SyncStatusAndActions extends HookConsumerWidget {
unawaited(ref.read(backgroundSyncProvider).syncRemote()); unawaited(ref.read(backgroundSyncProvider).syncRemote());
}, },
), ),
if (CurrentPlatform.isIOS && serverVersion.isAtLeast(major: 2, minor: 5)) if (CurrentPlatform.isIOS && serverVersion.supports(.cloudIdMetadata))
SettingListTile( SettingListTile(
title: 'Sync Cloud Ids', title: 'Sync Cloud Ids',
leading: const Icon(Icons.cloud_circle_rounded), leading: const Icon(Icons.cloud_circle_rounded),