diff --git a/packages/dynamite/lib/src/openapi_builder.dart b/packages/dynamite/lib/src/openapi_builder.dart index 02a037d5..59b01ddd 100644 --- a/packages/dynamite/lib/src/openapi_builder.dart +++ b/packages/dynamite/lib/src/openapi_builder.dart @@ -732,7 +732,7 @@ class OpenAPIBuilder implements Builder { ], Method( (final b) => b - ..name = '_doRequest' + ..name = 'doRequest' ..returns = refer('Future') ..modifier = MethodModifier.async ..requiredParameters.addAll([ @@ -779,7 +779,7 @@ class OpenAPIBuilder implements Builder { ..fields.add( Field( (final b) => b - ..name = '_client' + ..name = 'rootClient' ..type = refer('Client') ..modifier = FieldModifier.final$, ), @@ -789,7 +789,7 @@ class OpenAPIBuilder implements Builder { (final b) => b.requiredParameters.add( Parameter( (final b) => b - ..name = '_client' + ..name = 'rootClient' ..toThis = true, ), ), @@ -940,7 +940,7 @@ class OpenAPIBuilder implements Builder { code.write( ''' - final response = await ${isRootClient ? '' : '_client.'}_doRequest( + final response = await ${isRootClient ? '' : 'rootClient.'}doRequest( '$httpMethod', Uri(path: path, queryParameters: queryParameters).toString(), headers, diff --git a/packages/neon/lib/l10n/en.arb b/packages/neon/lib/l10n/en.arb index 76e1ccf5..251c5bba 100644 --- a/packages/neon/lib/l10n/en.arb +++ b/packages/neon/lib/l10n/en.arb @@ -29,11 +29,11 @@ } } }, - "errorUnsupportedNextcloudVersion": "Sorry this Nextcloud instance version is not supported. You need at least version {version} of Nextcloud.", - "@errorUnsupportedNextcloudVersion" : { + "errorUnsupportedVersion": "Sorry, this Nextcloud {name} version is not supported.", + "@errorUnsupportedVersion" : { "placeholders": { - "version": { - "type": "int" + "name": { + "type": "String" } } }, @@ -122,6 +122,7 @@ "accountOptionsInitialApp": "App to show initially", "accountOptionsAutomatic": "Automatic", "licenses": "Licenses", + "coreName": "Server", "filesName": "Files", "filesUploadFiles": "Upload files", "filesUploadImages": "Upload images", diff --git a/packages/neon/lib/l10n/localizations.dart b/packages/neon/lib/l10n/localizations.dart index 606eb57e..24c1e57d 100644 --- a/packages/neon/lib/l10n/localizations.dart +++ b/packages/neon/lib/l10n/localizations.dart @@ -179,11 +179,11 @@ abstract class AppLocalizations { /// **'Permission for {name} is missing'** String errorMissingPermission(String name); - /// No description provided for @errorUnsupportedNextcloudVersion. + /// No description provided for @errorUnsupportedVersion. /// /// In en, this message translates to: - /// **'Sorry this Nextcloud instance version is not supported. You need at least version {version} of Nextcloud.'** - String errorUnsupportedNextcloudVersion(int version); + /// **'Sorry, this Nextcloud {name} version is not supported.'** + String errorUnsupportedVersion(String name); /// No description provided for @errorEmptyField. /// @@ -533,6 +533,12 @@ abstract class AppLocalizations { /// **'Licenses'** String get licenses; + /// No description provided for @coreName. + /// + /// In en, this message translates to: + /// **'Server'** + String get coreName; + /// No description provided for @filesName. /// /// In en, this message translates to: diff --git a/packages/neon/lib/l10n/localizations_en.dart b/packages/neon/lib/l10n/localizations_en.dart index 6ca3f67a..d4497969 100644 --- a/packages/neon/lib/l10n/localizations_en.dart +++ b/packages/neon/lib/l10n/localizations_en.dart @@ -57,8 +57,8 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String errorUnsupportedNextcloudVersion(int version) { - return 'Sorry this Nextcloud instance version is not supported. You need at least version $version of Nextcloud.'; + String errorUnsupportedVersion(String name) { + return 'Sorry, this Nextcloud $name version is not supported.'; } @override @@ -243,6 +243,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get licenses => 'Licenses'; + @override + String get coreName => 'Server'; + @override String get filesName => 'Files'; diff --git a/packages/neon/lib/src/blocs/apps.dart b/packages/neon/lib/src/blocs/apps.dart index d5f0173d..a8b9e1e5 100644 --- a/packages/neon/lib/src/blocs/apps.dart +++ b/packages/neon/lib/src/blocs/apps.dart @@ -54,7 +54,7 @@ class AppsBloc extends $AppsBloc { ); } else if (result is ResultCached && result.data != null) { _appImplementationsSubject.add( - Result.success(_filteredAppImplementations((result as ResultCached>).data)), + ResultCached(_filteredAppImplementations((result as ResultCached>).data)), ); } diff --git a/packages/neon/lib/src/pages/home/home.dart b/packages/neon/lib/src/pages/home/home.dart index b062e2d1..55083d6b 100644 --- a/packages/neon/lib/src/pages/home/home.dart +++ b/packages/neon/lib/src/pages/home/home.dart @@ -54,26 +54,43 @@ class _HomePageState extends State with tray.TrayListener, WindowListe // ignore cached version and prevent duplicate dialogs if (result is ResultSuccess) { - const requiredMajorVersion = 24; - if (result.data!.version!.major! < requiredMajorVersion) { - await showDialog( - context: context, - builder: (final context) => AlertDialog( - title: Text(AppLocalizations.of(context).errorUnsupportedNextcloudVersion(requiredMajorVersion)), - actions: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - ), - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(AppLocalizations.of(context).close), - ), - ], - ), - ); - } + _appsBloc.appImplementations.listen((final appsResult) async { + // ignore cached version and prevent duplicate dialogs + if (appsResult is ResultSuccess) { + for (final id in [ + 'core', + ...appsResult.data!.map((final a) => a.id), + ]) { + try { + bool? supported; + switch (id) { + case 'core': + supported = await widget.account.client.core.isSupported(result.data!); + break; + case 'news': + supported = await widget.account.client.news.isSupported(); + break; + case 'notes': + supported = await widget.account.client.notes.isSupported(result.data!); + break; + } + if (!(supported ?? true)) { + if (!mounted) { + return; + } + await _showUnsupportedVersion( + id == 'core' + ? AppLocalizations.of(context).coreName + : appsResult.data!.singleWhere((final a) => a.id == id).name(context), + ); + } + } catch (e, s) { + debugPrint(e.toString()); + debugPrint(s.toString()); + } + } + } + }); } } }); @@ -259,6 +276,26 @@ class _HomePageState extends State with tray.TrayListener, WindowListe } } + Future _showUnsupportedVersion(final String appName) async { + await showDialog( + context: context, + builder: (final context) => AlertDialog( + title: Text(AppLocalizations.of(context).errorUnsupportedVersion(appName)), + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(AppLocalizations.of(context).close), + ), + ], + ), + ); + } + @override void dispose() { _capabilitiesBloc.dispose(); diff --git a/packages/neon/pubspec.lock b/packages/neon/pubspec.lock index bd76ca16..159d239f 100644 --- a/packages/neon/pubspec.lock +++ b/packages/neon/pubspec.lock @@ -1226,6 +1226,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + version: + dependency: transitive + description: + name: version + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" vm_service: dependency: transitive description: diff --git a/packages/nextcloud/lib/nextcloud.dart b/packages/nextcloud/lib/nextcloud.dart index ab075061..c92b3394 100644 --- a/packages/nextcloud/lib/nextcloud.dart +++ b/packages/nextcloud/lib/nextcloud.dart @@ -7,6 +7,7 @@ import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:intl/intl.dart'; import 'package:nextcloud/nextcloud.dart'; +import 'package:version/version.dart'; import 'package:xml/xml.dart' as xml; export 'package:crypton/crypton.dart' show RSAKeypair, RSAPublicKey, RSAPrivateKey; @@ -16,6 +17,7 @@ export 'src/nextcloud.openapi.dart'; part 'src/app_type.dart'; part 'src/client.dart'; part 'src/helpers.dart'; +part 'src/version_supported.dart'; part 'src/webdav/client.dart'; part 'src/webdav/file.dart'; part 'src/webdav/props.dart'; diff --git a/packages/nextcloud/lib/src/client.dart b/packages/nextcloud/lib/src/client.dart index dd756ce8..4e407e44 100644 --- a/packages/nextcloud/lib/src/client.dart +++ b/packages/nextcloud/lib/src/client.dart @@ -41,9 +41,8 @@ class NextcloudClient extends Client { } return _webdav = WebDavClient( - super.baseURL, - basePath: '/remote.php/dav/files/${(super.authentication! as HttpBasicAuthentication).username}', - baseHeaders: super.baseHeaders, + this, + '/remote.php/dav/files/${(super.authentication! as HttpBasicAuthentication).username}', ); } } diff --git a/packages/nextcloud/lib/src/nextcloud.openapi.dart b/packages/nextcloud/lib/src/nextcloud.openapi.dart index 593fc021..a9312f59 100644 --- a/packages/nextcloud/lib/src/nextcloud.openapi.dart +++ b/packages/nextcloud/lib/src/nextcloud.openapi.dart @@ -88,7 +88,7 @@ class Client { NotificationsClient get notifications => NotificationsClient(this); ProvisioningApiClient get provisioningApi => ProvisioningApiClient(this); UserStatusClient get userStatus => UserStatusClient(this); - Future _doRequest( + Future doRequest( String method, String path, Map headers, @@ -1342,16 +1342,16 @@ class CoreLoginFlowResult { } class CoreClient { - CoreClient(this._client); + CoreClient(this.rootClient); - final Client _client; + final Client rootClient; Future getStatus() async { var path = '/status.php'; final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1368,7 +1368,7 @@ class CoreClient { final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1385,7 +1385,7 @@ class CoreClient { final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1402,7 +1402,7 @@ class CoreClient { final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'post', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1420,7 +1420,7 @@ class CoreClient { final headers = {}; Uint8List? body; queryParameters['token'] = token.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'post', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1450,7 +1450,7 @@ class CoreClient { queryParameters['a'] = a.toString(); queryParameters['forceIcon'] = forceIcon.toString(); queryParameters['mode'] = mode.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1472,7 +1472,7 @@ class CoreClient { Uint8List? body; path = path.replaceAll('{userId}', Uri.encodeQueryComponent(userId.toString())); path = path.replaceAll('{size}', Uri.encodeQueryComponent(size.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1485,6 +1485,17 @@ class CoreClient { } } +@JsonSerializable() +class GetSupportedApiVersions { + GetSupportedApiVersions({this.apiLevels}); + + factory GetSupportedApiVersions.fromJson(Map json) => _$GetSupportedApiVersionsFromJson(json); + + final List? apiLevels; + + Map toJson() => _$GetSupportedApiVersionsToJson(this); +} + @JsonSerializable() class NewsArticle { NewsArticle({ @@ -1669,16 +1680,33 @@ class NewsListArticles { } class NewsClient { - NewsClient(this._client); + NewsClient(this.rootClient); + + final Client rootClient; - final Client _client; + Future getSupportedApiVersions() async { + var path = '/apps/news/api'; + final queryParameters = {}; + final headers = {}; + Uint8List? body; + final response = await rootClient.doRequest( + 'get', + Uri(path: path, queryParameters: queryParameters).toString(), + headers, + body, + ); + if (response.statusCode == 200) { + return GetSupportedApiVersions.fromJson(json.decode(utf8.decode(response.body)) as Map); + } + throw ApiException.fromResponse(response); + } Future listFolders() async { var path = '/apps/news/api/v1-2/folders'; final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1696,7 +1724,7 @@ class NewsClient { final headers = {}; Uint8List? body; queryParameters['name'] = name.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'post', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1718,7 +1746,7 @@ class NewsClient { Uint8List? body; path = path.replaceAll('{folderId}', Uri.encodeQueryComponent(folderId.toString())); queryParameters['name'] = name.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1736,7 +1764,7 @@ class NewsClient { final headers = {}; Uint8List? body; path = path.replaceAll('{folderId}', Uri.encodeQueryComponent(folderId.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'delete', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1758,7 +1786,7 @@ class NewsClient { Uint8List? body; path = path.replaceAll('{folderId}', Uri.encodeQueryComponent(folderId.toString())); queryParameters['newestItemId'] = newestItemId.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1775,7 +1803,7 @@ class NewsClient { final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1799,7 +1827,7 @@ class NewsClient { if (folderId != null) { queryParameters['folderId'] = folderId.toString(); } - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'post', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1817,7 +1845,7 @@ class NewsClient { final headers = {}; Uint8List? body; path = path.replaceAll('{feedId}', Uri.encodeQueryComponent(feedId.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'delete', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1841,7 +1869,7 @@ class NewsClient { if (folderId != null) { queryParameters['folderId'] = folderId.toString(); } - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1863,7 +1891,7 @@ class NewsClient { Uint8List? body; path = path.replaceAll('{feedId}', Uri.encodeQueryComponent(feedId.toString())); queryParameters['feedTitle'] = feedTitle.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1885,7 +1913,7 @@ class NewsClient { Uint8List? body; path = path.replaceAll('{feedId}', Uri.encodeQueryComponent(feedId.toString())); queryParameters['newestItemId'] = newestItemId.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1915,7 +1943,7 @@ class NewsClient { queryParameters['batchSize'] = batchSize.toString(); queryParameters['offset'] = offset.toString(); queryParameters['oldestFirst'] = oldestFirst.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1939,7 +1967,7 @@ class NewsClient { queryParameters['type'] = type.toString(); queryParameters['id'] = id.toString(); queryParameters['lastModified'] = lastModified.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1957,7 +1985,7 @@ class NewsClient { final headers = {}; Uint8List? body; path = path.replaceAll('{itemId}', Uri.encodeQueryComponent(itemId.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1975,7 +2003,7 @@ class NewsClient { final headers = {}; Uint8List? body; path = path.replaceAll('{itemId}', Uri.encodeQueryComponent(itemId.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -1997,7 +2025,7 @@ class NewsClient { Uint8List? body; path = path.replaceAll('{feedId}', Uri.encodeQueryComponent(feedId.toString())); path = path.replaceAll('{guidHash}', Uri.encodeQueryComponent(guidHash.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2019,7 +2047,7 @@ class NewsClient { Uint8List? body; path = path.replaceAll('{feedId}', Uri.encodeQueryComponent(feedId.toString())); path = path.replaceAll('{guidHash}', Uri.encodeQueryComponent(guidHash.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2112,9 +2140,9 @@ class NotesSettings { } class NotesClient { - NotesClient(this._client); + NotesClient(this.rootClient); - final Client _client; + final Client rootClient; Future> getNotes({ String? category, @@ -2140,7 +2168,7 @@ class NotesClient { if (ifNoneMatch != null) { headers['If-None-Match'] = ifNoneMatch.toString(); } - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2170,7 +2198,7 @@ class NotesClient { queryParameters['content'] = content.toString(); queryParameters['modified'] = modified.toString(); queryParameters['favorite'] = favorite.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'post', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2196,7 +2224,7 @@ class NotesClient { if (ifNoneMatch != null) { headers['If-None-Match'] = ifNoneMatch.toString(); } - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2238,7 +2266,7 @@ class NotesClient { if (ifMatch != null) { headers['If-Match'] = ifMatch.toString(); } - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2256,7 +2284,7 @@ class NotesClient { final headers = {}; Uint8List? body; path = path.replaceAll('{id}', Uri.encodeQueryComponent(id.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'delete', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2273,7 +2301,7 @@ class NotesClient { final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2292,7 +2320,7 @@ class NotesClient { Uint8List? body; headers['Content-Type'] = 'application/json'; body = Uint8List.fromList(utf8.encode(json.encode((notesSettings as NotesSettings).toJson()))); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2524,16 +2552,16 @@ class NotificationsPushServerRegistration { } class NotificationsClient { - NotificationsClient(this._client); + NotificationsClient(this.rootClient); - final Client _client; + final Client rootClient; Future listNotifications() async { var path = '/ocs/v1.php/apps/notifications/api/v2/notifications'; final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2550,7 +2578,7 @@ class NotificationsClient { final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'delete', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2568,7 +2596,7 @@ class NotificationsClient { final headers = {}; Uint8List? body; path = path.replaceAll('{id}', Uri.encodeQueryComponent(id.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2586,7 +2614,7 @@ class NotificationsClient { final headers = {}; Uint8List? body; path = path.replaceAll('{id}', Uri.encodeQueryComponent(id.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'delete', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2610,7 +2638,7 @@ class NotificationsClient { queryParameters['pushTokenHash'] = pushTokenHash.toString(); queryParameters['devicePublicKey'] = devicePublicKey.toString(); queryParameters['proxyServer'] = proxyServer.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'post', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2628,7 +2656,7 @@ class NotificationsClient { final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'delete', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2652,7 +2680,7 @@ class NotificationsClient { path = path.replaceAll('{userId}', Uri.encodeQueryComponent(userId.toString())); queryParameters['shortMessage'] = shortMessage.toString(); queryParameters['longMessage'] = longMessage.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'post', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2866,16 +2894,16 @@ class ProvisioningApiUser { } class ProvisioningApiClient { - ProvisioningApiClient(this._client); + ProvisioningApiClient(this.rootClient); - final Client _client; + final Client rootClient; Future getCurrentUser() async { var path = '/ocs/v1.php/cloud/user'; final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -2893,7 +2921,7 @@ class ProvisioningApiClient { final headers = {}; Uint8List? body; path = path.replaceAll('{userId}', Uri.encodeQueryComponent(userId.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -3321,16 +3349,16 @@ class UserStatusPredefinedStatuses { } class UserStatusClient { - UserStatusClient(this._client); + UserStatusClient(this.rootClient); - final Client _client; + final Client rootClient; Future findAllStatuses() async { var path = '/ocs/v1.php/apps/user_status/api/v1/statuses'; final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -3348,7 +3376,7 @@ class UserStatusClient { final headers = {}; Uint8List? body; path = path.replaceAll('{userId}', Uri.encodeQueryComponent(userId.toString())); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -3365,7 +3393,7 @@ class UserStatusClient { final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -3383,7 +3411,7 @@ class UserStatusClient { final headers = {}; Uint8List? body; queryParameters['statusType'] = statusType.value.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -3407,7 +3435,7 @@ class UserStatusClient { if (clearAt != null) { queryParameters['clearAt'] = clearAt.toString(); } - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -3437,7 +3465,7 @@ class UserStatusClient { if (clearAt != null) { queryParameters['clearAt'] = clearAt.toString(); } - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -3454,7 +3482,7 @@ class UserStatusClient { final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'delete', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -3471,7 +3499,7 @@ class UserStatusClient { final queryParameters = {}; final headers = {}; Uint8List? body; - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'get', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -3489,7 +3517,7 @@ class UserStatusClient { final headers = {}; Uint8List? body; queryParameters['status'] = status.value.toString(); - final response = await _client._doRequest( + final response = await rootClient.doRequest( 'put', Uri(path: path, queryParameters: queryParameters).toString(), headers, @@ -3894,6 +3922,10 @@ final _deserializers = { List: (final data) => (data as List) .map((final e) => CoreLoginFlowResult.fromJson(e as Map)) .toList(), + GetSupportedApiVersions: (final data) => GetSupportedApiVersions.fromJson(data as Map), + List: (final data) => (data as List) + .map((final e) => GetSupportedApiVersions.fromJson(e as Map)) + .toList(), NewsListFolders: (final data) => NewsListFolders.fromJson(data as Map), List: (final data) => (data as List).map((final e) => NewsListFolders.fromJson(e as Map)).toList(), @@ -4376,6 +4408,9 @@ final _serializers = { CoreLoginFlowResult: (final data) => (data as CoreLoginFlowResult).toJson(), List: (final data) => (data as List).map((final e) => (e as CoreLoginFlowResult).toJson()).toList(), + GetSupportedApiVersions: (final data) => (data as GetSupportedApiVersions).toJson(), + List: (final data) => + (data as List).map((final e) => (e as GetSupportedApiVersions).toJson()).toList(), NewsListFolders: (final data) => (data as NewsListFolders).toJson(), List: (final data) => (data as List).map((final e) => (e as NewsListFolders).toJson()).toList(), diff --git a/packages/nextcloud/lib/src/nextcloud.openapi.g.dart b/packages/nextcloud/lib/src/nextcloud.openapi.g.dart index 84e60386..f3a750b3 100644 --- a/packages/nextcloud/lib/src/nextcloud.openapi.g.dart +++ b/packages/nextcloud/lib/src/nextcloud.openapi.g.dart @@ -1036,6 +1036,14 @@ Map _$CoreLoginFlowResultToJson(CoreLoginFlowResult instance) = 'appPassword': instance.appPassword, }; +GetSupportedApiVersions _$GetSupportedApiVersionsFromJson(Map json) => GetSupportedApiVersions( + apiLevels: (json['apiLevels'] as List?)?.map((e) => e as String).toList(), + ); + +Map _$GetSupportedApiVersionsToJson(GetSupportedApiVersions instance) => { + 'apiLevels': instance.apiLevels, + }; + NewsArticle _$NewsArticleFromJson(Map json) => NewsArticle( id: json['id'] as int?, guid: json['guid'] as String?, diff --git a/packages/nextcloud/lib/src/nextcloud.openapi.json b/packages/nextcloud/lib/src/nextcloud.openapi.json index 1249ec74..2cbdb43f 100644 --- a/packages/nextcloud/lib/src/nextcloud.openapi.json +++ b/packages/nextcloud/lib/src/nextcloud.openapi.json @@ -1819,6 +1819,34 @@ } } }, + "/apps/news/api": { + "get": { + "operationId": "get-supported-api-versions", + "tags": [ + "news" + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "apiLevels": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, "/apps/news/api/v1-2/folders": { "get": { "operationId": "list-folders", diff --git a/packages/nextcloud/lib/src/version_supported.dart b/packages/nextcloud/lib/src/version_supported.dart new file mode 100644 index 00000000..cdc323c4 --- /dev/null +++ b/packages/nextcloud/lib/src/version_supported.dart @@ -0,0 +1,32 @@ +part of '../nextcloud.dart'; + +// ignore: public_member_api_docs +extension CoreVersionSupported on CoreClient { + /// Checks if the app on the server is supported by the client + Future isSupported([final CoreServerCapabilitiesOcsData? capabilities]) async => + (capabilities ?? (await rootClient.core.getCapabilities()).ocs!.data!).version!.major! == 24; +} + +// ignore: public_member_api_docs +extension NewsVersionSupported on NewsClient { + /// Checks if the app on the server is supported by the client + Future isSupported() async { + final versions = await getSupportedApiVersions(); + return versions.apiLevels!.contains('v1-2'); + } +} + +// ignore: public_member_api_docs +extension NotesVersionSupported on NotesClient { + /// Checks if the app on the server is supported by the client + Future isSupported([final CoreServerCapabilitiesOcsData? capabilities]) async => + (capabilities ?? (await rootClient.core.getCapabilities()).ocs!.data!) + .capabilities! + .notes! + .apiVersion! + .map(Version.parse) + .where((final version) => version.major == 1) + .isNotEmpty; +} + +// Notifications, ProvisioningApi, UserStatus and Webdav are shipped with the Nextcloud server, so their supported versions depend on the major version of the Nextcloud instance. diff --git a/packages/nextcloud/lib/src/webdav/client.dart b/packages/nextcloud/lib/src/webdav/client.dart index 3c0d95ba..2d3633d6 100644 --- a/packages/nextcloud/lib/src/webdav/client.dart +++ b/packages/nextcloud/lib/src/webdav/client.dart @@ -4,20 +4,16 @@ part of '../../nextcloud.dart'; class WebDavClient { // ignore: public_member_api_docs WebDavClient( - this.baseUrl, { - this.basePath = '', - this.baseHeaders, - }); + this.rootClient, + this.basePath, + ); - /// Base URL of the server - final String baseUrl; + // ignore: public_member_api_docs + final Client rootClient; /// Base path used on the server final String basePath; - /// Headers added to each request. Useful for authentication - final Map? baseHeaders; - /// XML namespaces supported by this client. /// /// For Nextcloud namespaces see [WebDav/Requesting properties](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/basic.html#requesting-properties). @@ -45,7 +41,7 @@ class WebDavClient { ..persistentConnection = true; for (final header in { HttpHeaders.contentTypeHeader: 'application/xml', - if (baseHeaders != null) ...baseHeaders!, + ...rootClient.baseHeaders, if (headers != null) ...headers, }.entries) { request.headers.add(header.key, header.value); @@ -75,7 +71,7 @@ class WebDavClient { namespaces.putIfAbsent(namespaceUri, () => prefix); String _constructPath([final String? path]) => [ - baseUrl, + rootClient.baseURL, basePath, if (path != null) ...[ path, diff --git a/packages/nextcloud/pubspec.yaml b/packages/nextcloud/pubspec.yaml index cfb29ca7..3c5054e2 100644 --- a/packages/nextcloud/pubspec.yaml +++ b/packages/nextcloud/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: intl: ^0.17.0 json_annotation: ^4.6.0 meta: ^1.7.0 + version: ^3.0.2 xml: ^6.1.0 dev_dependencies: diff --git a/packages/nextcloud/test/core_test.dart b/packages/nextcloud/test/core_test.dart index dccdba27..d1f08a90 100644 --- a/packages/nextcloud/test/core_test.dart +++ b/packages/nextcloud/test/core_test.dart @@ -1,3 +1,4 @@ +import 'package:nextcloud/nextcloud.dart'; import 'package:test/test.dart'; import 'helper.dart'; @@ -17,6 +18,11 @@ Future main() async { }); tearDown(() => client.destroy()); + test('Is supported', () async { + final response = await client.core.isSupported(); + expect(response, isTrue); + }); + test('Get status', () async { final status = await client.core.getStatus(); expect(status.installed, true); diff --git a/packages/nextcloud/test/news_test.dart b/packages/nextcloud/test/news_test.dart index 5ca0b713..1cc3d6e3 100644 --- a/packages/nextcloud/test/news_test.dart +++ b/packages/nextcloud/test/news_test.dart @@ -25,6 +25,11 @@ Future main() async { url: nasaFeedURL, ); + test('Is supported', () async { + final response = await client.news.isSupported(); + expect(response, isTrue); + }); + test('Add feed', () async { var response = await client.news.listFeeds(); expect(response.starredCount, 0); diff --git a/packages/nextcloud/test/notes_test.dart b/packages/nextcloud/test/notes_test.dart index c974cb36..f3f1b0d9 100644 --- a/packages/nextcloud/test/notes_test.dart +++ b/packages/nextcloud/test/notes_test.dart @@ -13,6 +13,11 @@ Future main() async { }); tearDown(() => client.destroy()); + test('Is supported', () async { + final response = await client.notes.isSupported(); + expect(response, isTrue); + }); + test('Create note favorite', () async { final response = await client.notes.createNote( title: 'a', diff --git a/specs/news.json b/specs/news.json index d384bcef..f27d15bd 100644 --- a/specs/news.json +++ b/specs/news.json @@ -236,6 +236,34 @@ } }, "paths": { + "/apps/news/api": { + "get": { + "operationId": "get-supported-api-versions", + "tags": [ + "news" + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "apiLevels": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, "/apps/news/api/v1-2/folders": { "get": { "operationId": "list-folders",