Compare commits

...

48 Commits

Author SHA1 Message Date
845eaab73e Merge pull request #909 from ImranR98/dev
Bugfix: HTML Source was broken (could not add apps)
2023-09-16 15:27:11 -04:00
298ff190ec Bugfix: HTML Source was broken (could not add apps) 2023-09-16 15:26:35 -04:00
734a548bc3 Merge pull request #906 from ImranR98/dev
Increment version
2023-09-16 09:28:57 -04:00
591973d97f Increment version 2023-09-16 09:28:45 -04:00
a2f57ecd66 Merge pull request #903 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-09-16 09:26:34 -04:00
13dcfb479d Merge pull request #902 from gidano/main
Update hu.json
2023-09-16 09:26:28 -04:00
3328d80130 Merge pull request #905 from saymanrifat/saymanrifat-721
Add back button to app detail page (#721)
2023-09-16 09:26:03 -04:00
f9df3ac0e2 Reduce top padding as we no longer need it 2023-09-16 09:25:26 -04:00
61d66e80d8 Add back button to app detail page (#721) 2023-09-16 18:31:57 +06:00
93e0360116 locale(pl): Update Polish translations 2023-09-16 12:45:50 +02:00
2cec5f7934 Update hu.json 2023-09-16 11:28:36 +02:00
8d4a09b007 Merge pull request #894 from markus-gitdev/main
Update de.json
2023-09-16 04:39:06 -04:00
6d8d2fa99d Merge branch 'main' into main 2023-09-16 04:38:48 -04:00
59ecd55530 Merge pull request #895 from DwainZwerg/patch-1
edit de.json
2023-09-16 04:38:05 -04:00
77fcc89e37 Merge branch 'main' into patch-1 2023-09-16 04:35:12 -04:00
7d63da4d9c Merge pull request #893 from bluefly000/japanese-translation
Update ja.json
2023-09-16 04:32:42 -04:00
8eea0bae99 Merge pull request #892 from iDazai/main
Update de.json
2023-09-16 04:32:35 -04:00
f5dcd8f776 Merge pull request #901 from ImranR98/dev
Fix background update issue (#896)
2023-09-16 04:31:29 -04:00
e1f2baeeda Fix background update issue (#896) 2023-09-16 04:31:03 -04:00
84d8ff8b0b Small improvement of the previous translation (especially "" instead of the English ''; my translations of the new features to be translated are mostly based on iDazai (https://github.com/ImranR98/Obtainium/pull/892/files) and markus-gitdev (https://github.com/ImranR98/Obtainium/pull/894/files)) 2023-09-14 08:46:51 +00:00
9862c22fdb Update de.json 2023-09-14 09:27:36 +02:00
8c82f53dd2 Update ja.json 2023-09-14 15:44:27 +09:00
5cf4c21ce3 Update de.json
Translated latest strings
2023-09-14 08:37:46 +02:00
4951f62d4a Merge pull request #890 from ImranR98/dev
- Fix export encoding (#887)
- Fix repeating background install for some apps (#886)
- More version filtering options for F-Droid (#885)
- Trim user URL input
2023-09-13 22:20:49 -04:00
4c0c4b7010 Trim user URL input 2023-09-13 22:19:09 -04:00
96051e614c Merge remote-tracking branch 'origin/main' into dev 2023-09-13 22:17:11 -04:00
a2e494b2ba More version filtering options for F-Droid (#885) 2023-09-13 22:15:10 -04:00
2d5a9bec84 Increment version, update packages 2023-09-13 21:27:18 -04:00
7d9571cfdd Fix repeating background install for some apps (#886) 2023-09-13 21:24:25 -04:00
acc6a780fa Fix export encoding (#887) 2023-09-13 21:01:39 -04:00
0e36d42a06 Merge pull request #863 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-09-13 19:22:57 -04:00
5cfddd807a locale(pl): Update Polish translations 2023-09-11 22:05:32 +02:00
17b5604f2a Merge pull request #882 from ImranR98/dev
Bugfix
2023-09-10 23:29:38 -04:00
fdb6eed6d0 Slight tweak 2023-09-10 23:28:37 -04:00
13de0437b8 Bugfix 2023-09-10 23:23:35 -04:00
a43c45f310 Merge pull request #881 from ImranR98/dev
Pick export dir + auto-export (#283, #600) + UI fix
2023-09-10 23:15:36 -04:00
9c56a4d1fc Bugs 2023-09-10 23:15:16 -04:00
2aea1d2631 Bugfixes 2023-09-10 23:11:06 -04:00
118e05a0fa Bugfix 2023-09-10 22:47:04 -04:00
05f497787e Build script tweak 2023-09-10 22:37:32 -04:00
53cf4d0234 Pick export dir + auto-export (#283, #600) 2023-09-10 22:35:28 -04:00
6e735b1763 Enable auto-export on update checks 2023-09-10 22:24:18 -04:00
873a1a0683 Allow for alternative app dirs (unfinished) 2023-09-10 15:55:34 -04:00
27b1149d1c Fix padding when touch targets not highlighted 2023-09-10 14:16:19 -04:00
c1e64f111e Merge pull request #878 from ImranR98/dev
Fix Codeberg not finding APKs (#877), Make touch highlight visible in dark theme (#860)
2023-09-10 14:03:48 -04:00
b2af8448fd Increment version 2023-09-10 14:02:26 -04:00
8f44338e76 Make touch highlight visible in dark theme (#860) 2023-09-10 13:56:39 -04:00
e4a55abcb3 Fix Codeberg not finding APKs (#877) 2023-09-10 13:49:04 -04:00
28 changed files with 448 additions and 159 deletions

View File

@ -254,6 +254,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Remover App?",
"other": "Remover Apps?"

View File

@ -111,7 +111,7 @@
"dark": "Tamna",
"light": "Svijetla",
"followSystem": "Pratite sistem",
"obtainium": "Obtainium",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Koristite čisto crnu tamnu temu",
"appSortBy": "Aplikacije sortirane po",
@ -251,7 +251,11 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"removeAppQuestion": {
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Želite li ukloniti aplikaciju?",
"other": "Želite li ukloniti aplikacije?"
},

View File

@ -28,8 +28,8 @@
"xIsTrackOnly": "{} ist nur zur Nachverfolgung",
"source": "Quelle",
"app": "App",
"appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind 'Nur Nachverfolgen'.",
"youPickedTrackOnly": "Sie haben die Option 'Nur Nachverfolgen' gewählt.",
"appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind nur zum Nachverfolgen.",
"youPickedTrackOnly": "Sie haben die Option Nur Nachverfolgen gewählt.",
"trackOnlyAppDescription": "Die App wird auf Updates überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.",
"cancelled": "Abgebrochen",
"appAlreadyAdded": "App bereits hinzugefügt",
@ -38,7 +38,7 @@
"appSourceURL": "Quell-URL der App",
"error": "Fehler",
"add": "Hinzufügen",
"searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)",
"searchSomeSourcesLabel": "Suche (nur für bestimmte Quellen)",
"search": "Suchen",
"additionalOptsFor": "Zusatzoptionen für {}",
"supportedSources": "Unterstützte Quellen",
@ -70,7 +70,7 @@
"yes": "Ja",
"markSelectedAppsUpdated": "Markiere ausgewählte Apps als aktuell",
"pinToTop": "Oben anheften",
"unpinFromTop": "'Oben anheften' aufheben",
"unpinFromTop": "Oben anheften aufheben",
"resetInstallStatusForSelectedAppsQuestion": "Installationsstatus für ausgewählte Apps zurücksetzen?",
"installStatusOfXWillBeResetExplanation": "Der Installationsstatus der ausgewählten Apps wird zurückgesetzt. Dies kann hilfreich sein, wenn die in Obtainium angezeigte App-Version aufgrund fehlgeschlagener Aktualisierungen oder anderer Probleme falsch ist.",
"shareSelectedAppURLs": "Ausgewählte App-URLs teilen",
@ -175,7 +175,7 @@
"appId": "App ID",
"appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden",
"reposHaveMultipleApps": "Repos können mehrere Apps enthalten",
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
"fdroidThirdPartyRepo": "F-Droid Drittparteienrepo",
"steam": "Steam",
"steamMobile": "Steam Mobile",
"steamChat": "Steam Chat",
@ -244,13 +244,17 @@
"xWasPossiblyUpdatedToY": "{} wurde möglicherweise aktualisiert auf {}.",
"backgroundUpdateReqsExplanation": "Die Hintergrundaktualisierung ist möglicherweise nicht für alle Apps möglich.",
"backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.",
"verifyLatestTag": "Überprüfe das 'latest' Tag",
"verifyLatestTag": "Überprüfe das latest Tag",
"exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)",
"bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"autoSelectHighestVersionCode": "Automatisch höchste APK-Code-Version auswählen",
"versionExtractionRegEx": "Versions-Extraktion RegEx",
"matchGroupToUse": "Zu verwendende Gruppe abgleichen",
"highlightTouchTargets": "Weniger offensichtliche Ziele hervorheben",
"pickExportDir": "Export-Verzeichnis wählen",
"autoExportOnChanges": "Automatischer Export bei Änderung",
"filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern",
"trySelectingSuggestedVersionCode": "Versuchen, die vorgeschlagene APK-Code-Version auszuwählen",
"removeAppQuestion": {
"one": "App entfernen?",
"other": "Apps entfernen?"

View File

@ -254,6 +254,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Remove App?",
"other": "Remove Apps?"

View File

@ -251,6 +251,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "¿Eliminar Aplicación?",
"other": "¿Eliminar Aplicaciones?"

View File

@ -251,6 +251,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟"

View File

@ -251,6 +251,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Supprimer l'application ?",
"other": "Supprimer les applications ?"

View File

@ -246,10 +246,14 @@
"verifyLatestTag": "Ellenőrizze a „legújabb” címkét",
"exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)",
"bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"autoSelectHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
"versionExtractionRegEx": "Verzió kibontása reguláris kifejezéssel",
"matchGroupToUse": "Párosítsa a csoportot a használathoz",
"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",
"filterVersionsByRegEx": "Verziók szűrése reguláris kifejezéssel",
"trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t",
"removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazást?"

View File

@ -251,6 +251,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Rimuovere l'app?",
"other": "Rimuovere le app?"

View File

@ -144,7 +144,7 @@
"updatesAvailable": "アップデートが利用可能",
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
"noNewUpdates": "新しいアップデートはありません",
"xHasAnUpdate": "{} のアップデートが利用可能です",
"xHasAnUpdate": "{} のアップデートが利用可能です",
"appsUpdated": "アプリをアップデートしました",
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
"xWasUpdatedToY": "{} が {} にアップデートされました",
@ -241,17 +241,23 @@
"customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')",
"appsPossiblyUpdated": "アプリのアップデートを試行",
"appsPossiblyUpdatedNotifDescription": "1つまたは複数のアプリのアップデートがバックグラウンドで適用された可能性があることをユーザーに通知する",
"xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります",
"xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります",
"enableBackgroundUpdates": "バックグラウンドアップデートを有効化する",
"backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。",
"backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。",
"verifyLatestTag": "'latest'タグを確認する",
"intermediateLinkRegex": "最初にアクセスする「中間」リンクをフィルタリングする",
"intermediateLinkNotFound": "中間リンクが見つかりませんでした",
"exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
"bgUpdatesOnWiFiOnly": "WiFiを使用していない場合バックグラウンドアップデートを無効にする",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する",
"versionExtractionRegEx": "バージョン抽出の正規表現",
"matchGroupToUse": "使用するマッチしたグループ",
"highlightTouchTargets": "目立たないタップ可能な対象をハイライトする",
"pickExportDir": "エクスポートディレクトリを選択",
"autoExportOnChanges": "変更があった際に自動でエクスポートする",
"filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする",
"trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
"removeAppQuestion": {
"one": "アプリを削除しますか?",
"other": "アプリを削除しますか?"
@ -293,15 +299,15 @@
"other": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})"
},
"xAndNMoreUpdatesAvailable": {
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です",
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です"
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です",
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です"
},
"xAndNMoreUpdatesInstalled": {
"one": "{} とさらに {} 個のアプリがアップデートされました",
"other": "{} とさらに {} 個のアプリがアップデートされました"
"one": "{} とさらに {} 個のアプリがアップデートされました",
"other": "{} とさらに {} 個のアプリがアップデートされました"
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} とさらに 1 個のアプリがアップデートされた可能性があります",
"other": "{} とさらに {} 個のアプリがアップデートされた可能性があります"
"one": "{} とさらに 1 個のアプリがアップデートされた可能性があります",
"other": "{} とさらに {} 個のアプリがアップデートされた可能性があります"
}
}

View File

@ -253,10 +253,14 @@
"verifyLatestTag": "Zweryfikuj najnowszy tag",
"exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)",
"bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK",
"versionExtractionRegEx": "Wyrażenie regularne wyodrębniające wersję",
"matchGroupToUse": "Dopasuj grupę do użycia",
"highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe",
"pickExportDir": "Wybierz katalog eksportu",
"autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian",
"filterVersionsByRegEx": "Filtruj wersje według wyrażenia regularnego",
"trySelectingSuggestedVersionCode": "Spróbuj wybierać sugerowany kod wersji APK",
"removeAppQuestion": {
"one": "Usunąć aplikację?",
"few": "Usunąć aplikacje?",

View File

@ -251,6 +251,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Удалить приложение?",
"other": "Удалить приложения?"

View File

@ -252,6 +252,10 @@
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "是否删除应用?",
"other": "是否删除应用?"

View File

@ -4,7 +4,9 @@
CURR_DIR="$(pwd)"
trap "cd "$CURR_DIR"" EXIT
git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date
if [ -z "$1" ]; then
git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date
fi
rm ./build/app/outputs/flutter-apk/* 2>/dev/null # Get rid of older builds if any
flutter build apk && flutter build apk --split-per-abi # Build (both split and combined APKs)
for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures

View File

@ -13,10 +13,24 @@ class FDroid extends AppSource {
name = tr('fdroid');
canSearch = true;
additionalSourceAppSpecificSettingFormItems = [
[
GeneratedFormTextField('filterVersionsByRegEx',
label: tr('filterVersionsByRegEx'),
required: false,
additionalValidators: [
(value) {
return regExValidator(value);
}
])
],
[
GeneratedFormSwitch('trySelectingSuggestedVersionCode',
label: tr('trySelectingSuggestedVersionCode'), defaultValue: true)
],
[
GeneratedFormSwitch('autoSelectHighestVersionCode',
label: tr('autoSelectHighestVersionCode'))
]
],
];
}
@ -45,25 +59,73 @@ class FDroid extends AppSource {
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
Response res, String apkUrlPrefix, String standardUrl,
{bool autoSelectHighestVersionCode = false}) {
{bool autoSelectHighestVersionCode = false,
bool trySelectingSuggestedVersionCode = false,
String? filterVersionsByRegEx}) {
if (res.statusCode == 200) {
List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
var response = jsonDecode(res.body);
List<dynamic> releases = response['packages'] ?? [];
if (releases.isEmpty) {
throw NoReleasesError();
}
String? latestVersion = releases[0]['versionName'];
if (latestVersion == null) {
String? version;
Iterable<dynamic> releaseChoices = [];
// Grab the versionCode suggested if the user chose to do that
// Only do so at this stage if the user has no release filter
if (trySelectingSuggestedVersionCode &&
response['suggestedVersionCode'] != null &&
filterVersionsByRegEx == null) {
var suggestedReleases = releases.where((element) =>
element['versionCode'] == response['suggestedVersionCode']);
if (suggestedReleases.isNotEmpty) {
releaseChoices = suggestedReleases;
version = suggestedReleases.first['versionName'];
}
}
// Apply the release filter if any
if (filterVersionsByRegEx != null) {
version = null;
releaseChoices = [];
for (var i = 0; i < releases.length; i++) {
if (RegExp(filterVersionsByRegEx)
.hasMatch(releases[i]['versionName'])) {
version = releases[i]['versionName'];
}
}
if (version == null) {
throw NoVersionError();
}
}
// Default to the highest version
version ??= releases[0]['versionName'];
if (version == null) {
throw NoVersionError();
}
Iterable<dynamic> latestReleases =
releases.where((element) => element['versionName'] == latestVersion);
if (latestReleases.length > 1 && autoSelectHighestVersionCode) {
latestReleases = [latestReleases.first];
// If a suggested release was not already picked, pick all those with the selected version
if (releaseChoices.isEmpty) {
releaseChoices =
releases.where((element) => element['versionName'] == version);
}
List<String> apkUrls = latestReleases
// For the remaining releases, use the toggles to auto-select one if possible
if (releaseChoices.length > 1) {
if (autoSelectHighestVersionCode) {
releaseChoices = [releaseChoices.first];
} else if (trySelectingSuggestedVersionCode &&
response['suggestedVersionCode'] != null) {
var suggestedReleases = releaseChoices.where((element) =>
element['versionCode'] == response['suggestedVersionCode']);
if (suggestedReleases.isNotEmpty) {
releaseChoices = suggestedReleases;
}
}
}
if (releaseChoices.isEmpty) {
throw NoReleasesError();
}
List<String> apkUrls = releaseChoices
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
.toList();
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
return APKDetails(version, getApkUrlsFromUrls(apkUrls),
AppNames(name, Uri.parse(standardUrl).pathSegments.last));
} else {
throw getObtainiumHttpError(res);
@ -82,7 +144,15 @@ class FDroid extends AppSource {
'https://$host/repo/$appId',
standardUrl,
autoSelectHighestVersionCode:
additionalSettings['autoSelectHighestVersionCode'] == true);
additionalSettings['autoSelectHighestVersionCode'] == true,
trySelectingSuggestedVersionCode:
additionalSettings['trySelectingSuggestedVersionCode'] == true,
filterVersionsByRegEx:
(additionalSettings['filterVersionsByRegEx'] as String?)
?.isNotEmpty ==
true
? additionalSettings['filterVersionsByRegEx']
: null);
}
@override

View File

@ -252,8 +252,10 @@ class GitHub extends AppSource {
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
(release['assets'] as List<dynamic>?)
?.map((e) {
return e['name'] != null && e['url'] != null
? MapEntry(e['name'] as String, e['url'] as String)
return (e['name'] != null) &&
((e['url'] ?? e['browser_download_url']) != null)
? MapEntry(e['name'] as String,
(e['url'] ?? e['browser_download_url']) as String)
: const MapEntry('', '');
})
.where((element) => element.key.toLowerCase().endsWith('.apk'))

View File

@ -123,11 +123,11 @@ class HTML extends AppSource {
textInputType: const TextInputType.numberWithOptions(),
additionalValidators: [
(value) {
value ??= '1';
if (int.tryParse(value) == null) {
return tr('invalidInput');
if (value?.isEmpty == true) {
value = null;
}
return null;
value ??= '1';
return intValidator(value);
}
])
]

View File

@ -40,6 +40,9 @@ class IzzyOnDroid extends AppSource {
'https://android.izzysoft.de/frepo/$appId',
standardUrl,
autoSelectHighestVersionCode:
additionalSettings['autoSelectHighestVersionCode'] == true);
additionalSettings['autoSelectHighestVersionCode'] == true,
trySelectingSuggestedVersionCode:
additionalSettings['trySelectingSuggestedVersionCode'] == true,
filterVersionsByRegEx: additionalSettings['filterVersionsByRegEx']);
}
}

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

View File

@ -148,7 +148,7 @@ class _AddAppPageState extends State<AddAppPage> {
userPickedTrackOnly))) {
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
app = await sourceProvider.getApp(
pickedSource!, userInput, additionalSettings,
pickedSource!, userInput.trim(), additionalSettings,
trackOnlyOverride: trackOnly,
overrideSource: pickedSourceOverride,
inferAppIdIfOptional: inferAppIdIfOptional);

View File

@ -152,7 +152,7 @@ class _AppPageState extends State<AppPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 125),
const SizedBox(height: 20),
app?.icon != null
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Image.memory(
@ -338,9 +338,9 @@ class _AppPageState extends State<AppPage> {
try {
HapticFeedback.heavyImpact();
var res = await appsProvider.downloadAndInstallLatestApps(
app?.app.id != null ? [app!.app.id] : [],
globalNavigatorKey.currentContext,
settingsProvider);
app?.app.id != null ? [app!.app.id] : [],
globalNavigatorKey.currentContext,
);
if (app?.app.installedVersion != null && !trackOnly) {
// ignore: use_build_context_synchronously
showError(tr('appsUpdated'), context);
@ -463,9 +463,18 @@ class _AppPageState extends State<AppPage> {
: null))
],
));
appScreenAppBar() => AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
),
);
return Scaffold(
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
appBar: settingsProvider.showAppWebpage ? AppBar() : appScreenAppBar(),
backgroundColor: Theme.of(context).colorScheme.surface,
body: RefreshIndicator(
child: settingsProvider.showAppWebpage

View File

@ -381,8 +381,7 @@ class AppsPageState extends State<AppsPage> {
: () {
appsProvider.downloadAndInstallLatestApps(
[listedApps[appIndex].app.id],
globalNavigatorKey.currentContext,
settingsProvider).catchError((e) {
globalNavigatorKey.currentContext).catchError((e) {
showError(e, context);
return <String>[];
});
@ -452,12 +451,16 @@ class AppsPageState extends State<AppsPage> {
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).primaryColor.withAlpha(
(settingsProvider.highlightTouchTargets &&
showChangesFn != null)
? 20
: 0)),
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
color: settingsProvider.highlightTouchTargets &&
showChangesFn != null
? (Theme.of(context).brightness == Brightness.light
? Theme.of(context).primaryColor
: Theme.of(context).primaryColorLight)
.withAlpha(20)
: null),
padding: settingsProvider.highlightTouchTargets
? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0)
: const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
@ -695,8 +698,8 @@ class AppsPageState extends State<AppsPage> {
toInstall.addAll(trackOnlyUpdateIdsAllOrSelected);
}
appsProvider
.downloadAndInstallLatestApps(toInstall,
globalNavigatorKey.currentContext, settingsProvider)
.downloadAndInstallLatestApps(
toInstall, globalNavigatorKey.currentContext)
.catchError((e) {
showError(e, context);
return <String>[];

View File

@ -28,8 +28,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
@override
Widget build(BuildContext context) {
SourceProvider sourceProvider = SourceProvider();
var appsProvider = context.read<AppsProvider>();
var settingsProvider = context.read<SettingsProvider>();
var appsProvider = context.watch<AppsProvider>();
var settingsProvider = context.watch<SettingsProvider>();
var outlineButtonStyle = ButtonStyle(
shape: MaterialStateProperty.all(
@ -102,10 +102,16 @@ class _ImportExportPageState extends State<ImportExportPage> {
});
}
runObtainiumExport() {
runObtainiumExport() async {
HapticFeedback.selectionClick();
appsProvider.exportApps().then((String path) {
showError(tr('exportedTo', args: [path]), context);
appsProvider
.exportApps(
pickOnly: (await settingsProvider.getExportDir()) == null,
sp: settingsProvider)
.then((String? result) {
if (result != null) {
showError(tr('exportedTo', args: [result]), context);
}
}).catchError((e) {
showError(e, context);
});
@ -301,27 +307,68 @@ class _ImportExportPageState extends State<ImportExportPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: appsProvider.apps.isEmpty ||
importInProgress
? null
: runObtainiumExport,
child: Text(tr('obtainiumExport')))),
const SizedBox(
width: 16,
),
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: importInProgress
? null
: runObtainiumImport,
child: Text(tr('obtainiumImport'))))
],
FutureBuilder(
future: settingsProvider.getExportDir(),
builder: (context, snapshot) {
return Column(
children: [
Row(
children: [
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: appsProvider.apps.isEmpty ||
importInProgress
? null
: runObtainiumExport,
child: Text(tr(snapshot.data != null
? 'obtainiumExport'
: 'pickExportDir')),
)),
const SizedBox(
width: 16,
),
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: importInProgress
? null
: runObtainiumImport,
child: Text(tr('obtainiumImport'))))
],
),
if (snapshot.data != null)
Column(
children: [
const SizedBox(height: 16),
GeneratedForm(
items: [
[
GeneratedFormSwitch(
'autoExportOnChanges',
label: tr('autoExportOnChanges'),
defaultValue: settingsProvider
.autoExportOnChanges,
)
]
],
onValueChanges:
(value, valid, isBuilding) {
if (valid && !isBuilding) {
if (value['autoExportOnChanges'] !=
null) {
settingsProvider
.autoExportOnChanges = value[
'autoExportOnChanges'] ==
true;
}
}
}),
],
),
],
);
},
),
if (importInProgress)
const Column(
@ -399,7 +446,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
fontStyle: FontStyle.italic, fontSize: 12)),
const SizedBox(
height: 8,
)
),
],
)))
]));

View File

@ -31,6 +31,7 @@ import 'package:obtainium/providers/source_provider.dart';
import 'package:http/http.dart';
import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter_archive/flutter_archive.dart';
import 'package:shared_storage/shared_storage.dart' as saf;
final pm = AndroidPackageManager();
@ -150,6 +151,7 @@ class AppsProvider with ChangeNotifier {
late Stream<FGBGType>? foregroundStream;
late StreamSubscription<FGBGType>? foregroundSubscription;
late Directory APKDir;
late SettingsProvider settingsProvider = SettingsProvider();
Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy());
@ -161,6 +163,7 @@ class AppsProvider with ChangeNotifier {
if (isForeground) await loadApps();
});
() async {
await settingsProvider.initializeSettings();
var cacheDirs = await getExternalCacheDirectories();
if (cacheDirs?.isNotEmpty ?? false) {
APKDir = cacheDirs!.first;
@ -369,8 +372,7 @@ class AppsProvider with ChangeNotifier {
.where((element) => element.downloadProgress != null)
.isNotEmpty;
Future<bool> canInstallSilently(
App app, SettingsProvider settingsProvider) async {
Future<bool> canInstallSilently(App app) async {
if (app.id == obtainiumId) {
return false;
}
@ -428,7 +430,8 @@ class AppsProvider with ChangeNotifier {
zipFile: File(filePath), destinationDir: Directory(destinationPath));
}
Future<void> installXApkDir(DownloadedXApkDir dir) async {
Future<void> installXApkDir(DownloadedXApkDir dir,
{bool needsBGWorkaround = false}) async {
// We don't know which APKs in an XAPK are supported by the user's device
// So we try installing all of them and assume success if at least one installed
// If 0 APKs installed, throw the first install error encountered
@ -441,7 +444,8 @@ class AppsProvider with ChangeNotifier {
if (file.path.toLowerCase().endsWith('.apk')) {
try {
somethingInstalled = somethingInstalled ||
await installApk(DownloadedApk(dir.appId, file));
await installApk(DownloadedApk(dir.appId, file),
needsBGWorkaround: needsBGWorkaround);
} catch (e) {
logs.add(
'Could not install APK from XAPK \'${file.path}\': ${e.toString()}');
@ -461,7 +465,8 @@ class AppsProvider with ChangeNotifier {
}
}
Future<bool> installApk(DownloadedApk file) async {
Future<bool> installApk(DownloadedApk file,
{bool needsBGWorkaround = false}) async {
var newInfo =
await pm.getPackageArchiveInfo(archiveFilePath: file.file.path);
PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id);
@ -470,6 +475,17 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) {
throw DowngradeError();
}
if (needsBGWorkaround) {
// The below 'await' will never return if we are in a background process
// To work around this, we should assume the install will be successful
// So we update the app's installed version first as we will never get to the later code
// We can't conditionally get rid of the 'await' as this causes install fails (BG process times out) - see #896
// TODO: When fixed, update this function and the calls to it accordingly
apps[file.appId]!.app.installedVersion =
apps[file.appId]!.app.latestVersion;
await saveApps([apps[file.appId]!.app],
attemptToCorrectInstallStatus: false);
}
int? code =
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
bool installed = false;
@ -539,7 +555,6 @@ class AppsProvider with ChangeNotifier {
getHost(apkUrl.value) != getHost(app.url) &&
context != null) {
// ignore: use_build_context_synchronously
var settingsProvider = context.read<SettingsProvider>();
if (!(settingsProvider.hideAPKOriginWarning) &&
// ignore: use_build_context_synchronously
await showDialog(
@ -560,8 +575,8 @@ class AppsProvider with ChangeNotifier {
// If no BuildContext is provided, apps that require user interaction are ignored
// If user input is needed and the App is in the background, a notification is sent to get the user's attention
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
Future<List<String>> downloadAndInstallLatestApps(List<String> appIds,
BuildContext? context, SettingsProvider settingsProvider,
Future<List<String>> downloadAndInstallLatestApps(
List<String> appIds, BuildContext? context,
{NotificationsProvider? notificationsProvider}) async {
notificationsProvider =
notificationsProvider ?? context?.read<NotificationsProvider>();
@ -590,8 +605,7 @@ class AppsProvider with ChangeNotifier {
apps[id]!.app.preferredApkIndex = urlInd;
await saveApps([apps[id]!.app]);
}
if (context != null ||
await canInstallSilently(apps[id]!.app, settingsProvider)) {
if (context != null || await canInstallSilently(apps[id]!.app)) {
appsToInstall.add(id);
}
}
@ -628,8 +642,7 @@ class AppsProvider with ChangeNotifier {
downloadedDir = downloadedArtifact as DownloadedXApkDir;
}
var appId = downloadedFile?.appId ?? downloadedDir!.appId;
bool willBeSilent =
await canInstallSilently(apps[appId]!.app, settingsProvider);
bool willBeSilent = await canInstallSilently(apps[appId]!.app);
if (!(await settingsProvider.getInstallPermission(enforce: false))) {
throw ObtainiumError(tr('cancelled'));
}
@ -642,15 +655,13 @@ class AppsProvider with ChangeNotifier {
try {
if (downloadedFile != null) {
if (willBeSilent && context == null) {
// Would await forever - workaround - TODO
installApk(downloadedFile);
installApk(downloadedFile, needsBGWorkaround: true);
} else {
await installApk(downloadedFile);
}
} else {
if (willBeSilent && context == null) {
// Would await forever - workaround - TODO
installXApkDir(downloadedDir!);
installXApkDir(downloadedDir!, needsBGWorkaround: true);
} else {
await installXApkDir(downloadedDir!);
}
@ -678,8 +689,8 @@ class AppsProvider with ChangeNotifier {
}
Future<Directory> getAppsDir() async {
Directory appsDir = Directory(
'${(await getExternalStorageDirectory())?.path as String}/app_data');
Directory appsDir =
Directory('${(await getExternalStorageDirectory())!.path}/app_data');
if (!appsDir.existsSync()) {
appsDir.createSync();
}
@ -879,8 +890,6 @@ class AppsProvider with ChangeNotifier {
.toList();
// After reconciliation, delete externally uninstalled Apps if needed
if (removedAppIds.isNotEmpty) {
var settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings();
if (settingsProvider.removeOnExternalUninstall) {
await removeApps(removedAppIds);
}
@ -919,6 +928,7 @@ class AppsProvider with ChangeNotifier {
}
}
notifyListeners();
exportApps(isAuto: true);
}
Future<void> removeApps(List<String> appIds) async {
@ -940,6 +950,7 @@ class AppsProvider with ChangeNotifier {
}
if (appIds.isNotEmpty) {
notifyListeners();
exportApps(isAuto: true);
}
}
@ -1096,32 +1107,52 @@ class AppsProvider with ChangeNotifier {
return updateAppIds;
}
Future<String> exportApps() async {
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
if (await Permission.storage.isDenied) {
await Permission.storage.request();
Future<String?> exportApps(
{bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async {
SettingsProvider settingsProvider = sp ?? this.settingsProvider;
var exportDir = await settingsProvider.getExportDir();
if (isAuto) {
if (settingsProvider.autoExportOnChanges != true) {
return null;
}
if (await Permission.storage.isDenied) {
throw ObtainiumError(tr('storagePermissionDenied'));
if (exportDir == null) {
logs.add('Skipping auto-export as dir is not set.');
return null;
}
logs.add('Started auto-export.');
var files = await saf
.listFiles(exportDir, columns: [saf.DocumentFileColumn.id])
.where((f) => f.uri.pathSegments.last.endsWith('-auto.json'))
.toList();
if (files.isNotEmpty) {
for (var f in files) {
saf.delete(f.uri);
}
logs.add('Previous auto-export deleted.');
}
}
Directory? exportDir = Directory('/storage/emulated/0/Download');
String path = 'Downloads'; // TODO: See if hardcoding this can be avoided
var downloadsAccessible = false;
try {
downloadsAccessible = exportDir.existsSync();
} catch (e) {
logs.add('Error accessing Downloads (will use fallback): $e');
if (exportDir == null || pickOnly) {
await settingsProvider.pickExportDir();
exportDir = await settingsProvider.getExportDir();
}
if (!downloadsAccessible) {
exportDir = await getExternalStorageDirectory();
path = exportDir!.path;
if (exportDir == null) {
return null;
}
File export = File(
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
export.writeAsStringSync(
jsonEncode(apps.values.map((e) => e.app.toJson()).toList()));
return path;
String? returnPath;
if (!pickOnly) {
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()))));
if (result == null) {
throw ObtainiumError(tr('unexpectedError'));
}
returnPath =
exportDir.pathSegments.join('/').replaceFirst('tree/primary:', '/');
}
return returnPath;
}
Future<int> importApps(String appsJSON) async {
@ -1298,14 +1329,12 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
NotificationsProvider notificationsProvider = NotificationsProvider();
AppsProvider appsProvider = AppsProvider(isBg: true);
await appsProvider.loadApps();
var settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings();
int maxAttempts = 4;
params ??= {};
if (params['toCheck'] == null) {
settingsProvider.lastBGCheckTime = DateTime.now();
appsProvider.settingsProvider.lastBGCheckTime = DateTime.now();
}
List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
...(params['toCheck']
@ -1335,7 +1364,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
var didCompleteChecking = false;
CheckingUpdatesNotification? notif;
var networkRestricted = false;
if (settingsProvider.bgUpdatesOnWiFiOnly) {
if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
var netResult = await (Connectivity().checkConnectivity());
networkRestricted = (netResult != ConnectivityResult.wifi) &&
(netResult != ConnectivityResult.ethernet);
@ -1355,8 +1384,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
App? newApp = await appsProvider.checkUpdate(appId);
if (newApp != null) {
if (networkRestricted ||
!(await appsProvider.canInstallSilently(
app!.app, settingsProvider))) {
!(await appsProvider.canInstallSilently(app!.app))) {
toNotify.add(newApp);
} else {
toInstall.add(MapEntry(appId, 0));
@ -1442,8 +1470,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
try {
logs.add(
'BG install task $taskId: Attempting to update $appId in the background.');
await appsProvider.downloadAndInstallLatestApps(
[appId], null, settingsProvider,
await appsProvider.downloadAndInstallLatestApps([appId], null,
notificationsProvider: notificationsProvider);
await Future.delayed(const Duration(
seconds:

View File

@ -9,8 +9,10 @@ import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/main.dart';
import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/source_provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_storage/shared_storage.dart' as saf;
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
String obtainiumId = 'dev.imranr.obtainium';
@ -35,6 +37,7 @@ List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0]
class SettingsProvider with ChangeNotifier {
SharedPreferences? prefs;
String? defaultAppDir;
bool justStarted = true;
String sourceUrl = 'https://github.com/ImranR98/Obtainium';
@ -42,6 +45,7 @@ class SettingsProvider with ChangeNotifier {
// Not done in constructor as we want to be able to await it
Future<void> initializeSettings() async {
prefs = await SharedPreferences.getInstance();
defaultAppDir = (await getExternalStorageDirectory())!.path;
notifyListeners();
}
@ -357,4 +361,49 @@ class SettingsProvider with ChangeNotifier {
prefs?.setBool('highlightTouchTargets', val);
notifyListeners();
}
Future<Uri?> getExportDir() async {
var uriString = prefs?.getString('exportDir');
if (uriString != null) {
Uri? uri = Uri.parse(uriString);
if (!(await saf.canRead(uri) ?? false) ||
!(await saf.canWrite(uri) ?? false)) {
uri = null;
prefs?.remove('exportDir');
notifyListeners();
}
return uri;
} else {
return null;
}
}
Future<void> pickExportDir({bool remove = false}) async {
var existingSAFPerms = (await saf.persistedUriPermissions()) ?? [];
var currentOneWayDataSyncDir = await getExportDir();
Uri? newOneWayDataSyncDir;
if (!remove) {
newOneWayDataSyncDir = (await saf.openDocumentTree());
}
if (currentOneWayDataSyncDir?.path != newOneWayDataSyncDir?.path) {
if (newOneWayDataSyncDir == null) {
prefs?.remove('exportDir');
} else {
prefs?.setString('exportDir', newOneWayDataSyncDir.toString());
}
notifyListeners();
}
for (var e in existingSAFPerms) {
await saf.releasePersistableUriPermission(e.uri);
}
}
bool get autoExportOnChanges {
return prefs?.getBool('autoExportOnChanges') ?? false;
}
set autoExportOnChanges(bool val) {
prefs?.setBool('autoExportOnChanges', val);
notifyListeners();
}
}

View File

@ -521,6 +521,20 @@ regExValidator(String? value) {
return null;
}
intValidator(String? value, {bool positive = false}) {
if (value == null) {
return tr('invalidInput');
}
var num = int.tryParse(value);
if (num == null) {
return tr('invalidInput');
}
if (positive && num <= 0) {
return tr('invalidInput');
}
return null;
}
class SourceProvider {
// Add more source classes here so they are available via the service
List<AppSource> get sources => [

View File

@ -46,10 +46,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e"
sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e
url: "https://pub.dev"
source: hosted
version: "3.3.8"
version: "3.3.9"
args:
dependency: transitive
description:
@ -198,10 +198,10 @@ packages:
dependency: "direct main"
description:
name: dynamic_color
sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d
sha256: "96bff3df72e3d428bda2b874c7a521e8c86f592cae626ea594922fcc8d166e0c"
url: "https://pub.dev"
source: hosted
version: "1.6.6"
version: "1.6.7"
easy_localization:
dependency: "direct main"
description:
@ -320,10 +320,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: d4a1cb250c4e059586af0235f32e02882860a508e189b61f2b31b8810c1e1330
sha256: a10979814c5f4ddbe2b6143fba25d927599e21e3ba65b3862995960606fae78f
url: "https://pub.dev"
source: hosted
version: "0.6.17+2"
version: "0.6.17+3"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -538,18 +538,18 @@ packages:
dependency: "direct main"
description:
name: permission_handler
sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
sha256: ad65ba9af42a3d067203641de3fd9f547ded1410bad3b84400c2b4899faede70
url: "https://pub.dev"
source: hosted
version: "10.4.5"
version: "11.0.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
sha256: f23cfe9af0d49c6b9fd8a8b09f7b3301ca7e346204939b5afef4404d36d2608f
url: "https://pub.dev"
source: hosted
version: "10.3.6"
version: "11.0.1"
permission_handler_apple:
dependency: transitive
description:
@ -686,6 +686,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.1"
shared_storage:
dependency: "direct main"
description:
name: shared_storage
sha256: "7c65a9d64f0f5521256be974cfd74010af12196657cec9f9fb7b03b2f11bcaf6"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
sky_engine:
dependency: transitive
description: flutter
@ -879,10 +887,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_android
sha256: "9427774649fd3c8b7ff53523051395d13aed2ca355822b822e6493d79f5fc05a"
sha256: ddc167c6676f57c8b367d19fcbee267d6dc6adf81bd6c3cb87981d30746e0a6d
url: "https://pub.dev"
source: hosted
version: "3.10.0"
version: "3.10.1"
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.11+203 # When changing this, update the tag in main() accordingly
version: 0.14.17+209 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=3.0.0 <4.0.0'
@ -46,7 +46,7 @@ dependencies:
html: ^0.15.0
shared_preferences: ^2.0.15
url_launcher: ^6.1.5
permission_handler: ^10.0.0
permission_handler: ^11.0.0
fluttertoast: ^8.0.9
device_info_plus: ^9.0.0
file_picker: ^5.2.10
@ -65,6 +65,7 @@ dependencies:
flutter_archive: ^5.0.0
hsluv: ^1.1.3
connectivity_plus: ^4.0.2
shared_storage: ^0.8.0
dev_dependencies:
flutter_test: