diff --git a/assets/translations/en.json b/assets/translations/en.json index 7c518067..c35ff0b5 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -39,6 +39,7 @@ "please_connect": "Please connect your server, domain and DNS provider to dive in!", "network_error": "Network error", "feature_unsupported_on_api_version": "This feature is only supported on server version {versionConstraint}. Your server is on version {currentVersion}.", + "server_is_outdated": "This SelfPrivacy version requires API version {versionConstraint}, but your server is on version {currentVersion}. Some features won't work and errors might occur. Please upgrade your server.", "error": "Error" }, "more_page": { diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index 293d85d4..e8fff195 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart'; import 'package:selfprivacy/logic/bloc/connection_status_bloc.dart'; import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart'; +import 'package:selfprivacy/logic/bloc/outdated_server_checker/outdated_server_checker_bloc.dart'; import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_bloc.dart'; import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart'; import 'package:selfprivacy/logic/bloc/server_logs/server_logs_bloc.dart'; @@ -38,6 +39,7 @@ class BlocAndProviderConfigState extends State { late final ServerDetailsCubit serverDetailsCubit; late final VolumesBloc volumesBloc; late final ServerLogsBloc serverLogsBloc; + late final OutdatedServerCheckerBloc outdatedServerCheckerBloc; @override void initState() { @@ -55,6 +57,7 @@ class BlocAndProviderConfigState extends State { serverDetailsCubit = ServerDetailsCubit(); volumesBloc = VolumesBloc(); serverLogsBloc = ServerLogsBloc(); + outdatedServerCheckerBloc = OutdatedServerCheckerBloc(); } @override @@ -100,6 +103,9 @@ class BlocAndProviderConfigState extends State { BlocProvider( create: (final _) => serverLogsBloc, ), + BlocProvider( + create: (final _) => outdatedServerCheckerBloc, + ), ], child: widget.child, ); diff --git a/lib/logic/bloc/outdated_server_checker/outdated_server_checker_bloc.dart b/lib/logic/bloc/outdated_server_checker/outdated_server_checker_bloc.dart new file mode 100644 index 00000000..13969931 --- /dev/null +++ b/lib/logic/bloc/outdated_server_checker/outdated_server_checker_bloc.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; + +part 'outdated_server_checker_event.dart'; +part 'outdated_server_checker_state.dart'; + +const String requiredServerVersion = '>=3.3.0'; + +class OutdatedServerCheckerBloc + extends Bloc { + OutdatedServerCheckerBloc() : super(OutdatedServerCheckerInitial()) { + on((final event, final emit) { + if (event.newVersion == null) { + emit(OutdatedServerCheckerInitial()); + return; + } + final requiredVersion = VersionConstraint.parse(requiredServerVersion); + final currentVersion = Version.parse(event.newVersion!); + if (requiredVersion.allows(currentVersion)) { + emit(OutdatedServerCheckerUpToDate(currentVersion)); + } else { + emit(OutdatedServerCheckerOutdated(currentVersion)); + } + }); + + _apiDataSubscription = getIt().dataStream.listen( + (final ApiData apiData) { + add(ServerApiVersionChanged(apiData.apiVersion.data)); + }, + ); + } + + @override + Future close() { + _apiDataSubscription.cancel(); + return super.close(); + } + + @override + void onChange(final Change change) { + super.onChange(change); + } + + late StreamSubscription _apiDataSubscription; +} diff --git a/lib/logic/bloc/outdated_server_checker/outdated_server_checker_event.dart b/lib/logic/bloc/outdated_server_checker/outdated_server_checker_event.dart new file mode 100644 index 00000000..63a2c9e9 --- /dev/null +++ b/lib/logic/bloc/outdated_server_checker/outdated_server_checker_event.dart @@ -0,0 +1,14 @@ +part of 'outdated_server_checker_bloc.dart'; + +sealed class OutdatedServerCheckerEvent extends Equatable { + const OutdatedServerCheckerEvent(); +} + +class ServerApiVersionChanged extends OutdatedServerCheckerEvent { + const ServerApiVersionChanged(this.newVersion); + + final String? newVersion; + + @override + List get props => [newVersion]; +} diff --git a/lib/logic/bloc/outdated_server_checker/outdated_server_checker_state.dart b/lib/logic/bloc/outdated_server_checker/outdated_server_checker_state.dart new file mode 100644 index 00000000..18f5b53f --- /dev/null +++ b/lib/logic/bloc/outdated_server_checker/outdated_server_checker_state.dart @@ -0,0 +1,31 @@ +part of 'outdated_server_checker_bloc.dart'; + +sealed class OutdatedServerCheckerState extends Equatable { + const OutdatedServerCheckerState(); + + VersionConstraint get requiredVersion => + VersionConstraint.parse(requiredServerVersion); +} + +final class OutdatedServerCheckerInitial extends OutdatedServerCheckerState { + @override + List get props => []; +} + +final class OutdatedServerCheckerOutdated extends OutdatedServerCheckerState { + const OutdatedServerCheckerOutdated(this.currentVersion); + + final Version currentVersion; + + @override + List get props => [currentVersion]; +} + +final class OutdatedServerCheckerUpToDate extends OutdatedServerCheckerState { + const OutdatedServerCheckerUpToDate(this.currentVersion); + + final Version currentVersion; + + @override + List get props => [currentVersion]; +} diff --git a/lib/ui/components/server_outdated_card/server_outdated_card.dart b/lib/ui/components/server_outdated_card/server_outdated_card.dart new file mode 100644 index 00000000..e6fbc21b --- /dev/null +++ b/lib/ui/components/server_outdated_card/server_outdated_card.dart @@ -0,0 +1,39 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/cards/filled_card.dart'; + +class ServerOutdatedCard extends StatelessWidget { + const ServerOutdatedCard({ + required this.requiredVersion, + required this.currentVersion, + super.key, + }); + + final String requiredVersion; + final String currentVersion; + + @override + Widget build(final BuildContext context) => FilledCard( + error: true, + child: ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + title: Text( + 'basis.server_is_outdated'.tr( + namedArgs: { + 'versionConstraint': requiredVersion, + 'currentVersion': currentVersion, + }, + ), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + trailing: Icon( + Icons.error_outline, + size: 16, + color: Theme.of(context).colorScheme.onTertiaryContainer, + ), + ), + ); +} diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index 8e70e609..c576b1e0 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart'; +import 'package:selfprivacy/logic/bloc/outdated_server_checker/outdated_server_checker_bloc.dart'; import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart'; import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; @@ -12,6 +13,7 @@ import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; +import 'package:selfprivacy/ui/components/server_outdated_card/server_outdated_card.dart'; import 'package:selfprivacy/ui/router/router.dart'; import 'package:selfprivacy/utils/breakpoints.dart'; @@ -40,6 +42,9 @@ class _ProvidersPageState extends State { final ServerInstallationState appConfig = context.watch().state; + final OutdatedServerCheckerState outdatedServerCheckerState = + context.watch().state; + StateType getServerStatus() { if (!isReady) { return StateType.uninitialized; @@ -76,6 +81,15 @@ class _ProvidersPageState extends State { const NotReadyCard(), const SizedBox(height: 16), ], + if (outdatedServerCheckerState is OutdatedServerCheckerOutdated) ...[ + ServerOutdatedCard( + requiredVersion: + outdatedServerCheckerState.requiredVersion.toString(), + currentVersion: + outdatedServerCheckerState.currentVersion.toString(), + ), + const SizedBox(height: 16), + ], _Card( state: getServerStatus(), icon: BrandIcons.server, diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index 15a1b3d9..2662ec09 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:gap/gap.dart'; import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/bloc/outdated_server_checker/outdated_server_checker_bloc.dart'; import 'package:selfprivacy/logic/bloc/services/services_bloc.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/service.dart'; @@ -11,6 +12,7 @@ import 'package:selfprivacy/logic/models/state_types.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; +import 'package:selfprivacy/ui/components/server_outdated_card/server_outdated_card.dart'; import 'package:selfprivacy/ui/helpers/empty_page_placeholder.dart'; import 'package:selfprivacy/ui/router/router.dart'; import 'package:selfprivacy/utils/breakpoints.dart'; @@ -31,6 +33,9 @@ class _ServicesPageState extends State { final isReady = context.watch().state is ServerInstallationFinished; + final OutdatedServerCheckerState outdatedServerCheckerState = + context.watch().state; + final services = [...context.watch().state.services]; services .sort((final a, final b) => a.status.index.compareTo(b.status.index)); @@ -53,6 +58,16 @@ class _ServicesPageState extends State { child: ListView( padding: paddingH15V0, children: [ + if (outdatedServerCheckerState + is OutdatedServerCheckerOutdated) ...[ + ServerOutdatedCard( + requiredVersion: + outdatedServerCheckerState.requiredVersion.toString(), + currentVersion: + outdatedServerCheckerState.currentVersion.toString(), + ), + const SizedBox(height: 16), + ], Text( 'basis.services_title'.tr(), style: Theme.of(context).textTheme.bodyLarge, diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index ea282f95..edaf2974 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/bloc/outdated_server_checker/outdated_server_checker_bloc.dart'; import 'package:selfprivacy/logic/bloc/users/users_bloc.dart'; import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; @@ -20,6 +21,7 @@ import 'package:selfprivacy/ui/components/buttons/outlined_button.dart'; import 'package:selfprivacy/ui/components/cards/filled_card.dart'; import 'package:selfprivacy/ui/components/info_box/info_box.dart'; import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart'; +import 'package:selfprivacy/ui/components/server_outdated_card/server_outdated_card.dart'; import 'package:selfprivacy/ui/helpers/empty_page_placeholder.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/router/router.dart'; @@ -42,6 +44,9 @@ class UsersPage extends StatelessWidget { is ServerInstallationFinished; Widget child; + final OutdatedServerCheckerState outdatedServerCheckerState = + context.watch().state; + if (!isReady) { child = EmptyPagePlaceholder( showReadyCard: true, @@ -93,6 +98,18 @@ class UsersPage extends StatelessWidget { }, child: Column( children: [ + if (outdatedServerCheckerState + is OutdatedServerCheckerOutdated) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: ServerOutdatedCard( + requiredVersion: + outdatedServerCheckerState.requiredVersion.toString(), + currentVersion: + outdatedServerCheckerState.currentVersion.toString(), + ), + ), + ], Padding( padding: const EdgeInsets.all(8.0), child: FilledButton.tonal(