mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-27 19:53:45 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			196 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			196 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:convert';
 | |
| import 'dart:io';
 | |
| 
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:http/http.dart';
 | |
| import 'package:obtainium/app_sources/github.dart';
 | |
| import 'package:obtainium/custom_errors.dart';
 | |
| import 'package:obtainium/providers/settings_provider.dart';
 | |
| import 'package:obtainium/providers/source_provider.dart';
 | |
| import 'package:obtainium/components/generated_form.dart';
 | |
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:url_launcher/url_launcher_string.dart';
 | |
| 
 | |
| class GitLab extends AppSource {
 | |
|   GitLab() {
 | |
|     hosts = ['gitlab.com'];
 | |
|     canSearch = true;
 | |
|     showReleaseDateAsVersionToggle = true;
 | |
| 
 | |
|     sourceConfigSettingFormItems = [
 | |
|       GeneratedFormTextField('gitlab-creds',
 | |
|           label: tr('gitlabPATLabel'),
 | |
|           password: true,
 | |
|           required: false,
 | |
|           belowWidgets: [
 | |
|             const SizedBox(
 | |
|               height: 4,
 | |
|             ),
 | |
|             GestureDetector(
 | |
|                 onTap: () {
 | |
|                   launchUrlString(
 | |
|                       'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token',
 | |
|                       mode: LaunchMode.externalApplication);
 | |
|                 },
 | |
|                 child: Text(
 | |
|                   tr('about'),
 | |
|                   style: const TextStyle(
 | |
|                       decoration: TextDecoration.underline, fontSize: 12),
 | |
|                 )),
 | |
|             const SizedBox(
 | |
|               height: 4,
 | |
|             )
 | |
|           ])
 | |
|     ];
 | |
| 
 | |
|     additionalSourceAppSpecificSettingFormItems = [
 | |
|       [
 | |
|         GeneratedFormSwitch('fallbackToOlderReleases',
 | |
|             label: tr('fallbackToOlderReleases'), defaultValue: true)
 | |
|       ]
 | |
|     ];
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   String sourceSpecificStandardizeURL(String url) {
 | |
|     RegExp standardUrlRegEx = RegExp(
 | |
|         '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
 | |
|         caseSensitive: false);
 | |
|     RegExpMatch? match = standardUrlRegEx.firstMatch(url);
 | |
|     if (match == null) {
 | |
|       throw InvalidURLError(name);
 | |
|     }
 | |
|     return match.group(0)!;
 | |
|   }
 | |
| 
 | |
|   Future<String?> getPATIfAny(Map<String, dynamic> additionalSettings) async {
 | |
|     SettingsProvider settingsProvider = SettingsProvider();
 | |
|     await settingsProvider.initializeSettings();
 | |
|     var sourceConfig =
 | |
|         await getSourceConfigValues(additionalSettings, settingsProvider);
 | |
|     String? creds = sourceConfig['gitlab-creds'];
 | |
|     return creds != null && creds.isNotEmpty ? creds : null;
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Future<Map<String, List<String>>> search(String query,
 | |
|       {Map<String, dynamic> querySettings = const {}}) async {
 | |
|     var url =
 | |
|         'https://${hosts[0]}/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
 | |
|     var res = await sourceRequest(url, {});
 | |
|     if (res.statusCode != 200) {
 | |
|       throw getObtainiumHttpError(res);
 | |
|     }
 | |
|     var json = jsonDecode(res.body) as List<dynamic>;
 | |
|     Map<String, List<String>> results = {};
 | |
|     for (var element in json) {
 | |
|       results['https://${hosts[0]}/${element['path_with_namespace']}'] = [
 | |
|         element['name_with_namespace'],
 | |
|         element['description'] ?? tr('noDescription')
 | |
|       ];
 | |
|     }
 | |
|     return results;
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   String? changeLogPageFromStandardUrl(String standardUrl) =>
 | |
|       '$standardUrl/-/releases';
 | |
| 
 | |
|   @override
 | |
|   Future<Map<String, String>?> getRequestHeaders(
 | |
|       Map<String, dynamic> additionalSettings,
 | |
|       {bool forAPKDownload = false}) async {
 | |
|     // Change headers to pacify, e.g. cloudflare protection
 | |
|     // Related to: (#1397, #1389, #1384, #1382, #1381, #1380, #1359, #854, #785, #697)
 | |
|     var headers = <String, String>{};
 | |
|     headers[HttpHeaders.refererHeader] = 'https://${hosts[0]}';
 | |
|     if (headers.isNotEmpty) {
 | |
|       return headers;
 | |
|     } else {
 | |
|       return null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Future<APKDetails> getLatestAPKDetails(
 | |
|     String standardUrl,
 | |
|     Map<String, dynamic> additionalSettings,
 | |
|   ) async {
 | |
|     // Prepare request params
 | |
|     var names = GitHub().getAppNames(standardUrl);
 | |
|     String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {});
 | |
|     String optionalAuth = (PAT != null) ? 'private_token=$PAT' : '';
 | |
| 
 | |
|     bool trackOnly = additionalSettings['trackOnly'] == true;
 | |
| 
 | |
|     // Request data from REST API
 | |
|     Response res = await sourceRequest(
 | |
|         'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth',
 | |
|         additionalSettings);
 | |
|     if (res.statusCode != 200) {
 | |
|       throw getObtainiumHttpError(res);
 | |
|     }
 | |
| 
 | |
|     // Extract .apk details from received data
 | |
|     Iterable<APKDetails> apkDetailsList = [];
 | |
|     var json = jsonDecode(res.body) as List<dynamic>;
 | |
|     apkDetailsList = json.map((e) {
 | |
|       var apkUrlsFromAssets = (e['assets']?['links'] as List<dynamic>? ?? [])
 | |
|           .map((e) {
 | |
|             return (e['direct_asset_url'] ?? e['url'] ?? '') as String;
 | |
|           })
 | |
|           .where((s) => s.isNotEmpty)
 | |
|           .toList();
 | |
|       List<String> uploadedAPKsFromDescription =
 | |
|           ((e['description'] ?? '') as String)
 | |
|               .split('](')
 | |
|               .join('\n')
 | |
|               .split('.apk)')
 | |
|               .join('.apk\n')
 | |
|               .split('\n')
 | |
|               .where((s) => s.startsWith('/uploads/') && s.endsWith('apk'))
 | |
|               .map((s) => '$standardUrl$s')
 | |
|               .toList();
 | |
|       var apkUrlsSet = apkUrlsFromAssets.toSet();
 | |
|       apkUrlsSet.addAll(uploadedAPKsFromDescription);
 | |
|       var releaseDateString = e['released_at'] ?? e['created_at'];
 | |
|       DateTime? releaseDate =
 | |
|           releaseDateString != null ? DateTime.parse(releaseDateString) : null;
 | |
|       return APKDetails(
 | |
|           e['tag_name'] ?? e['name'],
 | |
|           getApkUrlsFromUrls(apkUrlsSet.toList()),
 | |
|           GitHub().getAppNames(standardUrl),
 | |
|           releaseDate: releaseDate);
 | |
|     });
 | |
|     if (apkDetailsList.isEmpty) {
 | |
|       throw NoReleasesError();
 | |
|     }
 | |
|     var finalResult = apkDetailsList.first;
 | |
| 
 | |
|     // Fallback procedure
 | |
|     bool fallbackToOlderReleases =
 | |
|         additionalSettings['fallbackToOlderReleases'] == true;
 | |
|     if (finalResult.apkUrls.isEmpty && fallbackToOlderReleases && !trackOnly) {
 | |
|       apkDetailsList =
 | |
|           apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
 | |
|       finalResult = apkDetailsList.first;
 | |
|     }
 | |
| 
 | |
|     if (finalResult.apkUrls.isEmpty && !trackOnly) {
 | |
|       throw NoAPKError();
 | |
|     }
 | |
| 
 | |
|     finalResult.apkUrls = finalResult.apkUrls.map((apkUrl) {
 | |
|       if (RegExp('^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+\$')
 | |
|           .hasMatch(apkUrl.value)) {
 | |
|         return MapEntry(
 | |
|             apkUrl.key, apkUrl.value.replaceFirst('/file/', '/raw/'));
 | |
|       } else {
 | |
|         return apkUrl;
 | |
|       }
 | |
|     }).toList();
 | |
| 
 | |
|     return finalResult;
 | |
|   }
 | |
| }
 |