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:
Brandon Wees 2025-10-20 16:13:31 -05:00 committed by GitHub
parent 6f31f27218
commit 23a34bee6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 226 additions and 128 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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