diff --git a/packages/neon/neon/lib/src/blocs/apps.dart b/packages/neon/neon/lib/src/blocs/apps.dart index b65a67a3..eccca553 100644 --- a/packages/neon/neon/lib/src/blocs/apps.dart +++ b/packages/neon/neon/lib/src/blocs/apps.dart @@ -44,7 +44,7 @@ abstract interface class AppsBlocStates { BehaviorSubject get openNotifications; /// A collection of unsupported apps and their minimum required version. - BehaviorSubject> get appVersions; + BehaviorSubject> get appVersionChecks; } /// The Bloc responsible for managing the [AppImplementation]s. @@ -131,12 +131,12 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates return; } - final notSupported = {}; + final notSupported = {}; try { - final coreCheck = _account.client.core.isSupported(capabilities.requireData); + final coreCheck = _account.client.core.getVersionCheck(capabilities.requireData); if (!coreCheck.isSupported) { - notSupported['core'] = coreCheck.minimumVersion.toString(); + notSupported['core'] = coreCheck; } } catch (e, s) { debugPrint(e.toString()); @@ -145,14 +145,14 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates for (final app in apps.requireData) { try { - final check = await app.isSupported(_account, capabilities.requireData); + final check = await app.getVersionCheck(_account, capabilities.requireData); if (check == null) { continue; } if (!check.isSupported) { - notSupported[app.id] = check.minimumVersion; + notSupported[app.id] = check; } } catch (e, s) { debugPrint(e.toString()); @@ -161,7 +161,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates } if (notSupported.isNotEmpty) { - appVersions.add(notSupported); + appVersionChecks.add(notSupported); } } @@ -190,7 +190,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates unawaited(notificationsAppImplementation.close()); unawaited(activeApp.close()); unawaited(openNotifications.close()); - unawaited(appVersions.close()); + unawaited(appVersionChecks.close()); super.dispose(); } @@ -209,7 +209,7 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates BehaviorSubject openNotifications = BehaviorSubject(); @override - BehaviorSubject> appVersions = BehaviorSubject(); + BehaviorSubject> appVersionChecks = BehaviorSubject(); @override Future refresh() async { diff --git a/packages/neon/neon/lib/src/models/app_implementation.dart b/packages/neon/neon/lib/src/models/app_implementation.dart index b70cffa1..f26bd21c 100644 --- a/packages/neon/neon/lib/src/models/app_implementation.dart +++ b/packages/neon/neon/lib/src/models/app_implementation.dart @@ -15,7 +15,7 @@ import 'package:neon/src/settings/models/storage.dart'; import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/widgets/drawer_destination.dart'; import 'package:nextcloud/core.dart' as core; -import 'package:nextcloud/nextcloud.dart' show VersionSupported; +import 'package:nextcloud/nextcloud.dart' show VersionCheck; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart'; import 'package:vector_graphics/vector_graphics.dart'; @@ -61,7 +61,7 @@ abstract class AppImplementation /// A value of `null` means that it can not be known if the app is supported. /// This is the case for apps that depend on the server version like files and we assume that the app is supported. /// The server support is handled differently. - FutureOr?> isSupported( + FutureOr getVersionCheck( final Account account, final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities, ) => diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index 04df3e20..3bfb360d 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -16,6 +16,7 @@ import 'package:neon/src/widgets/drawer.dart'; import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/unified_search_results.dart'; import 'package:nextcloud/core.dart' as core; +import 'package:nextcloud/nextcloud.dart'; import 'package:provider/provider.dart'; /// The home page of Neon. @@ -35,7 +36,7 @@ class _HomePageState extends State { late global_options.GlobalOptions _globalOptions; late AccountsBloc _accountsBloc; late AppsBloc _appsBloc; - late StreamSubscription> _versionCheckSubscription; + late StreamSubscription> _versionCheckSubscription; @override void initState() { @@ -45,23 +46,19 @@ class _HomePageState extends State { _account = _accountsBloc.activeAccount.value!; _appsBloc = _accountsBloc.activeAppsBloc; - _versionCheckSubscription = _appsBloc.appVersions.listen((final values) { + _versionCheckSubscription = _appsBloc.appVersionChecks.listen((final values) { if (!mounted) { return; } final l10n = NeonLocalizations.of(context); - final buffer = StringBuffer()..writeln(); - for (final error in values.entries) { - final appId = error.key; - final minVersion = error.value; - final appName = l10n.appImplementationName(appId); + for (final entry in values.entries) { + final versionCheck = entry.value; + final appName = l10n.appImplementationName(entry.key); - if (appName.isNotEmpty && minVersion != null) { - buffer.writeln('- $appName $minVersion'); - } + buffer.writeln('- $appName >=${versionCheck.minimumVersion} <${versionCheck.maximumMajor + 1}.0.0'); } final message = l10n.errorUnsupportedAppVersions(buffer.toString()); diff --git a/packages/neon/neon/lib/src/pages/login_check_server_status.dart b/packages/neon/neon/lib/src/pages/login_check_server_status.dart index 042b024f..cdb99ed5 100644 --- a/packages/neon/neon/lib/src/pages/login_check_server_status.dart +++ b/packages/neon/neon/lib/src/pages/login_check_server_status.dart @@ -60,7 +60,8 @@ class _LoginCheckServerStatusPageState extends State child: ResultBuilder.behaviorSubject( subject: bloc.state, builder: (final context, final state) { - final success = state.hasData && state.requireData.isSupported && !state.requireData.maintenance; + final success = + state.hasData && state.requireData.versionCheck.isSupported && !state.requireData.maintenance; return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -121,7 +122,7 @@ class _LoginCheckServerStatusPageState extends State ); } - if (result.requireData.isSupported) { + if (result.requireData.versionCheck.isSupported) { return NeonValidationTile( title: NeonLocalizations.of(context).loginSupportedServerVersion(result.requireData.versionstring), state: ValidationState.success, diff --git a/packages/neon/neon/pubspec.yaml b/packages/neon/neon/pubspec.yaml index 90e2b6a5..f6139ab9 100644 --- a/packages/neon/neon/pubspec.yaml +++ b/packages/neon/neon/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: universal_io: ^2.0.0 url_launcher: ^6.1.0 vector_graphics: ^1.0.0 + version: ^3.0.0 window_manager: ^0.3.0 xml: ^6.0.0 diff --git a/packages/neon/neon_news/lib/neon_news.dart b/packages/neon/neon_news/lib/neon_news.dart index 53bb272a..f302834d 100644 --- a/packages/neon/neon_news/lib/neon_news.dart +++ b/packages/neon/neon_news/lib/neon_news.dart @@ -109,9 +109,9 @@ class NewsApp extends AppImplementation { BehaviorSubject getUnreadCounter(final NewsBloc bloc) => bloc.unreadCounter; @override - Future> isSupported( + Future getVersionCheck( final Account account, final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities, ) => - account.client.news.isSupported(); + account.client.news.getVersionCheck(); } diff --git a/packages/neon/neon_notes/lib/neon_notes.dart b/packages/neon/neon_notes/lib/neon_notes.dart index 4ca927e8..3d5ccfce 100644 --- a/packages/neon/neon_notes/lib/neon_notes.dart +++ b/packages/neon/neon_notes/lib/neon_notes.dart @@ -95,11 +95,9 @@ class NotesApp extends AppImplementation { final RouteBase route = $notesAppRoute; @override - VersionSupported isSupported( + VersionCheck getVersionCheck( final Account account, final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities, - ) { - final result = account.client.notes.isSupported(capabilities); - return (isSupported: result.isSupported, minimumVersion: result.minimumVersion.toString()); - } + ) => + account.client.notes.getVersionCheck(capabilities); } diff --git a/packages/nextcloud/lib/src/helpers/common.dart b/packages/nextcloud/lib/src/helpers/common.dart index f2e6b9a1..c92f7453 100644 --- a/packages/nextcloud/lib/src/helpers/common.dart +++ b/packages/nextcloud/lib/src/helpers/common.dart @@ -1,2 +1,42 @@ -/// The result of a version check. -typedef VersionSupported = ({bool isSupported, T minimumVersion}); +import 'package:meta/meta.dart'; +import 'package:version/version.dart'; + +/// Holds the [versions], [minimumVersion] and [maximumMajor] of an app. +@immutable +class VersionCheck { + /// Creates a new [VersionCheck]. + /// + /// If the [maximumMajor] is `null` the compatibility of the major of the [minimumVersion] is checked. + VersionCheck({ + required this.versions, + required this.minimumVersion, + required final int? maximumMajor, + }) : maximumMajor = maximumMajor ?? minimumVersion.major; + + /// Current version of the app. + final List? versions; + + /// Minimum version of the app. + final Version minimumVersion; + + /// Maximum major version of the app. + late final int maximumMajor; + + /// Whether the [versions] is allowed by the [minimumVersion] and [maximumMajor]. + /// + /// If [versions] is `null` or empty it is assumed that the app is supported. + /// Only one of the [versions] has to be supported to return `true`. + bool get isSupported { + if (versions == null || versions!.isEmpty) { + return true; + } + + for (final version in versions!) { + if (version >= minimumVersion && version.major <= maximumMajor) { + return true; + } + } + + return false; + } +} diff --git a/packages/nextcloud/lib/src/helpers/core.dart b/packages/nextcloud/lib/src/helpers/core.dart index de13b505..cf6af22e 100644 --- a/packages/nextcloud/lib/src/helpers/core.dart +++ b/packages/nextcloud/lib/src/helpers/core.dart @@ -2,26 +2,36 @@ import 'package:nextcloud/src/api/core.openapi.dart' as core; import 'package:nextcloud/src/helpers/common.dart'; +import 'package:version/version.dart'; -/// Version of core/Server supported -const supportedVersion = 27; +/// Minimum version of core/Server supported +final minVersion = Version(27, 0, 0); -extension CoreVersionSupported on core.Client { +extension CoreVersionCheck on core.Client { /// Check if the core/Server version is supported by this client /// - /// Also returns the supported version number - VersionSupported isSupported( - final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities, - ) => - ( - isSupported: capabilities.version.major == supportedVersion, - minimumVersion: supportedVersion, - ); + /// Also returns the minimum supported version + VersionCheck getVersionCheck(final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities) { + final version = Version( + capabilities.version.major, + capabilities.version.minor, + capabilities.version.micro, + ); + return VersionCheck( + versions: [version], + minimumVersion: minVersion, + maximumMajor: null, + ); + } } -extension CoreStatusVersionSupported on core.Status { +extension CoreStatusVersionCheck on core.Status { /// Check if the core/Server version is supported - bool get isSupported => version.startsWith('$supportedVersion.'); + VersionCheck get versionCheck => VersionCheck( + versions: [Version.parse(version)], + minimumVersion: minVersion, + maximumMajor: null, + ); } enum ShareType { diff --git a/packages/nextcloud/lib/src/helpers/news.dart b/packages/nextcloud/lib/src/helpers/news.dart index 0761a554..03835e73 100644 --- a/packages/nextcloud/lib/src/helpers/news.dart +++ b/packages/nextcloud/lib/src/helpers/news.dart @@ -2,19 +2,22 @@ import 'package:nextcloud/src/api/news.openapi.dart' as news; import 'package:nextcloud/src/helpers/common.dart'; +import 'package:version/version.dart'; -/// API version of the news app supported -const supportedVersion = 'v1-3'; +/// Minimum API version of the news app supported +final minVersion = Version(1, 3, 0); -extension NewsVersionSupported on news.Client { +extension NewsVersionCheck on news.Client { /// Check if the news app version is supported by this client /// /// Also returns the supported API version number - Future> isSupported() async { + Future getVersionCheck() async { final response = await getSupportedApiVersions(); - return ( - isSupported: response.body.apiLevels!.contains(supportedVersion), - minimumVersion: supportedVersion, + final versions = response.body.apiLevels; + return VersionCheck( + versions: versions?.map((final version) => Version.parse(version.substring(1).replaceAll('-', '.'))).toList(), + minimumVersion: minVersion, + maximumMajor: null, ); } } diff --git a/packages/nextcloud/lib/src/helpers/notes.dart b/packages/nextcloud/lib/src/helpers/notes.dart index e0c16c62..fa980162 100644 --- a/packages/nextcloud/lib/src/helpers/notes.dart +++ b/packages/nextcloud/lib/src/helpers/notes.dart @@ -1,25 +1,22 @@ -import 'package:collection/collection.dart'; import 'package:nextcloud/src/api/core.openapi.dart' as core; import 'package:nextcloud/src/api/notes.openapi.dart' as notes; import 'package:nextcloud/src/helpers/common.dart'; import 'package:version/version.dart'; -/// API version of the notes app supported -const supportedVersion = 1; +/// Minimum API version of the notes app supported +final minVersion = Version(1, 3, 0); // ignore: public_member_api_docs -extension NotesVersionSupported on notes.Client { +extension NotesVersionCheck on notes.Client { /// Check if the notes app version is supported by this client /// /// Also returns the supported API version number - VersionSupported isSupported( - final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities, - ) => - ( - isSupported: capabilities.capabilities.notesCapabilities?.notes.apiVersion - ?.map(Version.parse) - .firstWhereOrNull((final version) => version.major == supportedVersion) != - null, - minimumVersion: supportedVersion, - ); + VersionCheck getVersionCheck(final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities) { + final versions = capabilities.capabilities.notesCapabilities?.notes.apiVersion; + return VersionCheck( + versions: versions?.map(Version.parse).toList(), + minimumVersion: minVersion, + maximumMajor: null, + ); + } } diff --git a/packages/nextcloud/lib/src/helpers/spreed.dart b/packages/nextcloud/lib/src/helpers/spreed.dart index c4a0182a..d35aa7cb 100644 --- a/packages/nextcloud/lib/src/helpers/spreed.dart +++ b/packages/nextcloud/lib/src/helpers/spreed.dart @@ -3,19 +3,20 @@ import 'package:nextcloud/src/api/core.openapi.dart' as core; import 'package:nextcloud/src/api/spreed.openapi.dart' as spreed; import 'package:version/version.dart'; -/// The version of the spreed app that is supported. -const supportedVersion = 17; +/// The minimum version of the spreed app that is supported. +final minVersion = Version(17, 0, 0); /// Extension for checking whether spreed is supported. -extension SpreedVersionSupported on spreed.Client { +extension SpreedVersionCheck on spreed.Client { /// Checks whether the spreed app installed on the server is supported by this client. /// /// Also returns the supported version number. - VersionSupported isSupported(final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities) { + VersionCheck getVersionCheck(final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities) { final version = capabilities.capabilities.spreedPublicCapabilities?.spreedPublicCapabilities0?.spreed.version; - return ( - isSupported: version != null && Version.parse(version).major == supportedVersion, - minimumVersion: supportedVersion, + return VersionCheck( + versions: version != null ? [Version.parse(version)] : null, + minimumVersion: minVersion, + maximumMajor: null, ); } } diff --git a/packages/nextcloud/test/core_test.dart b/packages/nextcloud/test/core_test.dart index dbe9954a..b4248d35 100644 --- a/packages/nextcloud/test/core_test.dart +++ b/packages/nextcloud/test/core_test.dart @@ -22,7 +22,7 @@ void main() { expect(response.statusCode, 200); expect(() => response.headers, isA()); - final result = client.core.isSupported(response.body.ocs.data); + final result = client.core.getVersionCheck(response.body.ocs.data); expect(result.isSupported, isTrue); }); @@ -31,7 +31,7 @@ void main() { expect(response.statusCode, 200); expect(() => response.headers, isA()); - expect(response.body.isSupported, isTrue); + expect(response.body.versionCheck.isSupported, isTrue); }); test('Get status', () async { @@ -42,8 +42,8 @@ void main() { expect(response.body.installed, isTrue); expect(response.body.maintenance, isFalse); expect(response.body.needsDbUpgrade, isFalse); - expect(response.body.version, startsWith('${core.supportedVersion}.')); - expect(response.body.versionstring, startsWith('${core.supportedVersion}.')); + expect(response.body.version, isNotEmpty); + expect(response.body.versionstring, isNotEmpty); expect(response.body.edition, ''); expect(response.body.productname, 'Nextcloud'); expect(response.body.extendedSupport, isFalse); @@ -55,8 +55,6 @@ void main() { expect(response.statusCode, 200); expect(() => response.headers, isA()); - expect(response.body.ocs.data.version.major, core.supportedVersion); - expect(response.body.ocs.data.version.string, startsWith('${core.supportedVersion}.')); expect(response.body.ocs.data.capabilities.commentsCapabilities, isNotNull); expect(response.body.ocs.data.capabilities.davCapabilities, isNotNull); expect(response.body.ocs.data.capabilities.filesCapabilities, isNotNull); diff --git a/packages/nextcloud/test/news_test.dart b/packages/nextcloud/test/news_test.dart index b3ef3fd1..567c2d0c 100644 --- a/packages/nextcloud/test/news_test.dart +++ b/packages/nextcloud/test/news_test.dart @@ -29,7 +29,7 @@ void main() { ); test('Is supported', () async { - final result = await client.news.isSupported(); + final result = await client.news.getVersionCheck(); expect(result.isSupported, isTrue); }); diff --git a/packages/nextcloud/test/notes_test.dart b/packages/nextcloud/test/notes_test.dart index bc80c6fb..a0dad4ab 100644 --- a/packages/nextcloud/test/notes_test.dart +++ b/packages/nextcloud/test/notes_test.dart @@ -22,7 +22,7 @@ void main() { expect(response.statusCode, 200); expect(() => response.headers, isA()); - final result = client.notes.isSupported(response.body.ocs.data); + final result = client.notes.getVersionCheck(response.body.ocs.data); expect(result.isSupported, isTrue); }); diff --git a/packages/nextcloud/test/spreed_test.dart b/packages/nextcloud/test/spreed_test.dart index 5134ecdb..84ad5773 100644 --- a/packages/nextcloud/test/spreed_test.dart +++ b/packages/nextcloud/test/spreed_test.dart @@ -34,7 +34,7 @@ void main() { expect(response.statusCode, 200); expect(() => response.headers, isA()); - final result = client1.spreed.isSupported(response.body.ocs.data); + final result = client1.spreed.getVersionCheck(response.body.ocs.data); expect(result.isSupported, isTrue); }); diff --git a/packages/nextcloud/test/version_check_test.dart b/packages/nextcloud/test/version_check_test.dart new file mode 100644 index 00000000..7d7db8c7 --- /dev/null +++ b/packages/nextcloud/test/version_check_test.dart @@ -0,0 +1,100 @@ +import 'package:nextcloud/nextcloud.dart'; +import 'package:test/test.dart'; +import 'package:version/version.dart'; + +void main() { + group('Version check', () { + test('Null versions', () { + final check = VersionCheck( + versions: null, + // Invalid constraints to avoid accidental validation + minimumVersion: Version(2, 0, 0), + maximumMajor: 1, + ); + expect(check.isSupported, isTrue); + }); + + test('Empty versions', () { + final check = VersionCheck( + versions: const [], + // Invalid constraints to avoid accidental validation + minimumVersion: Version(2, 0, 0), + maximumMajor: 1, + ); + expect(check.isSupported, isTrue); + }); + + test('Multiple versions', () { + final check = VersionCheck( + versions: [ + Version(0, 9, 9), + Version(1, 5, 0), + Version(2, 0, 0), + ], + minimumVersion: Version(1, 0, 0), + maximumMajor: 1, + ); + expect(check.isSupported, isTrue); + }); + + test('With maximumMajor', () { + var check = VersionCheck( + versions: [Version(0, 9, 9)], + minimumVersion: Version(1, 0, 0), + maximumMajor: 1, + ); + expect(check.isSupported, isFalse); + + check = VersionCheck( + versions: [Version(1, 0, 0)], + minimumVersion: Version(1, 0, 0), + maximumMajor: 1, + ); + expect(check.isSupported, isTrue); + + check = VersionCheck( + versions: [Version(1, 5, 0)], + minimumVersion: Version(1, 0, 0), + maximumMajor: 1, + ); + expect(check.isSupported, isTrue); + + check = VersionCheck( + versions: [Version(1, 9, 9)], + minimumVersion: Version(1, 0, 0), + maximumMajor: 1, + ); + expect(check.isSupported, isTrue); + + check = VersionCheck( + versions: [Version(2, 0, 0)], + minimumVersion: Version(1, 0, 0), + maximumMajor: 1, + ); + expect(check.isSupported, isFalse); + }); + + test('Without maximumMajor', () { + var check = VersionCheck( + versions: [Version(0, 9, 9)], + minimumVersion: Version(1, 0, 0), + maximumMajor: null, + ); + expect(check.isSupported, isFalse); + + check = VersionCheck( + versions: [Version(1, 5, 0)], + minimumVersion: Version(1, 0, 0), + maximumMajor: null, + ); + expect(check.isSupported, isTrue); + + check = VersionCheck( + versions: [Version(2, 0, 0)], + minimumVersion: Version(1, 0, 0), + maximumMajor: null, + ); + expect(check.isSupported, isFalse); + }); + }); +}