mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 21:36:42 +02:00
Compare commits
26 Commits
v0.1.9-bet
...
v0.2.4-bet
Author | SHA1 | Date | |
---|---|---|---|
6c1ad94b4f | |||
7d7986f8bf | |||
3ddf9ea736 | |||
2272f8b4e6 | |||
9514062a3a | |||
da57018b90 | |||
87e31c37aa | |||
cb4dfff1b9 | |||
911b06bfb6 | |||
53513bfdd1 | |||
681092d895 | |||
0f6b6253de | |||
c724b276ab | |||
35369273bd | |||
0b1863a227 | |||
9e21f2d6e6 | |||
6f11f850e0 | |||
5e96b91029 | |||
5fc79af960 | |||
05f5590e7d | |||
50f8caeb47 | |||
f966a9e626 | |||
02a5749ba7 | |||
4ccf7cbc92 | |||
ab4efd85ce | |||
42bba0f64c |
@ -10,6 +10,7 @@ Currently supported App sources:
|
|||||||
- [GitHub](https://github.com/)
|
- [GitHub](https://github.com/)
|
||||||
- [GitLab](https://gitlab.com/)
|
- [GitLab](https://gitlab.com/)
|
||||||
- [F-Droid](https://f-droid.org/)
|
- [F-Droid](https://f-droid.org/)
|
||||||
|
- [IzzyOnDroid](https://android.izzysoft.de/)
|
||||||
- [Mullvad](https://mullvad.net/en/)
|
- [Mullvad](https://mullvad.net/en/)
|
||||||
- [Signal](https://signal.org/)
|
- [Signal](https://signal.org/)
|
||||||
|
|
||||||
|
29
lib/components/custom_app_bar.dart
Normal file
29
lib/components/custom_app_bar.dart
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CustomAppBar extends StatefulWidget {
|
||||||
|
const CustomAppBar({super.key, required this.title});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CustomAppBar> createState() => _CustomAppBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomAppBarState extends State<CustomAppBar> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverAppBar(
|
||||||
|
pinned: true,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
expandedHeight: 100,
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
titlePadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
|
title: Text(
|
||||||
|
widget.title,
|
||||||
|
style:
|
||||||
|
TextStyle(color: Theme.of(context).textTheme.bodyMedium!.color),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
80
lib/components/generated_form_modal.dart
Normal file
80
lib/components/generated_form_modal.dart
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class GeneratedFormItem {
|
||||||
|
late String message;
|
||||||
|
late bool required;
|
||||||
|
late int lines;
|
||||||
|
|
||||||
|
GeneratedFormItem(this.message, this.required, this.lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeneratedFormModal extends StatefulWidget {
|
||||||
|
const GeneratedFormModal(
|
||||||
|
{super.key, required this.title, required this.items});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final List<GeneratedFormItem> items;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GeneratedFormModal> createState() => _GeneratedFormModalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
final urlInputController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final formInputs = widget.items.map((e) {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
return [
|
||||||
|
controller,
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(helperText: e.message),
|
||||||
|
controller: controller,
|
||||||
|
minLines: e.lines <= 1 ? null : e.lines,
|
||||||
|
maxLines: e.lines <= 1 ? 1 : e.lines,
|
||||||
|
validator: e.required
|
||||||
|
? (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '${e.message} (required)';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}).toList();
|
||||||
|
return AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: Text(widget.title),
|
||||||
|
content: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [...formInputs.map((e) => e[1] as Widget)],
|
||||||
|
)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
},
|
||||||
|
child: const Text('Cancel')),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_formKey.currentState?.validate() == true) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
Navigator.of(context).pop(formInputs
|
||||||
|
.map((e) => (e[0] as TextEditingController).value.text)
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Continue'))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add support for larger textarea so this can be used for text/json imports
|
@ -12,7 +12,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';
|
||||||
|
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v0.1.9-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v0.2.4-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void bgTaskCallback() {
|
void bgTaskCallback() {
|
||||||
@ -81,11 +81,15 @@ class MyApp extends StatelessWidget {
|
|||||||
settingsProvider.initializeSettings();
|
settingsProvider.initializeSettings();
|
||||||
} else {
|
} else {
|
||||||
// Register the background update task according to the user's setting
|
// Register the background update task according to the user's setting
|
||||||
Workmanager().registerPeriodicTask('bg-update-check', 'bg-update-check',
|
if (settingsProvider.updateInterval > 0) {
|
||||||
frequency: Duration(minutes: settingsProvider.updateInterval),
|
Workmanager().registerPeriodicTask('bg-update-check', 'bg-update-check',
|
||||||
initialDelay: Duration(minutes: settingsProvider.updateInterval),
|
frequency: Duration(minutes: settingsProvider.updateInterval),
|
||||||
constraints: Constraints(networkType: NetworkType.connected),
|
initialDelay: Duration(minutes: settingsProvider.updateInterval),
|
||||||
existingWorkPolicy: ExistingWorkPolicy.replace);
|
constraints: Constraints(networkType: NetworkType.connected),
|
||||||
|
existingWorkPolicy: ExistingWorkPolicy.replace);
|
||||||
|
} else {
|
||||||
|
Workmanager().cancelByUniqueName('bg-update-check');
|
||||||
|
}
|
||||||
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
|
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
|
||||||
if (isFirstRun) {
|
if (isFirstRun) {
|
||||||
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
import 'package:obtainium/pages/app.dart';
|
import 'package:obtainium/pages/app.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
@ -22,113 +23,136 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
return Center(
|
return Scaffold(
|
||||||
child: Form(
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
key: _formKey,
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
child: Column(
|
const CustomAppBar(title: 'Add App'),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
SliverFillRemaining(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
hasScrollBody: false,
|
||||||
children: [
|
child: Center(
|
||||||
Container(),
|
child: Form(
|
||||||
Padding(
|
key: _formKey,
|
||||||
padding: const EdgeInsets.all(16),
|
child: Column(
|
||||||
child: Column(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
Container(),
|
||||||
decoration: const InputDecoration(
|
Padding(
|
||||||
hintText: 'https://github.com/Author/Project',
|
padding: const EdgeInsets.all(16),
|
||||||
helperText: 'Enter the App source URL'),
|
child: Column(
|
||||||
controller: urlInputController,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
validator: (value) {
|
children: [
|
||||||
if (value == null ||
|
TextFormField(
|
||||||
value.isEmpty ||
|
decoration: const InputDecoration(
|
||||||
Uri.tryParse(value) == null) {
|
hintText:
|
||||||
return 'Please enter a supported source URL';
|
'https://github.com/Author/Project',
|
||||||
}
|
helperText: 'Enter the App source URL'),
|
||||||
return null;
|
controller: urlInputController,
|
||||||
},
|
validator: (value) {
|
||||||
),
|
if (value == null ||
|
||||||
Padding(
|
value.isEmpty ||
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
Uri.tryParse(value) == null) {
|
||||||
child: ElevatedButton(
|
return 'Please enter a supported source URL';
|
||||||
onPressed: gettingAppInfo
|
}
|
||||||
? null
|
return null;
|
||||||
: () {
|
},
|
||||||
HapticFeedback.mediumImpact();
|
),
|
||||||
if (_formKey.currentState!.validate()) {
|
Padding(
|
||||||
setState(() {
|
padding:
|
||||||
gettingAppInfo = true;
|
const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
});
|
child: ElevatedButton(
|
||||||
sourceProvider
|
onPressed: gettingAppInfo
|
||||||
.getApp(urlInputController.value.text)
|
? null
|
||||||
.then((app) {
|
: () {
|
||||||
var appsProvider =
|
HapticFeedback.selectionClick();
|
||||||
context.read<AppsProvider>();
|
if (_formKey.currentState!
|
||||||
var settingsProvider =
|
.validate()) {
|
||||||
context.read<SettingsProvider>();
|
setState(() {
|
||||||
if (appsProvider.apps.containsKey(app.id)) {
|
gettingAppInfo = true;
|
||||||
throw 'App already added';
|
});
|
||||||
}
|
sourceProvider
|
||||||
settingsProvider
|
.getApp(urlInputController
|
||||||
.getInstallPermission()
|
.value.text)
|
||||||
.then((_) {
|
.then((app) {
|
||||||
appsProvider.saveApp(app).then((_) {
|
var appsProvider =
|
||||||
urlInputController.clear();
|
context.read<AppsProvider>();
|
||||||
Navigator.push(
|
var settingsProvider = context
|
||||||
context,
|
.read<SettingsProvider>();
|
||||||
MaterialPageRoute(
|
if (appsProvider.apps
|
||||||
builder: (context) =>
|
.containsKey(app.id)) {
|
||||||
AppPage(appId: app.id)));
|
throw 'App already added';
|
||||||
});
|
}
|
||||||
});
|
settingsProvider
|
||||||
}).catchError((e) {
|
.getInstallPermission()
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
.then((_) {
|
||||||
SnackBar(content: Text(e.toString())),
|
appsProvider
|
||||||
);
|
.saveApp(app)
|
||||||
}).whenComplete(() {
|
.then((_) {
|
||||||
setState(() {
|
urlInputController.clear();
|
||||||
gettingAppInfo = false;
|
Navigator.push(
|
||||||
});
|
context,
|
||||||
});
|
MaterialPageRoute(
|
||||||
}
|
builder: (context) =>
|
||||||
},
|
AppPage(
|
||||||
child: const Text('Add'),
|
appId:
|
||||||
),
|
app.id)));
|
||||||
),
|
});
|
||||||
],
|
});
|
||||||
),
|
}).catchError((e) {
|
||||||
),
|
ScaffoldMessenger.of(context)
|
||||||
Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
|
.showSnackBar(
|
||||||
const Text(
|
SnackBar(
|
||||||
'Supported Sources:',
|
content:
|
||||||
// style: TextStyle(fontWeight: FontWeight.bold),
|
Text(e.toString())),
|
||||||
// style: Theme.of(context).textTheme.bodySmall,
|
);
|
||||||
),
|
}).whenComplete(() {
|
||||||
const SizedBox(
|
setState(() {
|
||||||
height: 8,
|
gettingAppInfo = false;
|
||||||
),
|
});
|
||||||
...sourceProvider
|
});
|
||||||
.getSourceHosts()
|
}
|
||||||
.map((e) => GestureDetector(
|
},
|
||||||
onTap: () {
|
child: const Text('Add'),
|
||||||
launchUrlString('https://$e',
|
),
|
||||||
mode: LaunchMode.externalApplication);
|
),
|
||||||
},
|
],
|
||||||
child: Text(
|
),
|
||||||
e,
|
),
|
||||||
style: const TextStyle(
|
Column(
|
||||||
decoration: TextDecoration.underline,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
fontStyle: FontStyle.italic),
|
children: [
|
||||||
)))
|
const Text(
|
||||||
.toList()
|
'Supported Sources:',
|
||||||
]),
|
// style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
if (gettingAppInfo)
|
// style: Theme.of(context).textTheme.bodySmall,
|
||||||
const LinearProgressIndicator()
|
),
|
||||||
else
|
const SizedBox(
|
||||||
Container(),
|
height: 8,
|
||||||
],
|
),
|
||||||
)),
|
...sourceProvider
|
||||||
);
|
.getSourceHosts()
|
||||||
|
.map((e) => GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString('https://$e',
|
||||||
|
mode:
|
||||||
|
LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
e,
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration:
|
||||||
|
TextDecoration.underline,
|
||||||
|
fontStyle: FontStyle.italic),
|
||||||
|
)))
|
||||||
|
.toList()
|
||||||
|
]),
|
||||||
|
if (gettingAppInfo)
|
||||||
|
const LinearProgressIndicator()
|
||||||
|
else
|
||||||
|
Container(),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@ -25,61 +26,64 @@ class _AppPageState extends State<AppPage> {
|
|||||||
appsProvider.getUpdate(app!.app.id);
|
appsProvider.getUpdate(app!.app.id);
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
title: Text('${app?.app.author}/${app?.app.name}'),
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
),
|
CustomAppBar(title: '${app?.app.name}'),
|
||||||
body: settingsProvider.showAppWebpage
|
SliverFillRemaining(
|
||||||
? WebView(
|
child: settingsProvider.showAppWebpage
|
||||||
initialUrl: app?.app.url,
|
? WebView(
|
||||||
javascriptMode: JavascriptMode.unrestricted,
|
initialUrl: app?.app.url,
|
||||||
)
|
javascriptMode: JavascriptMode.unrestricted,
|
||||||
: Column(
|
)
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
Text(
|
children: [
|
||||||
app?.app.name ?? 'App',
|
Text(
|
||||||
textAlign: TextAlign.center,
|
app?.app.name ?? 'App',
|
||||||
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,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: Theme.of(context).textTheme.displayLarge,
|
||||||
decoration: TextDecoration.underline,
|
),
|
||||||
fontStyle: FontStyle.italic,
|
Text(
|
||||||
fontSize: 12),
|
'By ${app?.app.author ?? 'Unknown'}',
|
||||||
)),
|
textAlign: TextAlign.center,
|
||||||
const SizedBox(
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
height: 32,
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
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),
|
||||||
@ -91,6 +95,76 @@ class _AppPageState extends State<AppPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
|
if (app?.app.installedVersion != app?.app.latestVersion)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(
|
||||||
|
'App Already ${app?.app.installedVersion == null ? 'Installed' : 'Updated'}?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('No')),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
var updatedApp = app?.app;
|
||||||
|
if (updatedApp != null) {
|
||||||
|
updatedApp.installedVersion =
|
||||||
|
updatedApp.latestVersion;
|
||||||
|
appsProvider
|
||||||
|
.saveApp(updatedApp);
|
||||||
|
}
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'Yes, Mark as Installed'))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: 'Mark as Installed',
|
||||||
|
icon: const Icon(Icons.done))
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('App Not Installed?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('No')),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
var updatedApp = app?.app;
|
||||||
|
if (updatedApp != null) {
|
||||||
|
updatedApp.installedVersion =
|
||||||
|
null;
|
||||||
|
appsProvider
|
||||||
|
.saveApp(updatedApp);
|
||||||
|
}
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'Yes, Mark as Not Installed'))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: 'Mark as Not Installed',
|
||||||
|
icon: const Icon(Icons.no_cell_outlined)),
|
||||||
|
const SizedBox(width: 16.0),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: (app?.app.installedVersion == null ||
|
onPressed: (app?.app.installedVersion == null ||
|
||||||
@ -118,7 +192,6 @@ class _AppPageState extends State<AppPage> {
|
|||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -129,7 +202,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback
|
||||||
|
.selectionClick();
|
||||||
appsProvider
|
appsProvider
|
||||||
.removeApp(app!.app.id)
|
.removeApp(app!.app.id)
|
||||||
.then((_) {
|
.then((_) {
|
||||||
@ -142,7 +216,6 @@ class _AppPageState extends State<AppPage> {
|
|||||||
child: const Text('Remove')),
|
child: const Text('Remove')),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: const Text('Cancel'))
|
child: const Text('Cancel'))
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
import 'package:obtainium/pages/app.dart';
|
import 'package:obtainium/pages/app.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
@ -16,9 +17,26 @@ class _AppsPageState extends State<AppsPage> {
|
|||||||
@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>();
|
||||||
var existingUpdateAppIds = appsProvider.getExistingUpdates();
|
var existingUpdateAppIds = appsProvider.getExistingUpdates();
|
||||||
|
var sortedApps = appsProvider.apps.values.toList();
|
||||||
|
sortedApps.sort((a, b) {
|
||||||
|
int result = 0;
|
||||||
|
if (settingsProvider.sortColumn == SortColumnSettings.authorName) {
|
||||||
|
result =
|
||||||
|
(a.app.author + a.app.name).compareTo(b.app.author + b.app.name);
|
||||||
|
} else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
|
||||||
|
result =
|
||||||
|
(a.app.name + a.app.author).compareTo(b.app.name + b.app.author);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
if (settingsProvider.sortOrder == SortOrderSettings.ascending) {
|
||||||
|
sortedApps = sortedApps.reversed.toList();
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
floatingActionButton: existingUpdateAppIds.isEmpty
|
floatingActionButton: existingUpdateAppIds.isEmpty
|
||||||
? null
|
? null
|
||||||
: ElevatedButton.icon(
|
: ElevatedButton.icon(
|
||||||
@ -26,57 +44,56 @@ class _AppsPageState extends State<AppsPage> {
|
|||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
context
|
settingsProvider.getInstallPermission().then((_) {
|
||||||
.read<SettingsProvider>()
|
|
||||||
.getInstallPermission()
|
|
||||||
.then((_) {
|
|
||||||
appsProvider.downloadAndInstallLatestApp(
|
appsProvider.downloadAndInstallLatestApp(
|
||||||
existingUpdateAppIds, context);
|
existingUpdateAppIds, context);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.update),
|
icon: const Icon(Icons.install_mobile_outlined),
|
||||||
label: const Text('Update All')),
|
label: const Text('Install All')),
|
||||||
body: Center(
|
body: RefreshIndicator(
|
||||||
child: appsProvider.loadingApps
|
onRefresh: () {
|
||||||
? const CircularProgressIndicator()
|
HapticFeedback.lightImpact();
|
||||||
: appsProvider.apps.isEmpty
|
return appsProvider.checkUpdates();
|
||||||
? Text(
|
},
|
||||||
'No Apps',
|
child: CustomScrollView(slivers: <Widget>[
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
const CustomAppBar(title: 'Apps'),
|
||||||
)
|
if (appsProvider.loadingApps || appsProvider.apps.isEmpty)
|
||||||
: RefreshIndicator(
|
SliverFillRemaining(
|
||||||
onRefresh: () {
|
child: Center(
|
||||||
HapticFeedback.lightImpact();
|
child: appsProvider.loadingApps
|
||||||
return appsProvider.checkUpdates();
|
? const CircularProgressIndicator()
|
||||||
},
|
: Text(
|
||||||
child: ListView(
|
'No Apps',
|
||||||
children: appsProvider.apps.values
|
style:
|
||||||
.map(
|
Theme.of(context).textTheme.headlineMedium,
|
||||||
(e) => ListTile(
|
))),
|
||||||
title: Text('${e.app.author}/${e.app.name}'),
|
SliverList(
|
||||||
subtitle: Text(
|
delegate: SliverChildBuilderDelegate(
|
||||||
e.app.installedVersion ?? 'Not Installed'),
|
(BuildContext context, int index) {
|
||||||
trailing: e.downloadProgress != null
|
return ListTile(
|
||||||
? Text(
|
title: Text(
|
||||||
'Downloading - ${e.downloadProgress?.toInt()}%')
|
'${sortedApps[index].app.author}/${sortedApps[index].app.name}'),
|
||||||
: (e.app.installedVersion != null &&
|
subtitle: Text(sortedApps[index].app.installedVersion ??
|
||||||
e.app.installedVersion !=
|
'Not Installed'),
|
||||||
e.app.latestVersion
|
trailing: sortedApps[index].downloadProgress != null
|
||||||
? const Text('Update Available')
|
? Text(
|
||||||
: null),
|
'Downloading - ${sortedApps[index].downloadProgress?.toInt()}%')
|
||||||
onTap: () {
|
: (sortedApps[index].app.installedVersion != null &&
|
||||||
Navigator.push(
|
sortedApps[index].app.installedVersion !=
|
||||||
context,
|
sortedApps[index].app.latestVersion
|
||||||
MaterialPageRoute(
|
? const Text('Update Available')
|
||||||
builder: (context) =>
|
: null),
|
||||||
AppPage(appId: e.app.id)),
|
onTap: () {
|
||||||
);
|
Navigator.push(
|
||||||
},
|
context,
|
||||||
),
|
MaterialPageRoute(
|
||||||
)
|
builder: (context) =>
|
||||||
.toList(),
|
AppPage(appId: sortedApps[index].app.id)),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
));
|
);
|
||||||
|
}, childCount: sortedApps.length))
|
||||||
|
])));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import 'package:animations/animations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:obtainium/pages/add_app.dart';
|
import 'package:obtainium/pages/add_app.dart';
|
||||||
import 'package:obtainium/pages/apps.dart';
|
import 'package:obtainium/pages/apps.dart';
|
||||||
|
import 'package:obtainium/pages/import_export.dart';
|
||||||
import 'package:obtainium/pages/settings.dart';
|
import 'package:obtainium/pages/settings.dart';
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
@ -11,40 +13,78 @@ class HomePage extends StatefulWidget {
|
|||||||
State<HomePage> createState() => _HomePageState();
|
State<HomePage> createState() => _HomePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NavigationPageItem {
|
||||||
|
late String title;
|
||||||
|
late IconData icon;
|
||||||
|
late Widget widget;
|
||||||
|
|
||||||
|
NavigationPageItem(this.title, this.icon, this.widget);
|
||||||
|
}
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage> {
|
class _HomePageState extends State<HomePage> {
|
||||||
int selectedIndex = 1;
|
List<int> selectedIndexHistory = [];
|
||||||
List<Widget> pages = [
|
|
||||||
const SettingsPage(),
|
List<NavigationPageItem> pages = [
|
||||||
const AppsPage(),
|
NavigationPageItem('Apps', Icons.apps, const AppsPage()),
|
||||||
const AddAppPage()
|
NavigationPageItem('Add App', Icons.add, const AddAppPage()),
|
||||||
|
NavigationPageItem(
|
||||||
|
'Import/Export', Icons.import_export, const ImportExportPage()),
|
||||||
|
NavigationPageItem('Settings', Icons.settings, const SettingsPage())
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(title: const Text('Obtainium')),
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: pages.elementAt(selectedIndex),
|
body: PageTransitionSwitcher(
|
||||||
|
transitionBuilder: (
|
||||||
|
Widget child,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
) {
|
||||||
|
return SharedAxisTransition(
|
||||||
|
animation: animation,
|
||||||
|
secondaryAnimation: secondaryAnimation,
|
||||||
|
transitionType: SharedAxisTransitionType.horizontal,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: pages
|
||||||
|
.elementAt(selectedIndexHistory.isEmpty
|
||||||
|
? 0
|
||||||
|
: selectedIndexHistory.last)
|
||||||
|
.widget,
|
||||||
|
),
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
destinations: const [
|
destinations: pages
|
||||||
NavigationDestination(
|
.map((e) =>
|
||||||
icon: Icon(Icons.settings), label: 'Settings'),
|
NavigationDestination(icon: Icon(e.icon), label: e.title))
|
||||||
NavigationDestination(icon: Icon(Icons.apps), label: 'Apps'),
|
.toList(),
|
||||||
NavigationDestination(icon: Icon(Icons.add), label: 'Add App'),
|
|
||||||
],
|
|
||||||
onDestinationSelected: (int index) {
|
onDestinationSelected: (int index) {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.selectionClick();
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedIndex = index;
|
if (index == 0) {
|
||||||
|
selectedIndexHistory.clear();
|
||||||
|
} else if (selectedIndexHistory.isEmpty ||
|
||||||
|
(selectedIndexHistory.isNotEmpty &&
|
||||||
|
selectedIndexHistory.last != index)) {
|
||||||
|
int existingInd = selectedIndexHistory.indexOf(index);
|
||||||
|
if (existingInd >= 0) {
|
||||||
|
selectedIndexHistory.removeAt(existingInd);
|
||||||
|
}
|
||||||
|
selectedIndexHistory.add(index);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex:
|
||||||
|
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
if (selectedIndex != 1) {
|
if (selectedIndexHistory.isNotEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedIndex = 1;
|
selectedIndexHistory.removeLast();
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
349
lib/pages/import_export.dart
Normal file
349
lib/pages/import_export.dart
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
||||||
|
class ImportExportPage extends StatefulWidget {
|
||||||
|
const ImportExportPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImportExportPage> createState() => _ImportExportPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImportExportPageState extends State<ImportExportPage> {
|
||||||
|
bool importInProgress = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
|
var settingsProvider = context.read<SettingsProvider>();
|
||||||
|
var appsProvider = context.read<AppsProvider>();
|
||||||
|
var outlineButtonStyle = ButtonStyle(
|
||||||
|
shape: MaterialStateProperty.all(
|
||||||
|
StadiumBorder(
|
||||||
|
side: BorderSide(
|
||||||
|
width: 1,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<List<List<String>>> addApps(List<String> urls) async {
|
||||||
|
await settingsProvider.getInstallPermission();
|
||||||
|
List<dynamic> results = await sourceProvider.getApps(urls);
|
||||||
|
List<App> apps = results[0];
|
||||||
|
Map<String, dynamic> errorsMap = results[1];
|
||||||
|
for (var app in apps) {
|
||||||
|
if (appsProvider.apps.containsKey(app.id)) {
|
||||||
|
errorsMap.addAll({app.id: 'App already added'});
|
||||||
|
} else {
|
||||||
|
await appsProvider.saveApp(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<List<String>> errors =
|
||||||
|
errorsMap.keys.map((e) => [e, errorsMap[e].toString()]).toList();
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
|
const CustomAppBar(title: 'Import/Export'),
|
||||||
|
SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
|
style: outlineButtonStyle,
|
||||||
|
onPressed: appsProvider.apps.isEmpty ||
|
||||||
|
importInProgress
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
appsProvider
|
||||||
|
.exportApps()
|
||||||
|
.then((String path) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Exported to $path')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Obtainium Export'))),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
|
style: outlineButtonStyle,
|
||||||
|
onPressed: importInProgress
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
FilePicker.platform
|
||||||
|
.pickFiles()
|
||||||
|
.then((result) {
|
||||||
|
setState(() {
|
||||||
|
importInProgress = true;
|
||||||
|
});
|
||||||
|
if (result != null) {
|
||||||
|
String data = File(
|
||||||
|
result.files.single.path!)
|
||||||
|
.readAsStringSync();
|
||||||
|
try {
|
||||||
|
jsonDecode(data);
|
||||||
|
} catch (e) {
|
||||||
|
throw 'Invalid input';
|
||||||
|
}
|
||||||
|
appsProvider
|
||||||
|
.importApps(data)
|
||||||
|
.then((value) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'$value App${value == 1 ? '' : 's'} Imported')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// User canceled the picker
|
||||||
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(e.toString())),
|
||||||
|
);
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
importInProgress = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Obtainium Import')))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (importInProgress)
|
||||||
|
Column(
|
||||||
|
children: const [
|
||||||
|
SizedBox(
|
||||||
|
height: 14,
|
||||||
|
),
|
||||||
|
LinearProgressIndicator(),
|
||||||
|
SizedBox(
|
||||||
|
height: 14,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Divider(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: importInProgress
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: 'Import from URL List',
|
||||||
|
items: [
|
||||||
|
GeneratedFormItem(
|
||||||
|
'App URL List', true, 7)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).then((values) {
|
||||||
|
if (values != null) {
|
||||||
|
var urls =
|
||||||
|
(values[0] as String).split('\n');
|
||||||
|
setState(() {
|
||||||
|
importInProgress = true;
|
||||||
|
});
|
||||||
|
addApps(urls).then((errors) {
|
||||||
|
if (errors.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Imported ${urls.length} Apps')),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return ImportErrorDialog(
|
||||||
|
urlsLength: urls.length,
|
||||||
|
errors: errors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(content: Text(e.toString())),
|
||||||
|
);
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
importInProgress = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'Import from URL List',
|
||||||
|
)),
|
||||||
|
...sourceProvider.massSources
|
||||||
|
.map((source) => Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: importInProgress
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title:
|
||||||
|
'Import ${source.name}',
|
||||||
|
items: source
|
||||||
|
.requiredArgs
|
||||||
|
.map((e) =>
|
||||||
|
GeneratedFormItem(
|
||||||
|
e,
|
||||||
|
true,
|
||||||
|
1))
|
||||||
|
.toList());
|
||||||
|
}).then((values) {
|
||||||
|
if (values != null) {
|
||||||
|
source
|
||||||
|
.getUrls(values)
|
||||||
|
.then((urls) {
|
||||||
|
setState(() {
|
||||||
|
importInProgress = true;
|
||||||
|
});
|
||||||
|
addApps(urls)
|
||||||
|
.then((errors) {
|
||||||
|
if (errors.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Imported ${urls.length} Apps')),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(BuildContext
|
||||||
|
ctx) {
|
||||||
|
return ImportErrorDialog(
|
||||||
|
urlsLength: urls
|
||||||
|
.length,
|
||||||
|
errors:
|
||||||
|
errors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
importInProgress =
|
||||||
|
false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catchError((e) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
e.toString())),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text('Import ${source.name}'))
|
||||||
|
]))
|
||||||
|
.toList()
|
||||||
|
],
|
||||||
|
)))
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImportErrorDialog extends StatefulWidget {
|
||||||
|
const ImportErrorDialog(
|
||||||
|
{super.key, required this.urlsLength, required this.errors});
|
||||||
|
|
||||||
|
final int urlsLength;
|
||||||
|
final List<List<String>> errors;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImportErrorDialog> createState() => _ImportErrorDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImportErrorDialogState extends State<ImportErrorDialog> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: const Text('Import Errors'),
|
||||||
|
content:
|
||||||
|
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||||
|
Text(
|
||||||
|
'${widget.urlsLength - widget.errors.length} of ${widget.urlsLength} Apps imported.',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'The following URLs had errors:',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
...widget.errors.map((e) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(e[0]),
|
||||||
|
Text(
|
||||||
|
e[1],
|
||||||
|
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}).toList()
|
||||||
|
]),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
},
|
||||||
|
child: const Text('Okay'))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,5 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@ -17,244 +14,223 @@ class SettingsPage extends StatefulWidget {
|
|||||||
class _SettingsPageState extends State<SettingsPage> {
|
class _SettingsPageState extends State<SettingsPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
|
||||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||||
if (settingsProvider.prefs == null) {
|
if (settingsProvider.prefs == null) {
|
||||||
settingsProvider.initializeSettings();
|
settingsProvider.initializeSettings();
|
||||||
}
|
}
|
||||||
return Padding(
|
return Scaffold(
|
||||||
padding: const EdgeInsets.all(16),
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
child: settingsProvider.prefs == null
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
? Container()
|
const CustomAppBar(title: 'Settings'),
|
||||||
: Column(
|
SliverFillRemaining(
|
||||||
children: [
|
hasScrollBody: true,
|
||||||
DropdownButtonFormField(
|
child: Padding(
|
||||||
decoration: const InputDecoration(labelText: 'Theme'),
|
padding: const EdgeInsets.all(16),
|
||||||
value: settingsProvider.theme,
|
child: settingsProvider.prefs == null
|
||||||
items: const [
|
? Container()
|
||||||
DropdownMenuItem(
|
: Column(
|
||||||
value: ThemeSettings.dark,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Text('Dark'),
|
children: [
|
||||||
),
|
Text(
|
||||||
DropdownMenuItem(
|
'Appearance',
|
||||||
value: ThemeSettings.light,
|
style: TextStyle(
|
||||||
child: Text('Light'),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
DropdownMenuItem(
|
DropdownButtonFormField(
|
||||||
value: ThemeSettings.system,
|
decoration:
|
||||||
child: Text('Follow System'),
|
const InputDecoration(labelText: 'Theme'),
|
||||||
)
|
value: settingsProvider.theme,
|
||||||
],
|
items: const [
|
||||||
onChanged: (value) {
|
DropdownMenuItem(
|
||||||
if (value != null) {
|
value: ThemeSettings.dark,
|
||||||
settingsProvider.theme = value;
|
child: Text('Dark'),
|
||||||
}
|
),
|
||||||
}),
|
DropdownMenuItem(
|
||||||
const SizedBox(
|
value: ThemeSettings.light,
|
||||||
height: 16,
|
child: Text('Light'),
|
||||||
),
|
),
|
||||||
DropdownButtonFormField(
|
DropdownMenuItem(
|
||||||
decoration: const InputDecoration(labelText: 'Colour'),
|
value: ThemeSettings.system,
|
||||||
value: settingsProvider.colour,
|
child: Text('Follow System'),
|
||||||
items: const [
|
)
|
||||||
DropdownMenuItem(
|
],
|
||||||
value: ColourSettings.basic,
|
onChanged: (value) {
|
||||||
child: Text('Obtainium'),
|
if (value != null) {
|
||||||
),
|
settingsProvider.theme = value;
|
||||||
DropdownMenuItem(
|
}
|
||||||
value: ColourSettings.materialYou,
|
}),
|
||||||
child: Text('Material You'),
|
const SizedBox(
|
||||||
)
|
height: 16,
|
||||||
],
|
),
|
||||||
onChanged: (value) {
|
DropdownButtonFormField(
|
||||||
if (value != null) {
|
decoration:
|
||||||
settingsProvider.colour = value;
|
const InputDecoration(labelText: 'Colour'),
|
||||||
}
|
value: settingsProvider.colour,
|
||||||
}),
|
items: const [
|
||||||
const SizedBox(
|
DropdownMenuItem(
|
||||||
height: 16,
|
value: ColourSettings.basic,
|
||||||
),
|
child: Text('Obtainium'),
|
||||||
DropdownButtonFormField(
|
),
|
||||||
decoration: const InputDecoration(
|
DropdownMenuItem(
|
||||||
labelText: 'Background Update Checking Interval'),
|
value: ColourSettings.materialYou,
|
||||||
value: settingsProvider.updateInterval,
|
child: Text('Material You'),
|
||||||
items: const [
|
)
|
||||||
DropdownMenuItem(
|
],
|
||||||
value: 15,
|
onChanged: (value) {
|
||||||
child: Text('15 Minutes'),
|
if (value != null) {
|
||||||
),
|
settingsProvider.colour = value;
|
||||||
DropdownMenuItem(
|
}
|
||||||
value: 30,
|
}),
|
||||||
child: Text('30 Minutes'),
|
const SizedBox(
|
||||||
),
|
height: 16,
|
||||||
DropdownMenuItem(
|
),
|
||||||
value: 60,
|
Row(
|
||||||
child: Text('1 Hour'),
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
DropdownMenuItem(
|
children: [
|
||||||
value: 360,
|
Expanded(
|
||||||
child: Text('6 Hours'),
|
child: DropdownButtonFormField(
|
||||||
),
|
decoration: const InputDecoration(
|
||||||
DropdownMenuItem(
|
labelText: 'App Sort By'),
|
||||||
value: 720,
|
value: settingsProvider.sortColumn,
|
||||||
child: Text('12 Hours'),
|
items: const [
|
||||||
),
|
DropdownMenuItem(
|
||||||
DropdownMenuItem(
|
value:
|
||||||
value: 1440,
|
SortColumnSettings.authorName,
|
||||||
child: Text('1 Day'),
|
child: Text('Author/Name'),
|
||||||
),
|
),
|
||||||
],
|
DropdownMenuItem(
|
||||||
onChanged: (value) {
|
value:
|
||||||
if (value != null) {
|
SortColumnSettings.nameAuthor,
|
||||||
settingsProvider.updateInterval = value;
|
child: Text('Name/Author'),
|
||||||
}
|
),
|
||||||
}),
|
DropdownMenuItem(
|
||||||
const SizedBox(
|
value: SortColumnSettings.added,
|
||||||
height: 16,
|
child: Text('As Added'),
|
||||||
),
|
)
|
||||||
Row(
|
],
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
onChanged: (value) {
|
||||||
children: [
|
if (value != null) {
|
||||||
const Text('Show Source Webpage in App View'),
|
settingsProvider.sortColumn = value;
|
||||||
Switch(
|
}
|
||||||
value: settingsProvider.showAppWebpage,
|
})),
|
||||||
onChanged: (value) {
|
const SizedBox(
|
||||||
settingsProvider.showAppWebpage = value;
|
width: 16,
|
||||||
})
|
),
|
||||||
],
|
Expanded(
|
||||||
),
|
child: DropdownButtonFormField(
|
||||||
const SizedBox(
|
decoration: const InputDecoration(
|
||||||
height: 16,
|
labelText: 'App Sort Order'),
|
||||||
),
|
value: settingsProvider.sortOrder,
|
||||||
Row(
|
items: const [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
DropdownMenuItem(
|
||||||
children: [
|
value: SortOrderSettings.ascending,
|
||||||
ElevatedButton(
|
child: Text('Ascending'),
|
||||||
onPressed: appsProvider.apps.isEmpty
|
),
|
||||||
? null
|
DropdownMenuItem(
|
||||||
: () {
|
value: SortOrderSettings.descending,
|
||||||
HapticFeedback.lightImpact();
|
child: Text('Descending'),
|
||||||
appsProvider.exportApps().then((String path) {
|
),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
],
|
||||||
SnackBar(
|
onChanged: (value) {
|
||||||
content: Text('Exported to $path')),
|
if (value != null) {
|
||||||
);
|
settingsProvider.sortOrder = value;
|
||||||
});
|
}
|
||||||
},
|
})),
|
||||||
child: const Text('Export App List')),
|
],
|
||||||
ElevatedButton(
|
),
|
||||||
onPressed: () {
|
const SizedBox(
|
||||||
HapticFeedback.lightImpact();
|
height: 16,
|
||||||
showDialog(
|
),
|
||||||
context: context,
|
Row(
|
||||||
builder: (BuildContext ctx) {
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
final formKey = GlobalKey<FormState>();
|
children: [
|
||||||
final jsonInputController =
|
const Text('Show Source Webpage in App View'),
|
||||||
TextEditingController();
|
Switch(
|
||||||
|
value: settingsProvider.showAppWebpage,
|
||||||
return AlertDialog(
|
onChanged: (value) {
|
||||||
scrollable: true,
|
settingsProvider.showAppWebpage = value;
|
||||||
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:'),
|
const Divider(
|
||||||
Form(
|
height: 16,
|
||||||
key: formKey,
|
),
|
||||||
child: TextFormField(
|
const SizedBox(
|
||||||
minLines: 7,
|
height: 16,
|
||||||
maxLines: 7,
|
),
|
||||||
decoration: const InputDecoration(
|
Text(
|
||||||
helperText:
|
'More',
|
||||||
'Obtainium export data'),
|
style: TextStyle(
|
||||||
controller: jsonInputController,
|
color: Theme.of(context).colorScheme.primary),
|
||||||
validator: (value) {
|
),
|
||||||
if (value == null ||
|
DropdownButtonFormField(
|
||||||
value.isEmpty) {
|
decoration: const InputDecoration(
|
||||||
return 'Please enter your Obtainium export data';
|
labelText:
|
||||||
}
|
'Background Update Checking Interval'),
|
||||||
bool isJSON = true;
|
value: settingsProvider.updateInterval,
|
||||||
try {
|
items: const [
|
||||||
jsonDecode(value);
|
DropdownMenuItem(
|
||||||
} catch (e) {
|
value: 15,
|
||||||
isJSON = false;
|
child: Text('15 Minutes'),
|
||||||
}
|
),
|
||||||
if (!isJSON) {
|
DropdownMenuItem(
|
||||||
return 'Invalid input';
|
value: 30,
|
||||||
}
|
child: Text('30 Minutes'),
|
||||||
return null;
|
),
|
||||||
},
|
DropdownMenuItem(
|
||||||
),
|
value: 60,
|
||||||
)
|
child: Text('1 Hour'),
|
||||||
]),
|
),
|
||||||
actions: [
|
DropdownMenuItem(
|
||||||
TextButton(
|
value: 360,
|
||||||
onPressed: () {
|
child: Text('6 Hours'),
|
||||||
HapticFeedback.lightImpact();
|
),
|
||||||
Navigator.of(context).pop();
|
DropdownMenuItem(
|
||||||
},
|
value: 720,
|
||||||
child: const Text('Cancel')),
|
child: Text('12 Hours'),
|
||||||
TextButton(
|
),
|
||||||
onPressed: () {
|
DropdownMenuItem(
|
||||||
HapticFeedback.heavyImpact();
|
value: 1440,
|
||||||
if (formKey.currentState!
|
child: Text('1 Day'),
|
||||||
.validate()) {
|
),
|
||||||
appsProvider
|
DropdownMenuItem(
|
||||||
.importApps(
|
value: 0,
|
||||||
jsonInputController
|
child: Text('Never - Manual Only'),
|
||||||
.value.text)
|
),
|
||||||
.then((value) {
|
],
|
||||||
ScaffoldMessenger.of(context)
|
onChanged: (value) {
|
||||||
.showSnackBar(
|
if (value != null) {
|
||||||
SnackBar(
|
settingsProvider.updateInterval = value;
|
||||||
content: Text(
|
}
|
||||||
'$value App${value == 1 ? '' : 's'} Imported')),
|
}),
|
||||||
);
|
const Spacer(),
|
||||||
}).catchError((e) {
|
Row(
|
||||||
ScaffoldMessenger.of(context)
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
.showSnackBar(
|
children: [
|
||||||
SnackBar(
|
TextButton.icon(
|
||||||
content:
|
style: ButtonStyle(
|
||||||
Text(e.toString())),
|
foregroundColor:
|
||||||
);
|
MaterialStateProperty.resolveWith<
|
||||||
}).whenComplete(() {
|
Color>((Set<MaterialState> states) {
|
||||||
Navigator.of(context).pop();
|
return Colors.grey;
|
||||||
});
|
}),
|
||||||
}
|
),
|
||||||
},
|
onPressed: () {
|
||||||
child: const Text('Import')),
|
launchUrlString(settingsProvider.sourceUrl,
|
||||||
],
|
mode: LaunchMode.externalApplication);
|
||||||
);
|
},
|
||||||
});
|
icon: const Icon(Icons.code),
|
||||||
},
|
label: Text(
|
||||||
child: const Text('Import App List'))
|
'Source',
|
||||||
],
|
style:
|
||||||
),
|
Theme.of(context).textTheme.bodySmall,
|
||||||
const Spacer(),
|
),
|
||||||
Row(
|
)
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
],
|
||||||
children: [
|
),
|
||||||
TextButton.icon(
|
],
|
||||||
style: ButtonStyle(
|
)))
|
||||||
foregroundColor:
|
]));
|
||||||
MaterialStateProperty.resolveWith<Color>(
|
|
||||||
(Set<MaterialState> states) {
|
|
||||||
return Colors.grey;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
launchUrlString(settingsProvider.sourceUrl,
|
|
||||||
mode: LaunchMode.externalApplication);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.code),
|
|
||||||
label: Text(
|
|
||||||
'Source',
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
// If the picked APK comes from an origin different from the source, get user confirmation
|
// If the picked APK comes from an origin different from the source, get user confirmation
|
||||||
if (apkUrl != null &&
|
if (apkUrl != null &&
|
||||||
!apkUrl.toLowerCase().startsWith(apps[id]!.app.url.toLowerCase())) {
|
Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin) {
|
||||||
if (await showDialog(
|
if (await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -339,13 +339,12 @@ class _APKPickerState extends State<APKPicker> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
Navigator.of(context).pop(null);
|
Navigator.of(context).pop(null);
|
||||||
},
|
},
|
||||||
child: const Text('Cancel')),
|
child: const Text('Cancel')),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.selectionClick();
|
||||||
Navigator.of(context).pop(apkUrl);
|
Navigator.of(context).pop(apkUrl);
|
||||||
},
|
},
|
||||||
child: const Text('Continue'))
|
child: const Text('Continue'))
|
||||||
@ -376,13 +375,12 @@ class _APKOriginWarningDialogState extends State<APKOriginWarningDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
Navigator.of(context).pop(null);
|
Navigator.of(context).pop(null);
|
||||||
},
|
},
|
||||||
child: const Text('Cancel')),
|
child: const Text('Cancel')),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.selectionClick();
|
||||||
Navigator.of(context).pop(true);
|
Navigator.of(context).pop(true);
|
||||||
},
|
},
|
||||||
child: const Text('Continue'))
|
child: const Text('Continue'))
|
||||||
|
@ -9,6 +9,10 @@ enum ThemeSettings { system, light, dark }
|
|||||||
|
|
||||||
enum ColourSettings { basic, materialYou }
|
enum ColourSettings { basic, materialYou }
|
||||||
|
|
||||||
|
enum SortColumnSettings { added, nameAuthor, authorName }
|
||||||
|
|
||||||
|
enum SortOrderSettings { ascending, descending }
|
||||||
|
|
||||||
class SettingsProvider with ChangeNotifier {
|
class SettingsProvider with ChangeNotifier {
|
||||||
SharedPreferences? prefs;
|
SharedPreferences? prefs;
|
||||||
|
|
||||||
@ -45,7 +49,27 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set updateInterval(int min) {
|
set updateInterval(int min) {
|
||||||
prefs?.setInt('updateInterval', min < 15 ? 15 : min);
|
prefs?.setInt('updateInterval', (min < 15 && min != 0) ? 15 : min);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
SortColumnSettings get sortColumn {
|
||||||
|
return SortColumnSettings
|
||||||
|
.values[prefs?.getInt('sortColumn') ?? SortColumnSettings.added.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
set sortColumn(SortColumnSettings s) {
|
||||||
|
prefs?.setInt('sortColumn', s.index);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
SortOrderSettings get sortOrder {
|
||||||
|
return SortOrderSettings.values[
|
||||||
|
prefs?.getInt('sortOrder') ?? SortOrderSettings.descending.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
set sortOrder(SortOrderSettings s) {
|
||||||
|
prefs?.setInt('sortOrder', s.index);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,8 +296,7 @@ class FDroid implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
AppNames getAppNames(String standardUrl) {
|
AppNames getAppNames(String standardUrl) {
|
||||||
var name = Uri.parse(standardUrl).pathSegments.last;
|
return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last);
|
||||||
return AppNames(name, name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,8 +340,71 @@ class Mullvad implements AppSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class IzzyOnDroid implements AppSource {
|
||||||
|
@override
|
||||||
|
late String host = 'android.izzysoft.de';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://$host/repo/apk/[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw notValidURL;
|
||||||
|
}
|
||||||
|
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 parsedHtml = parse(res.body);
|
||||||
|
var multipleVersionApkUrls = parsedHtml
|
||||||
|
.querySelectorAll('a')
|
||||||
|
.where((element) =>
|
||||||
|
element.attributes['href']?.toLowerCase().endsWith('.apk') ??
|
||||||
|
false)
|
||||||
|
.map((e) => 'https://$host${e.attributes['href'] ?? ''}')
|
||||||
|
.toList();
|
||||||
|
if (multipleVersionApkUrls.isEmpty) {
|
||||||
|
throw noAPKFound;
|
||||||
|
}
|
||||||
|
var version = parsedHtml
|
||||||
|
.querySelector('#keydata')
|
||||||
|
?.querySelectorAll('b')
|
||||||
|
.where(
|
||||||
|
(element) => element.innerHtml.toLowerCase().contains('version'))
|
||||||
|
.toList()[0]
|
||||||
|
.parentNode
|
||||||
|
?.parentNode
|
||||||
|
?.children[1]
|
||||||
|
.innerHtml;
|
||||||
|
if (version == null) {
|
||||||
|
throw couldNotFindLatestVersion;
|
||||||
|
}
|
||||||
|
return APKDetails(version, [multipleVersionApkUrls[0]]);
|
||||||
|
} else {
|
||||||
|
throw couldNotFindReleases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AppNames getAppNames(String standardUrl) {
|
||||||
|
return AppNames('IzzyOnDroid', Uri.parse(standardUrl).pathSegments.last);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SourceProvider {
|
class SourceProvider {
|
||||||
List<AppSource> sources = [GitHub(), GitLab(), FDroid(), Mullvad(), Signal()];
|
List<AppSource> sources = [
|
||||||
|
GitHub(),
|
||||||
|
GitLab(),
|
||||||
|
FDroid(),
|
||||||
|
IzzyOnDroid(),
|
||||||
|
Mullvad(),
|
||||||
|
Signal()
|
||||||
|
];
|
||||||
|
|
||||||
|
List<MassAppSource> massSources = [GitHubStars()];
|
||||||
|
|
||||||
// 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) {
|
||||||
@ -382,5 +444,54 @@ class SourceProvider {
|
|||||||
apk.apkUrls.length - 1);
|
apk.apkUrls.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a length 2 list, where the first element is a list of Apps and
|
||||||
|
/// the second is a Map<String, dynamic> of URLs and errors
|
||||||
|
Future<List<dynamic>> getApps(List<String> urls) async {
|
||||||
|
List<App> apps = [];
|
||||||
|
Map<String, dynamic> errors = {};
|
||||||
|
for (var url in urls) {
|
||||||
|
try {
|
||||||
|
apps.add(await getApp(url));
|
||||||
|
} catch (e) {
|
||||||
|
errors.addAll(<String, dynamic>{url: e});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [apps, errors];
|
||||||
|
}
|
||||||
|
|
||||||
List<String> getSourceHosts() => sources.map((e) => e.host).toList();
|
List<String> getSourceHosts() => sources.map((e) => e.host).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class MassAppSource {
|
||||||
|
late String name;
|
||||||
|
late List<String> requiredArgs;
|
||||||
|
Future<List<String>> getUrls(List<String> args);
|
||||||
|
}
|
||||||
|
|
||||||
|
class GitHubStars implements MassAppSource {
|
||||||
|
@override
|
||||||
|
late String name = 'GitHub Starred Repos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
late List<String> requiredArgs = ['Username'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> getUrls(List<String> args) async {
|
||||||
|
if (args.length != requiredArgs.length) {
|
||||||
|
throw 'Wrong number of arguments provided';
|
||||||
|
}
|
||||||
|
Response res =
|
||||||
|
await get(Uri.parse('https://api.github.com/users/${args[0]}/starred'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
return (jsonDecode(res.body) as List<dynamic>)
|
||||||
|
.map((e) => e['html_url'] as String)
|
||||||
|
.toList();
|
||||||
|
} 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';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw 'Unable to find user\'s starred repos';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
45
pubspec.lock
45
pubspec.lock
@ -1,6 +1,13 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
animations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: animations
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.4"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -133,7 +140,7 @@ packages:
|
|||||||
name: device_info_plus_windows
|
name: device_info_plus_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.1.0"
|
||||||
dynamic_color:
|
dynamic_color:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -162,6 +169,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.4"
|
version: "6.1.4"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -194,21 +208,28 @@ 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.9.1"
|
version: "10.0.0"
|
||||||
flutter_local_notifications_linux:
|
flutter_local_notifications_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications_linux
|
name: flutter_local_notifications_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.1"
|
version: "1.0.0"
|
||||||
flutter_local_notifications_platform_interface:
|
flutter_local_notifications_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications_platform_interface
|
name: flutter_local_notifications_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.7"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -365,7 +386,7 @@ packages:
|
|||||||
name: path_provider_windows
|
name: path_provider_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.3"
|
||||||
permission_handler:
|
permission_handler:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -421,7 +442,7 @@ packages:
|
|||||||
name: plugin_platform_interface
|
name: plugin_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.3"
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -449,7 +470,7 @@ packages:
|
|||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.12"
|
version: "2.0.13"
|
||||||
shared_preferences_ios:
|
shared_preferences_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -538,7 +559,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.13"
|
version: "0.4.14"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -566,7 +587,7 @@ packages:
|
|||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.17"
|
version: "6.0.19"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -629,7 +650,7 @@ 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.10.0"
|
version: "2.10.1"
|
||||||
webview_flutter_platform_interface:
|
webview_flutter_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -643,14 +664,14 @@ packages:
|
|||||||
name: webview_flutter_wkwebview
|
name: webview_flutter_wkwebview
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.3"
|
version: "2.9.4"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "3.0.0"
|
||||||
workmanager:
|
workmanager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
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.1.9+10 # When changing this, update the tag in main() accordingly
|
version: 0.2.4+15 # 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'
|
||||||
@ -38,7 +38,7 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.5
|
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.9.1
|
flutter_local_notifications: ^10.0.0
|
||||||
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
|
||||||
@ -51,6 +51,8 @@ dependencies:
|
|||||||
permission_handler: ^10.0.0
|
permission_handler: ^10.0.0
|
||||||
fluttertoast: ^8.0.9
|
fluttertoast: ^8.0.9
|
||||||
device_info_plus: ^4.1.2
|
device_info_plus: ^4.1.2
|
||||||
|
file_picker: ^5.1.0
|
||||||
|
animations: ^2.0.4
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
Reference in New Issue
Block a user