Compare commits

...

12 Commits

Author SHA1 Message Date
Imran Remtulla
4252c2711b Merge pull request #240 from ImranR98/dev
APK RegEx Filter, Increased GitHub/Codeberg Release Range, UI Tweaks
Addresses #237, #238.
2023-01-28 00:17:10 -05:00
Imran Remtulla
52913b0450 Slight UI tweak 2023-01-28 00:15:52 -05:00
Imran Remtulla
427b0ed8d2 Changed a string 2023-01-28 00:13:03 -05:00
Imran Remtulla
a85d6d4f08 Increment version, remove comment 2023-01-28 00:11:40 -05:00
Imran Remtulla
05f712603c GitHub & Codeberg - get first 100 releases (not 30) 2023-01-28 00:08:17 -05:00
Imran Remtulla
fa2a80e34c APK RegEx Filter + Updated Packages 2023-01-28 00:04:57 -05:00
Imran Remtulla
f43e5a2ff1 Merge pull request #235 from ImranR98/dev
Increment version, update packages
2023-01-22 19:55:35 -05:00
Imran Remtulla
b72aa8273e Increment version, update packages 2023-01-22 19:55:14 -05:00
Imran Remtulla
520f186e4a Merge pull request #234 from p1gp1g/themed-icon
Add themed icon for Android 13
2023-01-22 19:53:43 -05:00
sim
e1e97672cf Add themed icon for Android 13 2023-01-23 01:09:14 +01:00
Imran Remtulla
1494bcd013 Merge pull request #232 from ImranR98/dev
GitHub (and Codeberg) bugfix (#231)
2023-01-20 12:49:35 -05:00
Imran Remtulla
3457a0a12f GitHub (and Codeberg) bugfix (#231) 2023-01-20 12:48:55 -05:00
19 changed files with 855 additions and 703 deletions

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -211,6 +211,7 @@
"language": "Sprache",
"storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
"tooManyRequestsTryAgainInMinutes": {
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
"other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"

View File

@@ -211,6 +211,7 @@
"language": "Language",
"storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
"tooManyRequestsTryAgainInMinutes": {
"one": "Too many requests (rate limited) - try again in {} minute",
"other": "Too many requests (rate limited) - try again in {} minutes"

View File

@@ -210,6 +210,7 @@
"language": "Nyelv",
"storagePermissionDenied": "Tárhely engedély megtagadva",
"selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.",
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
"tooManyRequestsTryAgainInMinutes": {
"one": "Túl sok kérés (korlátozott arány) próbálja újra {} perc múlva",
"other": "Túl sok kérés (korlátozott arány) próbálja újra {} perc múlva"

View File

@@ -211,6 +211,7 @@
"language": "Lingua",
"storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
"tooManyRequestsTryAgainInMinutes": {
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"

View File

@@ -211,6 +211,7 @@
"language": "言語",
"storagePermissionDenied": "ストレージ権限が拒否されました",
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
"tooManyRequestsTryAgainInMinutes": {
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"

View File

@@ -211,6 +211,7 @@
"language": "语言",
"storagePermissionDenied": "存储权限已被拒绝",
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
"tooManyRequestsTryAgainInMinutes": {
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"

View File

@@ -26,15 +26,7 @@ class Codeberg extends AppSource {
required: false,
additionalValidators: [
(value) {
if (value == null || value.isEmpty) {
return null;
}
try {
RegExp(value);
} catch (e) {
return tr('invalidRegEx');
}
return null;
return regExValidator(value);
}
])
]
@@ -72,7 +64,7 @@ class Codeberg extends AppSource {
? additionalSettings['filterReleaseTitlesByRegEx']
: null;
Response res = await get(Uri.parse(
'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases'));
'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100'));
if (res.statusCode == 200) {
var releases = jsonDecode(res.body) as List<dynamic>;
@@ -99,8 +91,8 @@ class Codeberg extends AppSource {
if (releases[i]['draft'] == true) {
// Draft releases not supported
}
var nameToFilter = releases[i]['name'] as String;
if (nameToFilter.trim().isEmpty) {
var nameToFilter = releases[i]['name'] as String?;
if (nameToFilter == null || nameToFilter.trim().isEmpty) {
// Some leave titles empty so tag is used
nameToFilter = releases[i]['tag_name'] as String;
}

View File

@@ -65,15 +65,7 @@ class GitHub extends AppSource {
required: false,
additionalValidators: [
(value) {
if (value == null || value.isEmpty) {
return null;
}
try {
RegExp(value);
} catch (e) {
return tr('invalidRegEx');
}
return null;
return regExValidator(value);
}
])
]
@@ -119,7 +111,7 @@ class GitHub extends AppSource {
? additionalSettings['filterReleaseTitlesByRegEx']
: null;
Response res = await get(Uri.parse(
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases'));
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100'));
if (res.statusCode == 200) {
var releases = jsonDecode(res.body) as List<dynamic>;
@@ -141,8 +133,8 @@ class GitHub extends AppSource {
if (!includePrereleases && releases[i]['prerelease'] == true) {
continue;
}
var nameToFilter = releases[i]['name'] as String;
if (nameToFilter.trim().isEmpty) {
var nameToFilter = releases[i]['name'] as String?;
if (nameToFilter == null || nameToFilter.trim().isEmpty) {
// Some leave titles empty so tag is used
nameToFilter = releases[i]['tag_name'] as String;
}

View File

@@ -29,7 +29,7 @@ class NoReleasesError extends ObtainiumError {
}
class NoAPKError extends ObtainiumError {
NoAPKError() : super(tr('noReleaseFound'));
NoAPKError() : super(tr('noAPKFound'));
}
class NoVersionError extends ObtainiumError {

View File

@@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.10.2';
const String currentVersion = '0.10.5';
const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES

View File

@@ -70,6 +70,7 @@ class _AddAppPageState extends State<AddAppPage> {
additionalSettings['noVersionDetection'] == true;
var cont = true;
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
// ignore: use_build_context_synchronously
await showDialog(
context: context,
builder: (BuildContext ctx) {
@@ -88,6 +89,7 @@ class _AddAppPageState extends State<AddAppPage> {
cont = false;
}
if (userPickedNoVersionDetection &&
// ignore: use_build_context_synchronously
await showDialog(
context: context,
builder: (BuildContext ctx) {

View File

@@ -317,7 +317,7 @@ class _AppPageState extends State<AppPage> {
tooltip: tr('more')),
const SizedBox(width: 16.0),
Expanded(
child: ElevatedButton(
child: TextButton(
onPressed: (app?.app.installedVersion == null ||
app?.app.installedVersion !=
app?.app.latestVersion) &&
@@ -356,7 +356,8 @@ class _AppPageState extends State<AppPage> {
? tr('update')
: tr('markUpdated')))),
const SizedBox(width: 16.0),
ElevatedButton(
Expanded(
child: TextButton(
onPressed: app?.downloadProgress != null
? null
: () {
@@ -401,7 +402,7 @@ class _AppPageState extends State<AppPage> {
surfaceTintColor:
Theme.of(context).colorScheme.error),
child: Text(tr('remove')),
),
)),
])),
if (app?.downloadProgress != null)
Padding(

View File

@@ -344,13 +344,15 @@ class AppsPageState extends State<AppsPage> {
));
}, childCount: sortedApps.length))
])),
persistentFooterButtons: [
persistentFooterButtons: appsProvider.apps.isEmpty
? null
: [
Row(
children: [
selectedApps.isEmpty
? TextButton.icon(
style:
const ButtonStyle(visualDensity: VisualDensity.compact),
style: const ButtonStyle(
visualDensity: VisualDensity.compact),
onPressed: () {
selectThese(sortedApps.map((e) => e.app).toList());
},
@@ -360,11 +362,12 @@ class AppsPageState extends State<AppsPage> {
),
label: Text(sortedApps.length.toString()))
: TextButton.icon(
style:
const ButtonStyle(visualDensity: VisualDensity.compact),
style: const ButtonStyle(
visualDensity: VisualDensity.compact),
onPressed: () {
selectedApps.isEmpty
? selectThese(sortedApps.map((e) => e.app).toList())
? selectThese(
sortedApps.map((e) => e.app).toList())
: clearSelected();
},
icon: Icon(
@@ -390,15 +393,15 @@ class AppsPageState extends State<AppsPage> {
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title:
tr('removeSelectedAppsQuestion'),
title: tr(
'removeSelectedAppsQuestion'),
items: const [],
initValid: true,
message: tr(
'xWillBeRemovedButRemainInstalled',
args: [
plural(
'apps', selectedApps.length)
plural('apps',
selectedApps.length)
]),
);
}).then((values) {
@@ -414,14 +417,19 @@ class AppsPageState extends State<AppsPage> {
),
IconButton(
visualDensity: VisualDensity.compact,
onPressed: appsProvider.areDownloadsRunning() ||
(existingUpdateIdsAllOrSelected.isEmpty &&
newInstallIdsAllOrSelected.isEmpty &&
trackOnlyUpdateIdsAllOrSelected.isEmpty)
onPressed: appsProvider
.areDownloadsRunning() ||
(existingUpdateIdsAllOrSelected
.isEmpty &&
newInstallIdsAllOrSelected
.isEmpty &&
trackOnlyUpdateIdsAllOrSelected
.isEmpty)
? null
: () {
HapticFeedback.heavyImpact();
List<GeneratedFormItem> formItems = [];
List<GeneratedFormItem> formItems =
[];
if (existingUpdateIdsAllOrSelected
.isNotEmpty) {
formItems.add(GeneratedFormSwitch(
@@ -434,7 +442,8 @@ class AppsPageState extends State<AppsPage> {
]),
defaultValue: true));
}
if (newInstallIdsAllOrSelected.isNotEmpty) {
if (newInstallIdsAllOrSelected
.isNotEmpty) {
formItems.add(GeneratedFormSwitch(
'installs',
label: tr('installX', args: [
@@ -451,7 +460,8 @@ class AppsPageState extends State<AppsPage> {
.isNotEmpty) {
formItems.add(GeneratedFormSwitch(
'trackonlies',
label: tr('markXTrackOnlyAsUpdated',
label: tr(
'markXTrackOnlyAsUpdated',
args: [
plural(
'apps',
@@ -467,8 +477,8 @@ class AppsPageState extends State<AppsPage> {
showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
var totalApps =
existingUpdateIdsAllOrSelected.length +
var totalApps = existingUpdateIdsAllOrSelected
.length +
newInstallIdsAllOrSelected
.length +
trackOnlyUpdateIdsAllOrSelected
@@ -560,7 +570,8 @@ class AppsPageState extends State<AppsPage> {
cont = await showDialog<
Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
builder:
(BuildContext ctx) {
return GeneratedFormModal(
title: tr('categorize'),
items: const [],
@@ -572,7 +583,9 @@ class AppsPageState extends State<AppsPage> {
null;
}
if (cont) {
await showDialog<Map<String, dynamic>?>(
// ignore: use_build_context_synchronously
await showDialog<
Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
@@ -586,11 +599,15 @@ class AppsPageState extends State<AppsPage> {
preselected: !showPrompt
? preselected ?? {}
: {},
showLabelWhenNotEmpty: false,
onSelected: (categories) {
showLabelWhenNotEmpty:
false,
onSelected:
(categories) {
appsProvider.saveApps(
selectedApps.map((e) {
e.categories = categories;
selectedApps
.map((e) {
e.categories =
categories;
return e;
}).toList());
},
@@ -618,7 +635,8 @@ class AppsPageState extends State<AppsPage> {
scrollable: true,
content: Padding(
padding:
const EdgeInsets.only(top: 6),
const EdgeInsets.only(
top: 6),
child: Row(
mainAxisAlignment:
MainAxisAlignment
@@ -636,30 +654,23 @@ class AppsPageState extends State<AppsPage> {
(BuildContext
ctx) {
return AlertDialog(
title: Text(tr(
'markXSelectedAppsAsUpdated',
args: [
title:
Text(tr('markXSelectedAppsAsUpdated', args: [
selectedApps.length.toString()
])),
content:
Text(
tr('onlyWorksWithNonEVDApps'),
style: const TextStyle(
fontWeight:
FontWeight.bold,
fontStyle: FontStyle.italic),
style: const TextStyle(fontWeight: FontWeight.bold, fontStyle: FontStyle.italic),
),
actions: [
TextButton(
onPressed:
() {
onPressed: () {
Navigator.of(context).pop();
},
child:
Text(tr('no'))),
child: Text(tr('no'))),
TextButton(
onPressed:
() {
onPressed: () {
HapticFeedback.selectionClick();
appsProvider.saveApps(selectedApps.map((a) {
if (a.installedVersion != null) {
@@ -670,8 +681,7 @@ class AppsPageState extends State<AppsPage> {
Navigator.of(context).pop();
},
child:
Text(tr('yes')))
child: Text(tr('yes')))
],
);
}).whenComplete(() {
@@ -686,29 +696,36 @@ class AppsPageState extends State<AppsPage> {
Icons.done)),
IconButton(
onPressed: () {
var pinStatus =
selectedApps
var pinStatus = selectedApps
.where((element) =>
element
.pinned)
.isEmpty;
appsProvider.saveApps(
selectedApps.map((e) {
e.pinned = pinStatus;
appsProvider
.saveApps(
selectedApps
.map(
(e) {
e.pinned =
pinStatus;
return e;
}).toList());
Navigator.of(context)
Navigator.of(
context)
.pop();
},
tooltip: selectedApps
.where((element) =>
element.pinned)
element
.pinned)
.isEmpty
? tr('pinToTop')
: tr('unpinFromTop'),
: tr(
'unpinFromTop'),
icon: Icon(selectedApps
.where((element) =>
element.pinned)
element
.pinned)
.isEmpty
? Icons
.bookmark_outline_rounded
@@ -720,53 +737,63 @@ class AppsPageState extends State<AppsPage> {
String urls = '';
for (var a
in selectedApps) {
urls += '${a.url}\n';
urls +=
'${a.url}\n';
}
urls = urls.substring(
0, urls.length - 1);
urls =
urls.substring(
0,
urls.length -
1);
Share.share(urls,
subject: tr(
'selectedAppURLsFromObtainium'));
Navigator.of(context)
Navigator.of(
context)
.pop();
},
tooltip: tr(
'shareSelectedAppURLs'),
icon:
const Icon(Icons.share),
icon: const Icon(
Icons.share),
),
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext
context:
context,
builder:
(BuildContext
ctx) {
return GeneratedFormModal(
title: tr(
'resetInstallStatusForSelectedAppsQuestion'),
items: const [],
initValid: true,
initValid:
true,
message: tr(
'installStatusOfXWillBeResetExplanation',
args: [
plural(
'app',
selectedApps
.length)
selectedApps.length)
]),
);
}).then((values) {
if (values != null) {
if (values !=
null) {
appsProvider.saveApps(
selectedApps
.map((e) {
.map(
(e) {
e.installedVersion =
null;
return e;
}).toList());
}
}).whenComplete(() {
Navigator.of(context)
Navigator.of(
context)
.pop();
});
},
@@ -807,11 +834,9 @@ class AppsPageState extends State<AppsPage> {
color: Theme.of(context).colorScheme.primary,
),
),
appsProvider.apps.isEmpty
? const SizedBox()
: TextButton.icon(
style:
const ButtonStyle(visualDensity: VisualDensity.compact),
TextButton.icon(
style: const ButtonStyle(
visualDensity: VisualDensity.compact),
label: Text(
filter.isIdenticalTo(neutralFilter, settingsProvider)
? tr('filter')
@@ -859,7 +884,8 @@ class AppsPageState extends State<AppsPage> {
CategoryEditorSelector(
preselected: filter.categoryFilter,
onSelected: (categories) {
filter.categoryFilter = categories.toSet();
filter.categoryFilter =
categories.toSet();
},
)
],

View File

@@ -4,10 +4,8 @@ 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';
import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/main.dart';
import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/logs_provider.dart';
import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart';

View File

@@ -262,6 +262,7 @@ class AppsProvider with ChangeNotifier {
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
if (app.apkUrls.length > 1 && context != null) {
// ignore: use_build_context_synchronously
apkUrl = await showDialog(
context: context,
builder: (BuildContext ctx) {
@@ -281,6 +282,7 @@ class AppsProvider with ChangeNotifier {
if (apkUrl != null &&
getHost(apkUrl) != getHost(app.url) &&
context != null) {
// ignore: use_build_context_synchronously
if (await showDialog(
context: context,
builder: (BuildContext ctx) {

View File

@@ -225,7 +225,19 @@ class AppSource {
label: tr('trackOnly'),
)
],
[GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))]
[
GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))
],
[
GeneratedFormTextField('apkFilterRegEx',
label: tr('filterAPKsByRegEx'),
required: false,
additionalValidators: [
(value) {
return regExValidator(value);
}
])
]
];
// Previous 2 variables combined into one at runtime for convenient usage
@@ -269,6 +281,18 @@ abstract class MassAppUrlSource {
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
}
regExValidator(String? value) {
if (value == null || value.isEmpty) {
return null;
}
try {
RegExp(value);
} catch (e) {
return tr('invalidRegEx');
}
return null;
}
class SourceProvider {
// Add more source classes here so they are available via the service
List<AppSource> sources = [
@@ -344,10 +368,13 @@ class SourceProvider {
}
Future<App> getApp(
AppSource source, String url, Map<String, dynamic> additionalSettings,
{App? currentApp,
AppSource source,
String url,
Map<String, dynamic> additionalSettings, {
App? currentApp,
bool trackOnlyOverride = false,
noVersionDetectionOverride = false}) async {
noVersionDetectionOverride = false,
}) async {
if (trackOnlyOverride || source.enforceTrackOnly) {
additionalSettings['trackOnly'] = true;
}
@@ -358,6 +385,11 @@ class SourceProvider {
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
APKDetails apk =
await source.getLatestAPKDetails(standardUrl, additionalSettings);
if (additionalSettings['apkFilterRegEx'] != null) {
var reg = RegExp(additionalSettings['apkFilterRegEx']);
apk.apkUrls =
apk.apkUrls.where((element) => reg.hasMatch(element)).toList();
}
if (apk.apkUrls.isEmpty && !trackOnly) {
throw NoAPKError();
}

File diff suppressed because it is too large Load Diff

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.10.2+108 # When changing this, update the tag in main() accordingly
version: 0.10.5+111 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=2.18.2 <3.0.0'