Compare commits

...

13 Commits

22 changed files with 382 additions and 86 deletions

View File

@ -4,19 +4,22 @@ Get Android App Updates Directly From the Source.
Obtainium allows you to install and update Open-Source Apps directly from their releases pages, and receive notifications when new releases are made available.
Currently supported App sources:
- GitHub
- GitLab
Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0)
Currently supported App sources:
- [GitHub](https://github.com/)
- [GitLab](https://gitlab.com/)
- [F-Droid](https://f-droid.org/)
- [Mullvad](https://mullvad.net/en/)
- [Signal](https://signal.org/)
## Limitations
- 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.
- For GitHub, data is gathered using Web scraping and can easily break due to changes in website design. More reliable methods are either insufficient (GitHub RSS) or subject to rate limits (GitHub API). This may also apply to new sources added in the future.
- 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).
## Screenshots
| <img src="./screenshots/1.apps.png" alt="Apps Page" /> | <img src="./screenshots/2.dark_theme.png" alt="Dark Theme" /> | <img src="./screenshots/3.material_you.png" alt="Material You" /> |
| <img src="./assets/screenshots/1.apps.png" alt="Apps Page" /> | <img src="./assets/screenshots/2.dark_theme.png" alt="Dark Theme" /> | <img src="./assets/screenshots/3.material_you.png" alt="Material You" /> |
| ------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
| <img src="./screenshots/4.app.png" alt="App Page" /> | <img src="./screenshots/5.apk_picker.png" alt="Multiple APK Support" /> | <img src="./screenshots/6.apk_install.png" alt="App Installation" /> |
| <img src="./assets/screenshots/4.app.png" alt="App Page" /> | <img src="./assets/screenshots/5.apk_picker.png" alt="Multiple APK Support" /> | <img src="./assets/screenshots/6.apk_install.png" alt="App Installation" /> |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/graphics/banner.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

BIN
assets/graphics/icon.psd Executable file

Binary file not shown.

BIN
assets/graphics/obtainium.psd Executable file

Binary file not shown.

BIN
assets/graphics/store-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 263 KiB

View File

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

View File

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -9,18 +9,19 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:workmanager/workmanager.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:device_info_plus/device_info_plus.dart';
const String currentReleaseTag =
'v0.1.5-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
'v0.1.8-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
@pragma('vm:entry-point')
void bgTaskCallback() {
// Background update checking process
Workmanager().executeTask((task, taskName) async {
var appsProvider = AppsProvider(bg: true);
var notificationsProvider = NotificationsProvider();
await notificationsProvider.notify(checkingUpdatesNotification);
try {
var appsProvider = AppsProvider();
await notificationsProvider
.cancel(ErrorCheckingUpdatesNotification('').id);
await appsProvider.loadApps();
@ -43,16 +44,22 @@ void bgTaskCallback() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt! >= 29) {
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
Workmanager().initialize(
bgTaskCallback,
);
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => AppsProvider()),
ChangeNotifierProvider(
create: (context) => AppsProvider(
shouldLoadApps: true,
shouldCheckUpdatesAfterLoad: true,
shouldDeleteAPKs: true)),
ChangeNotifierProvider(create: (context) => SettingsProvider()),
Provider(create: (context) => NotificationsProvider())
],
@ -71,12 +78,7 @@ class MyApp extends StatelessWidget {
AppsProvider appsProvider = context.read<AppsProvider>();
if (settingsProvider.prefs == null) {
settingsProvider.initializeSettings().then((value) {
// Delete past downloads and check for updates every time the app is launched
// Only runs once as the settings are only initialized once (so not on every build)
appsProvider.deleteSavedAPKs();
appsProvider.checkUpdates();
});
settingsProvider.initializeSettings();
} else {
// Register the background update task according to the user's setting
Workmanager().registerPeriodicTask('bg-update-check', 'bg-update-check',
@ -94,7 +96,9 @@ class MyApp extends StatelessWidget {
'ImranR98',
'Obtainium',
currentReleaseTag,
currentReleaseTag, []));
currentReleaseTag,
[],
0));
}
}

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.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:provider/provider.dart';
@ -17,6 +19,7 @@ class _AppPageState extends State<AppPage> {
@override
Widget build(BuildContext context) {
var appsProvider = context.watch<AppsProvider>();
var settingsProvider = context.watch<SettingsProvider>();
AppInMemory? app = appsProvider.apps[widget.appId];
if (app?.app.installedVersion != null) {
appsProvider.getUpdate(app!.app.id);
@ -25,10 +28,58 @@ class _AppPageState extends State<AppPage> {
appBar: AppBar(
title: Text('${app?.app.author}/${app?.app.name}'),
),
body: WebView(
initialUrl: app?.app.url,
javascriptMode: JavascriptMode.unrestricted,
),
body: settingsProvider.showAppWebpage
? WebView(
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(
padding: EdgeInsets.fromLTRB(
0, 0, 0, MediaQuery.of(context).padding.bottom),
@ -100,8 +151,10 @@ class _AppPageState extends State<AppPage> {
});
},
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).errorColor,
surfaceTintColor: Theme.of(context).errorColor),
foregroundColor:
Theme.of(context).colorScheme.error,
surfaceTintColor:
Theme.of(context).colorScheme.error),
child: const Text('Remove'),
),
])),

View File

@ -42,7 +42,7 @@ class _AppsPageState extends State<AppsPage> {
: appsProvider.apps.isEmpty
? Text(
'No Apps',
style: Theme.of(context).textTheme.headline4,
style: Theme.of(context).textTheme.headlineMedium,
)
: RefreshIndicator(
onRefresh: () {

View File

@ -110,7 +110,21 @@ class _SettingsPageState extends State<SettingsPage> {
}
}),
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(
mainAxisAlignment: MainAxisAlignment.spaceAround,
@ -127,7 +141,7 @@ class _SettingsPageState extends State<SettingsPage> {
);
});
},
child: const Text('Export Apps')),
child: const Text('Export App List')),
ElevatedButton(
onPressed: () {
HapticFeedback.lightImpact();
@ -140,7 +154,7 @@ class _SettingsPageState extends State<SettingsPage> {
return AlertDialog(
scrollable: true,
title: const Text('Import Apps'),
title: const Text('Import App List'),
content: Column(children: [
const Text(
'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(
SnackBar(
content: Text(
'$value Apps Imported')),
'$value App${value == 1 ? '' : 's'} Imported')),
);
}).catchError((e) {
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(),
@ -235,7 +249,7 @@ class _SettingsPageState extends State<SettingsPage> {
icon: const Icon(Icons.code),
label: Text(
'Source',
style: Theme.of(context).textTheme.caption,
style: Theme.of(context).textTheme.bodySmall,
),
)
],

View File

@ -39,14 +39,26 @@ class AppsProvider with ChangeNotifier {
late Stream<FGBGType> foregroundStream;
late StreamSubscription<FGBGType> foregroundSubscription;
AppsProvider({bool bg = false}) {
AppsProvider(
{bool shouldLoadApps = false,
bool shouldCheckUpdatesAfterLoad = false,
bool shouldDeleteAPKs = false}) {
// Subscribe to changes in the app foreground status
foregroundStream = FGBGEvents.stream.asBroadcastStream();
foregroundSubscription = foregroundStream.listen((event) async {
isForeground = event == FGBGType.foreground;
if (isForeground) await loadApps();
});
loadApps();
if (shouldDeleteAPKs) {
deleteSavedAPKs();
}
if (shouldLoadApps) {
loadApps().then((_) {
if (shouldCheckUpdatesAfterLoad) {
checkUpdates();
}
});
}
}
Future<ApkFile> downloadApp(String apkUrl, String appId) async {
@ -96,7 +108,8 @@ class AppsProvider with ChangeNotifier {
if (apps[id] == null) {
throw 'App not found';
}
String? apkUrl = apps[id]!.app.apkUrls.last;
// If the App has more than one APK, the user should pick one
String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex];
if (apps[id]!.app.apkUrls.length > 1) {
apkUrl = await showDialog(
context: context,
@ -104,7 +117,25 @@ class AppsProvider with ChangeNotifier {
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) {
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!);
}
}
@ -200,6 +231,9 @@ class AppsProvider with ChangeNotifier {
App newApp = await SourceProvider().getApp(currentApp.url);
if (newApp.latestVersion != currentApp.latestVersion) {
newApp.installedVersion = currentApp.installedVersion;
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
newApp.preferredApkIndex = currentApp.preferredApkIndex;
}
await saveApp(newApp);
return newApp;
}
@ -290,17 +324,17 @@ class _APKPickerState extends State<APKPicker> {
scrollable: true,
title: const Text('Pick an APK'),
content: Column(children: [
Text('${widget.app.name} has more than one package - pick one.'),
...widget.app.apkUrls.map((u) => ListTile(
Text('${widget.app.name} has more than one package:'),
const SizedBox(height: 16),
...widget.app.apkUrls.map((u) => RadioListTile<String>(
title: Text(Uri.parse(u).pathSegments.last),
leading: Radio<String>(
value: u,
groupValue: apkUrl,
onChanged: (String? val) {
setState(() {
apkUrl = val;
});
})))
value: u,
groupValue: apkUrl,
onChanged: (String? val) {
setState(() {
apkUrl = val;
});
}))
]),
actions: [
TextButton(
@ -311,7 +345,7 @@ class _APKPickerState extends State<APKPicker> {
child: const Text('Cancel')),
TextButton(
onPressed: () {
HapticFeedback.mediumImpact();
HapticFeedback.heavyImpact();
Navigator.of(context).pop(apkUrl);
},
child: const Text('Continue'))
@ -319,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

@ -29,8 +29,9 @@ class App {
String? installedVersion;
late String latestVersion;
List<String> apkUrls = [];
late int preferredApkIndex;
App(this.id, this.url, this.author, this.name, this.installedVersion,
this.latestVersion, this.apkUrls);
this.latestVersion, this.apkUrls, this.preferredApkIndex);
@override
String toString() {
@ -38,15 +39,19 @@ class App {
}
factory App.fromJson(Map<String, dynamic> json) => App(
json['id'] as String,
json['url'] as String,
json['author'] as String,
json['name'] as String,
json['installedVersion'] == null
? null
: json['installedVersion'] as String,
json['latestVersion'] as String,
List<String>.from(jsonDecode(json['apkUrls'])));
json['id'] as String,
json['url'] as String,
json['author'] as String,
json['name'] as String,
json['installedVersion'] == null
? null
: json['installedVersion'] as String,
json['latestVersion'] as String,
List<String>.from(jsonDecode(json['apkUrls'])),
json['preferredApkIndex'] == null
? 0
: json['preferredApkIndex'] as int,
);
Map<String, dynamic> toJson() => {
'id': id,
@ -56,6 +61,7 @@ class App {
'installedVersion': installedVersion,
'latestVersion': latestVersion,
'apkUrls': jsonEncode(apkUrls),
'preferredApkIndex': preferredApkIndex
};
}
@ -89,7 +95,7 @@ class GitHub implements AppSource {
@override
String standardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]*/[^/]*');
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) {
throw 'Not a valid URL';
@ -99,7 +105,6 @@ class GitHub implements AppSource {
@override
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'));
if (res.statusCode == 200) {
var standardUri = Uri.parse(standardUrl);
@ -148,7 +153,7 @@ class GitLab implements AppSource {
@override
String standardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]*/[^/]*');
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) {
throw 'Not a valid URL';
@ -158,7 +163,6 @@ class GitLab implements AppSource {
@override
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'));
if (res.statusCode == 200) {
var standardUri = Uri.parse(standardUrl);
@ -166,12 +170,19 @@ class GitLab implements AppSource {
var entry = parsedHtml.querySelector('entry');
var entryContent =
parse(parseFragment(entry?.querySelector('content')!.innerHtml).text);
var apkUrlList = getLinksFromParsedHTML(
entryContent,
RegExp(
'^${escapeRegEx(standardUri.path)}/uploads/[^/]+/[^/]+\\.apk\$',
caseSensitive: false),
standardUri.origin);
var apkUrlList = [
...getLinksFromParsedHTML(
entryContent,
RegExp(
'^${escapeRegEx(standardUri.path)}/uploads/[^/]+/[^/]+\\.apk\$',
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) {
throw 'No APK found';
}
@ -225,11 +236,98 @@ class Signal implements AppSource {
}
@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 {
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
AppSource getSource(String url) {
@ -265,7 +363,8 @@ class SourceProvider {
names.name[0].toUpperCase() + names.name.substring(1),
null,
apk.version,
apk.apkUrls);
apk.apkUrls,
apk.apkUrls.length - 1);
}
List<String> getSourceHosts() => sources.map((e) => e.host).toList();

View File

@ -92,13 +92,55 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: "direct main"
description:
name: dynamic_color
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.3"
version: "1.5.4"
fake_async:
dependency: transitive
description:
@ -152,7 +194,7 @@ packages:
name: flutter_local_notifications
url: "https://pub.dartlang.org"
source: hosted
version: "9.8.0+1"
version: "9.9.1"
flutter_local_notifications_linux:
dependency: transitive
description:
@ -435,7 +477,7 @@ packages:
name: shared_preferences_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0"
shared_preferences_web:
dependency: transitive
description:
@ -496,7 +538,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.12"
version: "0.4.13"
timezone:
dependency: transitive
description:
@ -573,7 +615,7 @@ packages:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
version: "2.1.3"
webview_flutter:
dependency: "direct main"
description:
@ -587,14 +629,14 @@ packages:
name: webview_flutter_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.9.5"
version: "2.10.0"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.2"
version: "1.9.3"
webview_flutter_wkwebview:
dependency: transitive
description:
@ -639,4 +681,4 @@ packages:
version: "3.1.1"
sdks:
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
# 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.
version: 0.1.5+6 # When changing this, update the tag in main() accordingly
version: 0.1.8+9 # When changing this, update the tag in main() accordingly
environment:
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.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
cupertino_icons: ^1.0.5
path_provider: ^2.0.11
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
http: ^0.13.5
webview_flutter: ^3.0.4
workmanager: ^0.5.0
dynamic_color: ^1.5.3
dynamic_color: ^1.5.4
install_plugin_v2: ^1.0.0 # Try replacing this
html: ^0.15.0
shared_preferences: ^2.0.15
url_launcher: ^6.1.5
permission_handler: ^10.0.0
fluttertoast: ^8.0.9
device_info_plus: ^4.1.2
dev_dependencies:
@ -62,13 +63,13 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
flutter_lints: ^2.0.1
flutter_icons:
android: true
image_path: "assets/icon.png"
image_path: "assets/graphics/icon.png"
adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/icon.png"
adaptive_icon_foreground: "assets/graphics/icon.png"
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec