mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-23 01:29:40 +02:00
Added GitHub starred import (+ general import/export changes)
This commit is contained in:
78
lib/components/generated_form_modal.dart
Normal file
78
lib/components/generated_form_modal.dart
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class GeneratedFormItem {
|
||||||
|
late String message;
|
||||||
|
late bool required;
|
||||||
|
|
||||||
|
GeneratedFormItem(this.message, this.required);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
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
|
@@ -2,6 +2,7 @@ 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 {
|
||||||
@@ -12,11 +13,12 @@ class HomePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage> {
|
class _HomePageState extends State<HomePage> {
|
||||||
int selectedIndex = 1;
|
List<int> selectedIndexHistory = [];
|
||||||
List<Widget> pages = [
|
List<Widget> pages = [
|
||||||
const SettingsPage(),
|
|
||||||
const AppsPage(),
|
const AppsPage(),
|
||||||
const AddAppPage()
|
const AddAppPage(),
|
||||||
|
const ImportExportPage(),
|
||||||
|
const SettingsPage()
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -24,27 +26,42 @@ class _HomePageState extends State<HomePage> {
|
|||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(title: const Text('Obtainium')),
|
appBar: AppBar(title: const Text('Obtainium')),
|
||||||
body: pages.elementAt(selectedIndex),
|
body: pages.elementAt(
|
||||||
|
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last),
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
destinations: const [
|
destinations: const [
|
||||||
NavigationDestination(
|
|
||||||
icon: Icon(Icons.settings), label: 'Settings'),
|
|
||||||
NavigationDestination(icon: Icon(Icons.apps), label: 'Apps'),
|
NavigationDestination(icon: Icon(Icons.apps), label: 'Apps'),
|
||||||
NavigationDestination(icon: Icon(Icons.add), label: 'Add App'),
|
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) {
|
onDestinationSelected: (int index) {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
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);
|
||||||
|
}
|
||||||
|
print(selectedIndexHistory);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
242
lib/pages/import_export.dart
Normal file
242
lib/pages/import_export.dart
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
Future<List<List<String>>> addApps(
|
||||||
|
MassAppSource source,
|
||||||
|
List<String> args,
|
||||||
|
SourceProvider sourceProvider,
|
||||||
|
SettingsProvider settingsProvider,
|
||||||
|
AppsProvider appsProvider) async {
|
||||||
|
var urls = await source.getUrls(args);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
|
var settingsProvider = context.read<SettingsProvider>();
|
||||||
|
var appsProvider = context.read<AppsProvider>();
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
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('Obtainium Export')),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
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('Obtainium Import')),
|
||||||
|
const Divider(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
...sourceProvider.massSources
|
||||||
|
.map((source) => TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: 'Import ${source.name}',
|
||||||
|
items: source.requiredArgs
|
||||||
|
.map((e) => GeneratedFormItem(e, true))
|
||||||
|
.toList());
|
||||||
|
}).then((values) {
|
||||||
|
if (values != null) {
|
||||||
|
source.getUrls(values).then((urls) {
|
||||||
|
addApps(source, values, sourceProvider,
|
||||||
|
settingsProvider, appsProvider)
|
||||||
|
.then((errors) {
|
||||||
|
if (errors.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content:
|
||||||
|
Text('Imported ${urls.length} Apps')),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: const Text('Import Errors'),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${urls.length - errors.length} of ${urls.length} Apps imported.',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'The following Apps had errors:',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyLarge,
|
||||||
|
),
|
||||||
|
...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'))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catchError((e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(e.toString())),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text('Import ${source.name}')))
|
||||||
|
.toList()
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@@ -127,112 +127,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(),
|
const Spacer(),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
@@ -6,6 +6,7 @@ import 'dart:convert';
|
|||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
|
|
||||||
class AppNames {
|
class AppNames {
|
||||||
late String author;
|
late String author;
|
||||||
@@ -404,6 +405,8 @@ class SourceProvider {
|
|||||||
IzzyOnDroid()
|
IzzyOnDroid()
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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) {
|
||||||
AppSource? source;
|
AppSource? source;
|
||||||
@@ -442,5 +445,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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user