mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-16 14:46:44 +02:00
Compare commits
5 Commits
v0.1.7-bet
...
v0.1.9-bet
Author | SHA1 | Date | |
---|---|---|---|
294327bde4 | |||
52b97662c6 | |||
f63da4b538 | |||
c30c692d87 | |||
d643d5a474 |
@ -16,7 +16,7 @@ Currently supported App sources:
|
|||||||
## Limitations
|
## Limitations
|
||||||
- App installs are assumed to have succeeded; failures and cancelled installs cannot be detected.
|
- App installs are assumed to have succeeded; failures and cancelled installs cannot be detected.
|
||||||
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
|
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
|
||||||
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods are either unavailable (e.g. Mullvad), insufficient (e.g. GitHub RSS) or subject to rate limits (e.g. GitHub API).
|
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
@ -9,9 +9,10 @@ import 'package:permission_handler/permission_handler.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:workmanager/workmanager.dart';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v0.1.7-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v0.1.9-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void bgTaskCallback() {
|
void bgTaskCallback() {
|
||||||
@ -43,10 +44,12 @@ void bgTaskCallback() {
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt! >= 29) {
|
||||||
const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
);
|
const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
);
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
}
|
||||||
Workmanager().initialize(
|
Workmanager().initialize(
|
||||||
bgTaskCallback,
|
bgTaskCallback,
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@ -17,6 +19,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var appsProvider = context.watch<AppsProvider>();
|
var appsProvider = context.watch<AppsProvider>();
|
||||||
|
var settingsProvider = context.watch<SettingsProvider>();
|
||||||
AppInMemory? app = appsProvider.apps[widget.appId];
|
AppInMemory? app = appsProvider.apps[widget.appId];
|
||||||
if (app?.app.installedVersion != null) {
|
if (app?.app.installedVersion != null) {
|
||||||
appsProvider.getUpdate(app!.app.id);
|
appsProvider.getUpdate(app!.app.id);
|
||||||
@ -25,10 +28,58 @@ class _AppPageState extends State<AppPage> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('${app?.app.author}/${app?.app.name}'),
|
title: Text('${app?.app.author}/${app?.app.name}'),
|
||||||
),
|
),
|
||||||
body: WebView(
|
body: settingsProvider.showAppWebpage
|
||||||
initialUrl: app?.app.url,
|
? WebView(
|
||||||
javascriptMode: JavascriptMode.unrestricted,
|
initialUrl: app?.app.url,
|
||||||
),
|
javascriptMode: JavascriptMode.unrestricted,
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
app?.app.name ?? 'App',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.displayLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'By ${app?.app.author ?? 'Unknown'}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (app?.app.url != null) {
|
||||||
|
launchUrlString(app?.app.url ?? '',
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
app?.app.url ?? '',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
fontSize: 12),
|
||||||
|
)),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Latest Version: ${app?.app.latestVersion ?? 'Unknown'}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Installed Version: ${app?.app.installedVersion ?? 'None'}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
bottomSheet: Padding(
|
bottomSheet: Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0, 0, 0, MediaQuery.of(context).padding.bottom),
|
0, 0, 0, MediaQuery.of(context).padding.bottom),
|
||||||
@ -100,8 +151,10 @@ class _AppPageState extends State<AppPage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: Theme.of(context).errorColor,
|
foregroundColor:
|
||||||
surfaceTintColor: Theme.of(context).errorColor),
|
Theme.of(context).colorScheme.error,
|
||||||
|
surfaceTintColor:
|
||||||
|
Theme.of(context).colorScheme.error),
|
||||||
child: const Text('Remove'),
|
child: const Text('Remove'),
|
||||||
),
|
),
|
||||||
])),
|
])),
|
||||||
|
@ -42,7 +42,7 @@ class _AppsPageState extends State<AppsPage> {
|
|||||||
: appsProvider.apps.isEmpty
|
: appsProvider.apps.isEmpty
|
||||||
? Text(
|
? Text(
|
||||||
'No Apps',
|
'No Apps',
|
||||||
style: Theme.of(context).textTheme.headline4,
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
)
|
)
|
||||||
: RefreshIndicator(
|
: RefreshIndicator(
|
||||||
onRefresh: () {
|
onRefresh: () {
|
||||||
|
@ -110,7 +110,21 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 32,
|
height: 16,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text('Show Source Webpage in App View'),
|
||||||
|
Switch(
|
||||||
|
value: settingsProvider.showAppWebpage,
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsProvider.showAppWebpage = value;
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
@ -127,7 +141,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: const Text('Export Apps')),
|
child: const Text('Export App List')),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
@ -140,7 +154,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: const Text('Import Apps'),
|
title: const Text('Import App List'),
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Copy the contents of the Obtainium export file and paste them into the field below:'),
|
'Copy the contents of the Obtainium export file and paste them into the field below:'),
|
||||||
@ -193,7 +207,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
.showSnackBar(
|
.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
'$value Apps Imported')),
|
'$value App${value == 1 ? '' : 's'} Imported')),
|
||||||
);
|
);
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
@ -212,7 +226,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: const Text('Import Apps'))
|
child: const Text('Import App List'))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@ -235,7 +249,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
icon: const Icon(Icons.code),
|
icon: const Icon(Icons.code),
|
||||||
label: Text(
|
label: Text(
|
||||||
'Source',
|
'Source',
|
||||||
style: Theme.of(context).textTheme.caption,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -108,6 +108,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (apps[id] == null) {
|
if (apps[id] == null) {
|
||||||
throw 'App not found';
|
throw 'App not found';
|
||||||
}
|
}
|
||||||
|
// If the App has more than one APK, the user should pick one
|
||||||
String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex];
|
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(
|
||||||
@ -116,6 +117,19 @@ class AppsProvider with ChangeNotifier {
|
|||||||
return APKPicker(app: apps[id]!.app, initVal: apkUrl);
|
return APKPicker(app: apps[id]!.app, initVal: apkUrl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// If the picked APK comes from an origin different from the source, get user confirmation
|
||||||
|
if (apkUrl != null &&
|
||||||
|
!apkUrl.toLowerCase().startsWith(apps[id]!.app.url.toLowerCase())) {
|
||||||
|
if (await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return APKOriginWarningDialog(
|
||||||
|
sourceUrl: apps[id]!.app.url, apkUrl: apkUrl!);
|
||||||
|
}) !=
|
||||||
|
true) {
|
||||||
|
apkUrl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (apkUrl != null) {
|
if (apkUrl != null) {
|
||||||
int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
|
int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
|
||||||
if (urlInd != apps[id]!.app.preferredApkIndex) {
|
if (urlInd != apps[id]!.app.preferredApkIndex) {
|
||||||
@ -331,7 +345,7 @@ class _APKPickerState extends State<APKPicker> {
|
|||||||
child: const Text('Cancel')),
|
child: const Text('Cancel')),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.heavyImpact();
|
||||||
Navigator.of(context).pop(apkUrl);
|
Navigator.of(context).pop(apkUrl);
|
||||||
},
|
},
|
||||||
child: const Text('Continue'))
|
child: const Text('Continue'))
|
||||||
@ -339,3 +353,40 @@ class _APKPickerState extends State<APKPicker> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class APKOriginWarningDialog extends StatefulWidget {
|
||||||
|
const APKOriginWarningDialog(
|
||||||
|
{super.key, required this.sourceUrl, required this.apkUrl});
|
||||||
|
|
||||||
|
final String sourceUrl;
|
||||||
|
final String apkUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<APKOriginWarningDialog> createState() => _APKOriginWarningDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _APKOriginWarningDialogState extends State<APKOriginWarningDialog> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: const Text('Warning'),
|
||||||
|
content: Text(
|
||||||
|
'The App source is \'${Uri.parse(widget.sourceUrl).host}\' but the release package comes from \'${Uri.parse(widget.apkUrl).host}\'. Continue?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
},
|
||||||
|
child: const Text('Cancel')),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
child: const Text('Continue'))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -69,4 +69,13 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get showAppWebpage {
|
||||||
|
return prefs?.getBool('showAppWebpage') ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
set showAppWebpage(bool show) {
|
||||||
|
prefs?.setBool('showAppWebpage', show);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,12 @@ escapeRegEx(String s) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const String couldNotFindReleases = 'Unable to fetch release info';
|
||||||
|
const String couldNotFindLatestVersion =
|
||||||
|
'Could not determine latest release version';
|
||||||
|
const String notValidURL = 'Not a valid URL';
|
||||||
|
const String noAPKFound = 'No APK found';
|
||||||
|
|
||||||
List<String> getLinksFromParsedHTML(
|
List<String> getLinksFromParsedHTML(
|
||||||
Document dom, RegExp hrefPattern, String prependToLinks) =>
|
Document dom, RegExp hrefPattern, String prependToLinks) =>
|
||||||
dom
|
dom
|
||||||
@ -98,44 +104,53 @@ class GitHub implements AppSource {
|
|||||||
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 notValidURL;
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return url.substring(0, match.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
Future<APKDetails> getLatestAPKDetails(String standardUrl) async {
|
||||||
Response res = await get(Uri.parse('$standardUrl/releases/latest'));
|
Response res = await get(Uri.parse(
|
||||||
|
'https://api.$host/repos${standardUrl.substring('https://$host'.length)}/releases'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var standardUri = Uri.parse(standardUrl);
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
var parsedHtml = parse(res.body);
|
// Right now, the latest non-prerelease version is picked
|
||||||
var apkUrlList = getLinksFromParsedHTML(
|
// If none exists, the latest prerelease version is picked
|
||||||
parsedHtml,
|
// In the future, the user could be given a choice
|
||||||
RegExp(
|
var nonPrereleaseReleases =
|
||||||
'^${escapeRegEx(standardUri.path)}/releases/download/[^/]+/[^/]+\\.apk\$',
|
releases.where((element) => element['prerelease'] != true).toList();
|
||||||
caseSensitive: false),
|
var latestRelease = nonPrereleaseReleases.isNotEmpty
|
||||||
standardUri.origin);
|
? nonPrereleaseReleases[0]
|
||||||
if (apkUrlList.isEmpty) {
|
: releases.isNotEmpty
|
||||||
throw 'No APK found';
|
? releases[0]
|
||||||
|
: null;
|
||||||
|
if (latestRelease == null) {
|
||||||
|
throw couldNotFindReleases;
|
||||||
}
|
}
|
||||||
String getTag(String url) {
|
List<dynamic>? assets = latestRelease['assets'];
|
||||||
List<String> parts = url.split('/');
|
List<String>? apkUrlList = assets
|
||||||
return parts[parts.length - 2];
|
?.map((e) {
|
||||||
|
return e['browser_download_url'] != null
|
||||||
|
? e['browser_download_url'] as String
|
||||||
|
: '';
|
||||||
|
})
|
||||||
|
.where((element) => element.toLowerCase().endsWith('.apk'))
|
||||||
|
.toList();
|
||||||
|
if (apkUrlList == null || apkUrlList.isEmpty) {
|
||||||
|
throw noAPKFound;
|
||||||
|
}
|
||||||
|
String? version = latestRelease['tag_name'];
|
||||||
|
if (version == null) {
|
||||||
|
throw couldNotFindLatestVersion;
|
||||||
|
}
|
||||||
|
return APKDetails(version, apkUrlList);
|
||||||
|
} else {
|
||||||
|
if (res.headers['x-ratelimit-remaining'] == '0') {
|
||||||
|
throw 'Rate limit reached - try again in ${(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / 60000000).toString()} minutes';
|
||||||
}
|
}
|
||||||
|
|
||||||
String latestTag = getTag(apkUrlList[0]);
|
throw couldNotFindReleases;
|
||||||
String? version = parsedHtml
|
|
||||||
.querySelector('.octicon-tag')
|
|
||||||
?.nextElementSibling
|
|
||||||
?.innerHtml
|
|
||||||
.trim();
|
|
||||||
if (version == null) {
|
|
||||||
throw 'Could not determine latest release version';
|
|
||||||
}
|
|
||||||
return APKDetails(version,
|
|
||||||
apkUrlList.where((element) => getTag(element) == latestTag).toList());
|
|
||||||
} else {
|
|
||||||
throw 'Unable to fetch release info';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +171,7 @@ class GitLab implements AppSource {
|
|||||||
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 notValidURL;
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return url.substring(0, match.end);
|
||||||
}
|
}
|
||||||
@ -170,25 +185,32 @@ class GitLab implements AppSource {
|
|||||||
var entry = parsedHtml.querySelector('entry');
|
var entry = parsedHtml.querySelector('entry');
|
||||||
var entryContent =
|
var entryContent =
|
||||||
parse(parseFragment(entry?.querySelector('content')!.innerHtml).text);
|
parse(parseFragment(entry?.querySelector('content')!.innerHtml).text);
|
||||||
var apkUrlList = getLinksFromParsedHTML(
|
var apkUrlList = [
|
||||||
entryContent,
|
...getLinksFromParsedHTML(
|
||||||
RegExp(
|
entryContent,
|
||||||
'^${escapeRegEx(standardUri.path)}/uploads/[^/]+/[^/]+\\.apk\$',
|
RegExp(
|
||||||
caseSensitive: false),
|
'^${escapeRegEx(standardUri.path)}/uploads/[^/]+/[^/]+\\.apk\$',
|
||||||
standardUri.origin);
|
caseSensitive: false),
|
||||||
|
standardUri.origin),
|
||||||
|
// GitLab releases may contain links to externally hosted APKs
|
||||||
|
...getLinksFromParsedHTML(entryContent,
|
||||||
|
RegExp('/[^/]+\\.apk\$', caseSensitive: false), '')
|
||||||
|
.where((element) => Uri.parse(element).host != '')
|
||||||
|
.toList()
|
||||||
|
];
|
||||||
if (apkUrlList.isEmpty) {
|
if (apkUrlList.isEmpty) {
|
||||||
throw 'No APK found';
|
throw noAPKFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
var entryId = entry?.querySelector('id')?.innerHtml;
|
var entryId = entry?.querySelector('id')?.innerHtml;
|
||||||
var version =
|
var version =
|
||||||
entryId == null ? null : Uri.parse(entryId).pathSegments.last;
|
entryId == null ? null : Uri.parse(entryId).pathSegments.last;
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw 'Could not determine latest release version';
|
throw couldNotFindLatestVersion;
|
||||||
}
|
}
|
||||||
return APKDetails(version, apkUrlList);
|
return APKDetails(version, apkUrlList);
|
||||||
} else {
|
} else {
|
||||||
throw 'Unable to fetch release info';
|
throw couldNotFindReleases;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,15 +238,15 @@ class Signal implements AppSource {
|
|||||||
var json = jsonDecode(res.body);
|
var json = jsonDecode(res.body);
|
||||||
String? apkUrl = json['url'];
|
String? apkUrl = json['url'];
|
||||||
if (apkUrl == null) {
|
if (apkUrl == null) {
|
||||||
throw 'No APK found';
|
throw noAPKFound;
|
||||||
}
|
}
|
||||||
String? version = json['versionName'];
|
String? version = json['versionName'];
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw 'Could not determine latest release version';
|
throw couldNotFindLatestVersion;
|
||||||
}
|
}
|
||||||
return APKDetails(version, [apkUrl]);
|
return APKDetails(version, [apkUrl]);
|
||||||
} else {
|
} else {
|
||||||
throw 'Unable to fetch release info';
|
throw couldNotFindReleases;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +263,7 @@ class FDroid implements AppSource {
|
|||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/packages/[^/]+');
|
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/packages/[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw 'Not a valid URL';
|
throw notValidURL;
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return url.substring(0, match.end);
|
||||||
}
|
}
|
||||||
@ -256,7 +278,7 @@ class FDroid implements AppSource {
|
|||||||
?.querySelector('.package-version-download a')
|
?.querySelector('.package-version-download a')
|
||||||
?.attributes['href'];
|
?.attributes['href'];
|
||||||
if (apkUrl == null) {
|
if (apkUrl == null) {
|
||||||
throw 'No APK found';
|
throw noAPKFound;
|
||||||
}
|
}
|
||||||
var version = latestReleaseDiv
|
var version = latestReleaseDiv
|
||||||
?.querySelector('.package-version-header b')
|
?.querySelector('.package-version-header b')
|
||||||
@ -264,11 +286,11 @@ class FDroid implements AppSource {
|
|||||||
.split(' ')
|
.split(' ')
|
||||||
.last;
|
.last;
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw 'Could not determine latest release version';
|
throw couldNotFindLatestVersion;
|
||||||
}
|
}
|
||||||
return APKDetails(version, [apkUrl]);
|
return APKDetails(version, [apkUrl]);
|
||||||
} else {
|
} else {
|
||||||
throw 'Unable to fetch release info';
|
throw couldNotFindReleases;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,7 +310,7 @@ class Mullvad implements AppSource {
|
|||||||
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 notValidURL;
|
||||||
}
|
}
|
||||||
return url.substring(0, match.end);
|
return url.substring(0, match.end);
|
||||||
}
|
}
|
||||||
@ -304,12 +326,12 @@ class Mullvad implements AppSource {
|
|||||||
?.split('/')
|
?.split('/')
|
||||||
.last;
|
.last;
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw 'Could not determine the latest release version';
|
throw couldNotFindLatestVersion;
|
||||||
}
|
}
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version, ['https://mullvad.net/download/app/apk/latest']);
|
version, ['https://mullvad.net/download/app/apk/latest']);
|
||||||
} else {
|
} else {
|
||||||
throw 'Unable to fetch release info';
|
throw couldNotFindReleases;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
58
pubspec.lock
58
pubspec.lock
@ -92,13 +92,55 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.8"
|
version: "0.7.8"
|
||||||
|
device_info_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: device_info_plus
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
device_info_plus_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus_linux
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
device_info_plus_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus_macos
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
device_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
device_info_plus_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
device_info_plus_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus_windows
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
dynamic_color:
|
dynamic_color:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dynamic_color
|
name: dynamic_color
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.3"
|
version: "1.5.4"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -152,7 +194,7 @@ packages:
|
|||||||
name: flutter_local_notifications
|
name: flutter_local_notifications
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.8.0+1"
|
version: "9.9.1"
|
||||||
flutter_local_notifications_linux:
|
flutter_local_notifications_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -435,7 +477,7 @@ packages:
|
|||||||
name: shared_preferences_platform_interface
|
name: shared_preferences_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.1.0"
|
||||||
shared_preferences_web:
|
shared_preferences_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -496,7 +538,7 @@ packages:
|
|||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.12"
|
version: "0.4.13"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -573,7 +615,7 @@ packages:
|
|||||||
name: vector_math
|
name: vector_math
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.3"
|
||||||
webview_flutter:
|
webview_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -587,14 +629,14 @@ packages:
|
|||||||
name: webview_flutter_android
|
name: webview_flutter_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.5"
|
version: "2.10.0"
|
||||||
webview_flutter_platform_interface:
|
webview_flutter_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_platform_interface
|
name: webview_flutter_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.2"
|
version: "1.9.3"
|
||||||
webview_flutter_wkwebview:
|
webview_flutter_wkwebview:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -639,4 +681,4 @@ packages:
|
|||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.19.0-79.0.dev <3.0.0"
|
dart: ">=2.19.0-79.0.dev <3.0.0"
|
||||||
flutter: ">=3.1.0-0.0.pre.1036"
|
flutter: ">=3.3.0"
|
||||||
|
11
pubspec.yaml
11
pubspec.yaml
@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# 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.7+8 # When changing this, update the tag in main() accordingly
|
version: 0.1.9+10 # 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'
|
||||||
@ -35,21 +35,22 @@ dependencies:
|
|||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.5
|
||||||
path_provider: ^2.0.11
|
path_provider: ^2.0.11
|
||||||
flutter_fgbg: ^0.2.0 # Try removing reliance on this
|
flutter_fgbg: ^0.2.0 # Try removing reliance on this
|
||||||
flutter_local_notifications: ^9.8.0+1
|
flutter_local_notifications: ^9.9.1
|
||||||
provider: ^6.0.3
|
provider: ^6.0.3
|
||||||
http: ^0.13.5
|
http: ^0.13.5
|
||||||
webview_flutter: ^3.0.4
|
webview_flutter: ^3.0.4
|
||||||
workmanager: ^0.5.0
|
workmanager: ^0.5.0
|
||||||
dynamic_color: ^1.5.3
|
dynamic_color: ^1.5.4
|
||||||
install_plugin_v2: ^1.0.0 # Try replacing this
|
install_plugin_v2: ^1.0.0 # Try replacing this
|
||||||
html: ^0.15.0
|
html: ^0.15.0
|
||||||
shared_preferences: ^2.0.15
|
shared_preferences: ^2.0.15
|
||||||
url_launcher: ^6.1.5
|
url_launcher: ^6.1.5
|
||||||
permission_handler: ^10.0.0
|
permission_handler: ^10.0.0
|
||||||
fluttertoast: ^8.0.9
|
fluttertoast: ^8.0.9
|
||||||
|
device_info_plus: ^4.1.2
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
@ -62,7 +63,7 @@ dev_dependencies:
|
|||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
# package. See that file for information about deactivating specific lint
|
# package. See that file for information about deactivating specific lint
|
||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^2.0.1
|
||||||
|
|
||||||
flutter_icons:
|
flutter_icons:
|
||||||
android: true
|
android: true
|
||||||
|
Reference in New Issue
Block a user