mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-31 05:23:28 +01:00 
			
		
		
		
	Merge pull request #519 from ImranR98/dev
Add Jenkins jobs as a Source (#514), switch to Apps page after App added (#508), Bugfixes (#510)
This commit is contained in:
		| @@ -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) | ||||
|   | ||||
| @@ -10,6 +10,66 @@ class HTML extends AppSource { | ||||
|     return url; | ||||
|   } | ||||
|  | ||||
|   int compareAlphaNumeric(String a, String b) { | ||||
|     List<String> aParts = _splitAlphaNumeric(a); | ||||
|     List<String> 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<String> _splitAlphaNumeric(String s) { | ||||
|     List<String> 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<APKDetails> 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(); | ||||
|   | ||||
							
								
								
									
										70
									
								
								lib/app_sources/jenkins.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								lib/app_sources/jenkins.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| 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 { | ||||
|   Jenkins() { | ||||
|     overrideVersionDetectionFormDefault('releaseDateAsVersion', true); | ||||
|   } | ||||
|  | ||||
|   @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<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, dynamic> 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<dynamic>) | ||||
|           .map((e) { | ||||
|             var path = (e['relativePath'] as String?); | ||||
|             if (path != null && path.isNotEmpty) { | ||||
|               path = '$standardUrl/lastSuccessfulBuild/artifact/$path'; | ||||
|             } | ||||
|             return path == null | ||||
|                 ? const MapEntry<String, String>('', '') | ||||
|                 : MapEntry<String, String>( | ||||
|                     (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); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -166,7 +166,9 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|           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; | ||||
|   | ||||
| @@ -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<HomePage> { | ||||
|   List<int> selectedIndexHistory = []; | ||||
|   int prevAppCount = -1; | ||||
|  | ||||
|   List<NavigationPageItem> pages = [ | ||||
|     NavigationPageItem(tr('appsString'), Icons.apps, | ||||
| @@ -36,6 +39,39 @@ class _HomePageState extends State<HomePage> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     AppsProvider appsProvider = context.watch<AppsProvider>(); | ||||
|  | ||||
|     switchToPage(int index) async { | ||||
|       if (index == 0) { | ||||
|         while ((pages[0].widget.key as GlobalKey<AppsPageState>).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<HomePage> { | ||||
|                 .toList(), | ||||
|             onDestinationSelected: (int index) async { | ||||
|               HapticFeedback.selectionClick(); | ||||
|               if (index == 0) { | ||||
|                 while ((pages[0].widget.key as GlobalKey<AppsPageState>) | ||||
|                         .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, | ||||
|   | ||||
| @@ -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(); | ||||
|         } | ||||
|       } | ||||
|   | ||||
| @@ -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'; | ||||
| @@ -319,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) { | ||||
| @@ -341,7 +358,7 @@ abstract class AppSource { | ||||
|       []; | ||||
|  | ||||
|   // Some additional data may be needed for Apps regardless of Source | ||||
|   final List<List<GeneratedFormItem>> | ||||
|   List<List<GeneratedFormItem>> | ||||
|       additionalAppSpecificSourceAgnosticSettingFormItems = [ | ||||
|     [ | ||||
|       GeneratedFormSwitch( | ||||
| @@ -440,6 +457,7 @@ class SourceProvider { | ||||
|         FDroid(), | ||||
|         IzzyOnDroid(), | ||||
|         FDroidRepo(), | ||||
|         Jenkins(), | ||||
|         SourceForge(), | ||||
|         APKMirror(), | ||||
|         Mullvad(), | ||||
|   | ||||
| @@ -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' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user