mirror of
https://github.com/immich-app/immich.git
synced 2025-10-27 08:42:33 -04:00
feat: improved update messaging on app bar server info (#22938)
* feat: improved update messaging on app bar server info * chore: message improvements * chore: failed to fetch version error message * feat: open latest release when tapping "Update" on server out of date message * fix: text alignment states * chore: code review updates * Apply suggestion from @alextran1502 Co-authored-by: Alex <alex.tran1502@gmail.com> * Apply suggestion from @alextran1502 Co-authored-by: Alex <alex.tran1502@gmail.com> * chore: lots of rework of the version checking code to be cleaner Added a semver utility class to simplify comparisons, broke the update notification logic into own widget, reworked view construction and colors. * fix: show warnign without having to tap on app bar icon * chore: colors --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
6f31f27218
commit
23a34bee6f
@ -474,6 +474,7 @@
|
|||||||
"app_bar_signout_dialog_title": "Sign out",
|
"app_bar_signout_dialog_title": "Sign out",
|
||||||
"app_download_links": "App Download Links",
|
"app_download_links": "App Download Links",
|
||||||
"app_settings": "App Settings",
|
"app_settings": "App Settings",
|
||||||
|
"app_update_available": "App update is available",
|
||||||
"appears_in": "Appears in",
|
"appears_in": "Appears in",
|
||||||
"apply_count": "Apply ({count, number})",
|
"apply_count": "Apply ({count, number})",
|
||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
@ -706,7 +707,6 @@
|
|||||||
"comments_and_likes": "Comments & likes",
|
"comments_and_likes": "Comments & likes",
|
||||||
"comments_are_disabled": "Comments are disabled",
|
"comments_are_disabled": "Comments are disabled",
|
||||||
"common_create_new_album": "Create new album",
|
"common_create_new_album": "Create new album",
|
||||||
"common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
|
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"confirm_admin_password": "Confirm Admin Password",
|
"confirm_admin_password": "Confirm Admin Password",
|
||||||
@ -1555,13 +1555,9 @@
|
|||||||
"privacy": "Privacy",
|
"privacy": "Privacy",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"profile_drawer_app_logs": "Logs",
|
"profile_drawer_app_logs": "Logs",
|
||||||
"profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
|
|
||||||
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
|
|
||||||
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
||||||
"profile_drawer_github": "GitHub",
|
"profile_drawer_github": "GitHub",
|
||||||
"profile_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.",
|
"profile_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.",
|
||||||
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
|
|
||||||
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
|
|
||||||
"profile_image_of_user": "Profile image of {user}",
|
"profile_image_of_user": "Profile image of {user}",
|
||||||
"profile_picture_set": "Profile picture set.",
|
"profile_picture_set": "Profile picture set.",
|
||||||
"public_album": "Public album",
|
"public_album": "Public album",
|
||||||
@ -1790,6 +1786,7 @@
|
|||||||
"server_online": "Server Online",
|
"server_online": "Server Online",
|
||||||
"server_privacy": "Server Privacy",
|
"server_privacy": "Server Privacy",
|
||||||
"server_stats": "Server Stats",
|
"server_stats": "Server Stats",
|
||||||
|
"server_update_available": "Server update is available",
|
||||||
"server_version": "Server Version",
|
"server_version": "Server Version",
|
||||||
"set": "Set",
|
"set": "Set",
|
||||||
"set_as_album_cover": "Set as album cover",
|
"set_as_album_cover": "Set as album cover",
|
||||||
@ -2031,6 +2028,7 @@
|
|||||||
"troubleshoot": "Troubleshoot",
|
"troubleshoot": "Troubleshoot",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"unable_to_change_pin_code": "Unable to change PIN code",
|
"unable_to_change_pin_code": "Unable to change PIN code",
|
||||||
|
"unable_to_check_version": "Unable to check app or server version",
|
||||||
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
||||||
"unarchive": "Unarchive",
|
"unarchive": "Unarchive",
|
||||||
"unarchive_action_prompt": "{count} removed from Archive",
|
"unarchive_action_prompt": "{count} removed from Archive",
|
||||||
|
|||||||
@ -49,3 +49,7 @@ const double kUploadStatusFailed = -1.0;
|
|||||||
const double kUploadStatusCanceled = -2.0;
|
const double kUploadStatusCanceled = -2.0;
|
||||||
|
|
||||||
const int kMinMonthsToEnableScrubberSnap = 12;
|
const int kMinMonthsToEnableScrubberSnap = 12;
|
||||||
|
|
||||||
|
const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id6449244941";
|
||||||
|
const String kImmichPlayStoreLink = "https://play.google.com/store/apps/details?id=app.alextran.immich";
|
||||||
|
const String kImmichLatestRelease = "https://github.com/immich-app/immich/releases/latest";
|
||||||
|
|||||||
@ -1,17 +1,30 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_config.model.dart';
|
import 'package:immich_mobile/models/server_info/server_config.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_features.model.dart';
|
import 'package:immich_mobile/models/server_info/server_features.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||||
|
|
||||||
|
enum VersionStatus {
|
||||||
|
upToDate,
|
||||||
|
clientOutOfDate,
|
||||||
|
serverOutOfDate,
|
||||||
|
error;
|
||||||
|
|
||||||
|
String get message => switch (this) {
|
||||||
|
VersionStatus.upToDate => "",
|
||||||
|
VersionStatus.clientOutOfDate => "app_update_available".tr(),
|
||||||
|
VersionStatus.serverOutOfDate => "server_update_available".tr(),
|
||||||
|
VersionStatus.error => "unable_to_check_version".tr(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class ServerInfo {
|
class ServerInfo {
|
||||||
final ServerVersion serverVersion;
|
final ServerVersion serverVersion;
|
||||||
final ServerVersion latestVersion;
|
final ServerVersion latestVersion;
|
||||||
final ServerFeatures serverFeatures;
|
final ServerFeatures serverFeatures;
|
||||||
final ServerConfig serverConfig;
|
final ServerConfig serverConfig;
|
||||||
final ServerDiskInfo serverDiskInfo;
|
final ServerDiskInfo serverDiskInfo;
|
||||||
final bool isVersionMismatch;
|
final VersionStatus versionStatus;
|
||||||
final bool isNewReleaseAvailable;
|
|
||||||
final String versionMismatchErrorMessage;
|
|
||||||
|
|
||||||
const ServerInfo({
|
const ServerInfo({
|
||||||
required this.serverVersion,
|
required this.serverVersion,
|
||||||
@ -19,9 +32,7 @@ class ServerInfo {
|
|||||||
required this.serverFeatures,
|
required this.serverFeatures,
|
||||||
required this.serverConfig,
|
required this.serverConfig,
|
||||||
required this.serverDiskInfo,
|
required this.serverDiskInfo,
|
||||||
required this.isVersionMismatch,
|
required this.versionStatus,
|
||||||
required this.isNewReleaseAvailable,
|
|
||||||
required this.versionMismatchErrorMessage,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ServerInfo copyWith({
|
ServerInfo copyWith({
|
||||||
@ -30,9 +41,7 @@ class ServerInfo {
|
|||||||
ServerFeatures? serverFeatures,
|
ServerFeatures? serverFeatures,
|
||||||
ServerConfig? serverConfig,
|
ServerConfig? serverConfig,
|
||||||
ServerDiskInfo? serverDiskInfo,
|
ServerDiskInfo? serverDiskInfo,
|
||||||
bool? isVersionMismatch,
|
VersionStatus? versionStatus,
|
||||||
bool? isNewReleaseAvailable,
|
|
||||||
String? versionMismatchErrorMessage,
|
|
||||||
}) {
|
}) {
|
||||||
return ServerInfo(
|
return ServerInfo(
|
||||||
serverVersion: serverVersion ?? this.serverVersion,
|
serverVersion: serverVersion ?? this.serverVersion,
|
||||||
@ -40,15 +49,13 @@ class ServerInfo {
|
|||||||
serverFeatures: serverFeatures ?? this.serverFeatures,
|
serverFeatures: serverFeatures ?? this.serverFeatures,
|
||||||
serverConfig: serverConfig ?? this.serverConfig,
|
serverConfig: serverConfig ?? this.serverConfig,
|
||||||
serverDiskInfo: serverDiskInfo ?? this.serverDiskInfo,
|
serverDiskInfo: serverDiskInfo ?? this.serverDiskInfo,
|
||||||
isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch,
|
versionStatus: versionStatus ?? this.versionStatus,
|
||||||
isNewReleaseAvailable: isNewReleaseAvailable ?? this.isNewReleaseAvailable,
|
|
||||||
versionMismatchErrorMessage: versionMismatchErrorMessage ?? this.versionMismatchErrorMessage,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ServerInfo(serverVersion: $serverVersion, latestVersion: $latestVersion, serverFeatures: $serverFeatures, serverConfig: $serverConfig, serverDiskInfo: $serverDiskInfo, isVersionMismatch: $isVersionMismatch, isNewReleaseAvailable: $isNewReleaseAvailable, versionMismatchErrorMessage: $versionMismatchErrorMessage)';
|
return 'ServerInfo(serverVersion: $serverVersion, latestVersion: $latestVersion, serverFeatures: $serverFeatures, serverConfig: $serverConfig, serverDiskInfo: $serverDiskInfo, versionStatus: $versionStatus)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -61,9 +68,7 @@ class ServerInfo {
|
|||||||
other.serverFeatures == serverFeatures &&
|
other.serverFeatures == serverFeatures &&
|
||||||
other.serverConfig == serverConfig &&
|
other.serverConfig == serverConfig &&
|
||||||
other.serverDiskInfo == serverDiskInfo &&
|
other.serverDiskInfo == serverDiskInfo &&
|
||||||
other.isVersionMismatch == isVersionMismatch &&
|
other.versionStatus == versionStatus;
|
||||||
other.isNewReleaseAvailable == isNewReleaseAvailable &&
|
|
||||||
other.versionMismatchErrorMessage == versionMismatchErrorMessage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -73,8 +78,6 @@ class ServerInfo {
|
|||||||
serverFeatures.hashCode ^
|
serverFeatures.hashCode ^
|
||||||
serverConfig.hashCode ^
|
serverConfig.hashCode ^
|
||||||
serverDiskInfo.hashCode ^
|
serverDiskInfo.hashCode ^
|
||||||
isVersionMismatch.hashCode ^
|
versionStatus.hashCode;
|
||||||
isNewReleaseAvailable.hashCode ^
|
|
||||||
versionMismatchErrorMessage.hashCode;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +1,13 @@
|
|||||||
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ServerVersion {
|
class ServerVersion extends SemVer {
|
||||||
final int major;
|
const ServerVersion({required super.major, required super.minor, required super.patch});
|
||||||
final int minor;
|
|
||||||
final int patch;
|
|
||||||
|
|
||||||
const ServerVersion({required this.major, required this.minor, required this.patch});
|
|
||||||
|
|
||||||
ServerVersion copyWith({int? major, int? minor, int? patch}) {
|
|
||||||
return ServerVersion(major: major ?? this.major, minor: minor ?? this.minor, patch: patch ?? this.patch);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ServerVersion(major: $major, minor: $minor, patch: $patch)';
|
return 'ServerVersion(major: $major, minor: $minor, patch: $patch)';
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerVersion.fromDto(ServerVersionResponseDto dto) : major = dto.major, minor = dto.minor, patch = dto.patch_;
|
ServerVersion.fromDto(ServerVersionResponseDto dto) : super(major: dto.major, minor: dto.minor, patch: dto.patch_);
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is ServerVersion && other.major == major && other.minor == minor && other.patch == patch;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return major.hashCode ^ minor.hashCode ^ patch.hashCode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_config.model.dart';
|
import 'package:immich_mobile/models/server_info/server_config.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_features.model.dart';
|
import 'package:immich_mobile/models/server_info/server_features.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||||
import 'package:immich_mobile/services/server_info.service.dart';
|
import 'package:immich_mobile/services/server_info.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
@ -24,9 +25,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
|||||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
),
|
),
|
||||||
serverDiskInfo: ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0),
|
serverDiskInfo: ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0),
|
||||||
isVersionMismatch: false,
|
versionStatus: VersionStatus.upToDate,
|
||||||
isNewReleaseAvailable: false,
|
|
||||||
versionMismatchErrorMessage: "",
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -43,73 +42,42 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
|||||||
try {
|
try {
|
||||||
final serverVersion = await _serverInfoService.getServerVersion();
|
final serverVersion = await _serverInfoService.getServerVersion();
|
||||||
|
|
||||||
|
// using isClientOutOfDate since that will show to users reguardless of if they are an admin
|
||||||
if (serverVersion == null) {
|
if (serverVersion == null) {
|
||||||
state = state.copyWith(isVersionMismatch: true, versionMismatchErrorMessage: "common_server_error".tr());
|
state = state.copyWith(versionStatus: VersionStatus.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _checkServerVersionMismatch(serverVersion);
|
await _checkServerVersionMismatch(serverVersion);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_log.severe("Failed to get server version", e, stackTrace);
|
_log.severe("Failed to get server version", e, stackTrace);
|
||||||
state = state.copyWith(isVersionMismatch: true);
|
state = state.copyWith(versionStatus: VersionStatus.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkServerVersionMismatch(ServerVersion serverVersion) async {
|
_checkServerVersionMismatch(ServerVersion serverVersion, {ServerVersion? latestVersion}) async {
|
||||||
state = state.copyWith(serverVersion: serverVersion);
|
state = state.copyWith(serverVersion: serverVersion, latestVersion: latestVersion);
|
||||||
|
|
||||||
var packageInfo = await PackageInfo.fromPlatform();
|
var packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
SemVer clientVersion = SemVer.fromString(packageInfo.version);
|
||||||
|
|
||||||
Map<String, int> appVersion = _getDetailVersion(packageInfo.version);
|
if (serverVersion < clientVersion || (latestVersion != null && serverVersion < latestVersion)) {
|
||||||
|
state = state.copyWith(versionStatus: VersionStatus.serverOutOfDate);
|
||||||
if (appVersion["major"]! > serverVersion.major) {
|
|
||||||
state = state.copyWith(
|
|
||||||
isVersionMismatch: true,
|
|
||||||
versionMismatchErrorMessage: "profile_drawer_server_out_of_date_major".tr(),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appVersion["major"]! < serverVersion.major) {
|
if (clientVersion < serverVersion) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(versionStatus: VersionStatus.clientOutOfDate);
|
||||||
isVersionMismatch: true,
|
|
||||||
versionMismatchErrorMessage: "profile_drawer_client_out_of_date_major".tr(),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appVersion["minor"]! > serverVersion.minor) {
|
state = state.copyWith(versionStatus: VersionStatus.upToDate);
|
||||||
state = state.copyWith(
|
|
||||||
isVersionMismatch: true,
|
|
||||||
versionMismatchErrorMessage: "profile_drawer_server_out_of_date_minor".tr(),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appVersion["minor"]! < serverVersion.minor) {
|
|
||||||
state = state.copyWith(
|
|
||||||
isVersionMismatch: true,
|
|
||||||
versionMismatchErrorMessage: "profile_drawer_client_out_of_date_minor".tr(),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.copyWith(isVersionMismatch: false, versionMismatchErrorMessage: "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewRelease(ServerVersion serverVersion, ServerVersion latestVersion) {
|
handleReleaseInfo(ServerVersion serverVersion, ServerVersion latestVersion) {
|
||||||
// Update local server version
|
// Update local server version
|
||||||
_checkServerVersionMismatch(serverVersion);
|
_checkServerVersionMismatch(serverVersion, latestVersion: latestVersion);
|
||||||
|
|
||||||
final majorEqual = latestVersion.major == serverVersion.major;
|
|
||||||
final minorEqual = majorEqual && latestVersion.minor == serverVersion.minor;
|
|
||||||
final newVersionAvailable =
|
|
||||||
latestVersion.major > serverVersion.major ||
|
|
||||||
(majorEqual && latestVersion.minor > serverVersion.minor) ||
|
|
||||||
(minorEqual && latestVersion.patch > serverVersion.patch);
|
|
||||||
|
|
||||||
state = state.copyWith(latestVersion: latestVersion, isNewReleaseAvailable: newVersionAvailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getServerFeatures() async {
|
getServerFeatures() async {
|
||||||
@ -127,18 +95,15 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
|||||||
}
|
}
|
||||||
state = state.copyWith(serverConfig: serverConfig);
|
state = state.copyWith(serverConfig: serverConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, int> _getDetailVersion(String version) {
|
|
||||||
List<String> detail = version.split(".");
|
|
||||||
|
|
||||||
var major = detail[0];
|
|
||||||
var minor = detail[1];
|
|
||||||
var patch = detail[2];
|
|
||||||
|
|
||||||
return {"major": int.parse(major), "minor": int.parse(minor), "patch": int.parse(patch.replaceAll("-DEBUG", ""))};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final serverInfoProvider = StateNotifierProvider<ServerInfoNotifier, ServerInfo>((ref) {
|
final serverInfoProvider = StateNotifierProvider<ServerInfoNotifier, ServerInfo>((ref) {
|
||||||
return ServerInfoNotifier(ref.read(serverInfoServiceProvider));
|
return ServerInfoNotifier(ref.read(serverInfoServiceProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final versionWarningPresentProvider = Provider.family<bool, UserDto?>((ref, user) {
|
||||||
|
final serverInfo = ref.watch(serverInfoProvider);
|
||||||
|
return serverInfo.versionStatus == VersionStatus.clientOutOfDate ||
|
||||||
|
serverInfo.versionStatus == VersionStatus.error ||
|
||||||
|
((user?.isAdmin ?? false) && serverInfo.versionStatus == VersionStatus.serverOutOfDate);
|
||||||
|
});
|
||||||
|
|||||||
@ -15,10 +15,10 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
|
|||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/services/sync.service.dart';
|
import 'package:immich_mobile/services/sync.service.dart';
|
||||||
import 'package:immich_mobile/utils/debounce.dart';
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:socket_io_client/socket_io_client.dart';
|
import 'package:socket_io_client/socket_io_client.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
|
||||||
|
|
||||||
enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash }
|
enum PendingAction { assetDelete, assetUploaded, assetHidden, assetTrash }
|
||||||
|
|
||||||
@ -307,7 +307,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
|
|
||||||
final serverVersion = ServerVersion.fromDto(serverVersionDto);
|
final serverVersion = ServerVersion.fromDto(serverVersionDto);
|
||||||
final releaseVersion = ServerVersion.fromDto(releaseVersionDto);
|
final releaseVersion = ServerVersion.fromDto(releaseVersionDto);
|
||||||
_ref.read(serverInfoProvider.notifier).handleNewRelease(serverVersion, releaseVersion);
|
_ref.read(serverInfoProvider.notifier).handleReleaseInfo(serverVersion, releaseVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSyncAssetUploadReady(dynamic data) {
|
void _handleSyncAssetUploadReady(dynamic data) {
|
||||||
|
|||||||
59
mobile/lib/utils/semver.dart
Normal file
59
mobile/lib/utils/semver.dart
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
class SemVer {
|
||||||
|
final int major;
|
||||||
|
final int minor;
|
||||||
|
final int patch;
|
||||||
|
|
||||||
|
const SemVer({required this.major, required this.minor, required this.patch});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '$major.$minor.$patch';
|
||||||
|
}
|
||||||
|
|
||||||
|
SemVer copyWith({int? major, int? minor, int? patch}) {
|
||||||
|
return SemVer(major: major ?? this.major, minor: minor ?? this.minor, patch: patch ?? this.patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory SemVer.fromString(String version) {
|
||||||
|
final parts = version.split('.');
|
||||||
|
return SemVer(major: int.parse(parts[0]), minor: int.parse(parts[1]), patch: int.parse(parts[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator >(SemVer other) {
|
||||||
|
if (major != other.major) {
|
||||||
|
return major > other.major;
|
||||||
|
}
|
||||||
|
if (minor != other.minor) {
|
||||||
|
return minor > other.minor;
|
||||||
|
}
|
||||||
|
return patch > other.patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator <(SemVer other) {
|
||||||
|
if (major != other.major) {
|
||||||
|
return major < other.major;
|
||||||
|
}
|
||||||
|
if (minor != other.minor) {
|
||||||
|
return minor < other.minor;
|
||||||
|
}
|
||||||
|
return patch < other.patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator >=(SemVer other) {
|
||||||
|
return this > other || this == other;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator <=(SemVer other) {
|
||||||
|
return this < other || this == other;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is SemVer && other.major == major && other.minor == minor && other.patch == patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode;
|
||||||
|
}
|
||||||
@ -7,7 +7,9 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
|
|||||||
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||||
import 'package:immich_mobile/providers/locale_provider.dart';
|
import 'package:immich_mobile/providers/locale_provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/utils/url_helper.dart';
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/app_bar_dialog/server_update_notification.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
class AppBarServerInfo extends HookConsumerWidget {
|
class AppBarServerInfo extends HookConsumerWidget {
|
||||||
@ -17,6 +19,8 @@ class AppBarServerInfo extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
ref.watch(localeProvider);
|
ref.watch(localeProvider);
|
||||||
ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
final bool showVersionWarning = ref.watch(versionWarningPresentProvider(user));
|
||||||
|
|
||||||
final appInfo = useState({});
|
final appInfo = useState({});
|
||||||
const titleFontSize = 12.0;
|
const titleFontSize = 12.0;
|
||||||
@ -45,17 +49,10 @@ class AppBarServerInfo extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
if (showVersionWarning) ...[
|
||||||
padding: const EdgeInsets.all(8.0),
|
const Padding(padding: EdgeInsets.symmetric(horizontal: 8.0), child: ServerUpdateNotification()),
|
||||||
child: Text(
|
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
|
||||||
serverInfoState.isVersionMismatch
|
],
|
||||||
? serverInfoState.versionMismatchErrorMessage
|
|
||||||
: "profile_drawer_client_server_up_to_date".tr(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 11, color: context.primaryColor, fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@ -182,7 +179,7 @@ class AppBarServerInfo extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(left: 10.0),
|
padding: const EdgeInsets.only(left: 10.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (serverInfoState.isNewReleaseAvailable)
|
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(right: 5.0),
|
padding: EdgeInsets.only(right: 5.0),
|
||||||
child: Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12),
|
child: Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12),
|
||||||
|
|||||||
@ -0,0 +1,83 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
class ServerUpdateNotification extends HookConsumerWidget {
|
||||||
|
const ServerUpdateNotification({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
|
||||||
|
Color errorColor = const Color.fromARGB(85, 253, 97, 83);
|
||||||
|
Color infoColor = context.isDarkTheme ? context.primaryColor.withAlpha(55) : context.primaryColor.withAlpha(25);
|
||||||
|
void openUpdateLink() {
|
||||||
|
String url;
|
||||||
|
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate) {
|
||||||
|
url = kImmichLatestRelease;
|
||||||
|
} else {
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
url = kImmichAppStoreLink;
|
||||||
|
} else if (Platform.isAndroid) {
|
||||||
|
url = kImmichPlayStoreLink;
|
||||||
|
} else {
|
||||||
|
// Fallback to latest release for other/unknown platforms
|
||||||
|
url = kImmichLatestRelease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
launchUrlString(url, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: serverInfoState.versionStatus == VersionStatus.error ? errorColor : infoColor,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
border: Border.all(
|
||||||
|
color: serverInfoState.versionStatus == VersionStatus.error
|
||||||
|
? errorColor
|
||||||
|
: context.primaryColor.withAlpha(50),
|
||||||
|
width: 0.75,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
serverInfoState.versionStatus.message,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate ||
|
||||||
|
serverInfoState.versionStatus == VersionStatus.clientOutOfDate) ...[
|
||||||
|
const Spacer(),
|
||||||
|
TextButton(
|
||||||
|
onPressed: openUpdateLink,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
minimumSize: const Size(0, 0),
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: serverInfoState.versionStatus == VersionStatus.clientOutOfDate
|
||||||
|
? Text("action_common_update".tr(context: context))
|
||||||
|
: Text("view".tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,6 @@ import 'package:flutter_svg/svg.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
@ -28,8 +27,8 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final BackUpState backupState = ref.watch(backupProvider);
|
final BackUpState backupState = ref.watch(backupProvider);
|
||||||
final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup;
|
final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup;
|
||||||
final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
|
final bool versionWarningPresent = ref.watch(versionWarningPresentProvider(user));
|
||||||
final isDarkTheme = context.isDarkTheme;
|
final isDarkTheme = context.isDarkTheme;
|
||||||
const widgetSize = 30.0;
|
const widgetSize = 30.0;
|
||||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||||
@ -46,8 +45,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
isLabelVisible:
|
isLabelVisible: versionWarningPresent,
|
||||||
serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable),
|
|
||||||
offset: const Offset(-2, -12),
|
offset: const Offset(-2, -12),
|
||||||
child: user == null
|
child: user == null
|
||||||
? const Icon(Icons.face_outlined, size: widgetSize)
|
? const Icon(Icons.face_outlined, size: widgetSize)
|
||||||
|
|||||||
@ -118,8 +118,10 @@ class _ProfileIndicator extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
|
final bool versionWarningPresent = ref.watch(versionWarningPresentProvider(user));
|
||||||
|
final serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
|
||||||
const widgetSize = 30.0;
|
const widgetSize = 30.0;
|
||||||
|
|
||||||
void toggleReadonlyMode() {
|
void toggleReadonlyMode() {
|
||||||
@ -143,13 +145,21 @@ class _ProfileIndicator extends ConsumerWidget {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
child: Badge(
|
child: Badge(
|
||||||
label: Container(
|
label: Container(
|
||||||
decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(widgetSize / 2)),
|
decoration: BoxDecoration(
|
||||||
child: const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: widgetSize / 2),
|
color: context.isDarkTheme ? Colors.black : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(widgetSize / 2),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.info,
|
||||||
|
color: serverInfoState.versionStatus == VersionStatus.error
|
||||||
|
? context.colorScheme.error
|
||||||
|
: context.primaryColor,
|
||||||
|
size: widgetSize / 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
isLabelVisible:
|
isLabelVisible: versionWarningPresent,
|
||||||
serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable),
|
|
||||||
offset: const Offset(-2, -12),
|
offset: const Offset(-2, -12),
|
||||||
child: user == null
|
child: user == null
|
||||||
? const Icon(Icons.face_outlined, size: widgetSize)
|
? const Icon(Icons.face_outlined, size: widgetSize)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user