Compare commits

...

22 Commits

Author SHA1 Message Date
Imran Remtulla
eef4d33431 Merge pull request #246 from ImranR98/dev
Bugfixes for #242 and #245 + Various UI Improvements
2023-01-29 17:35:18 -05:00
Imran Remtulla
d56342e907 Merge pull request #243 from bluefly000/japanese-translation
Update Japanese translation
2023-01-29 17:32:09 -05:00
Imran Remtulla
c72c0fdb57 Increment version 2023-01-29 17:31:19 -05:00
Imran Remtulla
ffe29009ed URL select modal now works when tapping text 2023-01-29 17:29:41 -05:00
Imran Remtulla
60e3b68ebd Search allows option changes (no direct add) 2023-01-29 17:23:35 -05:00
Imran Remtulla
ee4d0f259f Generated form bugfix (initState not running) - #245 2023-01-29 17:07:11 -05:00
bluefly000
0ecfbef0a0 Update Japanese translation 2023-01-29 17:28:54 +09:00
Imran Remtulla
1b60e75ca7 Added delay after Obtainium install prompt 2023-01-28 20:59:17 -05:00
Imran Remtulla
abcfa389e8 Merge pull request #241 from ImranR98/dev
Updated screenshots
2023-01-28 00:47:26 -05:00
Imran Remtulla
a64bd67ef1 Updated screenshots 2023-01-28 00:46:54 -05:00
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
30 changed files with 935 additions and 755 deletions

View File

@@ -31,4 +31,4 @@ Currently supported App sources:
| <img src="./assets/screenshots/1.apps.png" alt="Apps Page" /> | <img src="./assets/screenshots/2.dark_theme.png" alt="Dark Theme" /> | <img src="./assets/screenshots/3.material_you.png" alt="Material You" /> |
| ------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------- |
| <img src="./assets/screenshots/4.app.png" alt="App Page" /> | <img src="./assets/screenshots/5.apk_picker.png" alt="Multiple APK Support" /> | <img src="./assets/screenshots/6.apk_install.png" alt="App Installation" /> |
| <img src="./assets/screenshots/4.app.png" alt="App Page" /> | <img src="./assets/screenshots/5.app_opts.png" alt="App Options" /> | <img src="./assets/screenshots/6.app_webview.png" alt="App Web View" /> |

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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

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

@@ -7,7 +7,7 @@
"appIdMismatch": "ダウンロードしたパッケージのIDが既存のApp IDと一致しません",
"functionNotImplemented": "このクラスはこの機能を実装していません",
"placeholder": "プレースホルダー",
"someErrors": "いくつかのエラーが発生しました",
"someErrors": "何らかのエラーが発生しました",
"unexpectedError": "予期せぬエラーが発生しました",
"ok": "OK",
"and": "と",
@@ -82,7 +82,7 @@
"pinToTop": "トップに固定",
"unpinFromTop": "トップから固定解除",
"resetInstallStatusForSelectedAppsQuestion": "選択したアプリのインストール状態をリセットしますか?",
"installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗するなどして、Obtainiumに表示されるアプリのバージョンが正しくない場合に役立ちます。",
"installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗した場合など、Obtainiumに表示されるアプリのバージョンが正しくない場合に有効です。",
"shareSelectedAppURLs": "選択したアプリのURLを共有する",
"resetInstallStatus": "インストール状態をリセットする",
"more": "もっと見る",
@@ -109,7 +109,7 @@
"searchX": "{}で検索",
"noResults": "結果は見つかりませんでした",
"importX": "{}をインポートする",
"importedAppsIdDisclaimer": "インポートしたアプリが「未インストール」と表示されることがあります。\nこれを解決するには、Obtainiumから再インストールしてください。\nアプリのデータには影響しません。\n\nURLとサードパーティのインポートメソッドにのみ影響します。",
"importedAppsIdDisclaimer": "インポートしたアプリが「未インストール」と表示されることがあります。\nこれを解決するには、Obtainiumから再インストールしてください。\nアプリのデータには影響しません。\n\nURLとサードパーティのインポートメソッドにのみ影響します。",
"importErrors": "インポートエラー",
"importedXOfYApps": "{} / {} アプリをインポートしました",
"followingURLsHadErrors": "以下のURLでエラーが発生しました:",
@@ -133,7 +133,7 @@
"bgUpdateCheckInterval": "バックグラウンドでのアップデート確認の間隔",
"neverManualOnly": "手動",
"appearance": "外観",
"showWebInAppView": "アプリビューにソースウェブページを表示する",
"showWebInAppView": "アプリページにソースのWebページを表示する",
"pinUpdates": "アップデートがあるアプリをトップに固定する",
"updates": "アップデート",
"sourceSpecific": "Github アクセストークン",
@@ -184,7 +184,7 @@
"appIdOrName": "アプリのIDまたは名前",
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
"fdroidThirdPartyRepo": "F-Droid サードパーティリポジトリ",
"steam": "Steam",
"steamMobile": "Steam Mobile",
"steamChat": "Steam Chat",
@@ -211,6 +211,7 @@
"language": "言語",
"storagePermissionDenied": "ストレージ権限が拒否されました",
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
"filterAPKsByRegEx": "正規表現でAPKを絞り込む",
"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

@@ -150,6 +150,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
Map<String, dynamic> values = {};
late List<List<Widget>> formInputs;
List<List<Widget>> rows = [];
String? initKey;
// If any value changes, call this to update the parent with value and validity
void someValueChanged({bool isBuilding = false}) {
@@ -169,13 +170,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
widget.onValueChanges(returnValues, valid, isBuilding);
}
@override
void initState() {
super.initState();
initForm() {
initKey = widget.key.toString();
// Initialize form values as all empty
values.clear();
int j = 0;
for (var row in widget.items) {
for (var e in row) {
values[e.key] = e.defaultValue;
@@ -245,8 +243,17 @@ class _GeneratedFormState extends State<GeneratedForm> {
someValueChanged(isBuilding: true);
}
@override
void initState() {
super.initState();
initForm();
}
@override
Widget build(BuildContext context) {
if (widget.key.toString() != initKey) {
initForm();
}
for (var r = 0; r < formInputs.length; r++) {
for (var e = 0; e < formInputs[r].length; e++) {
if (widget.items[r][e] is GeneratedFormSwitch) {

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.6';
const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES

View File

@@ -32,6 +32,7 @@ class _AddAppPageState extends State<AddAppPage> {
Map<String, dynamic> additionalSettings = {};
bool additionalSettingsValid = true;
List<String> pickedCategories = [];
int searchnum = 0;
@override
Widget build(BuildContext context) {
@@ -40,10 +41,14 @@ class _AddAppPageState extends State<AddAppPage> {
bool doingSomething = gettingAppInfo || searching;
changeUserInput(String input, bool valid, bool isBuilding) {
changeUserInput(String input, bool valid, bool isBuilding,
{bool isSearch = false}) {
userInput = input;
if (!isBuilding) {
setState(() {
if (isSearch) {
searchnum++;
}
var source = valid ? sourceProvider.getSource(userInput) : null;
if (pickedSource.runtimeType != source.runtimeType) {
pickedSource = source;
@@ -70,6 +75,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 +94,7 @@ class _AddAppPageState extends State<AddAppPage> {
cont = false;
}
if (userPickedNoVersionDetection &&
// ignore: use_build_context_synchronously
await showDialog(
context: context,
builder: (BuildContext ctx) {
@@ -167,10 +174,12 @@ class _AddAppPageState extends State<AddAppPage> {
children: [
Expanded(
child: GeneratedForm(
key: Key(searchnum.toString()),
items: [
[
GeneratedFormTextField('appSourceURL',
label: tr('appSourceURL'),
defaultValue: userInput,
additionalValidators: [
(value) {
try {
@@ -294,8 +303,8 @@ class _AddAppPageState extends State<AddAppPage> {
if (selectedUrls != null &&
selectedUrls.isNotEmpty) {
changeUserInput(
selectedUrls[0], true, false);
addApp(resetUserInputAfter: true);
selectedUrls[0], true, false,
isSearch: true);
}
}).catchError((e) {
showError(e, context);
@@ -325,6 +334,7 @@ class _AddAppPageState extends State<AddAppPage> {
height: 16,
),
GeneratedForm(
key: Key(pickedSource.runtimeType.toString()),
items: pickedSource!
.combinedAppSpecificSettingFormItems,
onValueChanges: (values, valid, isBuilding) {

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

@@ -564,10 +564,7 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
content: Column(children: [
...urlWithDescriptionSelections.keys.map((urlWithD) {
return Row(children: [
Checkbox(
value: urlWithDescriptionSelections[urlWithD],
onChanged: (value) {
select(bool? value) {
setState(() {
value ??= false;
if (value! && widget.onlyOneSelectionAllowed) {
@@ -576,6 +573,13 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
urlWithDescriptionSelections[urlWithD] = value!;
}
});
}
return Row(children: [
Checkbox(
value: urlWithDescriptionSelections[urlWithD],
onChanged: (value) {
select(value);
}),
const SizedBox(
width: 8,
@@ -599,13 +603,18 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
const TextStyle(decoration: TextDecoration.underline),
textAlign: TextAlign.start,
)),
Text(
GestureDetector(
onTap: () {
select(!(urlWithDescriptionSelections[urlWithD] ?? false));
},
child: Text(
urlWithD.value.length > 128
? '${urlWithD.value.substring(0, 128)}...'
: urlWithD.value,
style: const TextStyle(
fontStyle: FontStyle.italic, fontSize: 12),
),
),
const SizedBox(
height: 8,
)

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

@@ -247,7 +247,11 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) {
throw DowngradeError();
}
await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium');
await InstallPlugin.installApk(file.file.path, obtainiumId);
if (file.appId == obtainiumId) {
// Obtainium prompt should be lowest
await Future.delayed(const Duration(milliseconds: 500));
}
apps[file.appId]!.app.installedVersion =
apps[file.appId]!.app.latestVersion;
// Don't correct install status as installation may not be done yet
@@ -262,6 +266,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 +286,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.6+112 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=2.18.2 <3.0.0'