Merge pull request #1161 from ImranR98/dev

- Custom link support (#368, #918)
- Export settings (#1157)
- Use public GitLab search API (#1147)
- Fix unauthorized error (#1153)
This commit is contained in:
Imran
2023-12-16 03:12:10 -06:00
committed by GitHub
28 changed files with 308 additions and 141 deletions

View File

@@ -28,8 +28,15 @@
<intent-filter>
<action
android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
android:exported="false"/>
android:exported="false" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="obtainium" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@@ -39,10 +46,10 @@
<service
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
android:exported="false" />
<receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver"
android:exported="false"/>
android:exported="false" />
<receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver"
android:enabled="false"
@@ -52,24 +59,24 @@
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="dev.imranr.obtainium"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
android:name="androidx.core.content.FileProvider"
android:authorities="dev.imranr.obtainium"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"/>
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
</manifest>

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Istaknite manje vidljive touch mete",
"pickExportDir": "Izaberite datoteku za izvoz",
"autoExportOnChanges": "Automatski izvezite pri promjenama",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filtrirajte verzije po regulatnom izrazu",
"trySelectingSuggestedVersionCode": "Probajte izabrati preloženu (verziju) versionCode APK-a",
"dontSortReleasesList": "Zadrži redosled izdanja iz API-a",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Zvýraznit méně zjevné cíle dotyku",
"pickExportDir": "Vybrat adresář pro export",
"autoExportOnChanges": "Automatický export při změnách",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filtrovat verze podle regulárního výrazu",
"trySelectingSuggestedVersionCode": "Zkusit vybrat navrhovaný kód verze APK",
"dontSortReleasesList": "Retain release order from API",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Weniger offensichtliche Touch-Ziele hervorheben",
"pickExportDir": "Export-Verzeichnis wählen",
"autoExportOnChanges": "Automatischer Export bei Änderung(en)",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern",
"trySelectingSuggestedVersionCode": "Versuchen, den vorgeschlagenen APK-Versionscode auszuwählen",
"dontSortReleasesList": "Freigaberelease von der API ordern",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Resaltar objetivos menos obvios",
"pickExportDir": "Selecciona el Directorio para Exportar",
"autoExportOnChanges": "Auto Exportar cuando haya cambios",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filtrar por Versiones",
"trySelectingSuggestedVersionCode": "Prueba seleccionando la versionCode APK sugerida",
"dontSortReleasesList": "Mantener el order de publicación desde API",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "اهداف لمسی کمتر واضح را برجسته کنید",
"pickExportDir": "فهرست صادرات را انتخاب کنید",
"autoExportOnChanges": "صادرات خودکار تغییرات",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "فیلتر کردن نسخه ها با RegEx",
"trySelectingSuggestedVersionCode": "نسخه پیشنهادی APK نسخه کد را انتخاب کنید",
"dontSortReleasesList": "حفظ سفارش انتشار از API",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API",

View File

@@ -255,6 +255,7 @@
"highlightTouchTargets": "Emelje ki a kevésbé nyilvánvaló érintési célokat",
"pickExportDir": "Válassza az Exportálási könyvtárat",
"autoExportOnChanges": "Auto-exportálás a változások után",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Verziók szűrése reguláris kifejezéssel",
"trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t",
"dontSortReleasesList": "Az API-ból származó kiadási sorrend megőrzése",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Evidenzia elementi toccabili meno ovvi",
"pickExportDir": "Scegli cartella esp.",
"autoExportOnChanges": "Auto-esporta dopo modifiche",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filtra versioni con espressione regolare",
"trySelectingSuggestedVersionCode": "Prova a selezionare APK con versionCode suggerito",
"dontSortReleasesList": "Conserva l'ordine di release da API",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "目立たないタップ可能な対象をハイライトする",
"pickExportDir": "エクスポートディレクトリを選択",
"autoExportOnChanges": "変更があった際に自動でエクスポートする",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする",
"trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
"dontSortReleasesList": "APIからのリリース順を保持する",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Markeer minder voor de hand liggende aanraakdoelen.",
"pickExportDir": "Kies de exportmap",
"autoExportOnChanges": "Automatisch exporteren bij wijzigingen",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filter versies met een reguliere expressie",
"trySelectingSuggestedVersionCode": "Probeer de voorgestelde versiecode APK te selecteren",
"dontSortReleasesList": "Volgorde van releases behouden vanuit de API",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe",
"pickExportDir": "Wybierz katalog eksportu",
"autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filtruj wersje według wyrażenia regularnego",
"trySelectingSuggestedVersionCode": "Spróbuj wybierać sugerowany kod wersji APK",
"dontSortReleasesList": "Utrzymaj kolejność wydań z interfejsu API",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Destaque areas de toque menos óbvias",
"pickExportDir": "Escolher Diretorio de Exportação",
"autoExportOnChanges": "Auto-exportar em mudanças",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filtrar Versões por Expressão Regular",
"trySelectingSuggestedVersionCode": "Tente selecionar a versão sugerida",
"dontSortReleasesList": "Reter a ordem de lançamento da API",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Выделить менее очевидные элементы управления касанием",
"pickExportDir": "Выбрать каталог для экспорта",
"autoExportOnChanges": "Автоэкспорт при изменениях",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Фильтровать версии по регулярному выражению",
"trySelectingSuggestedVersionCode": "Попробуйте выбрать предложенный код версии APK",
"dontSortReleasesList": "Сохранить порядок релизов от API",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Välj Exportsökväg",
"autoExportOnChanges": "Automatisk export vid ändringar",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Daha az belirgin dokunma hedeflerini vurgula",
"pickExportDir": "Dışa Aktarılacak Klasörü Seç",
"autoExportOnChanges": "Değişikliklerde otomatik olarak dışa aktar",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Sürümleri Düzenli İfade ile Filtrele",
"trySelectingSuggestedVersionCode": "Önerilen sürüm kodunu seçmeyi dene",
"dontSortReleasesList": "API'den sıralama düzenini koru",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "Đánh dấu các mục tiêu cảm ứng ít rõ ràng hơn",
"pickExportDir": "Chọn thư mục xuất",
"autoExportOnChanges": "Tự động xuất khi thay đổi",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Lọc phiên bản theo biểu thức chính quy",
"trySelectingSuggestedVersionCode": "Thử chọn APK Mã phiên bản được đề xuất",
"dontSortReleasesList": "Giữ lại thứ tự phát hành từ API",

View File

@@ -256,6 +256,7 @@
"highlightTouchTargets": "突出展示不明显的触摸区域",
"pickExportDir": "选择导出文件夹",
"autoExportOnChanges": "数据变更时自动导出",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "筛选版本号(正则表达式)",
"trySelectingSuggestedVersionCode": "尝试选择推荐版本的 APK 文件",
"dontSortReleasesList": "保持来自 API 的发行顺序",

View File

@@ -48,12 +48,6 @@ class GitLab extends AppSource {
label: tr('fallbackToOlderReleases'), defaultValue: true)
]
];
searchQuerySettingFormItems = [
GeneratedFormTextField('PAT',
label: tr('gitlabPATLabel').split('(')[0],
password: true,
required: false)
];
}
@override
@@ -86,18 +80,8 @@ class GitLab extends AppSource {
@override
Future<Map<String, List<String>>> search(String query,
{Map<String, dynamic> querySettings = const {}}) async {
String? PAT;
if (!hostChanged) {
PAT = await getPATIfAny({});
if (PAT == null) {
throw CredsNeededError(name);
}
}
if ((querySettings['PAT'] as String?)?.isNotEmpty == true) {
PAT = querySettings['PAT'];
}
var url =
'https://$host/api/v4/search?${PAT?.isNotEmpty == true ? 'private_token=$PAT&' : ''}scope=projects&search=${Uri.encodeQueryComponent(query)}';
'https://$host/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
var res = await sourceRequest(url);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);

View File

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

View File

@@ -21,10 +21,10 @@ class AddAppPage extends StatefulWidget {
const AddAppPage({super.key});
@override
State<AddAppPage> createState() => _AddAppPageState();
State<AddAppPage> createState() => AddAppPageState();
}
class _AddAppPageState extends State<AddAppPage> {
class AddAppPageState extends State<AddAppPage> {
bool gettingAppInfo = false;
bool searching = false;
@@ -36,9 +36,62 @@ class _AddAppPageState extends State<AddAppPage> {
bool additionalSettingsValid = true;
bool inferAppIdIfOptional = true;
List<String> pickedCategories = [];
int searchnum = 0;
int urlInputKey = 0;
SourceProvider sourceProvider = SourceProvider();
linkFn(String input) {
try {
if (input.isEmpty) {
throw UnsupportedURLError();
}
sourceProvider.getSource(input);
changeUserInput(input, true, false, updateUrlInput: true);
} catch (e) {
showError(e, context);
}
}
changeUserInput(String input, bool valid, bool isBuilding,
{bool updateUrlInput = false}) {
userInput = input;
if (!isBuilding) {
setState(() {
if (updateUrlInput) {
urlInputKey++;
}
var prevHost = pickedSource?.host;
try {
var naturalSource =
valid ? sourceProvider.getSource(userInput) : null;
if (naturalSource != null &&
naturalSource.runtimeType.toString() !=
HTML().runtimeType.toString()) {
// If input has changed to match a regular source, reset the override
pickedSourceOverride = null;
}
} catch (e) {
// ignore
}
var source = valid
? sourceProvider.getSource(userInput,
overrideSource: pickedSourceOverride)
: null;
if (pickedSource.runtimeType != source.runtimeType ||
(prevHost != null && prevHost != source?.host)) {
pickedSource = source;
additionalSettings = source != null
? getDefaultValuesFromFormItems(
source.combinedAppSpecificSettingFormItems)
: {};
additionalSettingsValid = source != null
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
: true;
inferAppIdIfOptional = true;
}
});
}
}
@override
Widget build(BuildContext context) {
AppsProvider appsProvider = context.read<AppsProvider>();
@@ -48,47 +101,6 @@ class _AddAppPageState extends State<AddAppPage> {
bool doingSomething = gettingAppInfo || searching;
changeUserInput(String input, bool valid, bool isBuilding,
{bool isSearch = false}) {
userInput = input;
if (!isBuilding) {
setState(() {
if (isSearch) {
searchnum++;
}
var prevHost = pickedSource?.host;
try {
var naturalSource =
valid ? sourceProvider.getSource(userInput) : null;
if (naturalSource != null &&
naturalSource.runtimeType.toString() !=
HTML().runtimeType.toString()) {
// If input has changed to match a regular source, reset the override
pickedSourceOverride = null;
}
} catch (e) {
// ignore
}
var source = valid
? sourceProvider.getSource(userInput,
overrideSource: pickedSourceOverride)
: null;
if (pickedSource.runtimeType != source.runtimeType ||
(prevHost != null && prevHost != source?.host)) {
pickedSource = source;
additionalSettings = source != null
? getDefaultValuesFromFormItems(
source.combinedAppSpecificSettingFormItems)
: {};
additionalSettingsValid = source != null
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
: true;
inferAppIdIfOptional = true;
}
});
}
}
Future<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly,
{bool ignoreHideSetting = false}) async {
var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
@@ -205,7 +217,7 @@ class _AddAppPageState extends State<AddAppPage> {
children: [
Expanded(
child: GeneratedForm(
key: Key(searchnum.toString()),
key: Key(urlInputKey.toString()),
items: [
[
GeneratedFormTextField('appSourceURL',
@@ -325,7 +337,7 @@ class _AddAppPageState extends State<AddAppPage> {
);
});
if (selectedUrls != null && selectedUrls.isNotEmpty) {
changeUserInput(selectedUrls[0], true, false, isSearch: true);
changeUserInput(selectedUrls[0], true, false, updateUrlInput: true);
}
}
} catch (e) {

View File

@@ -1,7 +1,11 @@
import 'dart:async';
import 'package:animations/animations.dart';
import 'package:app_links/app_links.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/pages/add_app.dart';
import 'package:obtainium/pages/apps.dart';
import 'package:obtainium/pages/import_export.dart';
@@ -30,58 +34,119 @@ class _HomePageState extends State<HomePage> {
bool isReversing = false;
int prevAppCount = -1;
bool prevIsLoading = true;
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
bool isLinkActivity = false;
List<NavigationPageItem> pages = [
NavigationPageItem(tr('appsString'), Icons.apps,
AppsPage(key: GlobalKey<AppsPageState>())),
NavigationPageItem(tr('addApp'), Icons.add, const AddAppPage()),
NavigationPageItem(
tr('addApp'), Icons.add, AddAppPage(key: GlobalKey<AddAppPageState>())),
NavigationPageItem(
tr('importExport'), Icons.import_export, const ImportExportPage()),
NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage())
];
@override
void initState() {
super.initState();
initDeepLinks();
}
Future<void> initDeepLinks() async {
_appLinks = AppLinks();
goToAddApp(String data) async {
switchToPage(1);
while (
(pages[1].widget.key as GlobalKey<AddAppPageState>?)?.currentState ==
null) {
await Future.delayed(const Duration(microseconds: 1));
}
(pages[1].widget.key as GlobalKey<AddAppPageState>?)
?.currentState
?.linkFn(data);
}
interpretLink(Uri uri) async {
isLinkActivity = true;
var action = uri.host;
var data = uri.path.length > 1 ? uri.path.substring(1) : "";
try {
if (action == 'add') {
await goToAddApp(data);
} else if (action == 'app') {
await context
.read<AppsProvider>()
.import('{ "apps": [${Uri.decodeComponent(data)}] }');
} else if (action == 'apps') {
await context
.read<AppsProvider>()
.import('{ "apps": ${Uri.decodeComponent(data)} }');
} else {
throw ObtainiumError(tr('unknown'));
}
} catch (e) {
showError(e, context);
}
}
// Check initial link if app was in cold state (terminated)
final appLink = await _appLinks.getInitialAppLink();
if (appLink != null) {
await interpretLink(appLink);
}
// Handle link when app is in warm state (front or background)
_linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
await interpretLink(uri);
});
}
setIsReversing(int targetIndex) {
bool reversing = selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last > targetIndex;
setState(() {
isReversing = reversing;
});
}
switchToPage(int index) async {
setIsReversing(index);
if (index == 0) {
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
null) {
// Avoid duplicate GlobalKey error
await Future.delayed(const Duration(microseconds: 1));
}
setState(() {
selectedIndexHistory.clear();
});
} else if (selectedIndexHistory.isEmpty ||
(selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last != index)) {
setState(() {
int existingInd = selectedIndexHistory.indexOf(index);
if (existingInd >= 0) {
selectedIndexHistory.removeAt(existingInd);
}
selectedIndexHistory.add(index);
});
}
}
@override
Widget build(BuildContext context) {
AppsProvider appsProvider = context.watch<AppsProvider>();
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
setIsReversing(int targetIndex) {
bool reversing = selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last > targetIndex;
setState(() {
isReversing = reversing;
});
}
switchToPage(int index) async {
setIsReversing(index);
if (index == 0) {
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
null) {
// Avoid duplicate GlobalKey error
await Future.delayed(const Duration(microseconds: 1));
}
setState(() {
selectedIndexHistory.clear();
});
} else if (selectedIndexHistory.isEmpty ||
(selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last != index)) {
setState(() {
int existingInd = selectedIndexHistory.indexOf(index);
if (existingInd >= 0) {
selectedIndexHistory.removeAt(existingInd);
}
selectedIndexHistory.add(index);
});
}
}
if (!prevIsLoading &&
prevAppCount >= 0 &&
appsProvider.apps.length > prevAppCount &&
selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last == 1) {
selectedIndexHistory.last == 1 &&
!isLinkActivity) {
switchToPage(0);
}
prevAppCount = appsProvider.apps.length;
@@ -129,6 +194,11 @@ class _HomePageState extends State<HomePage> {
),
),
onWillPop: () async {
if (isLinkActivity &&
selectedIndexHistory.length == 1 &&
selectedIndexHistory.last == 1) {
return true;
}
setIsReversing(selectedIndexHistory.length >= 2
? selectedIndexHistory.reversed.toList()[1]
: 0);
@@ -143,4 +213,10 @@ class _HomePageState extends State<HomePage> {
?.clearSelected();
});
}
@override
void dispose() {
super.dispose();
_linkSubscription?.cancel();
}
}

View File

@@ -106,7 +106,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
runObtainiumExport({bool pickOnly = false}) async {
HapticFeedback.selectionClick();
appsProvider
.exportApps(
.export(
pickOnly:
pickOnly || (await settingsProvider.getExportDir()) == null,
sp: settingsProvider)
@@ -132,7 +132,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
} catch (e) {
throw ObtainiumError(tr('invalidInput'));
}
appsProvider.importApps(data).then((value) {
appsProvider.import(data).then((value) {
var cats = settingsProvider.categories;
appsProvider.apps.forEach((key, value) {
for (var c in value.app.categories) {
@@ -143,7 +143,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
});
appsProvider.addMissingCategories(settingsProvider);
showMessage(
tr('importedX', args: [plural('apps', value)]), context);
'${tr('importedX', args: [
plural('apps', value.key)
])}${value.value ? ' + ${tr('settings')}' : ''}',
context);
});
} else {
// User canceled the picker
@@ -388,6 +391,14 @@ class _ImportExportPageState extends State<ImportExportPage> {
defaultValue: settingsProvider
.autoExportOnChanges,
)
],
[
GeneratedFormSwitch(
'exportSettings',
label: tr('includeSettings'),
defaultValue: settingsProvider
.exportSettings,
)
]
],
onValueChanges:
@@ -400,6 +411,12 @@ class _ImportExportPageState extends State<ImportExportPage> {
'autoExportOnChanges'] ==
true;
}
if (value['exportSettings'] !=
null) {
settingsProvider.exportSettings =
value['exportSettings'] ==
true;
}
}
}),
],

View File

@@ -974,7 +974,7 @@ class AppsProvider with ChangeNotifier {
}
}
notifyListeners();
exportApps(isAuto: true);
export(isAuto: true);
}
Future<void> removeApps(List<String> appIds) async {
@@ -996,7 +996,7 @@ class AppsProvider with ChangeNotifier {
}
if (appIds.isNotEmpty) {
notifyListeners();
exportApps(isAuto: true);
export(isAuto: true);
}
}
@@ -1173,7 +1173,7 @@ class AppsProvider with ChangeNotifier {
return updateAppIds;
}
Future<String?> exportApps(
Future<String?> export(
{bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async {
SettingsProvider settingsProvider = sp ?? this.settingsProvider;
var exportDir = await settingsProvider.getExportDir();
@@ -1203,12 +1203,22 @@ class AppsProvider with ChangeNotifier {
}
String? returnPath;
if (!pickOnly) {
Map<String, dynamic> finalExport = {};
finalExport['apps'] = apps.values.map((e) => e.app.toJson()).toList();
if (settingsProvider.exportSettings) {
finalExport['settings'] = Map<String, Object?>.fromEntries(
(settingsProvider.prefs
?.getKeys()
.map((key) =>
MapEntry(key, settingsProvider.prefs?.get(key)))
.toList()) ??
[]);
}
var result = await saf.createFile(exportDir,
displayName:
'${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json',
mimeType: 'application/json',
bytes: Uint8List.fromList(utf8.encode(
jsonEncode(apps.values.map((e) => e.app.toJson()).toList()))));
bytes: Uint8List.fromList(utf8.encode(jsonEncode(finalExport))));
if (result == null) {
throw ObtainiumError(tr('unexpectedError'));
}
@@ -1218,10 +1228,13 @@ class AppsProvider with ChangeNotifier {
return returnPath;
}
Future<int> importApps(String appsJSON) async {
List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>)
.map((e) => App.fromJson(e))
.toList();
Future<MapEntry<int, bool>> import(String appsJSON) async {
var decodedJSON = jsonDecode(appsJSON);
var newFormat = !(decodedJSON is List);
List<App> importedApps =
((newFormat ? decodedJSON['apps'] : decodedJSON) as List<dynamic>)
.map((e) => App.fromJson(e))
.toList();
while (loadingApps) {
await Future.delayed(const Duration(microseconds: 1));
}
@@ -1232,7 +1245,20 @@ class AppsProvider with ChangeNotifier {
}
await saveApps(importedApps, onlyIfExists: false);
notifyListeners();
return importedApps.length;
if (newFormat && decodedJSON['settings'] != null) {
var settingsMap = decodedJSON['settings'] as Map<String, Object?>;
settingsMap.forEach((key, value) {
if (value is int) {
settingsProvider.prefs?.setInt(key, value);
} else if (value is bool) {
settingsProvider.prefs?.setBool(key, value);
} else {
settingsProvider.prefs?.setString(key, value as String);
}
});
}
return MapEntry<int, bool>(
importedApps.length, newFormat && decodedJSON['settings'] != null);
}
@override

View File

@@ -213,7 +213,8 @@ class SettingsProvider with ChangeNotifier {
}
String? getSettingString(String settingId) {
return prefs?.getString(settingId);
String? str = prefs?.getString(settingId);
return str?.isNotEmpty == true ? str : null;
}
void setSettingString(String settingId, String value) {
@@ -415,4 +416,13 @@ class SettingsProvider with ChangeNotifier {
prefs?.setBool('onlyCheckInstalledOrTrackOnlyApps', val);
notifyListeners();
}
bool get exportSettings {
return prefs?.getBool('exportSettings') ?? false;
}
set exportSettings(bool val) {
prefs?.setBool('exportSettings', val);
notifyListeners();
}
}

View File

@@ -42,6 +42,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.10"
app_links:
dependency: "direct main"
description:
name: app_links
sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb"
url: "https://pub.dev"
source: hosted
version: "3.5.0"
archive:
dependency: transitive
description:
@@ -94,10 +102,10 @@ packages:
dependency: transitive
description:
name: cli_util
sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
url: "https://pub.dev"
source: hosted
version: "0.4.0"
version: "0.4.1"
clock:
dependency: transitive
description:
@@ -350,6 +358,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.2.4"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
url: "https://pub.dev"
source: hosted
version: "2.1.0"
hsluv:
dependency: "direct main"
description:
@@ -807,10 +823,10 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba
sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86
url: "https://pub.dev"
source: hosted
version: "6.2.1"
version: "6.2.2"
url_launcher_android:
dependency: transitive
description:
@@ -831,10 +847,10 @@ packages:
dependency: transitive
description:
name: url_launcher_linux
sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd"
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
url_launcher_macos:
dependency: transitive
description:
@@ -855,26 +871,26 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "138bd45b3a456dcfafc46d1a146787424f8d2edfbf2809c9324361e58f851cf7"
sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc"
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
uuid:
dependency: transitive
description:
name: uuid
sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
version: "4.2.2"
vector_math:
dependency: transitive
description:
@@ -903,10 +919,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_android
sha256: "8326ee235f87605a2bfc444a4abc897f4abc78d83f054ba7d3d1074ce82b4fbf"
sha256: e313dcdf45d4c95bcb8960351ef2389b7f0687b90bc92483f7f7983ae5758456
url: "https://pub.dev"
source: hosted
version: "3.12.1"
version: "3.13.0"
webview_flutter_platform_interface:
dependency: transitive
description:

View File

@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.14.36+230 # When changing this, update the tag in main() accordingly
version: 0.14.37+231 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=3.0.0 <4.0.0'
@@ -67,6 +67,7 @@ dependencies:
connectivity_plus: ^5.0.0
shared_storage: ^0.8.0
crypto: ^3.0.3
app_links: ^3.5.0
dev_dependencies:
flutter_test: