Compare commits
20 Commits
v0.6.7-bet
...
v0.7.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
55cae0620b | ||
|
ba6cea3ae6 | ||
|
4be33374c2 | ||
|
e2bf834981 | ||
|
9bd7ddb21b | ||
|
905a807ee9 | ||
|
ab57b97875 | ||
|
5db2c5f0b1 | ||
|
e158c23cca | ||
|
208f125e12 | ||
|
b7ccf3fa49 | ||
|
c746e89052 | ||
|
ee758e8470 | ||
|
68d903e092 | ||
|
c47b752344 | ||
|
62a05996cf | ||
|
1cda941fbe | ||
|
49cb908d04 | ||
|
139f44d31d | ||
|
ed955ac6a2 |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 918 B After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 21 KiB |
@@ -1,113 +0,0 @@
|
|||||||
import 'package:html/parser.dart';
|
|
||||||
import 'package:http/http.dart';
|
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
|
||||||
|
|
||||||
class APKMirror implements AppSource {
|
|
||||||
@override
|
|
||||||
late String host = 'apkmirror.com';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String standardizeURL(String url) {
|
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+');
|
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
|
||||||
if (match == null) {
|
|
||||||
throw InvalidURLError(runtimeType.toString());
|
|
||||||
}
|
|
||||||
return url.substring(0, match.end);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
|
||||||
'$standardUrl#whatsnew';
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
|
|
||||||
var originalUri = Uri.parse(apkUrl);
|
|
||||||
var res = await get(originalUri);
|
|
||||||
if (res.statusCode != 200) {
|
|
||||||
throw false;
|
|
||||||
}
|
|
||||||
var href =
|
|
||||||
parse(res.body).querySelector('.downloadButton')?.attributes['href'];
|
|
||||||
if (href == null) {
|
|
||||||
throw false;
|
|
||||||
}
|
|
||||||
var res2 = await get(Uri.parse('${originalUri.origin}$href'), headers: {
|
|
||||||
'User-Agent':
|
|
||||||
'Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0'
|
|
||||||
});
|
|
||||||
if (res2.statusCode != 200) {
|
|
||||||
throw false;
|
|
||||||
}
|
|
||||||
var links = parse(res2.body)
|
|
||||||
.querySelectorAll('a')
|
|
||||||
.where((element) => element.innerHtml == 'here')
|
|
||||||
.map((e) => e.attributes['href'])
|
|
||||||
.where((element) => element != null)
|
|
||||||
.toList();
|
|
||||||
if (links.isEmpty) {
|
|
||||||
throw false;
|
|
||||||
}
|
|
||||||
return '${originalUri.origin}${links[0]}';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
|
||||||
String standardUrl, List<String> additionalData) async {
|
|
||||||
Response res = await get(Uri.parse('$standardUrl/feed'));
|
|
||||||
if (res.statusCode != 200) {
|
|
||||||
throw NoReleasesError();
|
|
||||||
}
|
|
||||||
var nextUrl = parse(res.body)
|
|
||||||
.querySelector('item')
|
|
||||||
?.querySelector('link')
|
|
||||||
?.nextElementSibling
|
|
||||||
?.innerHtml;
|
|
||||||
if (nextUrl == null) {
|
|
||||||
throw NoReleasesError();
|
|
||||||
}
|
|
||||||
Response res2 = await get(Uri.parse(nextUrl), headers: {
|
|
||||||
'User-Agent':
|
|
||||||
'Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0'
|
|
||||||
});
|
|
||||||
if (res2.statusCode != 200) {
|
|
||||||
throw NoReleasesError();
|
|
||||||
}
|
|
||||||
var html2 = parse(res2.body);
|
|
||||||
var origin = Uri.parse(standardUrl).origin;
|
|
||||||
List<String> apkUrls = html2
|
|
||||||
.querySelectorAll('.apkm-badge')
|
|
||||||
.map((e) => e.innerHtml != 'APK'
|
|
||||||
? ''
|
|
||||||
: e.previousElementSibling?.attributes['href'] ?? '')
|
|
||||||
.where((element) => element.isNotEmpty)
|
|
||||||
.map((e) => '$origin$e')
|
|
||||||
.toList();
|
|
||||||
if (apkUrls.isEmpty) {
|
|
||||||
throw NoAPKError();
|
|
||||||
}
|
|
||||||
var version = html2.querySelector('span.active.accent_color')?.innerHtml;
|
|
||||||
if (version == null) {
|
|
||||||
throw NoVersionError();
|
|
||||||
}
|
|
||||||
return APKDetails(version, apkUrls);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
AppNames getAppNames(String standardUrl) {
|
|
||||||
String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
|
|
||||||
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
|
|
||||||
return AppNames(names[1], names[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> additionalDataDefaults = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
|
||||||
}
|
|
@@ -1,12 +1,12 @@
|
|||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
class FDroid implements AppSource {
|
class FDroid extends AppSource {
|
||||||
@override
|
FDroid() {
|
||||||
late String host = 'f-droid.org';
|
host = 'f-droid.org';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
@@ -77,13 +77,4 @@ class FDroid implements AppSource {
|
|||||||
AppNames getAppNames(String standardUrl) {
|
AppNames getAppNames(String standardUrl) {
|
||||||
return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last);
|
return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> additionalDataDefaults = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
|
||||||
}
|
}
|
||||||
|
@@ -7,9 +7,81 @@ import 'package:obtainium/providers/settings_provider.dart';
|
|||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class GitHub implements AppSource {
|
class GitHub extends AppSource {
|
||||||
@override
|
GitHub() {
|
||||||
late String host = 'github.com';
|
host = 'github.com';
|
||||||
|
|
||||||
|
additionalDataDefaults = ['true', 'true', ''];
|
||||||
|
|
||||||
|
moreSourceSettingsFormItems = [
|
||||||
|
GeneratedFormItem(
|
||||||
|
label: 'GitHub Personal Access Token (Increases Rate Limit)',
|
||||||
|
id: 'github-creds',
|
||||||
|
required: false,
|
||||||
|
additionalValidators: [
|
||||||
|
(value) {
|
||||||
|
if (value != null && value.trim().isNotEmpty) {
|
||||||
|
if (value
|
||||||
|
.split(':')
|
||||||
|
.where((element) => element.trim().isNotEmpty)
|
||||||
|
.length !=
|
||||||
|
2) {
|
||||||
|
return 'PAT must be in this format: username:token';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
hint: 'username:token',
|
||||||
|
belowWidgets: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(
|
||||||
|
'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token',
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'About GitHub PATs',
|
||||||
|
style: TextStyle(
|
||||||
|
decoration: TextDecoration.underline, fontSize: 12),
|
||||||
|
))
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
additionalDataFormItems = [
|
||||||
|
[
|
||||||
|
GeneratedFormItem(label: 'Include prereleases', type: FormItemType.bool)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormItem(
|
||||||
|
label: 'Fallback to older releases', type: FormItemType.bool)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormItem(
|
||||||
|
label: 'Filter Release Titles by Regular Expression',
|
||||||
|
type: FormItemType.string,
|
||||||
|
required: false,
|
||||||
|
additionalValidators: [
|
||||||
|
(value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
RegExp(value);
|
||||||
|
} catch (e) {
|
||||||
|
return 'Invalid regular expression';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
canSearch = true;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
@@ -72,7 +144,7 @@ class GitHub implements AppSource {
|
|||||||
|
|
||||||
if (regexFilter != null &&
|
if (regexFilter != null &&
|
||||||
!RegExp(regexFilter)
|
!RegExp(regexFilter)
|
||||||
.hasMatch((releases[i]['tag_name'] as String).trim())) {
|
.hasMatch((releases[i]['name'] as String).trim())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var apkUrls = getReleaseAPKUrls(releases[i]);
|
var apkUrls = getReleaseAPKUrls(releases[i]);
|
||||||
@@ -114,72 +186,23 @@ class GitHub implements AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<List<GeneratedFormItem>> additionalDataFormItems = [
|
Future<List<String>> search(String query) async {
|
||||||
[GeneratedFormItem(label: 'Include prereleases', type: FormItemType.bool)],
|
Response res = await get(Uri.parse(
|
||||||
[
|
'https://${await getCredentialPrefixIfAny()}api.$host/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100'));
|
||||||
GeneratedFormItem(
|
if (res.statusCode == 200) {
|
||||||
label: 'Fallback to older releases', type: FormItemType.bool)
|
return (jsonDecode(res.body)['items'] as List<dynamic>)
|
||||||
],
|
.map((e) => e['html_url'] as String)
|
||||||
[
|
.toList();
|
||||||
GeneratedFormItem(
|
} else {
|
||||||
label: 'Filter Release Titles by Regular Expression',
|
if (res.headers['x-ratelimit-remaining'] == '0') {
|
||||||
type: FormItemType.string,
|
throw RateLimitError(
|
||||||
required: false,
|
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
|
||||||
additionalValidators: [
|
60000000)
|
||||||
(value) {
|
.round());
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
try {
|
throw ObtainiumError(
|
||||||
RegExp(value);
|
res.reasonPhrase ?? 'Error ${res.statusCode.toString()}',
|
||||||
} catch (e) {
|
unexpected: true);
|
||||||
return 'Invalid regular expression';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
])
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> additionalDataDefaults = ['true', 'true', ''];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<GeneratedFormItem> moreSourceSettingsFormItems = [
|
|
||||||
GeneratedFormItem(
|
|
||||||
label: 'GitHub Personal Access Token (Increases Rate Limit)',
|
|
||||||
id: 'github-creds',
|
|
||||||
required: false,
|
|
||||||
additionalValidators: [
|
|
||||||
(value) {
|
|
||||||
if (value != null && value.trim().isNotEmpty) {
|
|
||||||
if (value
|
|
||||||
.split(':')
|
|
||||||
.where((element) => element.trim().isNotEmpty)
|
|
||||||
.length !=
|
|
||||||
2) {
|
|
||||||
return 'PAT must be in this format: username:token';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
],
|
|
||||||
hint: 'username:token',
|
|
||||||
belowWidgets: [
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
launchUrlString(
|
|
||||||
'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token',
|
|
||||||
mode: LaunchMode.externalApplication);
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'About GitHub PATs',
|
|
||||||
style: TextStyle(
|
|
||||||
decoration: TextDecoration.underline, fontSize: 12),
|
|
||||||
))
|
|
||||||
])
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
class GitLab implements AppSource {
|
class GitLab extends AppSource {
|
||||||
@override
|
GitLab() {
|
||||||
late String host = 'gitlab.com';
|
host = 'gitlab.com';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
@@ -72,13 +72,4 @@ class GitLab implements AppSource {
|
|||||||
// Same as GitHub
|
// Same as GitHub
|
||||||
return GitHub().getAppNames(standardUrl);
|
return GitHub().getAppNames(standardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> additionalDataDefaults = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
class IzzyOnDroid implements AppSource {
|
class IzzyOnDroid extends AppSource {
|
||||||
@override
|
IzzyOnDroid() {
|
||||||
late String host = 'android.izzysoft.de';
|
host = 'android.izzysoft.de';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
@@ -63,13 +63,4 @@ class IzzyOnDroid implements AppSource {
|
|||||||
AppNames getAppNames(String standardUrl) {
|
AppNames getAppNames(String standardUrl) {
|
||||||
return AppNames('IzzyOnDroid', Uri.parse(standardUrl).pathSegments.last);
|
return AppNames('IzzyOnDroid', Uri.parse(standardUrl).pathSegments.last);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> additionalDataDefaults = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
class Mullvad implements AppSource {
|
class Mullvad extends AppSource {
|
||||||
@override
|
Mullvad() {
|
||||||
late String host = 'mullvad.net';
|
host = 'mullvad.net';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
@@ -50,13 +50,4 @@ class Mullvad implements AppSource {
|
|||||||
AppNames getAppNames(String standardUrl) {
|
AppNames getAppNames(String standardUrl) {
|
||||||
return AppNames('Mullvad-VPN', 'Mullvad-VPN');
|
return AppNames('Mullvad-VPN', 'Mullvad-VPN');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> additionalDataDefaults = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
class Signal implements AppSource {
|
class Signal extends AppSource {
|
||||||
@override
|
Signal() {
|
||||||
late String host = 'signal.org';
|
host = 'signal.org';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
@@ -42,13 +42,4 @@ class Signal implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
AppNames getAppNames(String standardUrl) => AppNames('Signal', 'Signal');
|
AppNames getAppNames(String standardUrl) => AppNames('Signal', 'Signal');
|
||||||
|
|
||||||
@override
|
|
||||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> additionalDataDefaults = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
class SourceForge implements AppSource {
|
class SourceForge extends AppSource {
|
||||||
@override
|
SourceForge() {
|
||||||
late String host = 'sourceforge.net';
|
host = 'sourceforge.net';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
@@ -66,13 +66,4 @@ class SourceForge implements AppSource {
|
|||||||
return AppNames(runtimeType.toString(),
|
return AppNames(runtimeType.toString(),
|
||||||
standardUrl.substring(standardUrl.lastIndexOf('/') + 1));
|
standardUrl.substring(standardUrl.lastIndexOf('/') + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> additionalDataDefaults = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
|
||||||
|
|
||||||
class ObtainiumError {
|
class ObtainiumError {
|
||||||
late String message;
|
late String message;
|
||||||
ObtainiumError(this.message);
|
bool unexpected;
|
||||||
|
ObtainiumError(this.message, {this.unexpected = false});
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return message;
|
return message;
|
||||||
@@ -48,10 +48,14 @@ class IDChangedError extends ObtainiumError {
|
|||||||
: super('Downloaded package ID does not match existing App ID');
|
: super('Downloaded package ID does not match existing App ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NotImplementedError extends ObtainiumError {
|
||||||
|
NotImplementedError() : super('This class has not implemented this function');
|
||||||
|
}
|
||||||
|
|
||||||
class MultiAppMultiError extends ObtainiumError {
|
class MultiAppMultiError extends ObtainiumError {
|
||||||
Map<String, List<String>> content = {};
|
Map<String, List<String>> content = {};
|
||||||
|
|
||||||
MultiAppMultiError() : super('Multiple Errors Placeholder');
|
MultiAppMultiError() : super('Multiple Errors Placeholder', unexpected: true);
|
||||||
|
|
||||||
add(String appId, String string) {
|
add(String appId, String string) {
|
||||||
var tempIds = content.remove(string);
|
var tempIds = content.remove(string);
|
||||||
@@ -71,7 +75,7 @@ class MultiAppMultiError extends ObtainiumError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showError(dynamic e, BuildContext context) {
|
showError(dynamic e, BuildContext context) {
|
||||||
if (e is String || (e is ObtainiumError && e is! MultiAppMultiError)) {
|
if (e is String || (e is ObtainiumError && !e.unexpected)) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(e.toString())),
|
SnackBar(content: Text(e.toString())),
|
||||||
);
|
);
|
||||||
@@ -96,3 +100,19 @@ showError(dynamic e, BuildContext context) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String list2FriendlyString(List<String> list) {
|
||||||
|
return list.length == 2
|
||||||
|
? '${list[0]} and ${list[1]}'
|
||||||
|
: list
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((e) =>
|
||||||
|
e.value +
|
||||||
|
(e.key == list.length - 1
|
||||||
|
? ''
|
||||||
|
: e.key == list.length - 2
|
||||||
|
? ', and '
|
||||||
|
: ', '))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
@@ -15,7 +15,7 @@ import 'package:dynamic_color/dynamic_color.dart';
|
|||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
|
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
|
||||||
|
|
||||||
const String currentVersion = '0.6.7';
|
const String currentVersion = '0.7.0';
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
@@ -143,7 +143,8 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
[],
|
[],
|
||||||
0,
|
0,
|
||||||
['true'],
|
['true'],
|
||||||
null)
|
null,
|
||||||
|
false)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
// Register the background update task according to the user's setting
|
// Register the background update task according to the user's setting
|
||||||
|
@@ -57,6 +57,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e is String
|
return e is String
|
||||||
? e
|
? e
|
||||||
|
: e is ObtainiumError
|
||||||
|
? e.toString()
|
||||||
: 'Error';
|
: 'Error';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@@ -23,24 +23,24 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
AppsFilter? filter;
|
AppsFilter? filter;
|
||||||
var updatesOnlyFilter =
|
var updatesOnlyFilter =
|
||||||
AppsFilter(includeUptodate: false, includeNonInstalled: false);
|
AppsFilter(includeUptodate: false, includeNonInstalled: false);
|
||||||
Set<String> selectedIds = {};
|
Set<App> selectedApps = {};
|
||||||
DateTime? refreshingSince;
|
DateTime? refreshingSince;
|
||||||
|
|
||||||
clearSelected() {
|
clearSelected() {
|
||||||
if (selectedIds.isNotEmpty) {
|
if (selectedApps.isNotEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedIds.clear();
|
selectedApps.clear();
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectThese(List<String> appIds) {
|
selectThese(List<App> apps) {
|
||||||
if (selectedIds.isEmpty) {
|
if (selectedApps.isEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
for (var a in appIds) {
|
for (var a in apps) {
|
||||||
selectedIds.add(a);
|
selectedApps.add(a);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -54,16 +54,16 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
var currentFilterIsUpdatesOnly =
|
var currentFilterIsUpdatesOnly =
|
||||||
filter?.isIdenticalTo(updatesOnlyFilter) ?? false;
|
filter?.isIdenticalTo(updatesOnlyFilter) ?? false;
|
||||||
|
|
||||||
selectedIds = selectedIds
|
selectedApps = selectedApps
|
||||||
.where((element) => sortedApps.map((e) => e.app.id).contains(element))
|
.where((element) => sortedApps.map((e) => e.app).contains(element))
|
||||||
.toSet();
|
.toSet();
|
||||||
|
|
||||||
toggleAppSelected(String appId) {
|
toggleAppSelected(App app) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedIds.contains(appId)) {
|
if (selectedApps.contains(app)) {
|
||||||
selectedIds.remove(appId);
|
selectedApps.remove(app);
|
||||||
} else {
|
} else {
|
||||||
selectedIds.add(appId);
|
selectedApps.add(app);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -124,15 +124,15 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true);
|
var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true);
|
||||||
|
|
||||||
var existingUpdateIdsAllOrSelected = existingUpdates
|
var existingUpdateIdsAllOrSelected = existingUpdates
|
||||||
.where((element) => selectedIds.isEmpty
|
.where((element) => selectedApps.isEmpty
|
||||||
? sortedApps.where((a) => a.app.id == element).isNotEmpty
|
? sortedApps.where((a) => a.app.id == element).isNotEmpty
|
||||||
: selectedIds.contains(element))
|
: selectedApps.map((e) => e.id).contains(element))
|
||||||
.toList();
|
.toList();
|
||||||
var newInstallIdsAllOrSelected = appsProvider
|
var newInstallIdsAllOrSelected = appsProvider
|
||||||
.findExistingUpdates(nonInstalledOnly: true)
|
.findExistingUpdates(nonInstalledOnly: true)
|
||||||
.where((element) => selectedIds.isEmpty
|
.where((element) => selectedApps.isEmpty
|
||||||
? sortedApps.where((a) => a.app.id == element).isNotEmpty
|
? sortedApps.where((a) => a.app.id == element).isNotEmpty
|
||||||
: selectedIds.contains(element))
|
: selectedApps.map((e) => e.id).contains(element))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (settingsProvider.pinUpdates) {
|
if (settingsProvider.pinUpdates) {
|
||||||
@@ -147,6 +147,17 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
sortedApps = [...temp, ...sortedApps];
|
sortedApps = [...temp, ...sortedApps];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tempPinned = [];
|
||||||
|
var tempNotPinned = [];
|
||||||
|
for (var a in sortedApps) {
|
||||||
|
if (a.app.pinned) {
|
||||||
|
tempPinned.add(a);
|
||||||
|
} else {
|
||||||
|
tempNotPinned.add(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sortedApps = [...tempPinned, ...tempNotPinned];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
@@ -192,11 +203,16 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
selectedTileColor:
|
tileColor: sortedApps[index].app.pinned
|
||||||
Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
? Colors.grey.withOpacity(0.1)
|
||||||
selected: selectedIds.contains(sortedApps[index].app.id),
|
: Colors.transparent,
|
||||||
|
selectedTileColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
.withOpacity(sortedApps[index].app.pinned ? 0.2 : 0.1),
|
||||||
|
selected: selectedApps.contains(sortedApps[index].app),
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
toggleAppSelected(sortedApps[index].app.id);
|
toggleAppSelected(sortedApps[index].app);
|
||||||
},
|
},
|
||||||
leading: sortedApps[index].installedInfo != null
|
leading: sortedApps[index].installedInfo != null
|
||||||
? Image.memory(
|
? Image.memory(
|
||||||
@@ -204,9 +220,19 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
title: Text(sortedApps[index].installedInfo?.name ??
|
title: Text(
|
||||||
sortedApps[index].app.name),
|
sortedApps[index].installedInfo?.name ??
|
||||||
subtitle: Text('By ${sortedApps[index].app.author}'),
|
sortedApps[index].app.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: sortedApps[index].app.pinned
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal),
|
||||||
|
),
|
||||||
|
subtitle: Text('By ${sortedApps[index].app.author}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: sortedApps[index].app.pinned
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal)),
|
||||||
trailing: sortedApps[index].downloadProgress != null
|
trailing: sortedApps[index].downloadProgress != null
|
||||||
? Text(
|
? Text(
|
||||||
'Downloading - ${sortedApps[index].downloadProgress?.toInt()}%')
|
'Downloading - ${sortedApps[index].downloadProgress?.toInt()}%')
|
||||||
@@ -256,8 +282,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
)))),
|
)))),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (selectedIds.isNotEmpty) {
|
if (selectedApps.isNotEmpty) {
|
||||||
toggleAppSelected(sortedApps[index].app.id);
|
toggleAppSelected(sortedApps[index].app);
|
||||||
} else {
|
} else {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -275,25 +301,25 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
selectedIds.isEmpty
|
selectedApps.isEmpty
|
||||||
? selectThese(sortedApps.map((e) => e.app.id).toList())
|
? selectThese(sortedApps.map((e) => e.app).toList())
|
||||||
: clearSelected();
|
: clearSelected();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
selectedIds.isEmpty
|
selectedApps.isEmpty
|
||||||
? Icons.select_all_outlined
|
? Icons.select_all_outlined
|
||||||
: Icons.deselect_outlined,
|
: Icons.deselect_outlined,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
tooltip: selectedIds.isEmpty
|
tooltip: selectedApps.isEmpty
|
||||||
? 'Select All'
|
? 'Select All'
|
||||||
: 'Deselect ${selectedIds.length.toString()}'),
|
: 'Deselect ${selectedApps.length.toString()}'),
|
||||||
const VerticalDivider(),
|
const VerticalDivider(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
selectedIds.isEmpty
|
selectedApps.isEmpty
|
||||||
? const SizedBox()
|
? const SizedBox()
|
||||||
: IconButton(
|
: IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
@@ -307,11 +333,12 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
defaultValues: const [],
|
defaultValues: const [],
|
||||||
initValid: true,
|
initValid: true,
|
||||||
message:
|
message:
|
||||||
'${selectedIds.length} App${selectedIds.length == 1 ? '' : 's'} will be removed from Obtainium but remain installed. You still need to uninstall ${selectedIds.length == 1 ? 'it' : 'them'} manually.',
|
'${selectedApps.length} App${selectedApps.length == 1 ? '' : 's'} will be removed from Obtainium but remain installed. You still need to uninstall ${selectedApps.length == 1 ? 'it' : 'them'} manually.',
|
||||||
);
|
);
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
appsProvider.removeApps(selectedIds.toList());
|
appsProvider.removeApps(
|
||||||
|
selectedApps.map((e) => e.id).toList());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -347,7 +374,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title:
|
title:
|
||||||
'Install${selectedIds.isEmpty ? ' ' : ' Selected '}Apps?',
|
'Install${selectedApps.isEmpty ? ' ' : ' Selected '}Apps?',
|
||||||
message:
|
message:
|
||||||
'${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.',
|
'${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.',
|
||||||
items: formInputs,
|
items: formInputs,
|
||||||
@@ -386,11 +413,11 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
tooltip:
|
tooltip:
|
||||||
'Install/Update${selectedIds.isEmpty ? ' ' : ' Selected '}Apps',
|
'Install/Update${selectedApps.isEmpty ? ' ' : ' Selected '}Apps',
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.file_download_outlined,
|
Icons.file_download_outlined,
|
||||||
)),
|
)),
|
||||||
selectedIds.isEmpty
|
selectedApps.isEmpty
|
||||||
? const SizedBox()
|
? const SizedBox()
|
||||||
: IconButton(
|
: IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
@@ -419,7 +446,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
ctx) {
|
ctx) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(
|
title: Text(
|
||||||
'Mark ${selectedIds.length} Selected Apps as Updated?'),
|
'Mark ${selectedApps.length} Selected Apps as Updated?'),
|
||||||
content:
|
content:
|
||||||
const Text(
|
const Text(
|
||||||
'Only applies to installed but out of date Apps.'),
|
'Only applies to installed but out of date Apps.'),
|
||||||
@@ -438,9 +465,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
HapticFeedback
|
HapticFeedback
|
||||||
.selectionClick();
|
.selectionClick();
|
||||||
appsProvider
|
appsProvider
|
||||||
.saveApps(selectedIds.map((e) {
|
.saveApps(selectedApps.map((a) {
|
||||||
var a =
|
|
||||||
appsProvider.apps[e]!.app;
|
|
||||||
if (a.installedVersion !=
|
if (a.installedVersion !=
|
||||||
null) {
|
null) {
|
||||||
a.installedVersion = a.latestVersion;
|
a.installedVersion = a.latestVersion;
|
||||||
@@ -455,23 +480,50 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
'Yes'))
|
'Yes'))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
}).whenComplete(() {
|
||||||
|
Navigator.of(
|
||||||
|
context)
|
||||||
|
.pop();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
tooltip:
|
tooltip:
|
||||||
'Mark Selected Apps as Updated',
|
'Mark Selected Apps as Updated',
|
||||||
icon: const Icon(Icons.done)),
|
icon: const Icon(Icons.done)),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
var pinStatus = selectedApps
|
||||||
|
.where((element) =>
|
||||||
|
element.pinned)
|
||||||
|
.isEmpty;
|
||||||
|
appsProvider.saveApps(
|
||||||
|
selectedApps.map((e) {
|
||||||
|
e.pinned = pinStatus;
|
||||||
|
return e;
|
||||||
|
}).toList());
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
tooltip:
|
||||||
|
'${selectedApps.where((element) => element.pinned).isEmpty ? 'Pin to' : 'Unpin from'} top',
|
||||||
|
icon: Icon(selectedApps
|
||||||
|
.where((element) =>
|
||||||
|
element.pinned)
|
||||||
|
.isEmpty
|
||||||
|
? Icons.bookmark_outline_rounded
|
||||||
|
: Icons
|
||||||
|
.bookmark_remove_outlined),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
String urls = '';
|
String urls = '';
|
||||||
for (var id in selectedIds) {
|
for (var a in selectedApps) {
|
||||||
urls +=
|
urls += '${a.url}\n';
|
||||||
'${appsProvider.apps[id]!.app.url}\n';
|
|
||||||
}
|
}
|
||||||
urls = urls.substring(
|
urls = urls.substring(
|
||||||
0, urls.length - 1);
|
0, urls.length - 1);
|
||||||
Share.share(urls,
|
Share.share(urls,
|
||||||
subject:
|
subject:
|
||||||
'${selectedIds.length} Selected App URLs from Obtainium');
|
'${selectedApps.length} Selected App URLs from Obtainium');
|
||||||
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
tooltip: 'Share Selected App URLs',
|
tooltip: 'Share Selected App URLs',
|
||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share),
|
||||||
|
@@ -62,7 +62,6 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
const CustomAppBar(title: 'Import/Export'),
|
const CustomAppBar(title: 'Import/Export'),
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
hasScrollBody: false,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
@@ -224,7 +223,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
child: const Text(
|
child: const Text(
|
||||||
'Import from URL List',
|
'Import from URL List',
|
||||||
)),
|
)),
|
||||||
...sourceProvider.massUrlSources
|
...sourceProvider.sources
|
||||||
|
.where((element) => element.canSearch)
|
||||||
.map((source) => Column(
|
.map((source) => Column(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
CrossAxisAlignment.stretch,
|
CrossAxisAlignment.stretch,
|
||||||
@@ -234,52 +234,62 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
onPressed: importInProgress
|
onPressed: importInProgress
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
showDialog(
|
() async {
|
||||||
|
var values = await showDialog<
|
||||||
|
List<String>>(
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
(BuildContext ctx) {
|
(BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title:
|
title:
|
||||||
'Import ${source.name}',
|
'Search ${source.runtimeType}',
|
||||||
items: source
|
items: [
|
||||||
.requiredArgs
|
[
|
||||||
.map((e) => [
|
|
||||||
GeneratedFormItem(
|
GeneratedFormItem(
|
||||||
label: e)
|
label:
|
||||||
])
|
'${source.runtimeType} Search Query')
|
||||||
.toList(),
|
]
|
||||||
|
],
|
||||||
defaultValues: const [],
|
defaultValues: const [],
|
||||||
);
|
);
|
||||||
}).then((values) {
|
});
|
||||||
if (values != null) {
|
if (values != null &&
|
||||||
|
values[0].isNotEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = true;
|
importInProgress = true;
|
||||||
});
|
});
|
||||||
source
|
var urls = await source
|
||||||
.getUrls(values)
|
.search(values[0]);
|
||||||
.then((urls) {
|
if (urls.isNotEmpty) {
|
||||||
showDialog<List<String>?>(
|
var selectedUrls =
|
||||||
|
await showDialog<
|
||||||
|
List<
|
||||||
|
String>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
(BuildContext
|
(BuildContext
|
||||||
ctx) {
|
ctx) {
|
||||||
return UrlSelectionModal(
|
return UrlSelectionModal(
|
||||||
urls: urls);
|
urls: urls,
|
||||||
})
|
defaultSelected:
|
||||||
.then((selectedUrls) {
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
if (selectedUrls !=
|
if (selectedUrls !=
|
||||||
null) {
|
null &&
|
||||||
addApps(selectedUrls)
|
selectedUrls
|
||||||
.then((errors) {
|
.isNotEmpty) {
|
||||||
if (errors
|
var errors =
|
||||||
.isEmpty) {
|
await addApps(
|
||||||
|
selectedUrls);
|
||||||
|
if (errors.isEmpty) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
showError(
|
showError(
|
||||||
'Imported ${selectedUrls.length} Apps',
|
'Imported ${selectedUrls.length} Apps',
|
||||||
context);
|
context);
|
||||||
} else {
|
} else {
|
||||||
showDialog(
|
showDialog(
|
||||||
context:
|
context: context,
|
||||||
context,
|
|
||||||
builder:
|
builder:
|
||||||
(BuildContext
|
(BuildContext
|
||||||
ctx) {
|
ctx) {
|
||||||
@@ -291,32 +301,119 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
errors);
|
errors);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ObtainiumError(
|
||||||
|
'No results found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
.catchError((e) {
|
||||||
|
showError(e, context);
|
||||||
}).whenComplete(() {
|
}).whenComplete(() {
|
||||||
setState(() {
|
setState(() {
|
||||||
importInProgress =
|
importInProgress = false;
|
||||||
false;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Search ${source.runtimeType}'))
|
||||||
|
]))
|
||||||
|
.toList(),
|
||||||
|
...sourceProvider.massUrlSources
|
||||||
|
.map((source) => Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: importInProgress
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
() async {
|
||||||
|
var values = await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title:
|
||||||
|
'Import ${source.name}',
|
||||||
|
items:
|
||||||
|
source
|
||||||
|
.requiredArgs
|
||||||
|
.map(
|
||||||
|
(e) => [
|
||||||
|
GeneratedFormItem(label: e)
|
||||||
|
])
|
||||||
|
.toList(),
|
||||||
|
defaultValues: const [],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (values != null) {
|
||||||
|
setState(() {
|
||||||
|
importInProgress = true;
|
||||||
|
});
|
||||||
|
var urls = await source
|
||||||
|
.getUrls(values);
|
||||||
|
var selectedUrls =
|
||||||
|
await showDialog<
|
||||||
|
List<String>?>(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(BuildContext
|
||||||
|
ctx) {
|
||||||
|
return UrlSelectionModal(
|
||||||
|
urls: urls);
|
||||||
|
});
|
||||||
|
if (selectedUrls != null) {
|
||||||
|
var errors =
|
||||||
|
await addApps(
|
||||||
|
selectedUrls);
|
||||||
|
if (errors.isEmpty) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
showError(
|
||||||
|
'Imported ${selectedUrls.length} Apps',
|
||||||
|
context);
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
showDialog(
|
||||||
importInProgress =
|
context: context,
|
||||||
false;
|
builder:
|
||||||
|
(BuildContext
|
||||||
|
ctx) {
|
||||||
|
return ImportErrorDialog(
|
||||||
|
urlsLength:
|
||||||
|
selectedUrls
|
||||||
|
.length,
|
||||||
|
errors:
|
||||||
|
errors);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}).catchError((e) {
|
}
|
||||||
setState(() {
|
}()
|
||||||
importInProgress =
|
.catchError((e) {
|
||||||
false;
|
|
||||||
});
|
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
importInProgress = false;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Text('Import ${source.name}'))
|
child: Text('Import ${source.name}'))
|
||||||
]))
|
]))
|
||||||
.toList()
|
.toList(),
|
||||||
|
const Spacer(),
|
||||||
|
const Divider(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Imported Apps may incorrectly show as "Not Installed".\nTo fix this, re-install them through Obtainium.\nThis should not affect App data.\n\nOnly affects URL and third-party import methods.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontStyle: FontStyle.italic, fontSize: 12)),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
)))
|
)))
|
||||||
]));
|
]));
|
||||||
@@ -379,9 +476,11 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
|
|||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class UrlSelectionModal extends StatefulWidget {
|
class UrlSelectionModal extends StatefulWidget {
|
||||||
UrlSelectionModal({super.key, required this.urls});
|
UrlSelectionModal(
|
||||||
|
{super.key, required this.urls, this.defaultSelected = true});
|
||||||
|
|
||||||
List<String> urls;
|
List<String> urls;
|
||||||
|
bool defaultSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<UrlSelectionModal> createState() => _UrlSelectionModalState();
|
State<UrlSelectionModal> createState() => _UrlSelectionModalState();
|
||||||
@@ -393,7 +492,7 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
for (var url in widget.urls) {
|
for (var url in widget.urls) {
|
||||||
urlSelections.putIfAbsent(url, () => true);
|
urlSelections.putIfAbsent(url, () => widget.defaultSelected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,6 +5,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
@@ -138,12 +139,17 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// The former case should be handled (give the App its real ID), the latter is a security issue
|
// The former case should be handled (give the App its real ID), the latter is a security issue
|
||||||
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
||||||
if (app.id != newInfo.packageName) {
|
if (app.id != newInfo.packageName) {
|
||||||
if (apps[app.id] != null) {
|
if (apps[app.id] != null && !SourceProvider().isTempId(app.id)) {
|
||||||
throw IDChangedError();
|
throw IDChangedError();
|
||||||
}
|
}
|
||||||
|
var originalAppId = app.id;
|
||||||
app.id = newInfo.packageName;
|
app.id = newInfo.packageName;
|
||||||
downloadedFile = downloadedFile.renameSync(
|
downloadedFile = downloadedFile.renameSync(
|
||||||
'${downloadedFile.parent.path}/${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk');
|
'${downloadedFile.parent.path}/${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk');
|
||||||
|
if (apps[originalAppId] != null) {
|
||||||
|
await removeApps([originalAppId]);
|
||||||
|
await saveApps([app]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return DownloadedApk(app.id, downloadedFile);
|
return DownloadedApk(app.id, downloadedFile);
|
||||||
}
|
}
|
||||||
@@ -206,11 +212,18 @@ class AppsProvider with ChangeNotifier {
|
|||||||
Future<String?> confirmApkUrl(App app, BuildContext? context) async {
|
Future<String?> confirmApkUrl(App app, BuildContext? context) async {
|
||||||
// If the App has more than one APK, the user should pick one (if context provided)
|
// If the App has more than one APK, the user should pick one (if context provided)
|
||||||
String? apkUrl = app.apkUrls[app.preferredApkIndex];
|
String? apkUrl = app.apkUrls[app.preferredApkIndex];
|
||||||
|
// get device supported architecture
|
||||||
|
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
||||||
|
|
||||||
if (app.apkUrls.length > 1 && context != null) {
|
if (app.apkUrls.length > 1 && context != null) {
|
||||||
apkUrl = await showDialog(
|
apkUrl = await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return APKPicker(app: app, initVal: apkUrl);
|
return APKPicker(
|
||||||
|
app: app,
|
||||||
|
initVal: apkUrl,
|
||||||
|
archs: archs,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
||||||
@@ -327,6 +340,8 @@ class AppsProvider with ChangeNotifier {
|
|||||||
throw errors;
|
throw errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationsProvider().cancel(UpdateNotification([]).id);
|
||||||
|
|
||||||
return downloadedFiles.map((e) => e!.appId).toList();
|
return downloadedFiles.map((e) => e!.appId).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +490,8 @@ class AppsProvider with ChangeNotifier {
|
|||||||
currentApp.url,
|
currentApp.url,
|
||||||
currentApp.additionalData,
|
currentApp.additionalData,
|
||||||
name: currentApp.name,
|
name: currentApp.name,
|
||||||
id: currentApp.id);
|
id: currentApp.id,
|
||||||
|
pinned: currentApp.pinned);
|
||||||
newApp.installedVersion = currentApp.installedVersion;
|
newApp.installedVersion = currentApp.installedVersion;
|
||||||
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
|
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
|
||||||
newApp.preferredApkIndex = currentApp.preferredApkIndex;
|
newApp.preferredApkIndex = currentApp.preferredApkIndex;
|
||||||
@@ -586,10 +602,11 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class APKPicker extends StatefulWidget {
|
class APKPicker extends StatefulWidget {
|
||||||
const APKPicker({super.key, required this.app, this.initVal});
|
const APKPicker({super.key, required this.app, this.initVal, this.archs});
|
||||||
|
|
||||||
final App app;
|
final App app;
|
||||||
final String? initVal;
|
final String? initVal;
|
||||||
|
final List<String>? archs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<APKPicker> createState() => _APKPickerState();
|
State<APKPicker> createState() => _APKPickerState();
|
||||||
@@ -607,7 +624,8 @@ class _APKPickerState extends State<APKPicker> {
|
|||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
Text('${widget.app.name} has more than one package:'),
|
Text('${widget.app.name} has more than one package:'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
...widget.app.apkUrls.map((u) => RadioListTile<String>(
|
...widget.app.apkUrls.map(
|
||||||
|
(u) => RadioListTile<String>(
|
||||||
title: Text(Uri.parse(u)
|
title: Text(Uri.parse(u)
|
||||||
.pathSegments
|
.pathSegments
|
||||||
.where((element) => element.isNotEmpty)
|
.where((element) => element.isNotEmpty)
|
||||||
@@ -618,7 +636,17 @@ class _APKPickerState extends State<APKPicker> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
apkUrl = val;
|
apkUrl = val;
|
||||||
});
|
});
|
||||||
}))
|
}),
|
||||||
|
),
|
||||||
|
if (widget.archs != null)
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
if (widget.archs != null)
|
||||||
|
Text(
|
||||||
|
'Note:\nYour device supports the ${widget.archs!.length == 1 ? '\'${widget.archs![0]}\' CPU architecture.' : 'following CPU architectures: ${list2FriendlyString(widget.archs!.map((e) => '\'$e\'').toList())}.'}',
|
||||||
|
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@@ -27,7 +27,9 @@ class UpdateNotification extends ObtainiumNotification {
|
|||||||
'Updates Available',
|
'Updates Available',
|
||||||
'Notifies the user that updates are available for one or more Apps tracked by Obtainium',
|
'Notifies the user that updates are available for one or more Apps tracked by Obtainium',
|
||||||
Importance.max) {
|
Importance.max) {
|
||||||
message = updates.length == 1
|
message = updates.isEmpty
|
||||||
|
? "No new updates."
|
||||||
|
: updates.length == 1
|
||||||
? '${updates[0].name} has an update.'
|
? '${updates[0].name} has an update.'
|
||||||
: '${(updates.length == 2 ? '${updates[0].name} and ${updates[1].name}' : '${updates[0].name} and ${updates.length - 1} more apps')} have updates.';
|
: '${(updates.length == 2 ? '${updates[0].name} and ${updates[1].name}' : '${updates[0].name} and ${updates.length - 1} more apps')} have updates.';
|
||||||
}
|
}
|
||||||
|
@@ -40,6 +40,7 @@ class App {
|
|||||||
late int preferredApkIndex;
|
late int preferredApkIndex;
|
||||||
late List<String> additionalData;
|
late List<String> additionalData;
|
||||||
late DateTime? lastUpdateCheck;
|
late DateTime? lastUpdateCheck;
|
||||||
|
bool pinned = false;
|
||||||
App(
|
App(
|
||||||
this.id,
|
this.id,
|
||||||
this.url,
|
this.url,
|
||||||
@@ -50,11 +51,12 @@ class App {
|
|||||||
this.apkUrls,
|
this.apkUrls,
|
||||||
this.preferredApkIndex,
|
this.preferredApkIndex,
|
||||||
this.additionalData,
|
this.additionalData,
|
||||||
this.lastUpdateCheck);
|
this.lastUpdateCheck,
|
||||||
|
this.pinned);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALDATA: ${additionalData.toString()} LASTCHECK: ${lastUpdateCheck.toString()}';
|
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALDATA: ${additionalData.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned';
|
||||||
}
|
}
|
||||||
|
|
||||||
factory App.fromJson(Map<String, dynamic> json) => App(
|
factory App.fromJson(Map<String, dynamic> json) => App(
|
||||||
@@ -75,7 +77,8 @@ class App {
|
|||||||
: List<String>.from(jsonDecode(json['additionalData'])),
|
: List<String>.from(jsonDecode(json['additionalData'])),
|
||||||
json['lastUpdateCheck'] == null
|
json['lastUpdateCheck'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']));
|
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||||
|
json['pinned'] ?? false);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'id': id,
|
'id': id,
|
||||||
@@ -87,7 +90,8 @@ class App {
|
|||||||
'apkUrls': jsonEncode(apkUrls),
|
'apkUrls': jsonEncode(apkUrls),
|
||||||
'preferredApkIndex': preferredApkIndex,
|
'preferredApkIndex': preferredApkIndex,
|
||||||
'additionalData': jsonEncode(additionalData),
|
'additionalData': jsonEncode(additionalData),
|
||||||
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch
|
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
||||||
|
'pinned': pinned
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,17 +132,36 @@ List<String> getLinksFromParsedHTML(
|
|||||||
.map((e) => '$prependToLinks${e.attributes['href']!}')
|
.map((e) => '$prependToLinks${e.attributes['href']!}')
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
abstract class AppSource {
|
class AppSource {
|
||||||
late String host;
|
late String host;
|
||||||
String standardizeURL(String url);
|
String standardizeURL(String url) {
|
||||||
|
throw NotImplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, List<String> additionalData);
|
String standardUrl, List<String> additionalData) {
|
||||||
AppNames getAppNames(String standardUrl);
|
throw NotImplementedError();
|
||||||
late List<List<GeneratedFormItem>> additionalDataFormItems;
|
}
|
||||||
late List<String> additionalDataDefaults;
|
|
||||||
late List<GeneratedFormItem> moreSourceSettingsFormItems;
|
AppNames getAppNames(String standardUrl) {
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl);
|
throw NotImplementedError();
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl);
|
}
|
||||||
|
|
||||||
|
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
||||||
|
List<String> additionalDataDefaults = [];
|
||||||
|
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
||||||
|
String? changeLogPageFromStandardUrl(String standardUrl) {
|
||||||
|
throw NotImplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> apkUrlPrefetchModifier(String apkUrl) {
|
||||||
|
throw NotImplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canSearch = false;
|
||||||
|
Future<List<String>> search(String query) {
|
||||||
|
throw NotImplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class MassAppUrlSource {
|
abstract class MassAppUrlSource {
|
||||||
@@ -156,8 +179,7 @@ class SourceProvider {
|
|||||||
IzzyOnDroid(),
|
IzzyOnDroid(),
|
||||||
Mullvad(),
|
Mullvad(),
|
||||||
Signal(),
|
Signal(),
|
||||||
SourceForge(),
|
SourceForge()
|
||||||
// APKMirror()
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add more mass url source classes here so they are available via the service
|
// Add more mass url source classes here so they are available via the service
|
||||||
@@ -192,8 +214,21 @@ class SourceProvider {
|
|||||||
String generateTempID(AppNames names, AppSource source) =>
|
String generateTempID(AppNames names, AppSource source) =>
|
||||||
'${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}';
|
'${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}';
|
||||||
|
|
||||||
|
bool isTempId(String id) {
|
||||||
|
List<String> parts = id.split('_');
|
||||||
|
if (parts.length < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < parts.length - 1; i++) {
|
||||||
|
if (RegExp('.*[A-Z].*').hasMatch(parts[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getSourceHosts().contains(parts.last);
|
||||||
|
}
|
||||||
|
|
||||||
Future<App> getApp(AppSource source, String url, List<String> additionalData,
|
Future<App> getApp(AppSource source, String url, List<String> additionalData,
|
||||||
{String name = '', String? id}) async {
|
{String name = '', String? id, bool pinned = false}) async {
|
||||||
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
||||||
AppNames names = source.getAppNames(standardUrl);
|
AppNames names = source.getAppNames(standardUrl);
|
||||||
APKDetails apk =
|
APKDetails apk =
|
||||||
@@ -210,7 +245,8 @@ class SourceProvider {
|
|||||||
apk.apkUrls,
|
apk.apkUrls,
|
||||||
apk.apkUrls.length - 1,
|
apk.apkUrls.length - 1,
|
||||||
additionalData,
|
additionalData,
|
||||||
DateTime.now());
|
DateTime.now(),
|
||||||
|
pinned);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns errors in [results, errors] instead of throwing them
|
// Returns errors in [results, errors] instead of throwing them
|
||||||
|
20
pubspec.lock
@@ -21,7 +21,7 @@ packages:
|
|||||||
name: archive
|
name: archive
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.2"
|
version: "3.3.4"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -78,6 +78,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
cross_file:
|
cross_file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -173,7 +180,7 @@ packages:
|
|||||||
name: flutter_fgbg
|
name: flutter_fgbg
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.1"
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -457,6 +464,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
|
pointycastle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pointycastle
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.6.2"
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -706,7 +720,7 @@ packages:
|
|||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.1.1"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@@ -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
|
# 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
|
# 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.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.6.7+51 # When changing this, update the tag in main() accordingly
|
version: 0.7.0+56 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.18.2 <3.0.0'
|
sdk: '>=2.18.2 <3.0.0'
|
||||||
|