From 03b592521c46a4e9a8053519eb9af165f6c03797 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 30 Apr 2023 15:39:21 -0400 Subject: [PATCH 1/6] Fixed link sorting for HTML Source --- lib/app_sources/html.dart | 63 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index ece1796..c4c039f 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -10,6 +10,66 @@ class HTML extends AppSource { return url; } + int compareAlphaNumeric(String a, String b) { + List aParts = _splitAlphaNumeric(a); + List bParts = _splitAlphaNumeric(b); + + for (int i = 0; i < aParts.length && i < bParts.length; i++) { + String aPart = aParts[i]; + String bPart = bParts[i]; + + bool aIsNumber = _isNumeric(aPart); + bool bIsNumber = _isNumeric(bPart); + + if (aIsNumber && bIsNumber) { + int aNumber = int.parse(aPart); + int bNumber = int.parse(bPart); + int cmp = aNumber.compareTo(bNumber); + if (cmp != 0) { + return cmp; + } + } else if (!aIsNumber && !bIsNumber) { + int cmp = aPart.compareTo(bPart); + if (cmp != 0) { + return cmp; + } + } else { + // Alphanumeric strings come before numeric strings + return aIsNumber ? 1 : -1; + } + } + + return aParts.length.compareTo(bParts.length); + } + + List _splitAlphaNumeric(String s) { + List parts = []; + StringBuffer sb = StringBuffer(); + + bool isNumeric = _isNumeric(s[0]); + sb.write(s[0]); + + for (int i = 1; i < s.length; i++) { + bool currentIsNumeric = _isNumeric(s[i]); + if (currentIsNumeric == isNumeric) { + sb.write(s[i]); + } else { + parts.add(sb.toString()); + sb.clear(); + sb.write(s[i]); + isNumeric = currentIsNumeric; + } + } + + parts.add(sb.toString()); + + return parts; + } + + bool _isNumeric(String s) { + return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57; + } + @override Future getLatestAPKDetails( String standardUrl, @@ -23,7 +83,8 @@ class HTML extends AppSource { .map((element) => element.attributes['href'] ?? '') .where((element) => element.toLowerCase().endsWith('.apk')) .toList(); - links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last)); + links.sort( + (a, b) => compareAlphaNumeric(a.split('/').last, b.split('/').last)); if (additionalSettings['apkFilterRegEx'] != null) { var reg = RegExp(additionalSettings['apkFilterRegEx']); links = links.where((element) => reg.hasMatch(element)).toList(); From 36273fe02df9fd8dcb6f18c61cfd22e8f538ad41 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 30 Apr 2023 15:49:25 -0400 Subject: [PATCH 2/6] Switch to apps tab after app added (#508) --- lib/pages/home.dart | 58 +++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/lib/pages/home.dart b/lib/pages/home.dart index d233297..a830b1a 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -6,6 +6,8 @@ import 'package:obtainium/pages/add_app.dart'; import 'package:obtainium/pages/apps.dart'; import 'package:obtainium/pages/import_export.dart'; import 'package:obtainium/pages/settings.dart'; +import 'package:obtainium/providers/apps_provider.dart'; +import 'package:provider/provider.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -24,6 +26,7 @@ class NavigationPageItem { class _HomePageState extends State { List selectedIndexHistory = []; + int prevAppCount = -1; List pages = [ NavigationPageItem(tr('appsString'), Icons.apps, @@ -36,6 +39,39 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { + AppsProvider appsProvider = context.watch(); + + switchToPage(int index) async { + if (index == 0) { + while ((pages[0].widget.key as GlobalKey).currentState != + null) { + // Avoid duplicate GlobalKey error + await Future.delayed(const Duration(microseconds: 1)); + } + setState(() { + selectedIndexHistory.clear(); + }); + } else if (selectedIndexHistory.isEmpty || + (selectedIndexHistory.isNotEmpty && + selectedIndexHistory.last != index)) { + setState(() { + int existingInd = selectedIndexHistory.indexOf(index); + if (existingInd >= 0) { + selectedIndexHistory.removeAt(existingInd); + } + selectedIndexHistory.add(index); + }); + } + } + + if (prevAppCount >= 0 && + appsProvider.apps.length > prevAppCount && + selectedIndexHistory.isNotEmpty && + selectedIndexHistory.last == 1) { + switchToPage(0); + } + prevAppCount = appsProvider.apps.length; + return WillPopScope( child: Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, @@ -65,27 +101,7 @@ class _HomePageState extends State { .toList(), onDestinationSelected: (int index) async { HapticFeedback.selectionClick(); - if (index == 0) { - while ((pages[0].widget.key as GlobalKey) - .currentState != - null) { - // Avoid duplicate GlobalKey error - await Future.delayed(const Duration(microseconds: 1)); - } - setState(() { - selectedIndexHistory.clear(); - }); - } else if (selectedIndexHistory.isEmpty || - (selectedIndexHistory.isNotEmpty && - selectedIndexHistory.last != index)) { - setState(() { - int existingInd = selectedIndexHistory.indexOf(index); - if (existingInd >= 0) { - selectedIndexHistory.removeAt(existingInd); - } - selectedIndexHistory.add(index); - }); - } + await switchToPage(index); }, selectedIndex: selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last, From 76e316422c818e776fbb2412959fc5f381da1005 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 30 Apr 2023 16:28:09 -0400 Subject: [PATCH 3/6] Added Jenkins Source (#514) --- lib/app_sources/jenkins.dart | 66 ++++++++++++++++++++++++++++++ lib/providers/source_provider.dart | 2 + 2 files changed, 68 insertions(+) create mode 100644 lib/app_sources/jenkins.dart diff --git a/lib/app_sources/jenkins.dart b/lib/app_sources/jenkins.dart new file mode 100644 index 0000000..a695b68 --- /dev/null +++ b/lib/app_sources/jenkins.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/source_provider.dart'; + +class Jenkins extends AppSource { + @override + String trimJobUrl(String url) { + RegExp standardUrlRegEx = RegExp('.*/job/[^/]+'); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); + if (match == null) { + throw InvalidURLError(name); + } + return url.substring(0, match.end); + } + + @override + String? changeLogPageFromStandardUrl(String standardUrl) => + '$standardUrl/-/releases'; + + @override + Future getLatestAPKDetails( + String standardUrl, + Map additionalSettings, + ) async { + standardUrl = trimJobUrl(standardUrl); + Response res = + await get(Uri.parse('$standardUrl/lastSuccessfulBuild/api/json')); + if (res.statusCode == 200) { + var json = jsonDecode(res.body); + var releaseDate = json['timestamp'] == null + ? null + : DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int); + var version = + json['number'] == null ? null : (json['number'] as int).toString(); + if (version == null) { + throw NoVersionError(); + } + var apkUrls = (json['artifacts'] as List) + .map((e) { + var path = (e['relativePath'] as String?); + if (path != null && path.isNotEmpty) { + path = '$standardUrl/lastSuccessfulBuild/artifact/$path'; + } + return path == null + ? const MapEntry('', '') + : MapEntry( + (e['fileName'] ?? e['relativePath']) as String, path); + }) + .where((url) => + url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk')) + .toList(); + if (apkUrls.isEmpty) { + throw NoAPKError(); + } + return APKDetails( + version, + apkUrls, + releaseDate: releaseDate, + AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last)); + } else { + throw getObtainiumHttpError(res); + } + } +} diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 2606a37..211e87f 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -15,6 +15,7 @@ import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/gitlab.dart'; import 'package:obtainium/app_sources/izzyondroid.dart'; import 'package:obtainium/app_sources/html.dart'; +import 'package:obtainium/app_sources/jenkins.dart'; import 'package:obtainium/app_sources/mullvad.dart'; import 'package:obtainium/app_sources/neutroncode.dart'; import 'package:obtainium/app_sources/signal.dart'; @@ -440,6 +441,7 @@ class SourceProvider { FDroid(), IzzyOnDroid(), FDroidRepo(), + Jenkins(), SourceForge(), APKMirror(), Mullvad(), From 779de58f74fb766ab8b787b52e6e93c8f875211a Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 30 Apr 2023 17:00:00 -0400 Subject: [PATCH 4/6] Jenkins uses release dates only + APK delete bugfix --- lib/app_sources/jenkins.dart | 4 ++++ lib/pages/add_app.dart | 4 +++- lib/providers/apps_provider.dart | 2 +- lib/providers/source_provider.dart | 18 +++++++++++++++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/app_sources/jenkins.dart b/lib/app_sources/jenkins.dart index a695b68..366b76d 100644 --- a/lib/app_sources/jenkins.dart +++ b/lib/app_sources/jenkins.dart @@ -5,6 +5,10 @@ import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/providers/source_provider.dart'; class Jenkins extends AppSource { + Jenkins() { + overrideVersionDetectionFormDefault('releaseDateAsVersion', true); + } + @override String trimJobUrl(String url) { RegExp standardUrlRegEx = RegExp('.*/job/[^/]+'); diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 768f7e7..a81eb9c 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -166,7 +166,9 @@ class _AddAppPageState extends State { if (appsProvider.apps.containsKey(app.id)) { throw ObtainiumError(tr('appAlreadyAdded')); } - if (app.additionalSettings['trackOnly'] == true) { + if (app.additionalSettings['trackOnly'] == true || + app.additionalSettings['versionDetection'] != + 'standardVersionDetection') { app.installedVersion = app.latestVersion; } app.categories = pickedCategories; diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 4dd7668..9cc445c 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -212,7 +212,7 @@ class AppsProvider with ChangeNotifier { var fn = file.path.split('/').last; if (fn.startsWith('${app.id}-') && fn.endsWith('.apk') && - fn != fileName) { + fn != downloadedFile.path.split('/').last) { file.delete(); } } diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 211e87f..e27cdd8 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -320,6 +320,22 @@ abstract class AppSource { name = runtimeType.toString(); } + overrideVersionDetectionFormDefault(String vd, bool disableStandard) { + additionalAppSpecificSourceAgnosticSettingFormItems = + additionalAppSpecificSourceAgnosticSettingFormItems.map((e) { + return e.map((e2) { + if (e2.key == 'versionDetection') { + var item = e2 as GeneratedFormDropdown; + item.defaultValue = vd; + if (disableStandard) { + item.disabledOptKeys = ['standardVersionDetection']; + } + } + return e2; + }).toList(); + }).toList(); + } + String standardizeUrl(String url) { url = preStandardizeUrl(url); if (!hostChanged) { @@ -342,7 +358,7 @@ abstract class AppSource { []; // Some additional data may be needed for Apps regardless of Source - final List> + List> additionalAppSpecificSourceAgnosticSettingFormItems = [ [ GeneratedFormSwitch( From 9f5f1174ba8ca2780b1ce2958f62fa822d82711f Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 30 Apr 2023 17:02:33 -0400 Subject: [PATCH 5/6] Increment version --- lib/main.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 8b4973e..1c7caa6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; -const String currentVersion = '0.12.1'; +const String currentVersion = '0.12.2'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/pubspec.yaml b/pubspec.yaml index 008567c..89e6ec6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.12.1+161 # When changing this, update the tag in main() accordingly +version: 0.12.2+162 # When changing this, update the tag in main() accordingly environment: sdk: '>=2.18.2 <3.0.0' From f8b326529fbd761414748c5c2e75dfcca8c24854 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 30 Apr 2023 17:03:54 -0400 Subject: [PATCH 6/6] Add Jenkins to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 57ba978..b21d416 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Currently supported App sources: - [SourceForge](https://sourceforge.net/) - [APKMirror](https://apkmirror.com/) (Track-Only) - Third Party F-Droid Repos +- Jenkins Jobs - [Steam](https://store.steampowered.com/mobile) - [Telegram App](https://telegram.org) - [VLC](https://www.videolan.org/vlc/download-android.html)