From 17888e68ae907b10cf1d85aff7238f746b754bed Mon Sep 17 00:00:00 2001 From: bwees Date: Tue, 14 Oct 2025 13:27:17 -0500 Subject: [PATCH 1/8] feat: improved update messaging on app bar server info --- i18n/en.json | 6 +- .../models/server_info/server_info.model.dart | 20 ++++-- .../lib/providers/server_info.provider.dart | 38 ++++------- .../app_bar_dialog/app_bar_server_info.dart | 64 ++++++++++++++++--- mobile/lib/widgets/common/immich_app_bar.dart | 2 +- .../widgets/common/immich_sliver_app_bar.dart | 2 +- 6 files changed, 83 insertions(+), 49 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index e1702611a7..95b6246433 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1547,13 +1547,11 @@ "privacy": "Privacy", "profile": "Profile", "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_out_of_date": "Mobile App update available.", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", "profile_drawer_github": "GitHub", "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_drawer_server_out_of_date": "Server is out of date. Please update to the latest version.", "profile_image_of_user": "Profile image of {user}", "profile_picture_set": "Profile picture set.", "public_album": "Public album", diff --git a/mobile/lib/models/server_info/server_info.model.dart b/mobile/lib/models/server_info/server_info.model.dart index 0fa80d45d8..6201c74424 100644 --- a/mobile/lib/models/server_info/server_info.model.dart +++ b/mobile/lib/models/server_info/server_info.model.dart @@ -9,7 +9,8 @@ class ServerInfo { final ServerFeatures serverFeatures; final ServerConfig serverConfig; final ServerDiskInfo serverDiskInfo; - final bool isVersionMismatch; + final bool isClientOutOfDate; + final bool isServerOutOfDate; final bool isNewReleaseAvailable; final String versionMismatchErrorMessage; @@ -19,7 +20,8 @@ class ServerInfo { required this.serverFeatures, required this.serverConfig, required this.serverDiskInfo, - required this.isVersionMismatch, + required this.isClientOutOfDate, + required this.isServerOutOfDate, required this.isNewReleaseAvailable, required this.versionMismatchErrorMessage, }); @@ -30,7 +32,8 @@ class ServerInfo { ServerFeatures? serverFeatures, ServerConfig? serverConfig, ServerDiskInfo? serverDiskInfo, - bool? isVersionMismatch, + bool? isClientOutOfDate, + bool? isServerOutOfDate, bool? isNewReleaseAvailable, String? versionMismatchErrorMessage, }) { @@ -40,7 +43,8 @@ class ServerInfo { serverFeatures: serverFeatures ?? this.serverFeatures, serverConfig: serverConfig ?? this.serverConfig, serverDiskInfo: serverDiskInfo ?? this.serverDiskInfo, - isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch, + isClientOutOfDate: isClientOutOfDate ?? this.isClientOutOfDate, + isServerOutOfDate: isServerOutOfDate ?? this.isServerOutOfDate, isNewReleaseAvailable: isNewReleaseAvailable ?? this.isNewReleaseAvailable, versionMismatchErrorMessage: versionMismatchErrorMessage ?? this.versionMismatchErrorMessage, ); @@ -48,7 +52,7 @@ class ServerInfo { @override 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, isClientOutOfDate: $isClientOutOfDate, isServerOutOfDate: $isServerOutOfDate, isNewReleaseAvailable: $isNewReleaseAvailable, versionMismatchErrorMessage: $versionMismatchErrorMessage)'; } @override @@ -61,7 +65,8 @@ class ServerInfo { other.serverFeatures == serverFeatures && other.serverConfig == serverConfig && other.serverDiskInfo == serverDiskInfo && - other.isVersionMismatch == isVersionMismatch && + other.isClientOutOfDate == isClientOutOfDate && + other.isServerOutOfDate == isServerOutOfDate && other.isNewReleaseAvailable == isNewReleaseAvailable && other.versionMismatchErrorMessage == versionMismatchErrorMessage; } @@ -73,7 +78,8 @@ class ServerInfo { serverFeatures.hashCode ^ serverConfig.hashCode ^ serverDiskInfo.hashCode ^ - isVersionMismatch.hashCode ^ + isClientOutOfDate.hashCode ^ + isServerOutOfDate.hashCode ^ isNewReleaseAvailable.hashCode ^ versionMismatchErrorMessage.hashCode; } diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index 25b1002b7a..9a359f9e0d 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -24,7 +24,8 @@ class ServerInfoNotifier extends StateNotifier { mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', ), serverDiskInfo: ServerDiskInfo(diskAvailable: "0", diskSize: "0", diskUse: "0", diskUsagePercentage: 0), - isVersionMismatch: false, + isClientOutOfDate: false, + isServerOutOfDate: false, isNewReleaseAvailable: false, versionMismatchErrorMessage: "", ), @@ -43,15 +44,16 @@ class ServerInfoNotifier extends StateNotifier { try { final serverVersion = await _serverInfoService.getServerVersion(); + // using isClientOutOfDate since that will show to users reguardless of if they are an admin if (serverVersion == null) { - state = state.copyWith(isVersionMismatch: true, versionMismatchErrorMessage: "common_server_error".tr()); + state = state.copyWith(isClientOutOfDate: true, versionMismatchErrorMessage: "common_server_error".tr()); return; } await _checkServerVersionMismatch(serverVersion); } catch (e, stackTrace) { _log.severe("Failed to get server version", e, stackTrace); - state = state.copyWith(isVersionMismatch: true); + state = state.copyWith(isClientOutOfDate: true); return; } } @@ -63,39 +65,23 @@ class ServerInfoNotifier extends StateNotifier { Map appVersion = _getDetailVersion(packageInfo.version); - if (appVersion["major"]! > serverVersion.major) { + if (appVersion["major"]! > serverVersion.major || appVersion["minor"]! > serverVersion.minor) { state = state.copyWith( - isVersionMismatch: true, - versionMismatchErrorMessage: "profile_drawer_server_out_of_date_major".tr(), + isServerOutOfDate: true, + versionMismatchErrorMessage: "profile_drawer_server_out_of_date".tr(), ); return; } - if (appVersion["major"]! < serverVersion.major) { + if (appVersion["major"]! < serverVersion.major || appVersion["minor"]! < serverVersion.minor) { state = state.copyWith( - isVersionMismatch: true, - versionMismatchErrorMessage: "profile_drawer_client_out_of_date_major".tr(), + isClientOutOfDate: true, + versionMismatchErrorMessage: "profile_drawer_client_out_of_date".tr(), ); return; } - if (appVersion["minor"]! > serverVersion.minor) { - 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: ""); + state = state.copyWith(isClientOutOfDate: false, isServerOutOfDate: false, versionMismatchErrorMessage: ""); } handleNewRelease(ServerVersion serverVersion, ServerVersion latestVersion) { diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 4aacfb3322..929e0454fa 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; @@ -7,8 +9,10 @@ import 'package:immich_mobile/extensions/theme_extensions.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/server_info.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; class AppBarServerInfo extends HookConsumerWidget { const AppBarServerInfo({super.key}); @@ -18,16 +22,35 @@ class AppBarServerInfo extends HookConsumerWidget { ref.watch(localeProvider); ServerInfo serverInfoState = ref.watch(serverInfoProvider); + final user = ref.watch(currentUserProvider); + final appInfo = useState({}); const titleFontSize = 12.0; const contentFontSize = 11.0; + final showWarning = + serverInfoState.isClientOutOfDate || + ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable) && false; + getPackageInfo() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); appInfo.value = {"version": packageInfo.version, "buildNumber": packageInfo.buildNumber}; } + void openUpdateLink() { + if (Platform.isIOS) { + launchUrl(Uri.parse("https://apps.apple.com/app/id1613945652"), mode: LaunchMode.externalApplication); + } else if (Platform.isAndroid) { + launchUrl( + Uri.parse("https://play.google.com/store/apps/details?id=app.alextran.immich"), + mode: LaunchMode.externalApplication, + ); + } else { + launchUrl(Uri.parse("https://immich.app/download"), mode: LaunchMode.externalApplication); + } + } + useEffect(() { getPackageInfo(); return null; @@ -45,17 +68,38 @@ class AppBarServerInfo extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 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), + if (showWarning) ...[ + SizedBox( + width: double.infinity, + child: Container( + decoration: const BoxDecoration( + color: Color.fromARGB(80, 243, 188, 106), + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + Text( + serverInfoState.versionMismatchErrorMessage, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w500), + ), + if (serverInfoState.isClientOutOfDate) + IconButton( + onPressed: openUpdateLink, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + style: IconButton.styleFrom(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + icon: const Icon(Icons.open_in_new, size: 16), + ), + ], + ), + ), ), - ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)), + const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)), + ], Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 28b5c535d2..09fdfddbca 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -47,7 +47,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { backgroundColor: Colors.transparent, alignment: Alignment.bottomRight, isLabelVisible: - serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable), + serverInfoState.isClientOutOfDate || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable), offset: const Offset(-2, -12), child: user == null ? const Icon(Icons.face_outlined, size: widgetSize) diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 90c213599c..61382cd7ba 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -149,7 +149,7 @@ class _ProfileIndicator extends ConsumerWidget { backgroundColor: Colors.transparent, alignment: Alignment.bottomRight, isLabelVisible: - serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable), + serverInfoState.isClientOutOfDate || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable), offset: const Offset(-2, -12), child: user == null ? const Icon(Icons.face_outlined, size: widgetSize) From 9f928af0d78d87c84b05ed11793758311079de2e Mon Sep 17 00:00:00 2001 From: bwees Date: Thu, 16 Oct 2025 14:25:47 -0500 Subject: [PATCH 2/8] chore: message improvements --- i18n/en.json | 4 ++-- .../app_bar_dialog/app_bar_server_info.dart | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 95b6246433..8bc6ccd974 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1547,11 +1547,11 @@ "privacy": "Privacy", "profile": "Profile", "profile_drawer_app_logs": "Logs", - "profile_drawer_client_out_of_date": "Mobile App update available.", + "profile_drawer_client_out_of_date": "An app update is available.", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", "profile_drawer_github": "GitHub", "profile_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.", - "profile_drawer_server_out_of_date": "Server is out of date. Please update to the latest version.", + "profile_drawer_server_out_of_date": "A server update is available.", "profile_image_of_user": "Profile image of {user}", "profile_picture_set": "Profile picture set.", "public_album": "Public album", diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 929e0454fa..a80c33f3d3 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -29,8 +29,7 @@ class AppBarServerInfo extends HookConsumerWidget { const contentFontSize = 11.0; final showWarning = - serverInfoState.isClientOutOfDate || - ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable) && false; + serverInfoState.isClientOutOfDate || ((user?.isAdmin ?? false) && serverInfoState.isServerOutOfDate); getPackageInfo() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); @@ -76,23 +75,26 @@ class AppBarServerInfo extends HookConsumerWidget { color: Color.fromARGB(80, 243, 188, 106), borderRadius: BorderRadius.all(Radius.circular(8)), ), - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 8, + mainAxisAlignment: serverInfoState.isClientOutOfDate + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, children: [ Text( serverInfoState.versionMismatchErrorMessage, textAlign: TextAlign.center, - style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w500), + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), ), if (serverInfoState.isClientOutOfDate) - IconButton( + TextButton( onPressed: openUpdateLink, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - style: IconButton.styleFrom(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - icon: const Icon(Icons.open_in_new, size: 16), + style: TextButton.styleFrom( + padding: const EdgeInsets.all(4), + minimumSize: const Size(0, 0), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text("action_common_update".tr(context: context)), ), ], ), From 261f6578d3ee8e9a4e050928b185633d0054f5bc Mon Sep 17 00:00:00 2001 From: bwees Date: Thu, 16 Oct 2025 14:34:44 -0500 Subject: [PATCH 3/8] chore: failed to fetch version error message --- i18n/en.json | 2 +- .../models/server_info/server_info.model.dart | 12 +++++++++--- mobile/lib/providers/server_info.provider.dart | 11 +++++++++-- .../app_bar_dialog/app_bar_server_info.dart | 16 +++++++++++----- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 8bc6ccd974..39b22eab2c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -701,7 +701,6 @@ "comments_and_likes": "Comments & likes", "comments_are_disabled": "Comments are disabled", "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", "confirm": "Confirm", "confirm_admin_password": "Confirm Admin Password", @@ -1552,6 +1551,7 @@ "profile_drawer_github": "GitHub", "profile_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.", "profile_drawer_server_out_of_date": "A server update is available.", + "profile_drawer_unable_to_check_version": "Unable to check app or server version", "profile_image_of_user": "Profile image of {user}", "profile_picture_set": "Profile picture set.", "public_album": "Public album", diff --git a/mobile/lib/models/server_info/server_info.model.dart b/mobile/lib/models/server_info/server_info.model.dart index 6201c74424..b79c14403f 100644 --- a/mobile/lib/models/server_info/server_info.model.dart +++ b/mobile/lib/models/server_info/server_info.model.dart @@ -13,6 +13,7 @@ class ServerInfo { final bool isServerOutOfDate; final bool isNewReleaseAvailable; final String versionMismatchErrorMessage; + final bool errorGettingVersions; const ServerInfo({ required this.serverVersion, @@ -24,6 +25,7 @@ class ServerInfo { required this.isServerOutOfDate, required this.isNewReleaseAvailable, required this.versionMismatchErrorMessage, + required this.errorGettingVersions, }); ServerInfo copyWith({ @@ -36,6 +38,7 @@ class ServerInfo { bool? isServerOutOfDate, bool? isNewReleaseAvailable, String? versionMismatchErrorMessage, + bool? errorGettingVersions, }) { return ServerInfo( serverVersion: serverVersion ?? this.serverVersion, @@ -47,12 +50,13 @@ class ServerInfo { isServerOutOfDate: isServerOutOfDate ?? this.isServerOutOfDate, isNewReleaseAvailable: isNewReleaseAvailable ?? this.isNewReleaseAvailable, versionMismatchErrorMessage: versionMismatchErrorMessage ?? this.versionMismatchErrorMessage, + errorGettingVersions: errorGettingVersions ?? this.errorGettingVersions, ); } @override String toString() { - return 'ServerInfo(serverVersion: $serverVersion, latestVersion: $latestVersion, serverFeatures: $serverFeatures, serverConfig: $serverConfig, serverDiskInfo: $serverDiskInfo, isClientOutOfDate: $isClientOutOfDate, isServerOutOfDate: $isServerOutOfDate, isNewReleaseAvailable: $isNewReleaseAvailable, versionMismatchErrorMessage: $versionMismatchErrorMessage)'; + return 'ServerInfo(serverVersion: $serverVersion, latestVersion: $latestVersion, serverFeatures: $serverFeatures, serverConfig: $serverConfig, serverDiskInfo: $serverDiskInfo, isClientOutOfDate: $isClientOutOfDate, isServerOutOfDate: $isServerOutOfDate, isNewReleaseAvailable: $isNewReleaseAvailable, versionMismatchErrorMessage: $versionMismatchErrorMessage, errorGettingVersions: $errorGettingVersions)'; } @override @@ -68,7 +72,8 @@ class ServerInfo { other.isClientOutOfDate == isClientOutOfDate && other.isServerOutOfDate == isServerOutOfDate && other.isNewReleaseAvailable == isNewReleaseAvailable && - other.versionMismatchErrorMessage == versionMismatchErrorMessage; + other.versionMismatchErrorMessage == versionMismatchErrorMessage && + other.errorGettingVersions == errorGettingVersions; } @override @@ -81,6 +86,7 @@ class ServerInfo { isClientOutOfDate.hashCode ^ isServerOutOfDate.hashCode ^ isNewReleaseAvailable.hashCode ^ - versionMismatchErrorMessage.hashCode; + versionMismatchErrorMessage.hashCode ^ + errorGettingVersions.hashCode; } } diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index 9a359f9e0d..88d250ae10 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -28,6 +28,7 @@ class ServerInfoNotifier extends StateNotifier { isServerOutOfDate: false, isNewReleaseAvailable: false, versionMismatchErrorMessage: "", + errorGettingVersions: false, ), ); @@ -46,14 +47,20 @@ class ServerInfoNotifier extends StateNotifier { // using isClientOutOfDate since that will show to users reguardless of if they are an admin if (serverVersion == null) { - state = state.copyWith(isClientOutOfDate: true, versionMismatchErrorMessage: "common_server_error".tr()); + state = state.copyWith( + errorGettingVersions: true, + versionMismatchErrorMessage: "profile_drawer_unable_to_check_version".tr(), + ); return; } await _checkServerVersionMismatch(serverVersion); } catch (e, stackTrace) { _log.severe("Failed to get server version", e, stackTrace); - state = state.copyWith(isClientOutOfDate: true); + state = state.copyWith( + errorGettingVersions: true, + versionMismatchErrorMessage: "profile_drawer_unable_to_check_version".tr(), + ); return; } } diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index a80c33f3d3..b784ddfc68 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -29,7 +29,9 @@ class AppBarServerInfo extends HookConsumerWidget { const contentFontSize = 11.0; final showWarning = - serverInfoState.isClientOutOfDate || ((user?.isAdmin ?? false) && serverInfoState.isServerOutOfDate); + serverInfoState.isClientOutOfDate || + serverInfoState.errorGettingVersions || + ((user?.isAdmin ?? false) && serverInfoState.isServerOutOfDate); getPackageInfo() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); @@ -81,10 +83,14 @@ class AppBarServerInfo extends HookConsumerWidget { ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center, children: [ - Text( - serverInfoState.versionMismatchErrorMessage, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + Expanded( + child: Text( + serverInfoState.versionMismatchErrorMessage, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), ), if (serverInfoState.isClientOutOfDate) TextButton( From a3bbb746f1df79eafa4d3f98aacd50a68fedfa19 Mon Sep 17 00:00:00 2001 From: bwees Date: Thu, 16 Oct 2025 14:36:58 -0500 Subject: [PATCH 4/8] feat: open latest release when tapping "Update" on server out of date message --- .../common/app_bar_dialog/app_bar_server_info.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index b784ddfc68..7ee9e158d5 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -40,6 +40,14 @@ class AppBarServerInfo extends HookConsumerWidget { } void openUpdateLink() { + if (serverInfoState.isServerOutOfDate) { + launchUrl( + Uri.parse("https://github.com/immich-app/immich/releases/latest"), + mode: LaunchMode.externalApplication, + ); + return; + } + if (Platform.isIOS) { launchUrl(Uri.parse("https://apps.apple.com/app/id1613945652"), mode: LaunchMode.externalApplication); } else if (Platform.isAndroid) { @@ -92,7 +100,7 @@ class AppBarServerInfo extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), ), - if (serverInfoState.isClientOutOfDate) + if (serverInfoState.isClientOutOfDate || serverInfoState.isServerOutOfDate) TextButton( onPressed: openUpdateLink, style: TextButton.styleFrom( From ce2d3a30a5b1fb40441e36816b7754665127b1d5 Mon Sep 17 00:00:00 2001 From: bwees Date: Thu, 16 Oct 2025 14:41:14 -0500 Subject: [PATCH 5/8] fix: text alignment states --- .../common/app_bar_dialog/app_bar_server_info.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 7ee9e158d5..075c7a00cb 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -87,14 +87,14 @@ class AppBarServerInfo extends HookConsumerWidget { ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( - mainAxisAlignment: serverInfoState.isClientOutOfDate - ? MainAxisAlignment.spaceBetween - : MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( serverInfoState.versionMismatchErrorMessage, - textAlign: TextAlign.center, + textAlign: (serverInfoState.isClientOutOfDate || serverInfoState.isServerOutOfDate) + ? TextAlign.start + : TextAlign.center, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), maxLines: 3, overflow: TextOverflow.ellipsis, From 6d3e918a7b2321c371ae69d3daa3f43d7e49ecf7 Mon Sep 17 00:00:00 2001 From: bwees Date: Thu, 16 Oct 2025 22:44:54 -0500 Subject: [PATCH 6/8] chore: code review updates --- i18n/en.json | 6 +++--- mobile/lib/constants/constants.dart | 4 ++++ .../lib/providers/server_info.provider.dart | 20 ++++--------------- .../app_bar_dialog/app_bar_server_info.dart | 13 ++++++------ 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 39b22eab2c..90ce897dd9 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -470,6 +470,7 @@ "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "app_settings": "App Settings", + "app_update_available": "An app update is available.", "appears_in": "Appears in", "apply_count": "Apply ({count, number})", "archive": "Archive", @@ -1546,12 +1547,9 @@ "privacy": "Privacy", "profile": "Profile", "profile_drawer_app_logs": "Logs", - "profile_drawer_client_out_of_date": "An app update is available.", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", "profile_drawer_github": "GitHub", "profile_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.", - "profile_drawer_server_out_of_date": "A server update is available.", - "profile_drawer_unable_to_check_version": "Unable to check app or server version", "profile_image_of_user": "Profile image of {user}", "profile_picture_set": "Profile picture set.", "public_album": "Public album", @@ -1780,6 +1778,7 @@ "server_online": "Server Online", "server_privacy": "Server Privacy", "server_stats": "Server Stats", + "server_update_available": "A server update is available.", "server_version": "Server Version", "set": "Set", "set_as_album_cover": "Set as album cover", @@ -2021,6 +2020,7 @@ "troubleshoot": "Troubleshoot", "type": "Type", "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", "unarchive": "Unarchive", "unarchive_action_prompt": "{count} removed from Archive", diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 7429616f14..10f4e88f0f 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -49,3 +49,7 @@ const double kUploadStatusFailed = -1.0; const double kUploadStatusCanceled = -2.0; 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"; diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index 88d250ae10..1c44d32d9e 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -47,20 +47,14 @@ class ServerInfoNotifier extends StateNotifier { // using isClientOutOfDate since that will show to users reguardless of if they are an admin if (serverVersion == null) { - state = state.copyWith( - errorGettingVersions: true, - versionMismatchErrorMessage: "profile_drawer_unable_to_check_version".tr(), - ); + state = state.copyWith(errorGettingVersions: true, versionMismatchErrorMessage: "unable_to_check_version".tr()); return; } await _checkServerVersionMismatch(serverVersion); } catch (e, stackTrace) { _log.severe("Failed to get server version", e, stackTrace); - state = state.copyWith( - errorGettingVersions: true, - versionMismatchErrorMessage: "profile_drawer_unable_to_check_version".tr(), - ); + state = state.copyWith(errorGettingVersions: true, versionMismatchErrorMessage: "unable_to_check_version".tr()); return; } } @@ -73,18 +67,12 @@ class ServerInfoNotifier extends StateNotifier { Map appVersion = _getDetailVersion(packageInfo.version); if (appVersion["major"]! > serverVersion.major || appVersion["minor"]! > serverVersion.minor) { - state = state.copyWith( - isServerOutOfDate: true, - versionMismatchErrorMessage: "profile_drawer_server_out_of_date".tr(), - ); + state = state.copyWith(isServerOutOfDate: true, versionMismatchErrorMessage: "server_update_available".tr()); return; } if (appVersion["major"]! < serverVersion.major || appVersion["minor"]! < serverVersion.minor) { - state = state.copyWith( - isClientOutOfDate: true, - versionMismatchErrorMessage: "profile_drawer_client_out_of_date".tr(), - ); + state = state.copyWith(isClientOutOfDate: true, versionMismatchErrorMessage: "app_update_available".tr()); return; } diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 075c7a00cb..54c6aaf3c7 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; 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/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; @@ -48,16 +49,16 @@ class AppBarServerInfo extends HookConsumerWidget { return; } + String url; if (Platform.isIOS) { - launchUrl(Uri.parse("https://apps.apple.com/app/id1613945652"), mode: LaunchMode.externalApplication); + url = kImmichAppStoreLink; } else if (Platform.isAndroid) { - launchUrl( - Uri.parse("https://play.google.com/store/apps/details?id=app.alextran.immich"), - mode: LaunchMode.externalApplication, - ); + url = kImmichPlayStoreLink; } else { - launchUrl(Uri.parse("https://immich.app/download"), mode: LaunchMode.externalApplication); + url = kImmichLatestRelease; } + + launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } useEffect(() { From 19ac0dd46dd2dfa2004081bc3534a4c46b14a91c Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Fri, 17 Oct 2025 11:12:37 -0500 Subject: [PATCH 7/8] Apply suggestion from @alextran1502 Co-authored-by: Alex --- i18n/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en.json b/i18n/en.json index 90ce897dd9..794eac1e1b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1778,7 +1778,7 @@ "server_online": "Server Online", "server_privacy": "Server Privacy", "server_stats": "Server Stats", - "server_update_available": "A server update is available.", + "server_update_available": "A server update is available", "server_version": "Server Version", "set": "Set", "set_as_album_cover": "Set as album cover", From d83cb6a5f881e80132ff69686e3eb969e8a07432 Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Fri, 17 Oct 2025 11:12:44 -0500 Subject: [PATCH 8/8] Apply suggestion from @alextran1502 Co-authored-by: Alex --- i18n/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en.json b/i18n/en.json index 794eac1e1b..6aad00450b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -470,7 +470,7 @@ "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "app_settings": "App Settings", - "app_update_available": "An app update is available.", + "app_update_available": "An app update is available", "appears_in": "Appears in", "apply_count": "Apply ({count, number})", "archive": "Archive",