Compare commits

..

4 Commits

9 changed files with 218 additions and 38 deletions

View File

@ -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.8-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,
); );

View File

@ -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'),
), ),
])), ])),

View File

@ -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: () {

View File

@ -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,
), ),
) )
], ],

View File

@ -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'))
],
);
}
}

View File

@ -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();
}
} }

View File

@ -170,12 +170,19 @@ 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 'No APK found';
} }

View File

@ -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"

View File

@ -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.8+9 # 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