mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-15 06:06:44 +02:00
Compare commits
3 Commits
v0.1.5-bet
...
v0.1.6-bet
Author | SHA1 | Date | |
---|---|---|---|
96c1ed612d | |||
4d75a6a361 | |||
30075add1c |
@ -11,7 +11,7 @@ import 'package:workmanager/workmanager.dart';
|
|||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
|
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v0.1.5-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v0.1.6-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void bgTaskCallback() {
|
void bgTaskCallback() {
|
||||||
@ -94,7 +94,9 @@ class MyApp extends StatelessWidget {
|
|||||||
'ImranR98',
|
'ImranR98',
|
||||||
'Obtainium',
|
'Obtainium',
|
||||||
currentReleaseTag,
|
currentReleaseTag,
|
||||||
currentReleaseTag, []));
|
currentReleaseTag,
|
||||||
|
[],
|
||||||
|
0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (apps[id] == null) {
|
if (apps[id] == null) {
|
||||||
throw 'App not found';
|
throw 'App not found';
|
||||||
}
|
}
|
||||||
String? apkUrl = apps[id]!.app.apkUrls.last;
|
String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex];
|
||||||
if (apps[id]!.app.apkUrls.length > 1) {
|
if (apps[id]!.app.apkUrls.length > 1) {
|
||||||
apkUrl = await showDialog(
|
apkUrl = await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -105,6 +105,11 @@ class AppsProvider with ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (apkUrl != null) {
|
if (apkUrl != null) {
|
||||||
|
int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
|
||||||
|
if (urlInd != apps[id]!.app.preferredApkIndex) {
|
||||||
|
apps[id]!.app.preferredApkIndex = urlInd;
|
||||||
|
await saveApp(apps[id]!.app);
|
||||||
|
}
|
||||||
appsToInstall.putIfAbsent(id, () => apkUrl!);
|
appsToInstall.putIfAbsent(id, () => apkUrl!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,6 +205,9 @@ class AppsProvider with ChangeNotifier {
|
|||||||
App newApp = await SourceProvider().getApp(currentApp.url);
|
App newApp = await SourceProvider().getApp(currentApp.url);
|
||||||
if (newApp.latestVersion != currentApp.latestVersion) {
|
if (newApp.latestVersion != currentApp.latestVersion) {
|
||||||
newApp.installedVersion = currentApp.installedVersion;
|
newApp.installedVersion = currentApp.installedVersion;
|
||||||
|
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
|
||||||
|
newApp.preferredApkIndex = currentApp.preferredApkIndex;
|
||||||
|
}
|
||||||
await saveApp(newApp);
|
await saveApp(newApp);
|
||||||
return newApp;
|
return newApp;
|
||||||
}
|
}
|
||||||
@ -290,17 +298,17 @@ class _APKPickerState extends State<APKPicker> {
|
|||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: const Text('Pick an APK'),
|
title: const Text('Pick an APK'),
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
Text('${widget.app.name} has more than one package - pick one.'),
|
Text('${widget.app.name} has more than one package:'),
|
||||||
...widget.app.apkUrls.map((u) => ListTile(
|
const SizedBox(height: 16),
|
||||||
|
...widget.app.apkUrls.map((u) => RadioListTile<String>(
|
||||||
title: Text(Uri.parse(u).pathSegments.last),
|
title: Text(Uri.parse(u).pathSegments.last),
|
||||||
leading: Radio<String>(
|
value: u,
|
||||||
value: u,
|
groupValue: apkUrl,
|
||||||
groupValue: apkUrl,
|
onChanged: (String? val) {
|
||||||
onChanged: (String? val) {
|
setState(() {
|
||||||
setState(() {
|
apkUrl = val;
|
||||||
apkUrl = val;
|
});
|
||||||
});
|
}))
|
||||||
})))
|
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -29,8 +29,9 @@ class App {
|
|||||||
String? installedVersion;
|
String? installedVersion;
|
||||||
late String latestVersion;
|
late String latestVersion;
|
||||||
List<String> apkUrls = [];
|
List<String> apkUrls = [];
|
||||||
|
late int preferredApkIndex;
|
||||||
App(this.id, this.url, this.author, this.name, this.installedVersion,
|
App(this.id, this.url, this.author, this.name, this.installedVersion,
|
||||||
this.latestVersion, this.apkUrls);
|
this.latestVersion, this.apkUrls, this.preferredApkIndex);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -38,15 +39,19 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
factory App.fromJson(Map<String, dynamic> json) => App(
|
factory App.fromJson(Map<String, dynamic> json) => App(
|
||||||
json['id'] as String,
|
json['id'] as String,
|
||||||
json['url'] as String,
|
json['url'] as String,
|
||||||
json['author'] as String,
|
json['author'] as String,
|
||||||
json['name'] as String,
|
json['name'] as String,
|
||||||
json['installedVersion'] == null
|
json['installedVersion'] == null
|
||||||
? null
|
? null
|
||||||
: json['installedVersion'] as String,
|
: json['installedVersion'] as String,
|
||||||
json['latestVersion'] as String,
|
json['latestVersion'] as String,
|
||||||
List<String>.from(jsonDecode(json['apkUrls'])));
|
List<String>.from(jsonDecode(json['apkUrls'])),
|
||||||
|
json['preferredApkIndex'] == null
|
||||||
|
? 0
|
||||||
|
: json['preferredApkIndex'] as int,
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'id': id,
|
'id': id,
|
||||||
@ -56,6 +61,7 @@ class App {
|
|||||||
'installedVersion': installedVersion,
|
'installedVersion': installedVersion,
|
||||||
'latestVersion': latestVersion,
|
'latestVersion': latestVersion,
|
||||||
'apkUrls': jsonEncode(apkUrls),
|
'apkUrls': jsonEncode(apkUrls),
|
||||||
|
'preferredApkIndex': preferredApkIndex
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +95,7 @@ class GitHub implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]*/[^/]*');
|
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw 'Not a valid URL';
|
throw 'Not a valid URL';
|
||||||
@ -99,7 +105,6 @@ class GitHub implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
||||||
// The GitHub RSS feed does not contain asset download details, so we use web scraping (avoid API due to rate limits)
|
|
||||||
Response res = await get(Uri.parse('$standardUrl/releases/latest'));
|
Response res = await get(Uri.parse('$standardUrl/releases/latest'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var standardUri = Uri.parse(standardUrl);
|
var standardUri = Uri.parse(standardUrl);
|
||||||
@ -148,7 +153,7 @@ class GitLab implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]*/[^/]*');
|
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw 'Not a valid URL';
|
throw 'Not a valid URL';
|
||||||
@ -158,7 +163,6 @@ class GitLab implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
||||||
// GitLab provides an RSS feed with all the details we need
|
|
||||||
Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom'));
|
Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var standardUri = Uri.parse(standardUrl);
|
var standardUri = Uri.parse(standardUrl);
|
||||||
@ -225,11 +229,98 @@ class Signal implements AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AppNames getAppNames(String standardUrl) => AppNames('signal', 'signal');
|
AppNames getAppNames(String standardUrl) => AppNames('Signal', 'Signal');
|
||||||
|
}
|
||||||
|
|
||||||
|
class FDroid implements AppSource {
|
||||||
|
@override
|
||||||
|
late String host = 'f-droid.org';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/packages/[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw 'Not a valid URL';
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
||||||
|
Response res = await get(Uri.parse(standardUrl));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var latestReleaseDiv =
|
||||||
|
parse(res.body).querySelector('#latest.package-version');
|
||||||
|
var apkUrl = latestReleaseDiv
|
||||||
|
?.querySelector('.package-version-download a')
|
||||||
|
?.attributes['href'];
|
||||||
|
if (apkUrl == null) {
|
||||||
|
throw 'No APK found';
|
||||||
|
}
|
||||||
|
var version = latestReleaseDiv
|
||||||
|
?.querySelector('.package-version-header b')
|
||||||
|
?.innerHtml
|
||||||
|
.split(' ')
|
||||||
|
.last;
|
||||||
|
if (version == null) {
|
||||||
|
throw 'Could not determine latest release version';
|
||||||
|
}
|
||||||
|
return APKDetails(version, [apkUrl]);
|
||||||
|
} else {
|
||||||
|
throw 'Unable to fetch release info';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AppNames getAppNames(String standardUrl) {
|
||||||
|
var name = Uri.parse(standardUrl).pathSegments.last;
|
||||||
|
return AppNames(name, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Mullvad implements AppSource {
|
||||||
|
@override
|
||||||
|
late String host = 'mullvad.net';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://$host');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw 'Not a valid URL';
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
||||||
|
Response res = await get(Uri.parse('$standardUrl/en/download/android'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var version = parse(res.body)
|
||||||
|
.querySelector('p.subtitle.is-6')
|
||||||
|
?.querySelector('a')
|
||||||
|
?.attributes['href']
|
||||||
|
?.split('/')
|
||||||
|
.last;
|
||||||
|
if (version == null) {
|
||||||
|
throw 'Could not determine the latest release version';
|
||||||
|
}
|
||||||
|
return APKDetails(
|
||||||
|
version, ['https://mullvad.net/download/app/apk/latest']);
|
||||||
|
} else {
|
||||||
|
throw 'Unable to fetch release info';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AppNames getAppNames(String standardUrl) {
|
||||||
|
return AppNames('Mullvad-VPN', 'Mullvad-VPN');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SourceProvider {
|
class SourceProvider {
|
||||||
List<AppSource> sources = [GitHub(), GitLab(), Signal()];
|
List<AppSource> sources = [GitHub(), GitLab(), FDroid(), Mullvad(), Signal()];
|
||||||
|
|
||||||
// Add more source classes here so they are available via the service
|
// Add more source classes here so they are available via the service
|
||||||
AppSource getSource(String url) {
|
AppSource getSource(String url) {
|
||||||
@ -265,7 +356,8 @@ class SourceProvider {
|
|||||||
names.name[0].toUpperCase() + names.name.substring(1),
|
names.name[0].toUpperCase() + names.name.substring(1),
|
||||||
null,
|
null,
|
||||||
apk.version,
|
apk.version,
|
||||||
apk.apkUrls);
|
apk.apkUrls,
|
||||||
|
apk.apkUrls.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> getSourceHosts() => sources.map((e) => e.host).toList();
|
List<String> getSourceHosts() => sources.map((e) => e.host).toList();
|
||||||
|
@ -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.1.5+6 # When changing this, update the tag in main() accordingly
|
version: 0.1.6+7 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.19.0-79.0.dev <3.0.0'
|
sdk: '>=2.19.0-79.0.dev <3.0.0'
|
||||||
|
Reference in New Issue
Block a user