diff --git a/i18n/en.json b/i18n/en.json index d265c9b9d8..88d98f650c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -474,6 +474,7 @@ "app_bar_signout_dialog_title": "Sign out", "app_download_links": "App Download Links", "app_settings": "App Settings", + "app_update_available": "An app update is available", "appears_in": "Appears in", "apply_count": "Apply ({count, number})", "archive": "Archive", @@ -705,7 +706,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", @@ -1555,13 +1555,9 @@ "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_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_image_of_user": "Profile image of {user}", "profile_picture_set": "Profile picture set.", "public_album": "Public album", @@ -1790,6 +1786,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", @@ -2031,6 +2028,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/models/server_info/server_info.model.dart b/mobile/lib/models/server_info/server_info.model.dart index 0fa80d45d8..b79c14403f 100644 --- a/mobile/lib/models/server_info/server_info.model.dart +++ b/mobile/lib/models/server_info/server_info.model.dart @@ -9,9 +9,11 @@ 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; + final bool errorGettingVersions; const ServerInfo({ required this.serverVersion, @@ -19,9 +21,11 @@ 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, + required this.errorGettingVersions, }); ServerInfo copyWith({ @@ -30,9 +34,11 @@ class ServerInfo { ServerFeatures? serverFeatures, ServerConfig? serverConfig, ServerDiskInfo? serverDiskInfo, - bool? isVersionMismatch, + bool? isClientOutOfDate, + bool? isServerOutOfDate, bool? isNewReleaseAvailable, String? versionMismatchErrorMessage, + bool? errorGettingVersions, }) { return ServerInfo( serverVersion: serverVersion ?? this.serverVersion, @@ -40,15 +46,17 @@ 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, + errorGettingVersions: errorGettingVersions ?? this.errorGettingVersions, ); } @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, errorGettingVersions: $errorGettingVersions)'; } @override @@ -61,9 +69,11 @@ 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; + other.versionMismatchErrorMessage == versionMismatchErrorMessage && + other.errorGettingVersions == errorGettingVersions; } @override @@ -73,8 +83,10 @@ class ServerInfo { serverFeatures.hashCode ^ serverConfig.hashCode ^ serverDiskInfo.hashCode ^ - isVersionMismatch.hashCode ^ + 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 25b1002b7a..1c44d32d9e 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -24,9 +24,11 @@ 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: "", + errorGettingVersions: false, ), ); @@ -43,15 +45,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(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(isVersionMismatch: true); + state = state.copyWith(errorGettingVersions: true, versionMismatchErrorMessage: "unable_to_check_version".tr()); return; } } @@ -63,39 +66,17 @@ class ServerInfoNotifier extends StateNotifier { Map appVersion = _getDetailVersion(packageInfo.version); - if (appVersion["major"]! > serverVersion.major) { - state = state.copyWith( - isVersionMismatch: true, - versionMismatchErrorMessage: "profile_drawer_server_out_of_date_major".tr(), - ); + if (appVersion["major"]! > serverVersion.major || appVersion["minor"]! > serverVersion.minor) { + state = state.copyWith(isServerOutOfDate: true, versionMismatchErrorMessage: "server_update_available".tr()); return; } - if (appVersion["major"]! < serverVersion.major) { - state = state.copyWith( - isVersionMismatch: true, - versionMismatchErrorMessage: "profile_drawer_client_out_of_date_major".tr(), - ); + if (appVersion["major"]! < serverVersion.major || appVersion["minor"]! < serverVersion.minor) { + state = state.copyWith(isClientOutOfDate: true, versionMismatchErrorMessage: "app_update_available".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..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 @@ -1,14 +1,19 @@ +import 'dart:io'; + 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'; 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 +23,44 @@ 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 || + serverInfoState.errorGettingVersions || + ((user?.isAdmin ?? false) && serverInfoState.isServerOutOfDate); + getPackageInfo() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); appInfo.value = {"version": packageInfo.version, "buildNumber": packageInfo.buildNumber}; } + void openUpdateLink() { + if (serverInfoState.isServerOutOfDate) { + launchUrl( + Uri.parse("https://github.com/immich-app/immich/releases/latest"), + mode: LaunchMode.externalApplication, + ); + return; + } + + String url; + if (Platform.isIOS) { + url = kImmichAppStoreLink; + } else if (Platform.isAndroid) { + url = kImmichPlayStoreLink; + } else { + url = kImmichLatestRelease; + } + + launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); + } + useEffect(() { getPackageInfo(); return null; @@ -45,17 +78,45 @@ 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.symmetric(horizontal: 12, vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + serverInfoState.versionMismatchErrorMessage, + textAlign: (serverInfoState.isClientOutOfDate || serverInfoState.isServerOutOfDate) + ? TextAlign.start + : TextAlign.center, + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + if (serverInfoState.isClientOutOfDate || serverInfoState.isServerOutOfDate) + TextButton( + onPressed: openUpdateLink, + style: TextButton.styleFrom( + padding: const EdgeInsets.all(4), + minimumSize: const Size(0, 0), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text("action_common_update".tr(context: context)), + ), + ], + ), + ), ), - ), - 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)