From 587cca92a4fbeafa89e52789ee6c5b87cf0712f6 Mon Sep 17 00:00:00 2001 From: Tim Alenus Date: Thu, 15 May 2025 15:07:44 +0200 Subject: [PATCH 01/12] feat: expose downloaded manifest statically --- lib/src/crowdin.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/crowdin.dart b/lib/src/crowdin.dart index b1363ab..d75a278 100644 --- a/lib/src/crowdin.dart +++ b/lib/src/crowdin.dart @@ -50,6 +50,9 @@ class Crowdin { static bool get withRealTimeUpdates => _withRealTimeUpdates; + /// contains the manifest if it has been fetched before + static Map? manifest; + @visibleForTesting static set withRealTimeUpdates(bool value) { _withRealTimeUpdates = value; @@ -93,6 +96,7 @@ class Crowdin { var manifest = await _api.getManifest(distributionHash: _distributionHash); if (manifest != null) { + Crowdin.manifest = manifest; _distributionsMap = manifest['content']; /// fetch manifest file to check if new updates available From c214fbe5594eb98a29f562050cfaf7814d927961 Mon Sep 17 00:00:00 2001 From: Tim Alenus Date: Thu, 15 May 2025 16:38:32 +0200 Subject: [PATCH 02/12] fix: make sure manifest cannot be set from outside package --- lib/src/crowdin.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/crowdin.dart b/lib/src/crowdin.dart index d75a278..adf7233 100644 --- a/lib/src/crowdin.dart +++ b/lib/src/crowdin.dart @@ -50,8 +50,10 @@ class Crowdin { static bool get withRealTimeUpdates => _withRealTimeUpdates; + static Map? _manifest; + /// contains the manifest if it has been fetched before - static Map? manifest; + static Map? get manifest => _manifest; @visibleForTesting static set withRealTimeUpdates(bool value) { @@ -96,7 +98,7 @@ class Crowdin { var manifest = await _api.getManifest(distributionHash: _distributionHash); if (manifest != null) { - Crowdin.manifest = manifest; + _manifest = manifest; _distributionsMap = manifest['content']; /// fetch manifest file to check if new updates available From 00bc7c643fcf227cc13f3d1301f60a454da1410f Mon Sep 17 00:00:00 2001 From: Tim Alenus Date: Thu, 15 May 2025 17:07:05 +0200 Subject: [PATCH 03/12] feat: check if locale is supported when calling loadTranslations and manifest is set --- lib/src/crowdin.dart | 33 +++++++ lib/src/crowdin_mapper.dart | 10 ++ test/crowdin_manifest_test.dart | 161 ++++++++++++++++++++++++++++++++ test/crowdin_mapper_test.dart | 39 ++++++-- 4 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 test/crowdin_manifest_test.dart diff --git a/lib/src/crowdin.dart b/lib/src/crowdin.dart index adf7233..e07ff4c 100644 --- a/lib/src/crowdin.dart +++ b/lib/src/crowdin.dart @@ -5,6 +5,7 @@ import 'package:crowdin_sdk/src/crowdin_api.dart'; import 'package:crowdin_sdk/src/crowdin_storage.dart'; import 'package:crowdin_sdk/src/crowdin_extractor.dart'; import 'package:crowdin_sdk/src/crowdin_mapper.dart'; +import 'package:crowdin_sdk/src/exceptions/crowdin_exceptions.dart'; import 'package:crowdin_sdk/src/real_time_preview/crowdin_preview_manager.dart'; import 'package:flutter/widgets.dart'; @@ -55,6 +56,11 @@ class Crowdin { /// contains the manifest if it has been fetched before static Map? get manifest => _manifest; + @visibleForTesting + static set manifest(Map? value) { + _manifest = value; + } + @visibleForTesting static set withRealTimeUpdates(bool value) { _withRealTimeUpdates = value; @@ -118,10 +124,37 @@ class Crowdin { } } + @visibleForTesting + static void checkManifestForLocale(Locale locale) { + if (manifest == null) { + throw CrowdinException( + 'Crowdin manifest is not set. Please call Crowdin.init() before loading translations.'); + } + + final mappedLocale = CrowdinMapper.mapLocale(locale); + + var languages = (manifest!['languages'] as List? ?? []) + .map((v) => CrowdinMapper.localeFromLanguageCode(v.toString())); + var customLanguages = (manifest!['customLanguages'] as List? ?? []) + .map((v) => CrowdinMapper.localeFromLanguageCode(v.toString())); + var allLanguages = [...languages, ...customLanguages]; + + allLanguages.firstWhere( + (l) => l.toLanguageTag() == mappedLocale.toLanguageTag(), + orElse: () => allLanguages.firstWhere( + (l2) => l2.languageCode == mappedLocale.languageCode, + orElse: () => throw CrowdinException( + 'Locale ${locale.toLanguageTag()} is not supported for this Crowdin project.'))); + } + /// Load translations from Crowdin for a specific locale static Future loadTranslations(Locale locale) async { Map? distribution; + if (manifest != null) { + checkManifestForLocale(locale); + } + if (!await _isConnectionTypeAllowed(_connectionType)) { _arb = null; return; // return from function if connection type is forbidden for downloading translations diff --git a/lib/src/crowdin_mapper.dart b/lib/src/crowdin_mapper.dart index 469eaa6..c0cbbe6 100644 --- a/lib/src/crowdin_mapper.dart +++ b/lib/src/crowdin_mapper.dart @@ -11,6 +11,16 @@ class CrowdinMapper { : locale; } + /// Maps a lanugage code to a [Locale] object. + /// Reference https://support.crowdin.com/developer/language-codes/ + static Locale localeFromLanguageCode(String languageCode) { + final parts = languageCode.split('-'); + final lang = parts.first; + final country = parts.length > 1 ? parts.last : null; + return mapLocale( + Locale.fromSubtags(languageCode: lang, countryCode: country)); + } + // _localesMap contains language codes that is different on Crowdin and supported by GlobalMaterialLocalizations class static const Map _localesMap = { 'hy': 'hy-AM', // Armenian diff --git a/test/crowdin_manifest_test.dart b/test/crowdin_manifest_test.dart new file mode 100644 index 0000000..81e933d --- /dev/null +++ b/test/crowdin_manifest_test.dart @@ -0,0 +1,161 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:crowdin_sdk/src/crowdin.dart'; +import 'package:crowdin_sdk/src/exceptions/crowdin_exceptions.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + setUp(() { + var manifest = jsonDecode(''' + { + "files": [ + "/develop/lib/localization/locales/example_%locale_with_underscore%.arb" + ], + "languages": [ + "zh-CN", + "nl", + "nl-BE", + "en", + "en-GB", + "fi", + "fr", + "de", + "it", + "ja", + "pt", + "ru", + "es", + "sv", + "tr" + ], + "language_mapping": { + "nl": { + "locale_with_underscore": "nl" + }, + "fi": { + "locale_with_underscore": "fi" + }, + "fr": { + "locale_with_underscore": "fr" + }, + "de": { + "locale_with_underscore": "de" + }, + "it": { + "locale_with_underscore": "it" + }, + "ja": { + "locale_with_underscore": "ja" + }, + "pt": { + "locale_with_underscore": "pt" + }, + "ru": { + "locale_with_underscore": "ru" + }, + "es": { + "locale_with_underscore": "es" + }, + "sv": { + "locale_with_underscore": "sv" + }, + "tr": { + "locale_with_underscore": "tr" + }, + "zh-CN": { + "locale_with_underscore": "zh" + } + }, + "custom_languages": [], + "timestamp": 1747301471, + "content": { + "zh-CN": [ + "/content/develop/lib/localization/locales/example_zh.arb" + ], + "nl": [ + "/content/develop/lib/localization/locales/example_nl.arb" + ], + "nl-BE": [ + "/content/develop/lib/localization/locales/example_nl_BE.arb" + ], + "en": [ + "/content/develop/lib/localization/locales/example_en_US.arb" + ], + "en-GB": [ + "/content/develop/lib/localization/locales/example_en_GB.arb" + ], + "en-US": [ + "/content/develop/lib/localization/locales/example_en_US.arb" + ], + "fi": [ + "/content/develop/lib/localization/locales/example_fi.arb" + ], + "fr": [ + "/content/develop/lib/localization/locales/example_fr.arb" + ], + "de": [ + "/content/develop/lib/localization/locales/example_de.arb" + ], + "it": [ + "/content/develop/lib/localization/locales/example_it.arb" + ], + "ja": [ + "/content/develop/lib/localization/locales/example_ja.arb" + ], + "pt": [ + "/content/develop/lib/localization/locales/example_pt.arb" + ], + "ru": [ + "/content/develop/lib/localization/locales/example_ru.arb" + ], + "es": [ + "/content/develop/lib/localization/locales/example_es.arb" + ], + "sv": [ + "/content/develop/lib/localization/locales/example_sv.arb" + ], + "tr": [ + "/content/develop/lib/localization/locales/example_tr.arb" + ] + }, + "mapping": [ + "/mapping/develop/lib/localization/locales/example_en_US.arb" + ] + } +'''); + Crowdin.manifest = manifest; + }); + + group('Crowdin.checkManifestForLocale', () { + test('should throw if locale is not supported according to manifest', () { + expect( + () => Crowdin.checkManifestForLocale(const Locale('xx')), + throwsA(isA()), + ); + }); + test('should not throw if locale is supported according to manifest', () { + expect(() => Crowdin.checkManifestForLocale(const Locale('es')), + isA()); + }); + test( + 'should succeed and fallback on language code only if locale with both language and country code is not found', + () { + expect(() => Crowdin.checkManifestForLocale(const Locale('en', 'US')), + isA()); + }); + test('should throw if manifest not set', () { + Crowdin.manifest = null; + expect(() => Crowdin.checkManifestForLocale(const Locale('en')), + throwsA(isA())); + }); + }); + + group('Crowdin.loadTranslations', () { + test('should not throw if manifest not set', () async { + Crowdin.manifest = null; + expect(() async => await Crowdin.loadTranslations(const Locale('en')), + isA()); + }); + }); +} diff --git a/test/crowdin_mapper_test.dart b/test/crowdin_mapper_test.dart index 2c61173..5a36511 100644 --- a/test/crowdin_mapper_test.dart +++ b/test/crowdin_mapper_test.dart @@ -3,15 +3,40 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - test('should return same locale when locale is not in map', () { - expect(CrowdinMapper.mapLocale(const Locale('en')), const Locale('en')); - }); + group('CrowdinMapper.mapLocale', () { + test('should return same locale when locale is not in map', () { + expect(CrowdinMapper.mapLocale(const Locale('en')), const Locale('en')); + }); + + test('should map locale correctly when language tag is used', () { + expect( + CrowdinMapper.mapLocale(const Locale('hy')), const Locale('hy-AM')); + }); - test('should map locale correctly when language tag is used', () { - expect(CrowdinMapper.mapLocale(const Locale('hy')), const Locale('hy-AM')); + test('should return same locale when language tag is not in map', () { + expect(CrowdinMapper.mapLocale(const Locale('ja')), const Locale('ja')); + }); }); - test('should return same locale when language tag is not in map', () { - expect(CrowdinMapper.mapLocale(const Locale('ja')), const Locale('ja')); + group('CrowdinMapper.localeFromLanguageCode', () { + test('should return correct Locale for language code with country', () { + expect(CrowdinMapper.localeFromLanguageCode('nl-BE'), + const Locale('nl', 'BE')); + }); + + test( + 'should return Crowdin side locale correctly when language tag is used', + () { + expect(CrowdinMapper.localeFromLanguageCode('zh'), const Locale('zh-CN')); + }); + + test('should return correct Locale for language code without country', () { + expect(CrowdinMapper.localeFromLanguageCode('en'), const Locale('en')); + }); + + test('should return same locale when language code is not in map', () { + expect(CrowdinMapper.localeFromLanguageCode('xx-YY'), + const Locale('xx', 'YY')); + }); }); } From 3cb0409f3385dcb1d541b79ff6b7209daced0bd4 Mon Sep 17 00:00:00 2001 From: Tim Alenus Date: Mon, 19 May 2025 14:08:02 +0200 Subject: [PATCH 04/12] fix: do throw CrowdinException if country code is not supported according to manifest to match crowdin API behavior --- lib/src/crowdin.dart | 6 ++---- test/crowdin_manifest_test.dart | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/src/crowdin.dart b/lib/src/crowdin.dart index e07ff4c..1b5b8a6 100644 --- a/lib/src/crowdin.dart +++ b/lib/src/crowdin.dart @@ -141,10 +141,8 @@ class Crowdin { allLanguages.firstWhere( (l) => l.toLanguageTag() == mappedLocale.toLanguageTag(), - orElse: () => allLanguages.firstWhere( - (l2) => l2.languageCode == mappedLocale.languageCode, - orElse: () => throw CrowdinException( - 'Locale ${locale.toLanguageTag()} is not supported for this Crowdin project.'))); + orElse: () => throw CrowdinException( + 'Locale ${locale.toLanguageTag()} is not a target language for this Crowdin project.')); } /// Load translations from Crowdin for a specific locale diff --git a/test/crowdin_manifest_test.dart b/test/crowdin_manifest_test.dart index 81e933d..6ee19ae 100644 --- a/test/crowdin_manifest_test.dart +++ b/test/crowdin_manifest_test.dart @@ -139,10 +139,10 @@ void main() { isA()); }); test( - 'should succeed and fallback on language code only if locale with both language and country code is not found', + 'should throw if country variant of locale is not supported according to manifest', () { expect(() => Crowdin.checkManifestForLocale(const Locale('en', 'US')), - isA()); + throwsA(isA())); }); test('should throw if manifest not set', () { Crowdin.manifest = null; From 4fadc8a5af9d877ed4df2ba0a63cd37839426e70 Mon Sep 17 00:00:00 2001 From: Tim Alenus Date: Mon, 19 May 2025 14:10:15 +0200 Subject: [PATCH 05/12] feat: also make checkManifestForLocale publicly available and not only for testing --- lib/src/crowdin.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/crowdin.dart b/lib/src/crowdin.dart index 1b5b8a6..0be82e2 100644 --- a/lib/src/crowdin.dart +++ b/lib/src/crowdin.dart @@ -124,7 +124,6 @@ class Crowdin { } } - @visibleForTesting static void checkManifestForLocale(Locale locale) { if (manifest == null) { throw CrowdinException( From eec913d21edbc7d9b1c7b6c2b510e0d74498609a Mon Sep 17 00:00:00 2001 From: Yurii Date: Tue, 20 May 2025 14:33:58 +0300 Subject: [PATCH 06/12] feat: stop requests when distribution deleted --- lib/src/crowdin.dart | 14 ++- lib/src/crowdin_api.dart | 53 ++++++++-- lib/src/crowdin_request_limiter.dart | 96 +++++++++++++++++ lib/src/crowdin_storage.dart | 50 ++++++++- test/crowdin_api_test.dart | 136 +++++++++++++++++++++++++ test/crowdin_request_limiter_test.dart | 74 ++++++++++++++ test/crowdin_storage_test.dart | 24 ++--- 7 files changed, 415 insertions(+), 32 deletions(-) create mode 100644 lib/src/crowdin_request_limiter.dart create mode 100644 test/crowdin_api_test.dart create mode 100644 test/crowdin_request_limiter_test.dart diff --git a/lib/src/crowdin.dart b/lib/src/crowdin.dart index b1363ab..11f2688 100644 --- a/lib/src/crowdin.dart +++ b/lib/src/crowdin.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:crowdin_sdk/src/crowdin_api.dart'; +import 'package:crowdin_sdk/src/crowdin_request_limiter.dart'; import 'package:crowdin_sdk/src/crowdin_storage.dart'; import 'package:crowdin_sdk/src/crowdin_extractor.dart'; import 'package:crowdin_sdk/src/crowdin_mapper.dart'; @@ -71,7 +72,9 @@ class Crowdin { }) async { await _storage.init(); - _timestampCached = _storage.getTranslationTimestampFromStorage(); + CrowdinRequestLimiter().init(_storage); + + _timestampCached = _storage.getTranslationTimestamp(); _distributionHash = distributionHash; CrowdinLogger.printLog('distributionHash $_distributionHash'); @@ -116,7 +119,8 @@ class Crowdin { static Future loadTranslations(Locale locale) async { Map? distribution; - if (!await _isConnectionTypeAllowed(_connectionType)) { + if (!await _isConnectionTypeAllowed(_connectionType) || + CrowdinRequestLimiter().pauseRequests) { _arb = null; return; // return from function if connection type is forbidden for downloading translations } @@ -129,7 +133,7 @@ class Crowdin { try { if (!canUpdate) { - distribution = _storage.getTranslationFromStorage(locale); + distribution = _storage.getTranslation(locale); if (distribution != null) { _arb = AppResourceBundle(distribution); if (_withRealTimeUpdates) { @@ -155,7 +159,7 @@ class Crowdin { /// todo remove when distribution file locale will be fixed distribution['@@locale'] = locale.toString(); - _storage.setDistributionToStorage( + _storage.setDistribution( jsonEncode(distribution), ); _arb = AppResourceBundle(distribution); @@ -166,7 +170,7 @@ class Crowdin { } if (_timestamp != null && _timestamp != _timestampCached) { - _storage.setTranslationTimeStampStorage(_timestamp!); + _storage.setTranslationTimeStamp(_timestamp!); _timestampCached = _timestamp; } } diff --git a/lib/src/crowdin_api.dart b/lib/src/crowdin_api.dart index 2b6b830..ee9a43f 100644 --- a/lib/src/crowdin_api.dart +++ b/lib/src/crowdin_api.dart @@ -1,21 +1,29 @@ import 'dart:convert'; +import 'package:crowdin_sdk/src/crowdin_request_limiter.dart'; import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; import 'crowdin_logger.dart'; class CrowdinApi { + @visibleForTesting + http.Client client = http.Client(); + @visibleForTesting + CrowdinRequestLimiter requestLimiter = CrowdinRequestLimiter(); + Future?> loadTranslations({ required String distributionHash, required String timeStamp, String? path, }) async { try { - var response = await http.get( + var response = await client.crowdinGet( Uri.https('distributions.crowdin.net', '/$distributionHash$path', {'timestamp': timeStamp}), ); - Map responseDecoded = + if (response == null) return null; + Map? responseDecoded = jsonDecode(utf8.decode(response.bodyBytes)); return responseDecoded; } catch (ex) { @@ -29,13 +37,21 @@ class CrowdinApi { required String distributionHash, }) async { try { - var response = await http.get( + var response = await client.crowdinGet( Uri.parse( 'https://distributions.crowdin.net/$distributionHash/manifest.json'), ); - Map responseDecoded = - jsonDecode(utf8.decode(response.bodyBytes)); - return responseDecoded; + if (response == null) { + return null; + } else if (response.statusCode >= 400 && response.statusCode < 500) { + requestLimiter.incrementErrorCounter(); + return null; + } else { + Map responseDecoded = + jsonDecode(utf8.decode(response.bodyBytes)); + requestLimiter.reset(); + return responseDecoded; + } } catch (ex) { CrowdinLogger.printLog( "something went wrong. Crowdin couldn't download manifest file. Next exception occurred: $ex"); @@ -48,8 +64,9 @@ class CrowdinApi { required String mappingFilePath, }) async { try { - var response = await http.get(Uri.parse( + var response = await client.crowdinGet(Uri.parse( 'https://distributions.crowdin.net/$distributionHash$mappingFilePath')); + if (response == null) return null; Map responseDecoded = jsonDecode(utf8.decode(response.bodyBytes)); return responseDecoded; @@ -68,10 +85,11 @@ class CrowdinApi { try { String organizationDomain = organizationName != null ? '$organizationName.' : ''; - var response = await http.get( + var response = await client.crowdinGet( Uri.parse( 'https://${organizationDomain}api.crowdin.com/api/v2/distributions/metadata?hash=$distributionHash'), headers: {'Authorization': 'Bearer $accessToken'}); + if (response == null) return null; Map responseDecoded = jsonDecode(utf8.decode(response.bodyBytes)); return responseDecoded; @@ -90,7 +108,7 @@ class CrowdinApi { try { String organizationDomain = organizationName != null ? '$organizationName.' : ''; - var response = await http.post( + var response = await client.crowdinPost( Uri.parse( 'https://${organizationDomain}api.crowdin.com/api/v2/user/websocket-ticket'), headers: { @@ -101,6 +119,7 @@ class CrowdinApi { "event": event, "context": {"mode": "translate"} })); + if (response == null) return null; Map responseDecoded = jsonDecode(utf8.decode(response.bodyBytes)); return responseDecoded['data']['ticket']; @@ -111,3 +130,19 @@ class CrowdinApi { } } } + +extension _CrowdinHttpInterceptorExtension on http.Client { + Future crowdinGet(Uri url, + {Map? headers}) async { + return CrowdinRequestLimiter().pauseRequests + ? null + : get(url, headers: headers); + } + + Future crowdinPost(Uri url, + {Map? headers, Object? body, Encoding? encoding}) async { + return CrowdinRequestLimiter().pauseRequests + ? null + : post(url, headers: headers, body: body, encoding: encoding); + } +} diff --git a/lib/src/crowdin_request_limiter.dart b/lib/src/crowdin_request_limiter.dart new file mode 100644 index 0000000..5101a50 --- /dev/null +++ b/lib/src/crowdin_request_limiter.dart @@ -0,0 +1,96 @@ +import 'package:crowdin_sdk/src/crowdin_storage.dart'; +import 'package:intl/intl.dart'; + +const int maxErrors = 10; +const int maxDaysInRow = 3; + +class CrowdinRequestLimiter { + CrowdinRequestLimiter._(); + + static final CrowdinRequestLimiter _instance = CrowdinRequestLimiter._(); + + factory CrowdinRequestLimiter() { + return _instance; + } + + late CrowdinStorage _storage; + + final DateFormat _formatter = DateFormat('yyyy-MM-dd'); + Map _todayErrorMap = {}; + bool _pauseRequests = false; + bool _stopPermanently = false; + + bool get pauseRequests => + _stopPermanently || _pauseRequests || _checkIsPausedForToday(); + + init(CrowdinStorage storage) { + _storage = storage; + _stopPermanently = _storage.getIsPausedPermanently() ?? false; + _todayErrorMap = _storage.getErrorMap() ?? {}; + } + + bool _checkIsPausedForToday() { + String currentDateString = _formatter.format(DateTime.now()); + if (_todayErrorMap[currentDateString] != null && + _todayErrorMap[currentDateString]! >= maxErrors) { + _pauseRequests = true; + return true; + } else { + _pauseRequests = false; + return false; + } + } + + void incrementErrorCounter() { + DateFormat formatter = DateFormat('yyyy-MM-dd'); + String currentDateString = formatter.format(DateTime.now()); + if (_todayErrorMap[currentDateString] != null) { + if (_todayErrorMap[currentDateString]! < maxErrors) { + _todayErrorMap[currentDateString] = + _todayErrorMap[currentDateString]! + 1; + } else if (_todayErrorMap[currentDateString]! >= maxErrors) { + checkPausedDays(currentDateString); + } + } else { + _todayErrorMap = {currentDateString: 1}; + } + _storage.setErrorMap(_todayErrorMap); + } + + reset() { + if (!_stopPermanently) { + _pauseRequests = false; + _todayErrorMap = {}; + _storage.setErrorMap(_todayErrorMap); + } + } + + void checkPausedDays(String newDate) { + int daysInRow = 0; + if (_todayErrorMap.length >= maxDaysInRow) { + DateTime currentDate = DateTime.parse(newDate); + for (String date in _todayErrorMap.keys) { + if (DateTime.parse(date).isAfter( + currentDate.add(const Duration(days: -maxDaysInRow))) && + _todayErrorMap[date]! >= maxErrors) { + daysInRow++; + _pauseRequests = true; + } else { + _todayErrorMap.remove(date); + } + } + if (daysInRow >= maxDaysInRow) { + _todayErrorMap.clear(); + _stopRequestsPermanently(); + } + } + _storage.setErrorMap(_todayErrorMap); + } + + void _stopRequestsPermanently() { + _pauseRequests = true; + _stopPermanently = true; + _storage.setIsPausedPermanently(true); + _storage.setErrorMap(_todayErrorMap); + } +} diff --git a/lib/src/crowdin_storage.dart b/lib/src/crowdin_storage.dart index e98db9b..6545e60 100644 --- a/lib/src/crowdin_storage.dart +++ b/lib/src/crowdin_storage.dart @@ -1,11 +1,14 @@ import 'dart:convert'; import 'dart:ui'; +import 'package:crowdin_sdk/src/crowdin_logger.dart'; import 'package:crowdin_sdk/src/exceptions/crowdin_exceptions.dart'; import 'package:shared_preferences/shared_preferences.dart'; String _kCrowdinTexts = 'crowdin_texts'; String _kTranslationTimestamp = 'translation_timestamp'; +String _kIsPausedPermanently = 'is_paused_permanently'; +String _kErrorMap = 'errorMap'; class CrowdinStorage { CrowdinStorage(); @@ -17,7 +20,7 @@ class CrowdinStorage { return _sharedPrefs; } - Future setTranslationTimeStampStorage(int? timestamp) async { + Future setTranslationTimeStamp(int? timestamp) async { try { if (_sharedPrefs.containsKey(_kTranslationTimestamp)) { await _sharedPrefs.remove(_kTranslationTimestamp); @@ -28,7 +31,7 @@ class CrowdinStorage { } } - int? getTranslationTimestampFromStorage() { + int? getTranslationTimestamp() { try { int? translationTimestamp = _sharedPrefs.getInt(_kTranslationTimestamp); return translationTimestamp; @@ -37,7 +40,7 @@ class CrowdinStorage { } } - Future setDistributionToStorage(String distribution) async { + Future setDistribution(String distribution) async { try { if (_sharedPrefs.containsKey(_kCrowdinTexts)) { await _sharedPrefs.remove(_kCrowdinTexts); @@ -48,7 +51,7 @@ class CrowdinStorage { } } - Map? getTranslationFromStorage(Locale locale) { + Map? getTranslation(Locale locale) { try { String? distributionStr = _sharedPrefs.getString(_kCrowdinTexts); if (distributionStr != null) { @@ -65,4 +68,43 @@ class CrowdinStorage { } return null; } + + void setIsPausedPermanently(bool shouldPause) { + try { + _sharedPrefs.setBool(_kIsPausedPermanently, shouldPause); + } catch (ex) { + throw CrowdinException("Can't store the isPausedPermanently value"); + } + } + + bool? getIsPausedPermanently() { + try { + bool? isPausedPermanently = _sharedPrefs.getBool(_kIsPausedPermanently); + return isPausedPermanently; + } catch (ex) { + throw CrowdinException("Can't get isPausedPermanently from storage"); + } + } + + void setErrorMap(Map errorMap) { + try { + _sharedPrefs.setString(_kErrorMap, jsonEncode(errorMap)); + } catch (ex) { + throw CrowdinException("Can't store the errorMap"); + } + } + + Map? getErrorMap() { + try { + String? errorMapString = _sharedPrefs.getString(_kErrorMap); + if (errorMapString != null) { + Map decodedMap = jsonDecode(errorMapString); + return decodedMap.map((k, v) => MapEntry(k, v as int)); + } + return errorMapString != null ? jsonDecode(errorMapString) : null; + } catch (ex) { + CrowdinLogger.printLog("Can't get errorMap from storage"); + return null; + } + } } diff --git a/test/crowdin_api_test.dart b/test/crowdin_api_test.dart new file mode 100644 index 0000000..4c330bb --- /dev/null +++ b/test/crowdin_api_test.dart @@ -0,0 +1,136 @@ +import 'package:crowdin_sdk/src/crowdin_request_limiter.dart'; +import 'package:crowdin_sdk/src/crowdin_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:crowdin_sdk/src/crowdin_api.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'crowdin_request_limiter_test.dart'; + +class MockHttpClient extends Mock implements http.Client {} + +void main() { + late CrowdinApi crowdinApi; + late MockHttpClient mockHttpClient; + late CrowdinRequestLimiter requestLimiter; + late SharedPreferences sharedPrefs; + late CrowdinStorage storage; + + setUp(() async { + mockHttpClient = MockHttpClient(); + crowdinApi = CrowdinApi(); + requestLimiter = CrowdinRequestLimiter(); + SharedPreferences.setMockInitialValues({}); + registerFallbackValue(Uri()); + crowdinApi.requestLimiter = requestLimiter; + crowdinApi.client = mockHttpClient; + sharedPrefs = await SharedPreferences.getInstance(); + storage = CrowdinStorage(); + await storage.init(); + }); + + tearDown(() async { + await sharedPrefs.clear(); + }); + + group('CrowdinApi', () { + test('loadTranslations returns decoded response on success', () async { + final uri = Uri.https( + 'distributions.crowdin.net', '/hash/path', {'timestamp': '12345'}); + when(() => mockHttpClient.get(uri)).thenAnswer( + (_) async => http.Response('{"key": "value"}', 200), + ); + + final result = await crowdinApi.loadTranslations( + distributionHash: 'hash', + timeStamp: '12345', + path: '/path', + ); + + expect(result, {'key': 'value'}); + }); + + test('getMapping returns decoded response on success', () async { + final uri = Uri.parse('https://distributions.crowdin.net/hash/path'); + when(() => mockHttpClient.get(uri)).thenAnswer( + (_) async => http.Response('{"key": "value"}', 200), + ); + + final result = await crowdinApi.getMapping( + distributionHash: 'hash', + mappingFilePath: '/path', + ); + + expect(result, {'key': 'value'}); + }); + + test('getMetadata returns decoded response on success', () async { + final uri = Uri.parse( + 'https://api.crowdin.com/api/v2/distributions/metadata?hash=hash'); + when(() => mockHttpClient.get(uri, headers: any(named: 'headers'))) + .thenAnswer( + (_) async => http.Response('{"data": {"key": "value"}}', 200), + ); + + final result = await crowdinApi.getMetadata( + accessToken: 'token', + distributionHash: 'hash', + ); + + expect(result, { + 'data': {'key': 'value'} + }); + }); + + test('getWebsocketTicket returns ticket on success', () async { + final uri = + Uri.parse('https://api.crowdin.com/api/v2/user/websocket-ticket'); + when(() => mockHttpClient.post(uri, + headers: any(named: 'headers'), body: any(named: 'body'))).thenAnswer( + (_) async => http.Response('{"data": {"ticket": "ticket_value"}}', 200), + ); + + final result = await crowdinApi.getWebsocketTicket( + accessToken: 'token', + event: 'event_name', + ); + + expect(result, 'ticket_value'); + }); + + test('getManifest returns null and increment error count on 400 status', + () async { + await requestLimiter.init(storage); + + final uri = + Uri.parse('https://distributions.crowdin.net/hash/manifest.json'); + when(() => mockHttpClient.get(uri)).thenAnswer( + (_) async => http.Response('', 400), + ); + + final result = await crowdinApi.getManifest(distributionHash: 'hash'); + + expect(result, isNull); + expect(storage.getErrorMap(), {getTodayDateString(): 1}); + }); + + test( + 'getManifest returns null and do not call request when requests paused', + () async { + storage.setIsPausedPermanently(true); + await requestLimiter.init(storage); + + final uri = + Uri.parse('https://distributions.crowdin.net/hash/manifest.json'); + when(() => mockHttpClient.get(uri)).thenAnswer( + (_) async => http.Response('', 200), + ); + + final result = await crowdinApi.getManifest(distributionHash: 'hash'); + + verifyNever(() => mockHttpClient.get(any())); + expect(result, isNull); + }); + }); +} diff --git a/test/crowdin_request_limiter_test.dart b/test/crowdin_request_limiter_test.dart new file mode 100644 index 0000000..8279f89 --- /dev/null +++ b/test/crowdin_request_limiter_test.dart @@ -0,0 +1,74 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/intl.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:crowdin_sdk/src/crowdin_request_limiter.dart'; +import 'package:crowdin_sdk/src/crowdin_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class MockCrowdinStorage extends Mock implements CrowdinStorage {} + +final DateFormat _formatter = DateFormat('yyyy-MM-dd'); + +@visibleForTesting +String getTodayDateString() { + return _formatter.format(DateTime.now()); +} + +void main() { + late CrowdinRequestLimiter requestLimiter; + late CrowdinStorage storage; + late SharedPreferences sharedPrefs; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + sharedPrefs = await SharedPreferences.getInstance(); + storage = CrowdinStorage(); + requestLimiter = CrowdinRequestLimiter(); + await storage.init(); + }); + + tearDown(() async { + await sharedPrefs.clear(); + }); + + test('should initialize with storage values', () async { + storage.setIsPausedPermanently(true); + await requestLimiter.init(storage); + expect(storage.getIsPausedPermanently(), true); + expect(requestLimiter.pauseRequests, true); + }); + + test('should increment error counter', () { + requestLimiter.init(storage); + requestLimiter.incrementErrorCounter(); + expect(storage.getErrorMap(), {getTodayDateString(): 1}); + requestLimiter.incrementErrorCounter(); + expect(storage.getErrorMap(), {getTodayDateString(): 2}); + }); + + test('should pause requests after max errors in a day', () async { + storage.setErrorMap({getTodayDateString(): 10}); + await requestLimiter.init(storage); + expect(requestLimiter.pauseRequests, true); + }); + + test('should reset error map and pause state', () async { + storage.setErrorMap({getTodayDateString(): 10}); + await requestLimiter.init(storage); + expect(requestLimiter.pauseRequests, true); + requestLimiter.reset(); + expect(requestLimiter.pauseRequests, false); + }); + + test('should stop requests permanently after max days in a row', () async { + storage.setErrorMap({ + _formatter.format(DateTime.now()): 10, + _formatter.format(DateTime.now().subtract(const Duration(days: 1))): 10, + _formatter.format(DateTime.now().subtract(const Duration(days: 2))): 10, + }); + await requestLimiter.init(storage); + requestLimiter.incrementErrorCounter(); + expect(requestLimiter.pauseRequests, true); + }); +} diff --git a/test/crowdin_storage_test.dart b/test/crowdin_storage_test.dart index 9c11dae..64f0832 100644 --- a/test/crowdin_storage_test.dart +++ b/test/crowdin_storage_test.dart @@ -24,9 +24,8 @@ void main() { test('set and get translation timestamp', () async { const int timestamp = 123456; - await crowdinStorage.setTranslationTimeStampStorage(timestamp); - final int? retrievedTimestamp = - crowdinStorage.getTranslationTimestampFromStorage(); + await crowdinStorage.setTranslationTimeStamp(timestamp); + final int? retrievedTimestamp = crowdinStorage.getTranslationTimestamp(); expect(retrievedTimestamp, equals(timestamp)); }); @@ -36,33 +35,30 @@ void main() { final Map expectedDistribution = jsonDecode(distributionJson); - await crowdinStorage.setDistributionToStorage(distributionJson); + await crowdinStorage.setDistribution(distributionJson); final Map? retrievedDistribution = - crowdinStorage.getTranslationFromStorage(const Locale('en', 'US')); + crowdinStorage.getTranslation(const Locale('en', 'US')); expect(retrievedDistribution, equals(expectedDistribution)); }); test('get exception in case of empty distribution ', () async { - await crowdinStorage.setDistributionToStorage(''); + await crowdinStorage.setDistribution(''); - expect( - () => crowdinStorage - .getTranslationFromStorage(const Locale('en', 'US')), + expect(() => crowdinStorage.getTranslation(const Locale('en', 'US')), throwsA(const TypeMatcher())); }); test('get null if timestamp is missed', () async { - final int? retrievedTimestamp = - crowdinStorage.getTranslationTimestampFromStorage(); + final int? retrievedTimestamp = crowdinStorage.getTranslationTimestamp(); expect(retrievedTimestamp, isNull); }); test('get null if distribution is missed', () async { final Map? retrievedDistribution = - crowdinStorage.getTranslationFromStorage(const Locale('en', 'US')); + crowdinStorage.getTranslation(const Locale('en', 'US')); expect(retrievedDistribution, isNull); }); @@ -70,10 +66,10 @@ void main() { test('get null if distribution locale mismatched', () async { const String distributionJson = '{"@@locale": "en_US", "hello_world": "Hello, world!"}'; - await crowdinStorage.setDistributionToStorage(distributionJson); + await crowdinStorage.setDistribution(distributionJson); final Map? retrievedDistribution = - crowdinStorage.getTranslationFromStorage(const Locale('es', 'ES')); + crowdinStorage.getTranslation(const Locale('es', 'ES')); expect(retrievedDistribution, isNull); }); From b5376b13c01fca59200cca6955994259eaf2c5d0 Mon Sep 17 00:00:00 2001 From: Yurii Date: Tue, 20 May 2025 14:35:54 +0300 Subject: [PATCH 07/12] update example project --- example/android/app/build.gradle | 37 ++- example/android/build.gradle | 24 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/settings.gradle | 30 +- example/ios/Runner/AppDelegate.swift | 13 - example/pubspec.lock | 312 ++++++++++-------- 6 files changed, 229 insertions(+), 189 deletions(-) delete mode 100644 example/ios/Runner/AppDelegate.swift diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 7f0f1a5..b2df840 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,10 +12,10 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} +//def flutterRoot = localProperties.getProperty('flutter.sdk') +//if (flutterRoot == null) { +// throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +//} def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { @@ -21,21 +27,22 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +//apply plugin: 'com.android.application' +//apply plugin: 'kotlin-android' +//apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion + ndkVersion = "27.0.12077973" + namespace = "com.example.example" compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -47,7 +54,7 @@ android { applicationId "com.example.example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion 19 + minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -66,6 +73,6 @@ flutter { source '../..' } -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} +//dependencies { +// implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +//} diff --git a/example/android/build.gradle b/example/android/build.gradle index e394c2e..4239754 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,15 +1,15 @@ -buildscript { - ext.kotlin_version = '1.8.22' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} +//buildscript { +// ext.kotlin_version = '1.8.22' +// repositories { +// google() +// mavenCentral() +// } +// +// dependencies { +// classpath 'com.android.tools.build:gradle:7.1.2' +// classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" +// } +//} allprojects { repositories { diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index cb24abd..efdcc4a 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 44e62bc..7aa4b71 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" apply true + id "com.android.application" version "8.9.0" apply false + id "org.jetbrains.kotlin.android" version "2.1.20" apply false +} + +include ":app" \ No newline at end of file diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift deleted file mode 100644 index 70693e4..0000000 --- a/example/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit -import Flutter - -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/example/pubspec.lock b/example/pubspec.lock index 82be9ee..600f5aa 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,74 +5,98 @@ packages: dependency: transitive description: name: app_links - sha256: eb83c2b15b78a66db04e95132678e910fcdb8dc3a9b0aed0c138f50b2bef0dae + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" url: "https://pub.dev" source: hosted - version: "3.4.5" + version: "6.4.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" args: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" connectivity_plus: dependency: transitive description: name: connectivity_plus - sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.1.4" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" url: "https://pub.dev" source: hosted - version: "1.2.4" + version: "2.0.1" crowdin_sdk: dependency: "direct main" description: @@ -84,50 +108,50 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.8" dbus: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.11" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -137,10 +161,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" flutter_localizations: dependency: "direct main" description: flutter @@ -156,22 +180,30 @@ packages: description: flutter source: sdk version: "0.0.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" http: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.4.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -180,30 +212,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" - js: - dependency: transitive - description: - name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" - url: "https://pub.dev" - source: hosted - version: "0.6.5" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -216,18 +240,18 @@ packages: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.1" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -240,10 +264,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" nm: dependency: transitive description: @@ -256,122 +280,122 @@ packages: dependency: transitive description: name: oauth2 - sha256: "1e8376c222651904caf7785fd2fa01b1e2be608c94bec842a94e116deca88f13" + sha256: c84470642cbb2bec450ccab2f8520c079cd1ca546a76ffd5c40589e07f4e8bf4 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.3.0" petitparser: dependency: transitive description: name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "6.1.0" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.6" - process: + version: "2.1.8" + shared_preferences: dependency: transitive description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "4.2.4" - shared_preferences: + version: "2.5.3" + shared_preferences_android: dependency: transitive description: - name: shared_preferences - sha256: "82c1ae2a70b5b0236bab13dcad98bc1c0c88ddfb4ef2b7b8080b55868404b8c3" + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.0.4" - shared_preferences_linux: + version: "2.4.10" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_linux - sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.1.3" - shared_preferences_macos: + version: "2.5.4" + shared_preferences_linux: dependency: transitive description: - name: shared_preferences_macos - sha256: "81b6a60b2d27020eb0fc41f4cebc91353047309967901a79ee8203e40c42ed46" + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -381,122 +405,122 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" typed_data: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" url_launcher: dependency: transitive description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2 + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.4" vector_math: dependency: transitive description: @@ -509,50 +533,58 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" - web_socket_channel: + version: "14.3.1" + web: dependency: transitive description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "2.4.0" - win32: + version: "1.0.1" + web_socket_channel: dependency: transitive description: - name: win32 - sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "3.0.3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.1.0" xml: dependency: transitive description: name: xml - sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.5.0" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" From fa8a4b4ed322cd105b4aaac7a76fb92714fd659e Mon Sep 17 00:00:00 2001 From: Yurii Date: Tue, 20 May 2025 16:46:19 +0300 Subject: [PATCH 08/12] dependencies and github build.yml change Flutter version to 3.29.3 --- .github/workflows/build.yml | 2 +- example/pubspec.lock | 8 ++++++++ example/pubspec.yaml | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c6c289..9709356 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: branches: [ main ] env: - FLUTTER_VERSION: '3.0.5' + FLUTTER_VERSION: '3.29.3' jobs: test: diff --git a/example/pubspec.lock b/example/pubspec.lock index 600f5aa..9a3f9c1 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -268,6 +268,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" nm: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 17bee0e..bbfb2e5 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + mocktail: ^1.0.0 flutter_lints: ^2.0.0 From 192b5ad649d52dab88600575a7cc95c82d895c91 Mon Sep 17 00:00:00 2001 From: Yurii Date: Tue, 20 May 2025 17:01:25 +0300 Subject: [PATCH 09/12] fix dependencies --- example/pubspec.lock | 8 -------- example/pubspec.yaml | 1 - pubspec.yaml | 1 + 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 9a3f9c1..600f5aa 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -268,14 +268,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" - mocktail: - dependency: "direct dev" - description: - name: mocktail - sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" - url: "https://pub.dev" - source: hosted - version: "1.0.4" nm: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index bbfb2e5..17bee0e 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -25,7 +25,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mocktail: ^1.0.0 flutter_lints: ^2.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 8cad679..3213f6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: 1.0.4 + mocktail: ^1.0.0 platforms: android: From 5b20a855de19d4704371bb45d7ff003262612275 Mon Sep 17 00:00:00 2001 From: Yurii Date: Tue, 20 May 2025 17:44:13 +0300 Subject: [PATCH 10/12] remove commented code --- example/android/app/build.gradle | 13 ------------- example/android/build.gradle | 13 ------------- 2 files changed, 26 deletions(-) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index b2df840..7e82f6a 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -12,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -//def flutterRoot = localProperties.getProperty('flutter.sdk') -//if (flutterRoot == null) { -// throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -//} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -27,10 +22,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -//apply plugin: 'com.android.application' -//apply plugin: 'kotlin-android' -//apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { compileSdkVersion flutter.compileSdkVersion ndkVersion = "27.0.12077973" @@ -72,7 +63,3 @@ android { flutter { source '../..' } - -//dependencies { -// implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -//} diff --git a/example/android/build.gradle b/example/android/build.gradle index 4239754..bc157bd 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,16 +1,3 @@ -//buildscript { -// ext.kotlin_version = '1.8.22' -// repositories { -// google() -// mavenCentral() -// } -// -// dependencies { -// classpath 'com.android.tools.build:gradle:7.1.2' -// classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" -// } -//} - allprojects { repositories { google() From ef9bedda3d1cd42a7f3365c1d7948bf7a5fc5770 Mon Sep 17 00:00:00 2001 From: Yurii Date: Wed, 28 May 2025 22:23:16 +0300 Subject: [PATCH 11/12] clear unused days + comments --- lib/src/crowdin_request_limiter.dart | 54 +++++++++++++++++----------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/lib/src/crowdin_request_limiter.dart b/lib/src/crowdin_request_limiter.dart index 5101a50..e069a9e 100644 --- a/lib/src/crowdin_request_limiter.dart +++ b/lib/src/crowdin_request_limiter.dart @@ -16,7 +16,7 @@ class CrowdinRequestLimiter { late CrowdinStorage _storage; final DateFormat _formatter = DateFormat('yyyy-MM-dd'); - Map _todayErrorMap = {}; + Map _errorMap = {}; bool _pauseRequests = false; bool _stopPermanently = false; @@ -26,13 +26,14 @@ class CrowdinRequestLimiter { init(CrowdinStorage storage) { _storage = storage; _stopPermanently = _storage.getIsPausedPermanently() ?? false; - _todayErrorMap = _storage.getErrorMap() ?? {}; + _errorMap = _storage.getErrorMap() ?? {}; } + /// Checks if the requests should be paused for today based on the error count. bool _checkIsPausedForToday() { String currentDateString = _formatter.format(DateTime.now()); - if (_todayErrorMap[currentDateString] != null && - _todayErrorMap[currentDateString]! >= maxErrors) { + if (_errorMap[currentDateString] != null && + _errorMap[currentDateString]! >= maxErrors) { _pauseRequests = true; return true; } else { @@ -41,56 +42,67 @@ class CrowdinRequestLimiter { } } + /// Increments the error counter for the current date. void incrementErrorCounter() { DateFormat formatter = DateFormat('yyyy-MM-dd'); String currentDateString = formatter.format(DateTime.now()); - if (_todayErrorMap[currentDateString] != null) { - if (_todayErrorMap[currentDateString]! < maxErrors) { - _todayErrorMap[currentDateString] = - _todayErrorMap[currentDateString]! + 1; - } else if (_todayErrorMap[currentDateString]! >= maxErrors) { + if (_errorMap[currentDateString] != null) { + if (_errorMap[currentDateString]! < maxErrors) { + _errorMap[currentDateString] = _errorMap[currentDateString]! + 1; + } + if (_errorMap[currentDateString]! >= maxErrors) { checkPausedDays(currentDateString); } } else { - _todayErrorMap = {currentDateString: 1}; + _errorMap[currentDateString] = 1; } - _storage.setErrorMap(_todayErrorMap); + _storage.setErrorMap(_cleanErrorMapFromUnusedDays()); } reset() { if (!_stopPermanently) { _pauseRequests = false; - _todayErrorMap = {}; - _storage.setErrorMap(_todayErrorMap); + _storage.setErrorMap({}); } } + /// Checks if the number of errors in the last `maxDaysInRow` days exceeds `maxErrors`. void checkPausedDays(String newDate) { int daysInRow = 0; - if (_todayErrorMap.length >= maxDaysInRow) { + if (_errorMap.length >= maxDaysInRow) { DateTime currentDate = DateTime.parse(newDate); - for (String date in _todayErrorMap.keys) { + for (String date in _errorMap.keys) { if (DateTime.parse(date).isAfter( currentDate.add(const Duration(days: -maxDaysInRow))) && - _todayErrorMap[date]! >= maxErrors) { + _errorMap[date]! >= maxErrors) { daysInRow++; _pauseRequests = true; - } else { - _todayErrorMap.remove(date); } } if (daysInRow >= maxDaysInRow) { - _todayErrorMap.clear(); + _errorMap.clear(); _stopRequestsPermanently(); } } - _storage.setErrorMap(_todayErrorMap); } + /// Cleans the error map from unused days, keeping only the last `maxDaysInRow` days. + Map _cleanErrorMapFromUnusedDays() { + DateTime currentDate = DateTime.now(); + _errorMap.removeWhere((date, _) { + DateTime dateTime = DateTime.parse(date); + return dateTime + .isBefore(currentDate.subtract(const Duration(days: maxDaysInRow))); + }); + _storage.setErrorMap(_errorMap); + return _errorMap; + } + + /// Permanently stops requests by setting the pause flag and updating the storage. void _stopRequestsPermanently() { _pauseRequests = true; _stopPermanently = true; _storage.setIsPausedPermanently(true); - _storage.setErrorMap(_todayErrorMap); + _storage.setErrorMap(_errorMap); } } From b3e4ea8e6473609496c2c6449cfa147e2edeb25d Mon Sep 17 00:00:00 2001 From: Yurii Date: Wed, 28 May 2025 23:13:21 +0300 Subject: [PATCH 12/12] revert change for reset error map --- lib/src/crowdin_request_limiter.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/crowdin_request_limiter.dart b/lib/src/crowdin_request_limiter.dart index e069a9e..95959e2 100644 --- a/lib/src/crowdin_request_limiter.dart +++ b/lib/src/crowdin_request_limiter.dart @@ -62,7 +62,8 @@ class CrowdinRequestLimiter { reset() { if (!_stopPermanently) { _pauseRequests = false; - _storage.setErrorMap({}); + _errorMap = {}; + _storage.setErrorMap(_errorMap); } }