mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Compare commits
17 Commits
v0.13.0-be
...
v0.13.2-be
Author | SHA1 | Date | |
---|---|---|---|
0cd4385de7 | |||
0774b3ddc3 | |||
b60b1ed058 | |||
b196715d60 | |||
0673e90dff | |||
59cfa242fb | |||
65ab72ba90 | |||
408bca8951 | |||
480467492a | |||
219b04aedb | |||
a0709856ef | |||
577642850f | |||
e1db024034 | |||
cc268aeeda | |||
d5f7eced8b | |||
cc3c4cc79f | |||
89b61884f1 |
@ -121,12 +121,12 @@
|
||||
"followSystem": "System folgen",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"useBlackTheme": "Verwende Pure Black Dark Theme",
|
||||
"appSortBy": "App sortieren nach",
|
||||
"authorName": "Autor/Name",
|
||||
"nameAuthor": "Name/Autor",
|
||||
"asAdded": "Wie hinzugefügt",
|
||||
"appSortOrder": "App Sortierung nach",
|
||||
"appSortOrder": "App sortieren nach",
|
||||
"ascending": "Aufsteigend",
|
||||
"descending": "Absteigend",
|
||||
"bgUpdateCheckInterval": "Prüfintervall für Hintergrundaktualisierung",
|
||||
@ -207,7 +207,7 @@
|
||||
"addCategory": "Kategorie hinzufügen",
|
||||
"label": "Bezeichnung",
|
||||
"language": "Sprache",
|
||||
"copiedToClipboard": "Copied to Clipboard",
|
||||
"copiedToClipboard": "In die Zwischenablage kopiert",
|
||||
"storagePermissionDenied": "Speicherberechtigung verweigert",
|
||||
"selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.",
|
||||
"filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern",
|
||||
@ -218,7 +218,7 @@
|
||||
"releaseDateAsVersionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert, aber ein Veröffentlichungsdatum verfügbar ist.",
|
||||
"changes": "Änderungen",
|
||||
"releaseDate": "Veröffentlichungsdatum",
|
||||
"importFromURLsInFile": "Importieren von URLs aus Datei ( z.B. OPML)",
|
||||
"importFromURLsInFile": "Importieren von URLs aus Datei (z. B. OPML)",
|
||||
"versionDetection": "Versionserkennung",
|
||||
"standardVersionDetection": "Standardversionserkennung",
|
||||
"groupByCategory": "Nach Kategorie gruppieren",
|
||||
@ -227,10 +227,11 @@
|
||||
"dontShowAgain": "Nicht noch einmal zeigen",
|
||||
"dontShowTrackOnlyWarnings": "Warnung für 'Nur Nachverfolgen' nicht anzeigen",
|
||||
"dontShowAPKOriginWarnings": "Warnung für APK-Herkunft nicht anzeigen",
|
||||
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||
"moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token (Aktiviert Suche)",
|
||||
"about": "Über",
|
||||
"requiresCredentialsInSettings": "Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
|
||||
"checkOnStart": "Überprüfe einmalig beim Start",
|
||||
"removeAppQuestion": {
|
||||
"one": "App entfernen?",
|
||||
"other": "Apps entfernen?"
|
||||
@ -257,7 +258,7 @@
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} Minute",
|
||||
"other": "{} Minutes"
|
||||
"other": "{} Minuten"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} Stunde",
|
||||
|
@ -121,7 +121,7 @@
|
||||
"followSystem": "Follow System",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"useBlackTheme": "Use Pure Black Dark Theme",
|
||||
"appSortBy": "App Sort By",
|
||||
"authorName": "Author/Name",
|
||||
"nameAuthor": "Name/Author",
|
||||
@ -231,6 +231,7 @@
|
||||
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Check Once on Start",
|
||||
"removeAppQuestion": {
|
||||
"one": "Remove App?",
|
||||
"other": "Remove Apps?"
|
||||
|
@ -231,6 +231,7 @@
|
||||
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Check Once on Start",
|
||||
"removeAppQuestion": {
|
||||
"one": "¿Eliminar Aplicación?",
|
||||
"other": "¿Eliminar Aplicaciones?"
|
||||
|
@ -231,6 +231,7 @@
|
||||
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Check Once on Start",
|
||||
"removeAppQuestion": {
|
||||
"one": "برنامه حذف شود؟",
|
||||
"other": "برنامه ها حذف شوند؟"
|
||||
|
@ -121,7 +121,7 @@
|
||||
"followSystem": "Suivre le système",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"useBlackTheme": "Use Pure Black Dark Theme",
|
||||
"appSortBy": "Applications triées par",
|
||||
"authorName": "Auteur/Nom",
|
||||
"nameAuthor": "Nom/Auteur",
|
||||
@ -231,6 +231,7 @@
|
||||
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Check Once on Start",
|
||||
"removeAppQuestion": {
|
||||
"one": "Supprimer l'application ?",
|
||||
"other": "Supprimer les applications ?"
|
||||
|
@ -230,6 +230,7 @@
|
||||
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Check Once on Start",
|
||||
"removeAppQuestion": {
|
||||
"one": "Eltávolítja az alkalmazást?",
|
||||
"other": "Eltávolítja az alkalmazást?"
|
||||
|
@ -121,7 +121,7 @@
|
||||
"followSystem": "Segui sistema",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Use pure black dark theme",
|
||||
"useBlackTheme": "Use Pure Black Dark Theme",
|
||||
"appSortBy": "App ordinate per",
|
||||
"authorName": "Autore/Nome",
|
||||
"nameAuthor": "Nome/Autore",
|
||||
@ -231,6 +231,7 @@
|
||||
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Check Once on Start",
|
||||
"removeAppQuestion": {
|
||||
"one": "Rimuovere l'App?",
|
||||
"other": "Rimuovere le App?"
|
||||
|
@ -227,10 +227,11 @@
|
||||
"dontShowAgain": "二度と表示しない",
|
||||
"dontShowTrackOnlyWarnings": "「追跡のみ」の警告を表示しない",
|
||||
"dontShowAPKOriginWarnings": "APK Originの警告を表示しない",
|
||||
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||
"moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる",
|
||||
"gitlabPATLabel": "GitLab パーソナルアクセストークン (検索を有効化する)",
|
||||
"about": "概要",
|
||||
"requiresCredentialsInSettings": "これには追加の認証が必要です (設定にて)",
|
||||
"checkOnStart": "Check Once on Start",
|
||||
"removeAppQuestion": {
|
||||
"one": "アプリを削除しますか?",
|
||||
"other": "アプリを削除しますか?"
|
||||
|
@ -33,7 +33,7 @@
|
||||
"githubStarredRepos": "GitHub 已星标仓库",
|
||||
"uname": "用户名",
|
||||
"wrongArgNum": "参数数量错误",
|
||||
"xIsTrackOnly": "{} 为“仅追踪”模式",
|
||||
"xIsTrackOnly": "{}为“仅追踪”模式",
|
||||
"source": "源代码",
|
||||
"app": "应用",
|
||||
"appsFromSourceAreTrackOnly": "此来源的应用为“仅追踪”模式。",
|
||||
@ -50,8 +50,8 @@
|
||||
"search": "搜索",
|
||||
"additionalOptsFor": "{} 的更多选项",
|
||||
"supportedSourcesBelow": "支持的来源:",
|
||||
"trackOnlyInBrackets": "(仅追踪)",
|
||||
"searchableInBrackets": "(可搜索)",
|
||||
"trackOnlyInBrackets": "(仅追踪)",
|
||||
"searchableInBrackets": "(可搜索)",
|
||||
"appsString": "应用列表",
|
||||
"noApps": "无应用",
|
||||
"noAppsForFilter": "没有符合条件的应用",
|
||||
@ -59,9 +59,9 @@
|
||||
"percentProgress": "进度:{}%",
|
||||
"pleaseWait": "请稍候",
|
||||
"updateAvailable": "更新可用",
|
||||
"estimateInBracketsShort": "(预计)",
|
||||
"estimateInBracketsShort": "(推测)",
|
||||
"notInstalled": "未安装",
|
||||
"estimateInBrackets": "(预计)",
|
||||
"estimateInBrackets": "(推测)",
|
||||
"selectAll": "全选",
|
||||
"deselectN": "取消选择 {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
|
||||
@ -74,8 +74,8 @@
|
||||
"installUpdateApps": "安装/更新应用",
|
||||
"installUpdateSelectedApps": "安装/更新选中的应用",
|
||||
"markXSelectedAppsAsUpdated": "是否将选中的 {} 个应用标记为已更新?",
|
||||
"no": "不要",
|
||||
"yes": "好的",
|
||||
"no": "否",
|
||||
"yes": "是",
|
||||
"markSelectedAppsUpdated": "将选中的应用标记为已更新",
|
||||
"pinToTop": "置顶",
|
||||
"unpinFromTop": "取消置顶",
|
||||
@ -142,7 +142,7 @@
|
||||
"close": "关闭",
|
||||
"share": "分享",
|
||||
"appNotFound": "未找到应用",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-导出",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"pickAnAPK": "选择一个 APK 文件",
|
||||
"appHasMoreThanOnePackage": "{} 有多个架构可用:",
|
||||
"deviceSupportsXArch": "您的设备支持 {} 架构。",
|
||||
@ -172,13 +172,13 @@
|
||||
"versionCorrectionDisabled": "禁用版本号更正(插件似乎未起作用)",
|
||||
"unknown": "未知",
|
||||
"none": "无",
|
||||
"never": "从不",
|
||||
"never": "从未",
|
||||
"latestVersionX": "最新版本:{}",
|
||||
"installedVersionX": "当前版本:{}",
|
||||
"lastUpdateCheckX": "上次更新检查:{}",
|
||||
"remove": "删除",
|
||||
"yesMarkUpdated": "是的,标记为已更新",
|
||||
"fdroid": "F-Droid Official",
|
||||
"yesMarkUpdated": "是,标记为已更新",
|
||||
"fdroid": "F-Droid 官方存储库",
|
||||
"appIdOrName": "应用 ID 或名称",
|
||||
"appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用",
|
||||
"reposHaveMultipleApps": "存储库中可能包含多个应用",
|
||||
@ -193,7 +193,7 @@
|
||||
"additionalOptions": "附加选项",
|
||||
"disableVersionDetection": "禁用版本检测",
|
||||
"noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。",
|
||||
"downloadingX": "正在下载 {}",
|
||||
"downloadingX": "正在下载{}",
|
||||
"downloadNotifDescription": "提示应用的下载进度",
|
||||
"noAPKFound": "未找到 APK 文件",
|
||||
"noVersionDetection": "禁用版本检测",
|
||||
@ -222,15 +222,16 @@
|
||||
"versionDetection": "版本检测",
|
||||
"standardVersionDetection": "常规版本检测",
|
||||
"groupByCategory": "按类别分组显示",
|
||||
"autoApkFilterByArch": "如果可能,尝试按 CPU 架构筛选 APK 文件",
|
||||
"overrideSource": "Override Source",
|
||||
"dontShowAgain": "Don't show this again",
|
||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
||||
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||
"autoApkFilterByArch": "如果可能,尝试按设备支持的 CPU 架构筛选 APK 文件",
|
||||
"overrideSource": "覆盖来源",
|
||||
"dontShowAgain": "不再显示",
|
||||
"dontShowTrackOnlyWarnings": "不显示“仅追踪”模式警告",
|
||||
"dontShowAPKOriginWarnings": "不显示 APK 文件来源警告",
|
||||
"moveNonInstalledAppsToBottom": "将未安装应用置底",
|
||||
"gitlabPATLabel": "GitLab 个人访问令牌(用于搜索)",
|
||||
"about": "相关文档",
|
||||
"requiresCredentialsInSettings": "此功能需要额外的凭据(在“设置”中添加)",
|
||||
"checkOnStart": "启动时进行一次检查",
|
||||
"removeAppQuestion": {
|
||||
"one": "是否删除应用?",
|
||||
"other": "是否删除应用?"
|
||||
|
@ -57,9 +57,9 @@ class APKPure extends AppSource {
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
|
||||
List<MapEntry<String, String>> apkUrls = [
|
||||
MapEntry('$appId.apk', 'https://d.$host/b/APK/$appId?version=latest')
|
||||
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
|
||||
];
|
||||
String author = html
|
||||
.querySelector('span.info-sdk')
|
||||
|
@ -89,6 +89,13 @@ class HTML extends AppSource {
|
||||
overrideEligible = true;
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement requestHeaders choice, hardcoded for now
|
||||
Map<String, String>? get requestHeaders => {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
|
||||
};
|
||||
|
||||
@override
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
return url;
|
||||
|
@ -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.13.0';
|
||||
const String currentVersion = '0.13.2';
|
||||
const String currentReleaseTag =
|
||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||
|
||||
|
@ -159,9 +159,16 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
app.preferredApkIndex =
|
||||
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
|
||||
// ignore: use_build_context_synchronously
|
||||
var downloadedApk = await appsProvider.downloadApp(
|
||||
var downloadedArtifact = await appsProvider.downloadApp(
|
||||
app, globalNavigatorKey.currentContext);
|
||||
app.id = downloadedApk.appId;
|
||||
DownloadedApk? downloadedFile;
|
||||
DownloadedXApkDir? downloadedDir;
|
||||
if (downloadedArtifact is DownloadedApk) {
|
||||
downloadedFile = downloadedArtifact;
|
||||
} else {
|
||||
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||
}
|
||||
app.id = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||
}
|
||||
if (appsProvider.apps.containsKey(app.id)) {
|
||||
throw ObtainiumError(tr('appAlreadyAdded'));
|
||||
@ -276,6 +283,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
}
|
||||
si++;
|
||||
}
|
||||
if (res.isEmpty) {
|
||||
throw ObtainiumError(tr('noResults'));
|
||||
}
|
||||
List<String>? selectedUrls = res.isEmpty
|
||||
? []
|
||||
// ignore: use_build_context_synchronously
|
||||
@ -370,13 +380,15 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: searchQuery.isEmpty || doingSomething
|
||||
? null
|
||||
: () {
|
||||
runSearch();
|
||||
},
|
||||
child: Text(tr('search')))
|
||||
searching
|
||||
? const CircularProgressIndicator()
|
||||
: ElevatedButton(
|
||||
onPressed: searchQuery.isEmpty || doingSomething
|
||||
? null
|
||||
: () {
|
||||
runSearch();
|
||||
},
|
||||
child: Text(tr('search')))
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -444,7 +444,9 @@ class _AppPageState extends State<AppPage> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
||||
child: LinearProgressIndicator(
|
||||
value: app!.downloadProgress! / 100))
|
||||
value: app!.downloadProgress! >= 0
|
||||
? app.downloadProgress! / 100
|
||||
: null))
|
||||
],
|
||||
));
|
||||
|
||||
|
@ -52,6 +52,9 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
|
||||
GlobalKey<RefreshIndicatorState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var appsProvider = context.watch<AppsProvider>();
|
||||
@ -61,6 +64,27 @@ class AppsPageState extends State<AppsPage> {
|
||||
var currentFilterIsUpdatesOnly =
|
||||
filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
|
||||
|
||||
refresh() {
|
||||
HapticFeedback.lightImpact();
|
||||
setState(() {
|
||||
refreshingSince = DateTime.now();
|
||||
});
|
||||
return appsProvider.checkUpdates().catchError((e) {
|
||||
showError(e, context);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
refreshingSince = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!appsProvider.loadingApps &&
|
||||
appsProvider.apps.isNotEmpty &&
|
||||
settingsProvider.checkJustStarted() &&
|
||||
settingsProvider.checkOnStart) {
|
||||
_refreshIndicatorKey.currentState?.show();
|
||||
}
|
||||
|
||||
selectedAppIds = selectedAppIds
|
||||
.where((element) => listedApps.map((e) => e.app.id).contains(element))
|
||||
.toSet();
|
||||
@ -315,7 +339,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
?.isBefore(refreshingSince!) ??
|
||||
true))
|
||||
.length /
|
||||
appsProvider.apps.length,
|
||||
(appsProvider.apps.isNotEmpty ? appsProvider.apps.length : 1),
|
||||
),
|
||||
)
|
||||
];
|
||||
@ -515,10 +539,16 @@ class AppsPageState extends State<AppsPage> {
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal)),
|
||||
trailing: listedApps[index].downloadProgress != null
|
||||
? Text(tr('percentProgress', args: [
|
||||
listedApps[index].downloadProgress?.toInt().toString() ??
|
||||
'100'
|
||||
]))
|
||||
? SizedBox(
|
||||
width: 110,
|
||||
child: Text(tr('percentProgress', args: [
|
||||
listedApps[index].downloadProgress! >= 0
|
||||
? listedApps[index]
|
||||
.downloadProgress!
|
||||
.toInt()
|
||||
.toString()
|
||||
: tr('pleaseWait')
|
||||
])))
|
||||
: trailingRow,
|
||||
onTap: () {
|
||||
if (selectedAppIds.isNotEmpty) {
|
||||
@ -1017,19 +1047,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
HapticFeedback.lightImpact();
|
||||
setState(() {
|
||||
refreshingSince = DateTime.now();
|
||||
});
|
||||
return appsProvider.checkUpdates().catchError((e) {
|
||||
showError(e, context);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
refreshingSince = null;
|
||||
});
|
||||
});
|
||||
},
|
||||
key: _refreshIndicatorKey,
|
||||
onRefresh: refresh,
|
||||
child: CustomScrollView(slivers: <Widget>[
|
||||
CustomAppBar(title: tr('appsString')),
|
||||
...getLoadingWidgets(),
|
||||
|
@ -228,6 +228,18 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
intervalDropdown,
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(child: Text(tr('checkOnStart'))),
|
||||
Switch(
|
||||
value: settingsProvider.checkOnStart,
|
||||
onChanged: (value) {
|
||||
settingsProvider.checkOnStart = value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height32,
|
||||
Text(
|
||||
tr('sourceSpecific'),
|
||||
|
@ -27,6 +27,7 @@ import 'package:flutter_fgbg/flutter_fgbg.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:android_intent_plus/android_intent.dart';
|
||||
import 'package:archive/archive.dart';
|
||||
|
||||
class AppInMemory {
|
||||
late App app;
|
||||
@ -46,6 +47,13 @@ class DownloadedApk {
|
||||
DownloadedApk(this.appId, this.file);
|
||||
}
|
||||
|
||||
class DownloadedXApkDir {
|
||||
String appId;
|
||||
File file;
|
||||
Directory extracted;
|
||||
DownloadedXApkDir(this.appId, this.file, this.extracted);
|
||||
}
|
||||
|
||||
List<String> generateStandardVersionRegExStrings() {
|
||||
// TODO: Look into RegEx for non-Latin characters / non-Arabic numerals
|
||||
var basics = [
|
||||
@ -114,25 +122,34 @@ class AppsProvider with ChangeNotifier {
|
||||
// Load Apps into memory (in background, this is done later instead of in the constructor)
|
||||
await loadApps();
|
||||
// Delete any partial APKs
|
||||
var cutoff = DateTime.now().subtract(const Duration(days: 7));
|
||||
(await getExternalCacheDirectories())
|
||||
?.first
|
||||
.listSync()
|
||||
.where((element) => element.path.endsWith('.apk.part'))
|
||||
.where((element) =>
|
||||
element.path.endsWith('.part') ||
|
||||
element.statSync().modified.isBefore(cutoff))
|
||||
.forEach((partialApk) {
|
||||
partialApk.delete();
|
||||
});
|
||||
}();
|
||||
}
|
||||
|
||||
downloadFile(String url, String fileName, Function? onProgress,
|
||||
Future<File> downloadFile(
|
||||
String url, String fileNameNoExt, Function? onProgress,
|
||||
{bool useExisting = true, Map<String, String>? headers}) async {
|
||||
var destDir = (await getExternalCacheDirectories())!.first.path;
|
||||
var req = Request('GET', Uri.parse(url));
|
||||
if (headers != null) {
|
||||
req.headers.addAll(headers);
|
||||
}
|
||||
StreamedResponse response = await Client().send(req);
|
||||
File downloadedFile = File('$destDir/$fileName');
|
||||
var client = Client();
|
||||
StreamedResponse response = await client.send(req);
|
||||
var ext = response.headers['content-disposition']!.split('.').last;
|
||||
if (ext.endsWith('"') || ext.endsWith("other")) {
|
||||
ext = ext.substring(0, ext.length - 1);
|
||||
}
|
||||
File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
|
||||
if (!(downloadedFile.existsSync() && useExisting)) {
|
||||
File tempDownloadedFile = File('${downloadedFile.path}.part');
|
||||
if (tempDownloadedFile.existsSync()) {
|
||||
@ -160,11 +177,34 @@ class AppsProvider with ChangeNotifier {
|
||||
throw response.reasonPhrase ?? tr('unexpectedError');
|
||||
}
|
||||
tempDownloadedFile.renameSync(downloadedFile.path);
|
||||
} else {
|
||||
client.close();
|
||||
}
|
||||
return downloadedFile;
|
||||
}
|
||||
|
||||
Future<DownloadedApk> downloadApp(App app, BuildContext? context) async {
|
||||
Future<File> handleAPKIDChange(App app, PackageArchiveInfo newInfo,
|
||||
File downloadedFile, String downloadUrl) async {
|
||||
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
|
||||
// The former case should be handled (give the App its real ID), the latter is a security issue
|
||||
if (app.id != newInfo.packageName) {
|
||||
var isTempId = SourceProvider().isTempId(app);
|
||||
if (apps[app.id] != null && !isTempId) {
|
||||
throw IDChangedError();
|
||||
}
|
||||
var originalAppId = app.id;
|
||||
app.id = newInfo.packageName;
|
||||
downloadedFile = downloadedFile.renameSync(
|
||||
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}');
|
||||
if (apps[originalAppId] != null) {
|
||||
await removeApps([originalAppId]);
|
||||
await saveApps([app], onlyIfExists: !isTempId);
|
||||
}
|
||||
}
|
||||
return downloadedFile;
|
||||
}
|
||||
|
||||
Future<Object> downloadApp(App app, BuildContext? context) async {
|
||||
NotificationsProvider? notificationsProvider =
|
||||
context?.read<NotificationsProvider>();
|
||||
var notifId = DownloadNotification(app.finalName, 0).id;
|
||||
@ -177,11 +217,11 @@ class AppsProvider with ChangeNotifier {
|
||||
.getSource(app.url, overrideSource: app.overrideSource);
|
||||
String downloadUrl = await source.apkUrlPrefetchModifier(
|
||||
app.apkUrls[app.preferredApkIndex].value, app.url);
|
||||
var fileName = '${app.id}-${downloadUrl.hashCode}.apk';
|
||||
var notif = DownloadNotification(app.finalName, 100);
|
||||
notificationsProvider?.cancel(notif.id);
|
||||
int? prevProg;
|
||||
File downloadedFile = await downloadFile(downloadUrl, fileName,
|
||||
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
|
||||
var downloadedFile = await downloadFile(downloadUrl, fileNameNoExt,
|
||||
headers: source.requestHeaders, (double? progress) {
|
||||
int? prog = progress?.ceil();
|
||||
if (apps[app.id] != null) {
|
||||
@ -194,33 +234,45 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
prevProg = prog;
|
||||
});
|
||||
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
|
||||
// The former case should be handled (give the App its real ID), the latter is a security issue
|
||||
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
||||
if (app.id != newInfo.packageName) {
|
||||
var isTempId = SourceProvider().isTempId(app);
|
||||
if (apps[app.id] != null && !isTempId) {
|
||||
throw IDChangedError();
|
||||
}
|
||||
var originalAppId = app.id;
|
||||
app.id = newInfo.packageName;
|
||||
downloadedFile = downloadedFile.renameSync(
|
||||
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.apk');
|
||||
if (apps[originalAppId] != null) {
|
||||
await removeApps([originalAppId]);
|
||||
await saveApps([app], onlyIfExists: !isTempId);
|
||||
}
|
||||
// Set to 90 for remaining steps, will make null in 'finally'
|
||||
if (apps[app.id] != null) {
|
||||
apps[app.id]!.downloadProgress = -1;
|
||||
notifyListeners();
|
||||
notif = DownloadNotification(app.finalName, -1);
|
||||
notificationsProvider?.notify(notif);
|
||||
}
|
||||
// Delete older versions of the APK if any
|
||||
PackageArchiveInfo? newInfo;
|
||||
var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk');
|
||||
Directory? xapkDir;
|
||||
if (isAPK) {
|
||||
newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
||||
} else {
|
||||
// Assume XAPK
|
||||
String xapkDirPath = '${downloadedFile.path}-dir';
|
||||
unzipFile(downloadedFile.path, '${downloadedFile.path}-dir');
|
||||
xapkDir = Directory(xapkDirPath);
|
||||
var apks = xapkDir
|
||||
.listSync()
|
||||
.where((e) => e.path.toLowerCase().endsWith('.apk'))
|
||||
.toList();
|
||||
newInfo = await PackageArchiveInfo.fromPath(apks.first.path);
|
||||
}
|
||||
downloadedFile =
|
||||
await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl);
|
||||
// Delete older versions of the file if any
|
||||
for (var file in downloadedFile.parent.listSync()) {
|
||||
var fn = file.path.split('/').last;
|
||||
if (fn.startsWith('${app.id}-') &&
|
||||
fn.endsWith('.apk') &&
|
||||
fn != downloadedFile.path.split('/').last) {
|
||||
FileSystemEntity.isFileSync(file.path) &&
|
||||
file.path != downloadedFile.path) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
return DownloadedApk(app.id, downloadedFile);
|
||||
if (isAPK) {
|
||||
return DownloadedApk(app.id, downloadedFile);
|
||||
} else {
|
||||
return DownloadedXApkDir(app.id, downloadedFile, xapkDir!);
|
||||
}
|
||||
} finally {
|
||||
notificationsProvider?.cancel(notifId);
|
||||
if (apps[app.id] != null) {
|
||||
@ -267,11 +319,43 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Unfortunately this 'await' does not actually wait for the APK to finish installing
|
||||
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
|
||||
// If appropriate criteria are met, the update (never a fresh install) happens silently in the background
|
||||
// But even then, we don't know if it actually succeeded
|
||||
Future<void> installApk(DownloadedApk file, {bool silent = false}) async {
|
||||
void unzipFile(String filePath, String destinationPath) {
|
||||
final bytes = File(filePath).readAsBytesSync();
|
||||
final archive = ZipDecoder().decodeBytes(bytes);
|
||||
|
||||
for (final file in archive) {
|
||||
final filename = '$destinationPath/${file.name}';
|
||||
if (file.isFile) {
|
||||
final data = file.content as List<int>;
|
||||
File(filename)
|
||||
..createSync(recursive: true)
|
||||
..writeAsBytesSync(data);
|
||||
} else {
|
||||
Directory(filename).create(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> installXApkDir(DownloadedXApkDir dir,
|
||||
{bool silent = false}) async {
|
||||
try {
|
||||
var somethingInstalled = false;
|
||||
for (var apk in dir.extracted
|
||||
.listSync()
|
||||
.where((f) => f is File && f.path.toLowerCase().endsWith('.apk'))) {
|
||||
somethingInstalled = somethingInstalled ||
|
||||
await installApk(DownloadedApk(dir.appId, apk as File),
|
||||
silent: silent);
|
||||
}
|
||||
if (somethingInstalled) {
|
||||
dir.file.delete();
|
||||
}
|
||||
} finally {
|
||||
dir.extracted.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> installApk(DownloadedApk file, {bool silent = false}) async {
|
||||
// TODO: Use 'silent' when/if ever possible
|
||||
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
|
||||
AppInfo? appInfo;
|
||||
@ -287,14 +371,17 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
int? code =
|
||||
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
|
||||
bool installed = false;
|
||||
if (code != null && code != 0 && code != 3) {
|
||||
throw InstallError(code);
|
||||
} else if (code == 0) {
|
||||
installed = true;
|
||||
apps[file.appId]!.app.installedVersion =
|
||||
apps[file.appId]!.app.latestVersion;
|
||||
file.file.delete();
|
||||
}
|
||||
await saveApps([apps[file.appId]!.app]);
|
||||
return installed;
|
||||
}
|
||||
|
||||
void uninstallApp(String appId) async {
|
||||
@ -420,9 +507,16 @@ class AppsProvider with ChangeNotifier {
|
||||
for (var id in appsToInstall) {
|
||||
try {
|
||||
// ignore: use_build_context_synchronously
|
||||
var downloadedFile = await downloadApp(apps[id]!.app, context);
|
||||
bool willBeSilent =
|
||||
await canInstallSilently(apps[downloadedFile.appId]!.app);
|
||||
var downloadedArtifact = await downloadApp(apps[id]!.app, context);
|
||||
DownloadedApk? downloadedFile;
|
||||
DownloadedXApkDir? downloadedDir;
|
||||
if (downloadedArtifact is DownloadedApk) {
|
||||
downloadedFile = downloadedArtifact;
|
||||
} else {
|
||||
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||
}
|
||||
bool willBeSilent = await canInstallSilently(
|
||||
apps[downloadedFile?.appId ?? downloadedDir!.appId]!.app);
|
||||
willBeSilent = false; // TODO: Remove this when silent updates work
|
||||
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
|
||||
true)) {
|
||||
@ -432,7 +526,18 @@ class AppsProvider with ChangeNotifier {
|
||||
// ignore: use_build_context_synchronously
|
||||
await waitForUserToReturnToForeground(context);
|
||||
}
|
||||
await installApk(downloadedFile, silent: willBeSilent);
|
||||
apps[id]?.downloadProgress = -1;
|
||||
notifyListeners();
|
||||
try {
|
||||
if (downloadedFile != null) {
|
||||
await installApk(downloadedFile, silent: willBeSilent);
|
||||
} else {
|
||||
await installXApkDir(downloadedDir!, silent: willBeSilent);
|
||||
}
|
||||
} finally {
|
||||
apps[id]?.downloadProgress = null;
|
||||
notifyListeners();
|
||||
}
|
||||
installedIds.add(id);
|
||||
} catch (e) {
|
||||
errors.add(id, e.toString());
|
||||
@ -684,11 +789,18 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> removeApps(List<String> appIds) async {
|
||||
var apkFiles = (await getExternalCacheDirectories())?.first.listSync();
|
||||
for (var appId in appIds) {
|
||||
File file = File('${(await getAppsDir()).path}/$appId.json');
|
||||
if (file.existsSync()) {
|
||||
file.deleteSync();
|
||||
}
|
||||
apkFiles
|
||||
?.where(
|
||||
(element) => element.path.split('/').last.startsWith('$appId-'))
|
||||
.forEach((element) {
|
||||
element.delete();
|
||||
});
|
||||
if (apps.containsKey(appId)) {
|
||||
apps.remove(appId);
|
||||
}
|
||||
@ -734,7 +846,7 @@ class AppsProvider with ChangeNotifier {
|
||||
apps[i].installedVersion = null;
|
||||
}
|
||||
}
|
||||
await saveApps(apps, attemptToCorrectInstallStatus: !remove);
|
||||
await saveApps(apps, attemptToCorrectInstallStatus: false);
|
||||
}
|
||||
if (remove) {
|
||||
await removeApps(apps.map((e) => e.id).toList());
|
||||
|
@ -167,7 +167,8 @@ class NotificationsProvider {
|
||||
progress: progPercent ?? 0,
|
||||
maxProgress: 100,
|
||||
showProgress: progPercent != null,
|
||||
onlyAlertOnce: onlyAlertOnce)));
|
||||
onlyAlertOnce: onlyAlertOnce,
|
||||
indeterminate: progPercent != null && progPercent < 0)));
|
||||
}
|
||||
|
||||
Future<void> notify(ObtainiumNotification notif,
|
||||
|
@ -35,6 +35,7 @@ List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0]
|
||||
|
||||
class SettingsProvider with ChangeNotifier {
|
||||
SharedPreferences? prefs;
|
||||
bool justStarted = true;
|
||||
|
||||
String sourceUrl = 'https://github.com/ImranR98/Obtainium';
|
||||
|
||||
@ -92,6 +93,15 @@ class SettingsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get checkOnStart {
|
||||
return prefs?.getBool('checkOnStart') ?? false;
|
||||
}
|
||||
|
||||
set checkOnStart(bool checkOnStart) {
|
||||
prefs?.setBool('checkOnStart', checkOnStart);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
SortColumnSettings get sortColumn {
|
||||
return SortColumnSettings.values[
|
||||
prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index];
|
||||
@ -120,6 +130,14 @@ class SettingsProvider with ChangeNotifier {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool checkJustStarted() {
|
||||
if (justStarted) {
|
||||
justStarted = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> getInstallPermission({bool enforce = false}) async {
|
||||
while (!(await Permission.requestInstallPackages.isGranted)) {
|
||||
// Explicit request as InstallPlugin request sometimes bugged
|
||||
|
44
pubspec.lock
44
pubspec.lock
@ -34,6 +34,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
archive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: archive
|
||||
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.7"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -82,6 +90,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -410,10 +426,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4
|
||||
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.14"
|
||||
version: "2.0.15"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -426,10 +442,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183
|
||||
sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.2.3"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -518,6 +534,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.3"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -554,10 +578,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b"
|
||||
sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -570,10 +594,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07"
|
||||
sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.2"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -623,10 +647,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: acf091c6e55c50d00b30b8532b2dd23e393cf775861665ebd0f15cdd6ebfb079
|
||||
sha256: "3a82c9a216b46b88617e3714dd74227eaca20c501c4abcc213e56db26b9caa00"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.8+1"
|
||||
version: "2.2.8+2"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -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.13.0+164 # When changing this, update the tag in main() accordingly
|
||||
version: 0.13.2+166 # When changing this, update the tag in main() accordingly
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.2 <3.0.0'
|
||||
@ -63,6 +63,7 @@ dependencies:
|
||||
easy_localization: ^3.0.1
|
||||
android_intent_plus: ^3.1.5
|
||||
flutter_markdown: ^0.6.14
|
||||
archive: ^3.3.7
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
Reference in New Issue
Block a user