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

@ -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/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:immich_mobile/widgets/common/app_bar_dialog/server_update_notification.dart';
import 'package:package_info_plus/package_info_plus.dart';
class AppBarServerInfo extends HookConsumerWidget {
@ -17,6 +19,8 @@ class AppBarServerInfo extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
ref.watch(localeProvider);
ServerInfo serverInfoState = ref.watch(serverInfoProvider);
final user = ref.watch(currentUserProvider);
final bool showVersionWarning = ref.watch(versionWarningPresentProvider(user));
final appInfo = useState({});
const titleFontSize = 12.0;
@ -45,17 +49,10 @@ 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),
),
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
if (showVersionWarning) ...[
const Padding(padding: EdgeInsets.symmetric(horizontal: 8.0), child: ServerUpdateNotification()),
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -182,7 +179,7 @@ class AppBarServerInfo extends HookConsumerWidget {
padding: const EdgeInsets.only(left: 10.0),
child: Row(
children: [
if (serverInfoState.isNewReleaseAvailable)
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate)
const Padding(
padding: EdgeInsets.only(right: 5.0),
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:immich_mobile/extensions/build_context_extensions.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/cast.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) {
final BackUpState backupState = ref.watch(backupProvider);
final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup;
final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
final user = ref.watch(currentUserProvider);
final bool versionWarningPresent = ref.watch(versionWarningPresentProvider(user));
final isDarkTheme = context.isDarkTheme;
const widgetSize = 30.0;
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
@ -46,8 +45,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
),
backgroundColor: Colors.transparent,
alignment: Alignment.bottomRight,
isLabelVisible:
serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable),
isLabelVisible: versionWarningPresent,
offset: const Offset(-2, -12),
child: user == null
? const Icon(Icons.face_outlined, size: widgetSize)

View file

@ -118,8 +118,10 @@ class _ProfileIndicator extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
final user = ref.watch(currentUserProvider);
final bool versionWarningPresent = ref.watch(versionWarningPresentProvider(user));
final serverInfoState = ref.watch(serverInfoProvider);
const widgetSize = 30.0;
void toggleReadonlyMode() {
@ -143,13 +145,21 @@ class _ProfileIndicator extends ConsumerWidget {
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Badge(
label: Container(
decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(widgetSize / 2)),
child: const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: widgetSize / 2),
decoration: BoxDecoration(
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,
alignment: Alignment.bottomRight,
isLabelVisible:
serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable),
isLabelVisible: versionWarningPresent,
offset: const Offset(-2, -12),
child: user == null
? const Icon(Icons.face_outlined, size: widgetSize)