mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-01 21:30:16 +02:00
Internationalization (#131)
Replaced hardcoded English strings with locale-based variables based on the [easy_localization](https://pub.dev/packages/easy_localization) Flutter plugin.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
@@ -15,7 +16,7 @@ class GitHub extends AppSource {
|
||||
|
||||
additionalSourceSpecificSettingFormItems = [
|
||||
GeneratedFormItem(
|
||||
label: 'GitHub Personal Access Token (Increases Rate Limit)',
|
||||
label: tr('githubPATLabel'),
|
||||
id: 'github-creds',
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
@@ -26,13 +27,13 @@ class GitHub extends AppSource {
|
||||
.where((element) => element.trim().isNotEmpty)
|
||||
.length !=
|
||||
2) {
|
||||
return 'PAT must be in this format: username:token';
|
||||
return tr('githubPATHint');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
],
|
||||
hint: 'username:token',
|
||||
hint: tr('githubPATFormat'),
|
||||
belowWidgets: [
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
@@ -43,9 +44,9 @@ class GitHub extends AppSource {
|
||||
'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token',
|
||||
mode: LaunchMode.externalApplication);
|
||||
},
|
||||
child: const Text(
|
||||
'About GitHub PATs',
|
||||
style: TextStyle(
|
||||
child: Text(
|
||||
tr('githubPATLinkText'),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline, fontSize: 12),
|
||||
))
|
||||
])
|
||||
@@ -53,15 +54,16 @@ class GitHub extends AppSource {
|
||||
|
||||
additionalSourceAppSpecificFormItems = [
|
||||
[
|
||||
GeneratedFormItem(label: 'Include prereleases', type: FormItemType.bool)
|
||||
GeneratedFormItem(
|
||||
label: tr('includePrereleases'), type: FormItemType.bool)
|
||||
],
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: 'Fallback to older releases', type: FormItemType.bool)
|
||||
label: tr('fallbackToOlderReleases'), type: FormItemType.bool)
|
||||
],
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: 'Filter Release Titles by Regular Expression',
|
||||
label: tr('filterReleaseTitlesByRegEx'),
|
||||
type: FormItemType.string,
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
@@ -72,7 +74,7 @@ class GitHub extends AppSource {
|
||||
try {
|
||||
RegExp(value);
|
||||
} catch (e) {
|
||||
return 'Invalid regular expression';
|
||||
return tr('invalidRegEx');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -184,7 +186,7 @@ class GitHub extends AppSource {
|
||||
urlsWithDescriptions.addAll({
|
||||
e['html_url'] as String: e['description'] != null
|
||||
? e['description'] as String
|
||||
: 'No description'
|
||||
: tr('noDescription')
|
||||
});
|
||||
}
|
||||
return urlsWithDescriptions;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum FormItemType { string, bool }
|
||||
@@ -108,7 +109,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
maxLines: e.value.max <= 1 ? 1 : e.value.max,
|
||||
validator: (value) {
|
||||
if (e.value.required && (value == null || value.trim().isEmpty)) {
|
||||
return '${e.value.label} (required)';
|
||||
return '${e.value.label} ${tr('requiredInBrackets')}';
|
||||
}
|
||||
for (var validator in e.value.additionalValidators) {
|
||||
String? result = validator(value);
|
||||
@@ -122,10 +123,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
} else if (e.value.type == FormItemType.string &&
|
||||
e.value.opts != null) {
|
||||
if (e.value.opts!.isEmpty) {
|
||||
return const Text('ERROR: DROPDOWN MUST HAVE AT LEAST ONE OPT.');
|
||||
return Text(tr('dropdownNoOptsError'));
|
||||
}
|
||||
return DropdownButtonFormField(
|
||||
decoration: const InputDecoration(labelText: 'Colour'),
|
||||
decoration: InputDecoration(labelText: tr('colour')),
|
||||
value: values[row.key][e.key],
|
||||
items: e.value.opts!
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
@@ -64,7 +65,7 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(null);
|
||||
},
|
||||
child: const Text('Cancel')),
|
||||
child: Text(tr('cancel'))),
|
||||
TextButton(
|
||||
onPressed: !valid
|
||||
? null
|
||||
@@ -74,7 +75,7 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
||||
Navigator.of(context).pop(values);
|
||||
}
|
||||
},
|
||||
child: const Text('Continue'))
|
||||
child: Text(tr('continue')))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:obtainium/providers/logs_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -18,46 +19,46 @@ class RateLimitError {
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Too many requests (rate limited) - try again in $remainingMinutes minutes';
|
||||
plural('tooManyRequestsTryAgainInMinutes', remainingMinutes);
|
||||
}
|
||||
|
||||
class InvalidURLError extends ObtainiumError {
|
||||
InvalidURLError(String sourceName) : super('Not a valid $sourceName App URL');
|
||||
InvalidURLError(String sourceName)
|
||||
: super(tr('invalidURLForSource', args: [sourceName]));
|
||||
}
|
||||
|
||||
class NoReleasesError extends ObtainiumError {
|
||||
NoReleasesError() : super('Could not find a suitable release');
|
||||
NoReleasesError() : super(tr('noReleaseFound'));
|
||||
}
|
||||
|
||||
class NoAPKError extends ObtainiumError {
|
||||
NoAPKError() : super('Could not find a suitable release');
|
||||
NoAPKError() : super(tr('noReleaseFound'));
|
||||
}
|
||||
|
||||
class NoVersionError extends ObtainiumError {
|
||||
NoVersionError() : super('Could not determine release version');
|
||||
NoVersionError() : super(tr('noVersionFound'));
|
||||
}
|
||||
|
||||
class UnsupportedURLError extends ObtainiumError {
|
||||
UnsupportedURLError() : super('URL does not match a known source');
|
||||
UnsupportedURLError() : super(tr('urlMatchesNoSource'));
|
||||
}
|
||||
|
||||
class DowngradeError extends ObtainiumError {
|
||||
DowngradeError() : super('Cannot install an older version of an App');
|
||||
DowngradeError() : super(tr('cantInstallOlderVersion'));
|
||||
}
|
||||
|
||||
class IDChangedError extends ObtainiumError {
|
||||
IDChangedError()
|
||||
: super('Downloaded package ID does not match existing App ID');
|
||||
IDChangedError() : super(tr('appIdMismatch'));
|
||||
}
|
||||
|
||||
class NotImplementedError extends ObtainiumError {
|
||||
NotImplementedError() : super('This class has not implemented this function');
|
||||
NotImplementedError() : super(tr('functionNotImplemented'));
|
||||
}
|
||||
|
||||
class MultiAppMultiError extends ObtainiumError {
|
||||
Map<String, List<String>> content = {};
|
||||
|
||||
MultiAppMultiError() : super('Multiple Errors Placeholder', unexpected: true);
|
||||
MultiAppMultiError() : super(tr('placeholder'), unexpected: true);
|
||||
|
||||
add(String appId, String string) {
|
||||
var tempIds = content.remove(string);
|
||||
@@ -90,15 +91,15 @@ showError(dynamic e, BuildContext context) {
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(e is MultiAppMultiError
|
||||
? 'Some Errors Occurred'
|
||||
: 'Unexpected Error'),
|
||||
? tr('someErrors')
|
||||
: tr('unexpectedError')),
|
||||
content: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(null);
|
||||
},
|
||||
child: const Text('Ok')),
|
||||
child: Text(tr('ok'))),
|
||||
],
|
||||
);
|
||||
});
|
||||
@@ -107,7 +108,7 @@ showError(dynamic e, BuildContext context) {
|
||||
|
||||
String list2FriendlyString(List<String> list) {
|
||||
return list.length == 2
|
||||
? '${list[0]} and ${list[1]}'
|
||||
? '${list[0]} ${tr('and')} ${list[1]}'
|
||||
: list
|
||||
.asMap()
|
||||
.entries
|
||||
|
@@ -15,6 +15,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
const String currentVersion = '0.8.1';
|
||||
const String currentReleaseTag =
|
||||
@@ -25,14 +26,14 @@ const int bgUpdateCheckAlarmId = 666;
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
||||
LogsProvider logs = LogsProvider();
|
||||
logs.add('Started BG update check task');
|
||||
logs.add(tr('startedBgUpdateTask'));
|
||||
int? ignoreAfterMicroseconds = params?['ignoreAfterMicroseconds'];
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await AndroidAlarmManager.initialize();
|
||||
DateTime? ignoreAfter = ignoreAfterMicroseconds != null
|
||||
? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds)
|
||||
: null;
|
||||
logs.add('Bg update ignoreAfter is $ignoreAfter');
|
||||
logs.add(tr('bgUpdateIgnoreAfterIs', args: [ignoreAfter.toString()]));
|
||||
var notificationsProvider = NotificationsProvider();
|
||||
await notificationsProvider.notify(checkingUpdatesNotification);
|
||||
try {
|
||||
@@ -44,14 +45,14 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
||||
DateTime nextIgnoreAfter = DateTime.now();
|
||||
String? err;
|
||||
try {
|
||||
logs.add('Started actual BG update checking');
|
||||
logs.add(tr('startedActualBGUpdateCheck'));
|
||||
await appsProvider.checkUpdates(
|
||||
ignoreAppsCheckedAfter: ignoreAfter, throwErrorsForRetry: true);
|
||||
} catch (e) {
|
||||
if (e is RateLimitError || e is SocketException) {
|
||||
var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15;
|
||||
logs.add(
|
||||
'BG update checking encountered a ${e.runtimeType}, will schedule a retry check in $remainingMinutes minutes');
|
||||
logs.add(plural('bgUpdateGotErrorRetryInMinutes', remainingMinutes,
|
||||
args: [e.runtimeType.toString()]));
|
||||
AndroidAlarmManager.oneShot(Duration(minutes: remainingMinutes),
|
||||
Random().nextInt(pow(2, 31) as int), bgUpdateCheck, params: {
|
||||
'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch
|
||||
@@ -80,7 +81,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
||||
// cancelExisting: true);
|
||||
// }
|
||||
logs.add(
|
||||
'BG update checking found ${newUpdates.length} updates - will notify user if needed');
|
||||
plural('bgCheckFoundUpdatesWillNotifyIfNeeded', newUpdates.length));
|
||||
if (newUpdates.isNotEmpty) {
|
||||
notificationsProvider.notify(UpdateNotification(newUpdates));
|
||||
}
|
||||
@@ -91,13 +92,14 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
||||
notificationsProvider
|
||||
.notify(ErrorCheckingUpdatesNotification(e.toString()));
|
||||
} finally {
|
||||
logs.add('Finished BG update check task');
|
||||
logs.add(tr('bgUpdateTaskFinished'));
|
||||
await notificationsProvider.cancel(checkingUpdatesNotification.id);
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await EasyLocalization.ensureInitialized();
|
||||
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
|
||||
@@ -112,7 +114,11 @@ void main() async {
|
||||
Provider(create: (context) => NotificationsProvider()),
|
||||
Provider(create: (context) => LogsProvider())
|
||||
],
|
||||
child: const Obtainium(),
|
||||
child: EasyLocalization(
|
||||
supportedLocales: const [Locale('en')],
|
||||
path: 'assets/translations',
|
||||
fallbackLocale: const Locale('en'),
|
||||
child: const Obtainium()),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -139,7 +145,7 @@ class _ObtainiumState extends State<Obtainium> {
|
||||
} else {
|
||||
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
|
||||
if (isFirstRun) {
|
||||
logs.add('This is the first ever run of Obtainium');
|
||||
logs.add(tr('firstRun'));
|
||||
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
||||
Permission.notification.request();
|
||||
appsProvider.saveApps([
|
||||
@@ -161,8 +167,8 @@ class _ObtainiumState extends State<Obtainium> {
|
||||
// Register the background update task according to the user's setting
|
||||
if (existingUpdateInterval != settingsProvider.updateInterval) {
|
||||
if (existingUpdateInterval != -1) {
|
||||
logs.add(
|
||||
'Setting update interval to ${settingsProvider.updateInterval}');
|
||||
logs.add(tr('settingUpdateCheckIntervalTo',
|
||||
args: [settingsProvider.updateInterval.toString()]));
|
||||
}
|
||||
existingUpdateInterval = settingsProvider.updateInterval;
|
||||
if (existingUpdateInterval == 0) {
|
||||
@@ -195,6 +201,9 @@ class _ObtainiumState extends State<Obtainium> {
|
||||
}
|
||||
return MaterialApp(
|
||||
title: 'Obtainium',
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: settingsProvider.theme == ThemeSettings.dark
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
@@ -7,10 +8,10 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class GitHubStars implements MassAppUrlSource {
|
||||
@override
|
||||
late String name = 'GitHub Starred Repos';
|
||||
late String name = tr('githubStarredRepos');
|
||||
|
||||
@override
|
||||
late List<String> requiredArgs = ['Username'];
|
||||
late List<String> requiredArgs = [tr('uname')];
|
||||
|
||||
Future<Map<String, String>> getOnePageOfUserStarredUrlsWithDescriptions(
|
||||
String username, int page) async {
|
||||
@@ -22,7 +23,7 @@ class GitHubStars implements MassAppUrlSource {
|
||||
urlsWithDescriptions.addAll({
|
||||
e['html_url'] as String: e['description'] != null
|
||||
? e['description'] as String
|
||||
: 'No description'
|
||||
: tr('noDescription')
|
||||
});
|
||||
}
|
||||
return urlsWithDescriptions;
|
||||
@@ -36,7 +37,7 @@ class GitHubStars implements MassAppUrlSource {
|
||||
@override
|
||||
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args) async {
|
||||
if (args.length != requiredArgs.length) {
|
||||
throw ObtainiumError('Wrong number of arguments provided');
|
||||
throw ObtainiumError(tr('wrongArgNum'));
|
||||
}
|
||||
Map<String, String> urlsWithDescriptions = {};
|
||||
var page = 1;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/components/custom_app_bar.dart';
|
||||
@@ -75,12 +76,15 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title:
|
||||
'${pickedSource!.enforceTrackOnly ? 'Source' : 'App'} is Track-Only',
|
||||
title: tr('xIsTrackOnly', args: [
|
||||
pickedSource!.enforceTrackOnly
|
||||
? tr('source')
|
||||
: tr('app')
|
||||
]),
|
||||
items: const [],
|
||||
defaultValues: const [],
|
||||
message:
|
||||
'${pickedSource!.enforceTrackOnly ? 'Apps from this source are \'Track-Only\'.' : 'You have selected the \'Track-Only\' option.'}\n\nThe App will be tracked for updates, but Obtainium will not be able to download or install it.',
|
||||
'${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
|
||||
);
|
||||
}) ==
|
||||
null) {
|
||||
@@ -100,14 +104,14 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
// ignore: use_build_context_synchronously
|
||||
var apkUrl = await appsProvider.confirmApkUrl(app, context);
|
||||
if (apkUrl == null) {
|
||||
throw ObtainiumError('Cancelled');
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
}
|
||||
app.preferredApkIndex = app.apkUrls.indexOf(apkUrl);
|
||||
var downloadedApk = await appsProvider.downloadApp(app);
|
||||
app.id = downloadedApk.appId;
|
||||
}
|
||||
if (appsProvider.apps.containsKey(app.id)) {
|
||||
throw ObtainiumError('App already added');
|
||||
throw ObtainiumError(tr('appAlreadyAdded'));
|
||||
}
|
||||
if (app.trackOnly) {
|
||||
app.installedVersion = app.latestVersion;
|
||||
@@ -137,7 +141,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: CustomScrollView(slivers: <Widget>[
|
||||
const CustomAppBar(title: 'Add App'),
|
||||
CustomAppBar(title: tr('addApp')),
|
||||
SliverFillRemaining(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -151,7 +155,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
items: [
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: 'App Source Url',
|
||||
label: tr('appSourceURL'),
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
try {
|
||||
@@ -165,7 +169,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
? e
|
||||
: e is ObtainiumError
|
||||
? e.toString()
|
||||
: 'Error';
|
||||
: tr('error');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -195,7 +199,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
!otherAdditionalDataIsValid)
|
||||
? null
|
||||
: addApp,
|
||||
child: const Text('Add'))
|
||||
child: Text(tr('add')))
|
||||
],
|
||||
),
|
||||
if (sourceProvider.sources
|
||||
@@ -218,7 +222,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
items: [
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: 'Search (Some Sources Only)',
|
||||
label: tr('searchSomeSourcesLabel'),
|
||||
required: false),
|
||||
]
|
||||
],
|
||||
@@ -281,7 +285,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
showError(e, context);
|
||||
});
|
||||
},
|
||||
child: const Text('Search'))
|
||||
child: Text(tr('search')))
|
||||
],
|
||||
),
|
||||
if (pickedSource != null &&
|
||||
@@ -301,7 +305,10 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
height: 64,
|
||||
),
|
||||
Text(
|
||||
'Additional Options for ${pickedSource?.runtimeType}',
|
||||
tr('additionalOptsFor', args: [
|
||||
pickedSource?.runtimeType.toString() ??
|
||||
tr('source')
|
||||
]),
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary)),
|
||||
@@ -365,8 +372,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
const Text(
|
||||
'Supported Sources:',
|
||||
Text(
|
||||
tr('supportedSourcesBelow'),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
@@ -379,7 +386,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
LaunchMode.externalApplication);
|
||||
},
|
||||
child: Text(
|
||||
'${e.runtimeType.toString()}${e.enforceTrackOnly ? ' (Track-Only)' : ''}${e.canSearch ? ' (Searchable)' : ''}',
|
||||
'${e.runtimeType.toString()}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
|
||||
style: const TextStyle(
|
||||
decoration:
|
||||
TextDecoration.underline,
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/components/custom_app_bar.dart';
|
||||
@@ -191,7 +192,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
});
|
||||
},
|
||||
child: CustomScrollView(slivers: <Widget>[
|
||||
const CustomAppBar(title: 'Apps'),
|
||||
CustomAppBar(title: tr('appsString')),
|
||||
if (appsProvider.loadingApps || sortedApps.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
@@ -199,8 +200,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
? const CircularProgressIndicator()
|
||||
: Text(
|
||||
appsProvider.apps.isEmpty
|
||||
? 'No Apps'
|
||||
: 'No Apps for Filter',
|
||||
? tr('noApps')
|
||||
: tr('noAppsForFilter'),
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
textAlign: TextAlign.center,
|
||||
))),
|
||||
@@ -244,14 +245,19 @@ class AppsPageState extends State<AppsPage> {
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal),
|
||||
),
|
||||
subtitle: Text('By ${sortedApps[index].app.author}',
|
||||
subtitle: Text(tr('byX', args: [sortedApps[index].app.author]),
|
||||
style: TextStyle(
|
||||
fontWeight: sortedApps[index].app.pinned
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal)),
|
||||
trailing: sortedApps[index].downloadProgress != null
|
||||
? Text(
|
||||
'Downloading - ${sortedApps[index].downloadProgress?.toInt()}%')
|
||||
? Text(tr('percentProgress', args: [
|
||||
sortedApps[index]
|
||||
.downloadProgress
|
||||
?.toInt()
|
||||
.toString() ??
|
||||
'100'
|
||||
]))
|
||||
: (sortedApps[index].app.installedVersion != null &&
|
||||
sortedApps[index].app.installedVersion !=
|
||||
sortedApps[index].app.latestVersion
|
||||
@@ -260,8 +266,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(appsProvider.areDownloadsRunning()
|
||||
? 'Please Wait...'
|
||||
: 'Update Available${sortedApps[index].app.trackOnly ? ' (Est.)' : ''}'),
|
||||
? tr('pleaseWait')
|
||||
: '${tr('updateAvailable')}${sortedApps[index].app.trackOnly ? ' ${tr('estimateInBracketsShort')}' : ''}'),
|
||||
SourceProvider()
|
||||
.getSource(sortedApps[index].app.url)
|
||||
.changeLogPageFromStandardUrl(
|
||||
@@ -292,7 +298,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
child: Text(
|
||||
'${sortedApps[index].app.installedVersion ?? 'Not Installed'} ${sortedApps[index].app.trackOnly == true ? '(Estimate)' : ''}',
|
||||
'${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.trackOnly == true ? ' ${tr('estimateInBrackets')}' : ''}',
|
||||
overflow: TextOverflow.fade,
|
||||
textAlign: TextAlign.end,
|
||||
)))),
|
||||
@@ -327,8 +333,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
tooltip: selectedApps.isEmpty
|
||||
? 'Select All'
|
||||
: 'Deselect ${selectedApps.length.toString()}'),
|
||||
? tr('selectAll')
|
||||
: tr('deselectN', args: [selectedApps.length.toString()])),
|
||||
const VerticalDivider(),
|
||||
Expanded(
|
||||
child: Row(
|
||||
@@ -343,12 +349,15 @@ class AppsPageState extends State<AppsPage> {
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: 'Remove Selected Apps?',
|
||||
title: tr('removeSelectedAppsQuestion'),
|
||||
items: const [],
|
||||
defaultValues: const [],
|
||||
initValid: true,
|
||||
message:
|
||||
'${selectedApps.length} App${selectedApps.length == 1 ? '' : 's'} will be removed from Obtainium but remain installed. You still need to uninstall ${selectedApps.length == 1 ? 'it' : 'them'} manually.',
|
||||
message: tr(
|
||||
'xWillBeRemovedButRemainInstalled',
|
||||
args: [
|
||||
plural('apps', selectedApps.length)
|
||||
]),
|
||||
);
|
||||
}).then((values) {
|
||||
if (values != null) {
|
||||
@@ -357,7 +366,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
});
|
||||
},
|
||||
tooltip: 'Remove Selected Apps',
|
||||
tooltip: tr('removeSelectedApps'),
|
||||
icon: const Icon(Icons.delete_outline_outlined),
|
||||
),
|
||||
IconButton(
|
||||
@@ -373,16 +382,20 @@ class AppsPageState extends State<AppsPage> {
|
||||
List<String> defaultValues = [];
|
||||
if (existingUpdateIdsAllOrSelected.isNotEmpty) {
|
||||
formInputs.add(GeneratedFormItem(
|
||||
label:
|
||||
'Update ${existingUpdateIdsAllOrSelected.length} App${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'}',
|
||||
label: tr('updateX', args: [
|
||||
plural('apps',
|
||||
existingUpdateIdsAllOrSelected.length)
|
||||
]),
|
||||
type: FormItemType.bool,
|
||||
key: 'updates'));
|
||||
defaultValues.add('true');
|
||||
}
|
||||
if (newInstallIdsAllOrSelected.isNotEmpty) {
|
||||
formInputs.add(GeneratedFormItem(
|
||||
label:
|
||||
'Install ${newInstallIdsAllOrSelected.length} new App${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}',
|
||||
label: tr('installX', args: [
|
||||
plural('apps',
|
||||
newInstallIdsAllOrSelected.length)
|
||||
]),
|
||||
type: FormItemType.bool,
|
||||
key: 'installs'));
|
||||
defaultValues
|
||||
@@ -390,8 +403,10 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) {
|
||||
formInputs.add(GeneratedFormItem(
|
||||
label:
|
||||
'Mark ${trackOnlyUpdateIdsAllOrSelected.length} Track-Only\nApp${trackOnlyUpdateIdsAllOrSelected.length == 1 ? '' : 's'} as Updated',
|
||||
label: tr('markXTrackOnlyAsUpdated', args: [
|
||||
plural('apps',
|
||||
trackOnlyUpdateIdsAllOrSelected.length)
|
||||
]),
|
||||
type: FormItemType.bool,
|
||||
key: 'trackonlies'));
|
||||
defaultValues
|
||||
@@ -405,8 +420,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
newInstallIdsAllOrSelected.length +
|
||||
trackOnlyUpdateIdsAllOrSelected.length;
|
||||
return GeneratedFormModal(
|
||||
title:
|
||||
'Change $totalApps App${totalApps == 1 ? '' : 's'}',
|
||||
title: tr('changeX',
|
||||
args: [plural('apps', totalApps)]),
|
||||
items: formInputs.map((e) => [e]).toList(),
|
||||
defaultValues: defaultValues,
|
||||
initValid: true,
|
||||
@@ -459,8 +474,9 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
});
|
||||
},
|
||||
tooltip:
|
||||
'Install/Update${selectedApps.isEmpty ? ' ' : ' Selected '}Apps',
|
||||
tooltip: selectedApps.isEmpty
|
||||
? tr('installUpdateApps')
|
||||
: tr('installUpdateSelectedApps'),
|
||||
icon: const Icon(
|
||||
Icons.file_download_outlined,
|
||||
)),
|
||||
@@ -492,11 +508,15 @@ class AppsPageState extends State<AppsPage> {
|
||||
(BuildContext
|
||||
ctx) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Mark ${selectedApps.length} Selected Apps as Updated?'),
|
||||
content:
|
||||
const Text(
|
||||
'Only applies to installed but out of date Apps.'),
|
||||
title: Text(tr(
|
||||
'markXSelectedAppsAsUpdated',
|
||||
args: [
|
||||
selectedApps
|
||||
.length
|
||||
.toString()
|
||||
])),
|
||||
content: Text(
|
||||
tr('onlyAppliesToInstalledAndOutdatedApps')),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
@@ -504,8 +524,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
},
|
||||
child: const Text(
|
||||
'No')),
|
||||
child: Text(
|
||||
tr('no'))),
|
||||
TextButton(
|
||||
onPressed:
|
||||
() {
|
||||
@@ -523,8 +543,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Yes'))
|
||||
child: Text(
|
||||
tr('yes')))
|
||||
],
|
||||
);
|
||||
}).whenComplete(() {
|
||||
@@ -534,7 +554,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
});
|
||||
},
|
||||
tooltip:
|
||||
'Mark Selected Apps as Updated',
|
||||
tr('markSelectedAppsUpdated'),
|
||||
icon: const Icon(Icons.done)),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
@@ -549,8 +569,12 @@ class AppsPageState extends State<AppsPage> {
|
||||
}).toList());
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
tooltip:
|
||||
'${selectedApps.where((element) => element.pinned).isEmpty ? 'Pin to' : 'Unpin from'} top',
|
||||
tooltip: selectedApps
|
||||
.where((element) =>
|
||||
element.pinned)
|
||||
.isEmpty
|
||||
? tr('pinToTop')
|
||||
: tr('unpinFromTop'),
|
||||
icon: Icon(selectedApps
|
||||
.where((element) =>
|
||||
element.pinned)
|
||||
@@ -568,11 +592,11 @@ class AppsPageState extends State<AppsPage> {
|
||||
urls = urls.substring(
|
||||
0, urls.length - 1);
|
||||
Share.share(urls,
|
||||
subject:
|
||||
'${selectedApps.length} Selected App URLs from Obtainium');
|
||||
subject: tr(
|
||||
'selectedAppURLsFromObtainium'));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
tooltip: 'Share Selected App URLs',
|
||||
tooltip: tr('shareSelectedAppURLs'),
|
||||
icon: const Icon(Icons.share),
|
||||
),
|
||||
IconButton(
|
||||
@@ -581,13 +605,19 @@ class AppsPageState extends State<AppsPage> {
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title:
|
||||
'Reset Install Status for Selected Apps?',
|
||||
title: tr(
|
||||
'resetInstallStatusForSelectedAppsQuestion'),
|
||||
items: const [],
|
||||
defaultValues: const [],
|
||||
initValid: true,
|
||||
message:
|
||||
'The install status of ${selectedApps.length} App${selectedApps.length == 1 ? '' : 's'} will be reset.\n\nThis can help when the App version shown in Obtainium is incorrect due to failed updates or other issues.',
|
||||
message: tr(
|
||||
'installStatusOfXWillBeResetExplanation',
|
||||
args: [
|
||||
plural(
|
||||
'app',
|
||||
selectedApps
|
||||
.length)
|
||||
]),
|
||||
);
|
||||
}).then((values) {
|
||||
if (values != null) {
|
||||
@@ -601,7 +631,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
},
|
||||
tooltip: 'Reset Install Status',
|
||||
tooltip: tr('resetInstallStatus'),
|
||||
icon: const Icon(
|
||||
Icons.restore_page_outlined),
|
||||
),
|
||||
@@ -610,7 +640,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
);
|
||||
});
|
||||
},
|
||||
tooltip: 'More',
|
||||
tooltip: tr('more'),
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
),
|
||||
],
|
||||
@@ -628,8 +658,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
});
|
||||
},
|
||||
tooltip: currentFilterIsUpdatesOnly
|
||||
? 'Remove Out-of-Date App Filter'
|
||||
: 'Show Out-of-Date Apps Only',
|
||||
? tr('removeOutdatedFilter')
|
||||
: tr('showOutdatedOnly'),
|
||||
icon: Icon(
|
||||
currentFilterIsUpdatesOnly
|
||||
? Icons.update_disabled_rounded
|
||||
@@ -641,7 +671,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
? const SizedBox()
|
||||
: TextButton.icon(
|
||||
label: Text(
|
||||
filter == null ? 'Filter' : 'Filter *',
|
||||
filter == null ? tr('filter') : tr('filterActive'),
|
||||
style: TextStyle(
|
||||
fontWeight: filter == null
|
||||
? FontWeight.normal
|
||||
@@ -652,22 +682,22 @@ class AppsPageState extends State<AppsPage> {
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: 'Filter Apps',
|
||||
title: tr('filterApps'),
|
||||
items: [
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: 'App Name', required: false),
|
||||
label: tr('appName'), required: false),
|
||||
GeneratedFormItem(
|
||||
label: 'Author', required: false)
|
||||
label: tr('author'), required: false)
|
||||
],
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: 'Up to Date Apps',
|
||||
label: tr('upToDateApps'),
|
||||
type: FormItemType.bool)
|
||||
],
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: 'Non-Installed Apps',
|
||||
label: tr('nonInstalledApps'),
|
||||
type: FormItemType.bool)
|
||||
]
|
||||
],
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/pages/add_app.dart';
|
||||
@@ -25,12 +26,12 @@ class _HomePageState extends State<HomePage> {
|
||||
List<int> selectedIndexHistory = [];
|
||||
|
||||
List<NavigationPageItem> pages = [
|
||||
NavigationPageItem(tr('appsString'), Icons.apps,
|
||||
AppsPage(key: GlobalKey<AppsPageState>())),
|
||||
NavigationPageItem(tr('addApp'), Icons.add, const AddAppPage()),
|
||||
NavigationPageItem(
|
||||
'Apps', Icons.apps, AppsPage(key: GlobalKey<AppsPageState>())),
|
||||
NavigationPageItem('Add App', Icons.add, const AddAppPage()),
|
||||
NavigationPageItem(
|
||||
'Import/Export', Icons.import_export, const ImportExportPage()),
|
||||
NavigationPageItem('Settings', Icons.settings, const SettingsPage())
|
||||
tr('importExport'), Icons.import_export, const ImportExportPage()),
|
||||
NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage())
|
||||
];
|
||||
|
||||
@override
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/components/custom_app_bar.dart';
|
||||
@@ -41,7 +42,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: CustomScrollView(slivers: <Widget>[
|
||||
const CustomAppBar(title: 'Import/Export'),
|
||||
CustomAppBar(title: tr('importExport')),
|
||||
SliverFillRemaining(
|
||||
child: Padding(
|
||||
padding:
|
||||
@@ -63,10 +64,11 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
.exportApps()
|
||||
.then((String path) {
|
||||
showError(
|
||||
'Exported to $path', context);
|
||||
tr('exportedTo', args: [path]),
|
||||
context);
|
||||
});
|
||||
},
|
||||
child: const Text('Obtainium Export'))),
|
||||
child: Text(tr('obtainiumExport')))),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
@@ -91,13 +93,15 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
jsonDecode(data);
|
||||
} catch (e) {
|
||||
throw ObtainiumError(
|
||||
'Invalid input');
|
||||
tr('invalidInput'));
|
||||
}
|
||||
appsProvider
|
||||
.importApps(data)
|
||||
.then((value) {
|
||||
showError(
|
||||
'$value App${value == 1 ? '' : 's'} Imported',
|
||||
tr('importedX', args: [
|
||||
plural('apps', value)
|
||||
]),
|
||||
context);
|
||||
});
|
||||
} else {
|
||||
@@ -111,7 +115,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
});
|
||||
});
|
||||
},
|
||||
child: const Text('Obtainium Import')))
|
||||
child: Text(tr('obtainiumImport'))))
|
||||
],
|
||||
),
|
||||
if (importInProgress)
|
||||
@@ -138,11 +142,11 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: 'Import from URL List',
|
||||
title: tr('importFromURLList'),
|
||||
items: [
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label: 'App URL List',
|
||||
label: tr('appURLList'),
|
||||
max: 7,
|
||||
additionalValidators: [
|
||||
(String? value) {
|
||||
@@ -159,7 +163,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
.getSource(
|
||||
lines[i]);
|
||||
} catch (e) {
|
||||
return 'Line ${i + 1}: $e';
|
||||
return '${tr('line')} ${i + 1}: $e';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,7 +186,9 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
.then((errors) {
|
||||
if (errors.isEmpty) {
|
||||
showError(
|
||||
'Imported ${urls.length} Apps',
|
||||
tr('importedX', args: [
|
||||
plural('apps', urls.length)
|
||||
]),
|
||||
context);
|
||||
} else {
|
||||
showDialog(
|
||||
@@ -203,8 +209,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
}
|
||||
});
|
||||
},
|
||||
child: const Text(
|
||||
'Import from URL List',
|
||||
child: Text(
|
||||
tr('importFromURLList'),
|
||||
)),
|
||||
...sourceProvider.sources
|
||||
.where((element) => element.canSearch)
|
||||
@@ -224,13 +230,17 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
builder:
|
||||
(BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title:
|
||||
'Search ${source.runtimeType}',
|
||||
title: tr('searchX',
|
||||
args: [
|
||||
source
|
||||
.runtimeType
|
||||
.toString()
|
||||
]),
|
||||
items: [
|
||||
[
|
||||
GeneratedFormItem(
|
||||
label:
|
||||
'${source.runtimeType} Search Query')
|
||||
label: tr(
|
||||
'searchQuery'))
|
||||
]
|
||||
],
|
||||
defaultValues: const [],
|
||||
@@ -272,7 +282,13 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
if (errors.isEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(
|
||||
'Imported ${selectedUrls.length} Apps',
|
||||
tr('importedX',
|
||||
args: [
|
||||
plural(
|
||||
'app',
|
||||
selectedUrls
|
||||
.length)
|
||||
]),
|
||||
context);
|
||||
} else {
|
||||
showDialog(
|
||||
@@ -291,7 +307,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
}
|
||||
} else {
|
||||
throw ObtainiumError(
|
||||
'No results found');
|
||||
tr('noResults'));
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -303,8 +319,9 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
});
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'Search ${source.runtimeType}'))
|
||||
child: Text(tr('searchX', args: [
|
||||
source.runtimeType.toString()
|
||||
])))
|
||||
]))
|
||||
.toList(),
|
||||
...sourceProvider.massUrlSources
|
||||
@@ -323,8 +340,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
builder:
|
||||
(BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title:
|
||||
'Import ${source.name}',
|
||||
title: tr('importX',
|
||||
args: [
|
||||
source.name
|
||||
]),
|
||||
items:
|
||||
source
|
||||
.requiredArgs
|
||||
@@ -363,7 +382,13 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
if (errors.isEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(
|
||||
'Imported ${selectedUrls.length} Apps',
|
||||
tr('importedX',
|
||||
args: [
|
||||
plural(
|
||||
'app',
|
||||
selectedUrls
|
||||
.length)
|
||||
]),
|
||||
context);
|
||||
} else {
|
||||
showDialog(
|
||||
@@ -390,17 +415,17 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
});
|
||||
});
|
||||
},
|
||||
child: Text('Import ${source.name}'))
|
||||
child: Text(
|
||||
tr('importX', args: [source.name])))
|
||||
]))
|
||||
.toList(),
|
||||
const Spacer(),
|
||||
const Divider(
|
||||
height: 32,
|
||||
),
|
||||
const Text(
|
||||
'Imported Apps may incorrectly show as "Not Installed".\nTo fix this, re-install them through Obtainium.\nThis should not affect App data.\n\nOnly affects URL and third-party import methods.',
|
||||
Text(tr('importedAppsIdDisclaimer'),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontStyle: FontStyle.italic, fontSize: 12)),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
@@ -427,16 +452,19 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: const Text('Import Errors'),
|
||||
title: Text(tr('importErrors')),
|
||||
content:
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
Text(
|
||||
'${widget.urlsLength - widget.errors.length} of ${widget.urlsLength} Apps imported.',
|
||||
tr('importedXOfYApps', args: [
|
||||
(widget.urlsLength - widget.errors.length).toString(),
|
||||
widget.urlsLength.toString()
|
||||
]),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'The following URLs had errors:',
|
||||
tr('followingURLsHadErrors'),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
...widget.errors.map((e) {
|
||||
@@ -459,7 +487,7 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(null);
|
||||
},
|
||||
child: const Text('Okay'))
|
||||
child: Text(tr('okay')))
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -505,8 +533,8 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title:
|
||||
Text(widget.onlyOneSelectionAllowed ? 'Select URL' : 'Select URLs'),
|
||||
title: Text(
|
||||
widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
|
||||
content: Column(children: [
|
||||
...urlWithDescriptionSelections.keys.map((urlWithD) {
|
||||
return Row(children: [
|
||||
@@ -564,7 +592,7 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Cancel')),
|
||||
child: Text(tr('cancel'))),
|
||||
TextButton(
|
||||
onPressed:
|
||||
urlWithDescriptionSelections.values.where((b) => b).isEmpty
|
||||
@@ -577,8 +605,14 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
||||
.toList());
|
||||
},
|
||||
child: Text(widget.onlyOneSelectionAllowed
|
||||
? 'Pick'
|
||||
: 'Import ${urlWithDescriptionSelections.values.where((b) => b).length} URLs'))
|
||||
? tr('pick')
|
||||
: tr('importX', args: [
|
||||
plural(
|
||||
'url',
|
||||
urlWithDescriptionSelections.values
|
||||
.where((b) => b)
|
||||
.length)
|
||||
])))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:obtainium/components/custom_app_bar.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
@@ -26,20 +27,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
|
||||
var themeDropdown = DropdownButtonFormField(
|
||||
decoration: const InputDecoration(labelText: 'Theme'),
|
||||
decoration: InputDecoration(labelText: tr('theme')),
|
||||
value: settingsProvider.theme,
|
||||
items: const [
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: ThemeSettings.dark,
|
||||
child: Text('Dark'),
|
||||
child: Text(tr('dark')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeSettings.light,
|
||||
child: Text('Light'),
|
||||
child: Text(tr('light')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ThemeSettings.system,
|
||||
child: Text('Follow System'),
|
||||
child: Text(tr('followSystem')),
|
||||
)
|
||||
],
|
||||
onChanged: (value) {
|
||||
@@ -49,16 +50,16 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
});
|
||||
|
||||
var colourDropdown = DropdownButtonFormField(
|
||||
decoration: const InputDecoration(labelText: 'Colour'),
|
||||
decoration: InputDecoration(labelText: tr('colour')),
|
||||
value: settingsProvider.colour,
|
||||
items: const [
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: ColourSettings.basic,
|
||||
child: Text('Obtainium'),
|
||||
child: Text(tr('obtainium')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ColourSettings.materialYou,
|
||||
child: Text('Material You'),
|
||||
child: Text(tr('materialYou')),
|
||||
)
|
||||
],
|
||||
onChanged: (value) {
|
||||
@@ -68,20 +69,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
});
|
||||
|
||||
var sortDropdown = DropdownButtonFormField(
|
||||
decoration: const InputDecoration(labelText: 'App Sort By'),
|
||||
decoration: InputDecoration(labelText: tr('appSortBy')),
|
||||
value: settingsProvider.sortColumn,
|
||||
items: const [
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: SortColumnSettings.authorName,
|
||||
child: Text('Author/Name'),
|
||||
child: Text(tr('authorName')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: SortColumnSettings.nameAuthor,
|
||||
child: Text('Name/Author'),
|
||||
child: Text(tr('nameAuthor')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: SortColumnSettings.added,
|
||||
child: Text('As Added'),
|
||||
child: Text(tr('asAdded')),
|
||||
)
|
||||
],
|
||||
onChanged: (value) {
|
||||
@@ -91,16 +92,16 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
});
|
||||
|
||||
var orderDropdown = DropdownButtonFormField(
|
||||
decoration: const InputDecoration(labelText: 'App Sort Order'),
|
||||
decoration: InputDecoration(labelText: tr('appSortOrder')),
|
||||
value: settingsProvider.sortOrder,
|
||||
items: const [
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: SortOrderSettings.ascending,
|
||||
child: Text('Ascending'),
|
||||
child: Text(tr('ascending')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: SortOrderSettings.descending,
|
||||
child: Text('Descending'),
|
||||
child: Text(tr('descending')),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
@@ -110,8 +111,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
});
|
||||
|
||||
var intervalDropdown = DropdownButtonFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Background Update Checking Interval'),
|
||||
decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')),
|
||||
value: settingsProvider.updateInterval,
|
||||
items: updateIntervals.map((e) {
|
||||
int displayNum = (e < 60
|
||||
@@ -120,15 +120,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
? e / 60
|
||||
: e / 1440)
|
||||
.round();
|
||||
var displayUnit = (e < 60
|
||||
? 'Minute'
|
||||
: e < 1440
|
||||
? 'Hour'
|
||||
: 'Day');
|
||||
|
||||
String display = e == 0
|
||||
? 'Never - Manual Only'
|
||||
: '$displayNum $displayUnit${displayNum == 1 ? '' : 's'}';
|
||||
? tr('neverManualOnly')
|
||||
: (e < 60
|
||||
? plural('minute', displayNum)
|
||||
: e < 1440
|
||||
? plural('hour', displayNum)
|
||||
: plural('day', displayNum));
|
||||
return DropdownMenuItem(value: e, child: Text(display));
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
@@ -167,7 +165,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: CustomScrollView(slivers: <Widget>[
|
||||
const CustomAppBar(title: 'Settings'),
|
||||
CustomAppBar(title: tr('settings')),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -177,7 +175,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Appearance',
|
||||
tr('appearance'),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
@@ -200,7 +198,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Show Source Webpage in App View'),
|
||||
Text(tr('showWebInAppView')),
|
||||
Switch(
|
||||
value: settingsProvider.showAppWebpage,
|
||||
onChanged: (value) {
|
||||
@@ -212,7 +210,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Pin Updates to Top of Apps View'),
|
||||
Text(tr('pinUpdates')),
|
||||
Switch(
|
||||
value: settingsProvider.pinUpdates,
|
||||
onChanged: (value) {
|
||||
@@ -225,7 +223,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
height16,
|
||||
Text(
|
||||
'Updates',
|
||||
tr('updates'),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
@@ -234,7 +232,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
height: 48,
|
||||
),
|
||||
Text(
|
||||
'Source-Specific',
|
||||
tr('sourceSpecific'),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
@@ -256,15 +254,15 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
mode: LaunchMode.externalApplication);
|
||||
},
|
||||
icon: const Icon(Icons.code),
|
||||
label: const Text(
|
||||
'App Source',
|
||||
label: Text(
|
||||
tr('appSource'),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
context.read<LogsProvider>().get().then((logs) {
|
||||
if (logs.isEmpty) {
|
||||
showError(ObtainiumError('No Logs'), context);
|
||||
showError(ObtainiumError(tr('noLogs')), context);
|
||||
} else {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -275,7 +273,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.bug_report_outlined),
|
||||
label: const Text('App Logs')),
|
||||
label: Text(tr('appLogs'))),
|
||||
],
|
||||
),
|
||||
height16,
|
||||
@@ -306,7 +304,7 @@ class _LogsDialogState extends State<LogsDialog> {
|
||||
.then((value) {
|
||||
setState(() {
|
||||
String l = value.map((e) => e.toString()).join('\n\n');
|
||||
logString = l.isNotEmpty ? l : 'No Logs';
|
||||
logString = l.isNotEmpty ? l : tr('noLogs');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -317,7 +315,7 @@ class _LogsDialogState extends State<LogsDialog> {
|
||||
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: const Text('Obtainium App Logs'),
|
||||
title: Text(tr('appLogs')),
|
||||
content: Column(
|
||||
children: [
|
||||
DropdownButtonFormField(
|
||||
@@ -325,7 +323,7 @@ class _LogsDialogState extends State<LogsDialog> {
|
||||
items: days
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text('$e Day${e == 1 ? '' : 's'}'),
|
||||
child: Text(plural('day', e)),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (d) {
|
||||
@@ -342,13 +340,13 @@ class _LogsDialogState extends State<LogsDialog> {
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Close')),
|
||||
child: Text(tr('close'))),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Share.share(logString ?? '', subject: 'Obtainium App Logs');
|
||||
Share.share(logString ?? '', subject: tr('appLogs'));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Share'))
|
||||
child: Text(tr('share')))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
@@ -105,7 +106,7 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
if (response.statusCode != 200) {
|
||||
tempDownloadedFile.deleteSync();
|
||||
throw response.reasonPhrase ?? 'Unknown Error';
|
||||
throw response.reasonPhrase ?? tr('unexpectedError');
|
||||
}
|
||||
tempDownloadedFile.renameSync(downloadedFile.path);
|
||||
}
|
||||
@@ -127,7 +128,8 @@ class AppsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
} else if ((prog == 25 || prog == 50 || prog == 75) && prevProg != prog) {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Progress: $prog%', toastLength: Toast.LENGTH_SHORT);
|
||||
msg: tr('percentProgress', args: [prog.toString()]),
|
||||
toastLength: Toast.LENGTH_SHORT);
|
||||
}
|
||||
prevProg = prog;
|
||||
});
|
||||
@@ -272,7 +274,7 @@ class AppsProvider with ChangeNotifier {
|
||||
// 2. That cannot be installed silently (IF no buildContext was given for interactive install)
|
||||
for (var id in appIds) {
|
||||
if (apps[id] == null) {
|
||||
throw ObtainiumError('App not found');
|
||||
throw ObtainiumError(tr('appNotFound'));
|
||||
}
|
||||
String? apkUrl;
|
||||
if (!apps[id]!.app.trackOnly) {
|
||||
@@ -413,6 +415,7 @@ class AppsProvider with ChangeNotifier {
|
||||
if (installedInfo != null && app.installedVersion == null) {
|
||||
if (app.latestVersion.characters
|
||||
.where((p0) => [
|
||||
// TODO: Won't work for other charsets
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
@@ -601,13 +604,13 @@ class AppsProvider with ChangeNotifier {
|
||||
|
||||
Future<String> exportApps() async {
|
||||
Directory? exportDir = Directory('/storage/emulated/0/Download');
|
||||
String path = 'Downloads';
|
||||
String path = 'Downloads'; // TODO: Is this true on non-english phones?
|
||||
if (!exportDir.existsSync()) {
|
||||
exportDir = await getExternalStorageDirectory();
|
||||
path = exportDir!.path;
|
||||
}
|
||||
File export = File(
|
||||
'${exportDir.path}/obtainium-export-${DateTime.now().millisecondsSinceEpoch}.json');
|
||||
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
|
||||
export.writeAsStringSync(
|
||||
jsonEncode(apps.values.map((e) => e.app.toJson()).toList()));
|
||||
return path;
|
||||
@@ -643,7 +646,7 @@ class AppsProvider with ChangeNotifier {
|
||||
Map<String, dynamic> errorsMap = results[1];
|
||||
for (var app in pps) {
|
||||
if (apps.containsKey(app.id)) {
|
||||
errorsMap.addAll({app.id: 'App already added'});
|
||||
errorsMap.addAll({app.id: tr('appAlreadyAdded')});
|
||||
} else {
|
||||
await saveApps([app]);
|
||||
}
|
||||
@@ -673,9 +676,9 @@ class _APKPickerState extends State<APKPicker> {
|
||||
apkUrl ??= widget.initVal;
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: const Text('Pick an APK'),
|
||||
title: Text(tr('pickAnAPK')),
|
||||
content: Column(children: [
|
||||
Text('${widget.app.name} has more than one package:'),
|
||||
Text(tr('appHasMoreThanOnePackage', args: [widget.app.name])),
|
||||
const SizedBox(height: 16),
|
||||
...widget.app.apkUrls.map(
|
||||
(u) => RadioListTile<String>(
|
||||
@@ -697,7 +700,11 @@ class _APKPickerState extends State<APKPicker> {
|
||||
),
|
||||
if (widget.archs != null)
|
||||
Text(
|
||||
'Note:\nYour device supports the ${widget.archs!.length == 1 ? '\'${widget.archs![0]}\' CPU architecture.' : 'following CPU architectures: ${list2FriendlyString(widget.archs!.map((e) => '\'$e\'').toList())}.'}',
|
||||
widget.archs!.length == 1
|
||||
? tr('deviceSupportsXArch', args: [widget.archs![0]])
|
||||
: tr('deviceSupportsFollowingArchs') +
|
||||
list2FriendlyString(
|
||||
widget.archs!.map((e) => '\'$e\'').toList()),
|
||||
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
||||
),
|
||||
]),
|
||||
@@ -706,13 +713,13 @@ class _APKPickerState extends State<APKPicker> {
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(null);
|
||||
},
|
||||
child: const Text('Cancel')),
|
||||
child: Text(tr('cancel'))),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).pop(apkUrl);
|
||||
},
|
||||
child: const Text('Continue'))
|
||||
child: Text(tr('continue')))
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -734,21 +741,23 @@ class _APKOriginWarningDialogState extends State<APKOriginWarningDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: const Text('Warning'),
|
||||
content: Text(
|
||||
'The App source is \'${Uri.parse(widget.sourceUrl).host}\' but the release package comes from \'${Uri.parse(widget.apkUrl).host}\'. Continue?'),
|
||||
title: Text(tr('warning')),
|
||||
content: Text(tr('sourceIsXButPackageFromYPrompt', args: [
|
||||
Uri.parse(widget.sourceUrl).host,
|
||||
Uri.parse(widget.apkUrl).host
|
||||
])),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(null);
|
||||
},
|
||||
child: const Text('Cancel')),
|
||||
child: Text(tr('cancel'))),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: const Text('Continue'))
|
||||
child: Text(tr('continue')))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
@@ -85,7 +86,9 @@ create table if not exists $logTable (
|
||||
var res = await (await getDB())
|
||||
.delete(logTable, where: where.key, whereArgs: where.value);
|
||||
if (res > 0) {
|
||||
add('Cleared $res logs (before = $before, after = $after)');
|
||||
add(plural('clearedNLogsBeforeXAfterY', res,
|
||||
namedArgs: {'before': before.toString(), 'after': after.toString()},
|
||||
name: 'n'));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// Exposes functions that can be used to send notifications to the user
|
||||
// Contains a set of pre-defined ObtainiumNotification objects that should be used throughout the app
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@@ -21,33 +22,30 @@ class UpdateNotification extends ObtainiumNotification {
|
||||
UpdateNotification(List<App> updates)
|
||||
: super(
|
||||
2,
|
||||
'Updates Available',
|
||||
tr('updatesAvailable'),
|
||||
'',
|
||||
'UPDATES_AVAILABLE',
|
||||
'Updates Available',
|
||||
'Notifies the user that updates are available for one or more Apps tracked by Obtainium',
|
||||
tr('updatesAvailable'),
|
||||
tr('updatesAvailableNotifDescription'),
|
||||
Importance.max) {
|
||||
message = updates.isEmpty
|
||||
? "No new updates."
|
||||
? tr('noNewUpdates')
|
||||
: updates.length == 1
|
||||
? '${updates[0].name} has an update.'
|
||||
: '${(updates.length == 2 ? '${updates[0].name} and ${updates[1].name}' : '${updates[0].name} and ${updates.length - 1} more apps')} have updates.';
|
||||
? tr('xHasAnUpdate', args: [updates[0].name])
|
||||
: plural('xAndNMoreUpdatesAvailable', updates.length - 1,
|
||||
args: [updates[0].name]);
|
||||
}
|
||||
}
|
||||
|
||||
class SilentUpdateNotification extends ObtainiumNotification {
|
||||
SilentUpdateNotification(List<App> updates)
|
||||
: super(
|
||||
3,
|
||||
'Apps Updated',
|
||||
'',
|
||||
'APPS_UPDATED',
|
||||
'Apps Updated',
|
||||
'Notifies the user that updates to one or more Apps were applied in the background',
|
||||
Importance.defaultImportance) {
|
||||
: super(3, tr('appsUpdated'), '', 'APPS_UPDATED', tr('appsUpdated'),
|
||||
tr('appsUpdatedNotifDescription'), Importance.defaultImportance) {
|
||||
message = updates.length == 1
|
||||
? '${updates[0].name} was updated to ${updates[0].latestVersion}.'
|
||||
: '${(updates.length == 2 ? '${updates[0].name} and ${updates[1].name}' : '${updates[0].name} and ${updates.length - 1} more apps')} were updated.';
|
||||
? tr('xWasUpdatedToY',
|
||||
args: [updates[0].name, updates[0].latestVersion])
|
||||
: plural('xAndNMoreUpdatesInstalled', updates.length - 1,
|
||||
args: [updates[0].name]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,27 +53,21 @@ class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
|
||||
ErrorCheckingUpdatesNotification(String error)
|
||||
: super(
|
||||
5,
|
||||
'Error Checking for Updates',
|
||||
tr('errorCheckingUpdates'),
|
||||
error,
|
||||
'BG_UPDATE_CHECK_ERROR',
|
||||
'Error Checking for Updates',
|
||||
'A notification that shows when background update checking fails',
|
||||
tr('errorCheckingUpdates'),
|
||||
tr('errorCheckingUpdatesNotifDescription'),
|
||||
Importance.high);
|
||||
}
|
||||
|
||||
class AppsRemovedNotification extends ObtainiumNotification {
|
||||
AppsRemovedNotification(List<List<String>> namedReasons)
|
||||
: super(
|
||||
6,
|
||||
'Apps Removed',
|
||||
'',
|
||||
'APPS_REMOVED',
|
||||
'Apps Removed',
|
||||
'Notifies the user that one or more Apps were removed due to errors while loading them',
|
||||
Importance.max) {
|
||||
: super(6, tr('appsRemoved'), '', 'APPS_REMOVED', tr('appsRemoved'),
|
||||
tr('appsRemovedNotifDescription'), Importance.max) {
|
||||
message = '';
|
||||
for (var r in namedReasons) {
|
||||
message += '${r[0]} was removed due to this error: ${r[1]}. \n';
|
||||
message += '${tr('xWasRemovedDueToErrorY', args: [r[0], r[1]])} \n';
|
||||
}
|
||||
message = message.trim();
|
||||
}
|
||||
@@ -83,20 +75,20 @@ class AppsRemovedNotification extends ObtainiumNotification {
|
||||
|
||||
final completeInstallationNotification = ObtainiumNotification(
|
||||
1,
|
||||
'Complete App Installation',
|
||||
'Obtainium must be open to install Apps',
|
||||
tr('completeAppInstallation'),
|
||||
tr('obtainiumMustBeOpenToInstallApps'),
|
||||
'COMPLETE_INSTALL',
|
||||
'Complete App Installation',
|
||||
'Asks the user to return to Obtanium to finish installing an App',
|
||||
tr('completeAppInstallation'),
|
||||
tr('completeAppInstallationNotifDescription'),
|
||||
Importance.max);
|
||||
|
||||
final checkingUpdatesNotification = ObtainiumNotification(
|
||||
4,
|
||||
'Checking for Updates',
|
||||
tr('checkingForUpdates'),
|
||||
'',
|
||||
'BG_UPDATE_CHECK',
|
||||
'Checking for Updates',
|
||||
'Transient notification that appears when checking for updates',
|
||||
tr('checkingForUpdates'),
|
||||
tr('checkingForUpdatesNotifDescription'),
|
||||
Importance.min);
|
||||
|
||||
class NotificationsProvider {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
// Exposes functions used to save/load app settings
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
@@ -109,8 +110,7 @@ class SettingsProvider with ChangeNotifier {
|
||||
while (!(await Permission.requestInstallPackages.isGranted)) {
|
||||
// Explicit request as InstallPlugin request sometimes bugged
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Please allow Obtainium to install Apps',
|
||||
toastLength: Toast.LENGTH_LONG);
|
||||
msg: tr('pleaseAllowInstallPerm'), toastLength: Toast.LENGTH_LONG);
|
||||
if ((await Permission.requestInstallPackages.request()) ==
|
||||
PermissionStatus.granted) {
|
||||
break;
|
||||
|
@@ -3,6 +3,7 @@
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
||||
@@ -120,13 +121,6 @@ preStandardizeUrl(String url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const String couldNotFindReleases = 'Could not find a suitable release';
|
||||
const String couldNotFindLatestVersion =
|
||||
'Could not determine latest release version';
|
||||
String notValidURL(String sourceName) {
|
||||
return 'Not a valid $sourceName App URL';
|
||||
}
|
||||
|
||||
const String noAPKFound = 'No APK found';
|
||||
|
||||
List<String> getLinksFromParsedHTML(
|
||||
@@ -164,7 +158,7 @@ class AppSource {
|
||||
// Some additional data may be needed for Apps regardless of Source
|
||||
final List<GeneratedFormItem> additionalAppSpecificSourceAgnosticFormItems = [
|
||||
GeneratedFormItem(
|
||||
label: 'Track-Only',
|
||||
label: tr('trackOnly'),
|
||||
type: FormItemType.bool,
|
||||
key: 'trackOnlyFormItemKey')
|
||||
];
|
||||
@@ -192,8 +186,8 @@ class AppSource {
|
||||
}
|
||||
|
||||
ObtainiumError getObtainiumHttpError(Response res) {
|
||||
return ObtainiumError(
|
||||
res.reasonPhrase ?? 'Error ${res.statusCode.toString()}');
|
||||
return ObtainiumError(res.reasonPhrase ??
|
||||
tr('errorWithHttpStatusCode', args: [res.statusCode.toString()]));
|
||||
}
|
||||
|
||||
abstract class MassAppUrlSource {
|
||||
@@ -254,6 +248,7 @@ class SourceProvider {
|
||||
}
|
||||
for (int i = 0; i < parts.length - 1; i++) {
|
||||
if (RegExp('.*[A-Z].*').hasMatch(parts[i])) {
|
||||
// TODO: RegEx won't work for non-eng chars
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user