Compare commits

...

9 Commits

Author SHA1 Message Date
5e96b91029 Updated version 2022-09-17 01:43:54 -04:00
5fc79af960 Added App sorting 2022-09-17 01:41:38 -04:00
05f5590e7d Updated modules, removed unneeded imports 2022-09-17 01:10:34 -04:00
50f8caeb47 Added "Already Installed" button 2022-09-17 00:59:15 -04:00
f966a9e626 Finished import/export changes 2022-09-17 00:39:56 -04:00
02a5749ba7 Removed redundant code 2022-09-17 00:09:46 -04:00
4ccf7cbc92 Added GitHub starred import (+ general import/export changes) 2022-09-16 23:52:58 -04:00
ab4efd85ce Added IzzyOnDroid App Source
+ Bugfix for third party APK URL support
+ F-Droid apps have F-Droid as Author now
2022-09-16 20:24:47 -04:00
42bba0f64c Added option to disable background update checking 2022-09-16 19:53:57 -04:00
12 changed files with 657 additions and 146 deletions

View File

@ -0,0 +1,81 @@
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: () {
HapticFeedback.lightImpact();
Navigator.of(context).pop(null);
},
child: const Text('Cancel')),
TextButton(
onPressed: () {
if (_formKey.currentState?.validate() == true) {
HapticFeedback.heavyImpact();
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

View File

@ -12,7 +12,7 @@ import 'package:dynamic_color/dynamic_color.dart';
import 'package:device_info_plus/device_info_plus.dart';
const String currentReleaseTag =
'v0.1.9-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
'v0.2.0-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
@pragma('vm:entry-point')
void bgTaskCallback() {
@ -81,11 +81,15 @@ class MyApp extends StatelessWidget {
settingsProvider.initializeSettings();
} else {
// Register the background update task according to the user's setting
Workmanager().registerPeriodicTask('bg-update-check', 'bg-update-check',
frequency: Duration(minutes: settingsProvider.updateInterval),
initialDelay: Duration(minutes: settingsProvider.updateInterval),
constraints: Constraints(networkType: NetworkType.connected),
existingWorkPolicy: ExistingWorkPolicy.replace);
if (settingsProvider.updateInterval > 0) {
Workmanager().registerPeriodicTask('bg-update-check', 'bg-update-check',
frequency: Duration(minutes: settingsProvider.updateInterval),
initialDelay: Duration(minutes: settingsProvider.updateInterval),
constraints: Constraints(networkType: NetworkType.connected),
existingWorkPolicy: ExistingWorkPolicy.replace);
} else {
Workmanager().cancelByUniqueName('bg-update-check');
}
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
if (isFirstRun) {
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list

View File

@ -91,6 +91,42 @@ class _AppPageState extends State<AppPage> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (app?.app.installedVersion == null)
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
title: const Text(
'App Already Installed?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('No')),
TextButton(
onPressed: () {
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)),
if (app?.app.installedVersion == null)
const SizedBox(width: 16.0),
Expanded(
child: ElevatedButton(
onPressed: (app?.app.installedVersion == null ||

View File

@ -16,7 +16,23 @@ class _AppsPageState extends State<AppsPage> {
@override
Widget build(BuildContext context) {
var appsProvider = context.watch<AppsProvider>();
var settingsProvider = context.watch<SettingsProvider>();
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(
floatingActionButton: existingUpdateAppIds.isEmpty
@ -26,10 +42,7 @@ class _AppsPageState extends State<AppsPage> {
? null
: () {
HapticFeedback.heavyImpact();
context
.read<SettingsProvider>()
.getInstallPermission()
.then((_) {
settingsProvider.getInstallPermission().then((_) {
appsProvider.downloadAndInstallLatestApp(
existingUpdateAppIds, context);
});
@ -50,7 +63,7 @@ class _AppsPageState extends State<AppsPage> {
return appsProvider.checkUpdates();
},
child: ListView(
children: appsProvider.apps.values
children: sortedApps
.map(
(e) => ListTile(
title: Text('${e.app.author}/${e.app.name}'),

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:obtainium/pages/add_app.dart';
import 'package:obtainium/pages/apps.dart';
import 'package:obtainium/pages/import_export.dart';
import 'package:obtainium/pages/settings.dart';
class HomePage extends StatefulWidget {
@ -12,11 +13,12 @@ class HomePage extends StatefulWidget {
}
class _HomePageState extends State<HomePage> {
int selectedIndex = 1;
List<int> selectedIndexHistory = [];
List<Widget> pages = [
const SettingsPage(),
const AppsPage(),
const AddAppPage()
const AddAppPage(),
const ImportExportPage(),
const SettingsPage()
];
@override
@ -24,27 +26,41 @@ class _HomePageState extends State<HomePage> {
return WillPopScope(
child: Scaffold(
appBar: AppBar(title: const Text('Obtainium')),
body: pages.elementAt(selectedIndex),
body: pages.elementAt(
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last),
bottomNavigationBar: NavigationBar(
destinations: const [
NavigationDestination(
icon: Icon(Icons.settings), label: 'Settings'),
NavigationDestination(icon: Icon(Icons.apps), label: 'Apps'),
NavigationDestination(icon: Icon(Icons.add), label: 'Add App'),
NavigationDestination(
icon: Icon(Icons.import_export), label: 'Import/Export'),
NavigationDestination(
icon: Icon(Icons.settings), label: 'Settings'),
],
onDestinationSelected: (int index) {
HapticFeedback.lightImpact();
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 {
if (selectedIndex != 1) {
if (selectedIndexHistory.isNotEmpty) {
setState(() {
selectedIndex = 1;
selectedIndexHistory.removeLast();
});
return false;
}

View File

@ -0,0 +1,284 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.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';
class ImportExportPage extends StatefulWidget {
const ImportExportPage({super.key});
@override
State<ImportExportPage> createState() => _ImportExportPageState();
}
class _ImportExportPageState extends State<ImportExportPage> {
bool gettingAppInfo = false;
@override
Widget build(BuildContext context) {
SourceProvider sourceProvider = SourceProvider();
var settingsProvider = context.read<SettingsProvider>();
var appsProvider = context.read<AppsProvider>();
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 Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: appsProvider.apps.isEmpty || gettingAppInfo
? null
: () {
HapticFeedback.lightImpact();
appsProvider.exportApps().then((String path) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Exported to $path')),
);
});
},
child: const Text('Obtainium Export')),
const SizedBox(
height: 8,
),
ElevatedButton(
onPressed: gettingAppInfo
? null
: () {
HapticFeedback.lightImpact();
showDialog(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: 'Obtainium Import',
items: [
GeneratedFormItem(
'Obtainium Export JSON Data', true, 7)
]);
}).then((values) {
if (values != null) {
try {
jsonDecode(values[0]);
} catch (e) {
throw 'Invalid input';
}
appsProvider.importApps(values[0]).then((value) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'$value App${value == 1 ? '' : 's'} Imported')),
);
});
}
}).catchError((e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString())),
);
});
},
child: const Text('Obtainium Import')),
if (gettingAppInfo)
Column(
children: const [
SizedBox(
height: 14,
),
LinearProgressIndicator(),
SizedBox(
height: 14,
),
],
)
else
const Divider(
height: 32,
),
TextButton(
onPressed: gettingAppInfo
? 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(() {
gettingAppInfo = 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(() {
gettingAppInfo = false;
});
});
}
});
},
child: const Text('Import from URL List')),
...sourceProvider.massSources
.map((source) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 8),
TextButton(
onPressed: gettingAppInfo
? 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(() {
gettingAppInfo = 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(() {
gettingAppInfo = 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: () {
HapticFeedback.lightImpact();
Navigator.of(context).pop(null);
},
child: const Text('Okay'))
],
);
}
}

View File

@ -1,8 +1,5 @@
import 'dart:convert';
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:provider/provider.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -17,7 +14,6 @@ class SettingsPage extends StatefulWidget {
class _SettingsPageState extends State<SettingsPage> {
@override
Widget build(BuildContext context) {
AppsProvider appsProvider = context.read<AppsProvider>();
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
if (settingsProvider.prefs == null) {
settingsProvider.initializeSettings();
@ -103,6 +99,10 @@ class _SettingsPageState extends State<SettingsPage> {
value: 1440,
child: Text('1 Day'),
),
DropdownMenuItem(
value: 0,
child: Text('Never - Manual Only'),
),
],
onChanged: (value) {
if (value != null) {
@ -112,6 +112,54 @@ class _SettingsPageState extends State<SettingsPage> {
const SizedBox(
height: 16,
),
DropdownButtonFormField(
decoration:
const InputDecoration(labelText: 'App Sort By'),
value: settingsProvider.sortColumn,
items: const [
DropdownMenuItem(
value: SortColumnSettings.authorName,
child: Text('Author/Name'),
),
DropdownMenuItem(
value: SortColumnSettings.nameAuthor,
child: Text('Name/Author'),
),
DropdownMenuItem(
value: SortColumnSettings.added,
child: Text('As Added'),
)
],
onChanged: (value) {
if (value != null) {
settingsProvider.sortColumn = value;
}
}),
const SizedBox(
height: 16,
),
DropdownButtonFormField(
decoration:
const InputDecoration(labelText: 'App Sort Order'),
value: settingsProvider.sortOrder,
items: const [
DropdownMenuItem(
value: SortOrderSettings.ascending,
child: Text('Ascending'),
),
DropdownMenuItem(
value: SortOrderSettings.descending,
child: Text('Descending'),
),
],
onChanged: (value) {
if (value != null) {
settingsProvider.sortOrder = value;
}
}),
const SizedBox(
height: 16,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -123,112 +171,6 @@ class _SettingsPageState extends State<SettingsPage> {
})
],
),
const SizedBox(
height: 16,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: appsProvider.apps.isEmpty
? null
: () {
HapticFeedback.lightImpact();
appsProvider.exportApps().then((String path) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Exported to $path')),
);
});
},
child: const Text('Export App List')),
ElevatedButton(
onPressed: () {
HapticFeedback.lightImpact();
showDialog(
context: context,
builder: (BuildContext ctx) {
final formKey = GlobalKey<FormState>();
final jsonInputController =
TextEditingController();
return AlertDialog(
scrollable: true,
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:'),
Form(
key: formKey,
child: TextFormField(
minLines: 7,
maxLines: 7,
decoration: const InputDecoration(
helperText:
'Obtainium export data'),
controller: jsonInputController,
validator: (value) {
if (value == null ||
value.isEmpty) {
return 'Please enter your Obtainium export data';
}
bool isJSON = true;
try {
jsonDecode(value);
} catch (e) {
isJSON = false;
}
if (!isJSON) {
return 'Invalid input';
}
return null;
},
),
)
]),
actions: [
TextButton(
onPressed: () {
HapticFeedback.lightImpact();
Navigator.of(context).pop();
},
child: const Text('Cancel')),
TextButton(
onPressed: () {
HapticFeedback.heavyImpact();
if (formKey.currentState!
.validate()) {
appsProvider
.importApps(
jsonInputController
.value.text)
.then((value) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
'$value App${value == 1 ? '' : 's'} Imported')),
);
}).catchError((e) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content:
Text(e.toString())),
);
}).whenComplete(() {
Navigator.of(context).pop();
});
}
},
child: const Text('Import')),
],
);
});
},
child: const Text('Import App List'))
],
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.center,

View File

@ -119,7 +119,7 @@ class AppsProvider with ChangeNotifier {
}
// 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())) {
Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin) {
if (await showDialog(
context: context,
builder: (BuildContext ctx) {

View File

@ -9,6 +9,10 @@ enum ThemeSettings { system, light, dark }
enum ColourSettings { basic, materialYou }
enum SortColumnSettings { added, nameAuthor, authorName }
enum SortOrderSettings { ascending, descending }
class SettingsProvider with ChangeNotifier {
SharedPreferences? prefs;
@ -45,7 +49,27 @@ class SettingsProvider with ChangeNotifier {
}
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();
}

View File

@ -296,8 +296,7 @@ class FDroid implements AppSource {
@override
AppNames getAppNames(String standardUrl) {
var name = Uri.parse(standardUrl).pathSegments.last;
return AppNames(name, name);
return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last);
}
}
@ -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 {
List<AppSource> sources = [GitHub(), GitLab(), FDroid(), Mullvad(), Signal()];
List<AppSource> sources = [
GitHub(),
GitLab(),
FDroid(),
Mullvad(),
Signal(),
IzzyOnDroid()
];
List<MassAppSource> massSources = [GitHubStars()];
// Add more source classes here so they are available via the service
AppSource getSource(String url) {
@ -382,5 +444,54 @@ class SourceProvider {
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();
}
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';
}
}
}

View File

@ -133,7 +133,7 @@ packages:
name: device_info_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
version: "4.1.0"
dynamic_color:
dependency: "direct main"
description:
@ -365,7 +365,7 @@ packages:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
version: "2.1.3"
permission_handler:
dependency: "direct main"
description:
@ -421,7 +421,7 @@ packages:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
version: "2.1.3"
process:
dependency: transitive
description:
@ -449,7 +449,7 @@ packages:
name: shared_preferences_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.12"
version: "2.0.13"
shared_preferences_ios:
dependency: transitive
description:
@ -538,7 +538,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.13"
version: "0.4.14"
timezone:
dependency: transitive
description:
@ -566,7 +566,7 @@ packages:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.17"
version: "6.0.19"
url_launcher_ios:
dependency: transitive
description:
@ -629,7 +629,7 @@ packages:
name: webview_flutter_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.10.0"
version: "2.10.1"
webview_flutter_platform_interface:
dependency: transitive
description:
@ -643,14 +643,14 @@ packages:
name: webview_flutter_wkwebview
url: "https://pub.dartlang.org"
source: hosted
version: "2.9.3"
version: "2.9.4"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.7.0"
version: "3.0.0"
workmanager:
dependency: "direct main"
description:

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.9+10 # When changing this, update the tag in main() accordingly
version: 0.2.0+11 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=2.19.0-79.0.dev <3.0.0'