mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Compare commits
73 Commits
v0.14.35-b
...
v0.14.41-b
Author | SHA1 | Date | |
---|---|---|---|
45fa0a165a | |||
0e5c07a078 | |||
601a742c71 | |||
c972401b6e | |||
024e81cf01 | |||
975ed402d5 | |||
b9e8083744 | |||
bb859708bc | |||
3cf2c221ac | |||
6edd7edcd2 | |||
4e26a02d78 | |||
bb36a57053 | |||
b291c800f1 | |||
b63a798d86 | |||
eacf3777a4 | |||
a5a7436bb1 | |||
2a4cc35df7 | |||
cdccf58b76 | |||
27300383a1 | |||
375b9bce30 | |||
b6b8db48df | |||
36e6c267b9 | |||
13066b3b4a | |||
ccbe9d00c8 | |||
ce291582cb | |||
bb37bc3b51 | |||
5a7747acd1 | |||
1bc2ec9461 | |||
2b977fc2b0 | |||
de60c4ee9e | |||
de67e40c00 | |||
cc4b016c64 | |||
f64f561d6f | |||
80bddf8a6b | |||
cbaaec961c | |||
5477b3f936 | |||
fd59a93ede | |||
cd316b7138 | |||
d1955192ed | |||
9beb839bf4 | |||
29ea303093 | |||
feff6751ca | |||
ca33fdf752 | |||
fdcdfe89d6 | |||
48ed2115a7 | |||
65988f4e08 | |||
ede65eda6c | |||
5da56acac8 | |||
5720c55301 | |||
ffefa4b30e | |||
80e4986b23 | |||
dc92ccda0a | |||
f9bab18076 | |||
2dec52e221 | |||
7413f693d7 | |||
415460df75 | |||
125a194468 | |||
32e9afbf36 | |||
3eca704f4a | |||
9c95129311 | |||
bf34c1bcdb | |||
284c687d77 | |||
09afb5a3f5 | |||
0138721451 | |||
2d5f610941 | |||
864fa7762b | |||
4fde38ee6a | |||
6cdf0f10d4 | |||
b66592c25f | |||
43616c566d | |||
62f1dc17a0 | |||
0e9a8a937a | |||
9a86b245ce |
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -9,6 +9,7 @@ assignees: ''
|
||||
|
||||
**Prerequisites**
|
||||
<!-- Please ensure your request is not part of an existing issue. -->
|
||||
<!-- Please ensure you have checked the Obtainium Wiki. -->
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -9,6 +9,7 @@ assignees: ''
|
||||
|
||||
**Prerequisites**
|
||||
<!-- Please ensure your request is not part of an existing issue. -->
|
||||
<!-- Please ensure you have checked the Obtainium Wiki. -->
|
||||
|
||||
**Describe the feature**
|
||||
<!-- A clear and concise description of what you want to happen.
|
||||
|
@ -6,6 +6,8 @@ Obtainium allows you to install and update Apps directly from their releases pag
|
||||
|
||||
Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0)
|
||||
|
||||
Wiki: [https://github.com/ImranR98/Obtainium/wiki](https://github.com/ImranR98/Obtainium/wiki)
|
||||
|
||||
Currently supported App sources:
|
||||
- Open Source - General:
|
||||
- [GitHub](https://github.com/)
|
||||
|
@ -23,6 +23,7 @@ if (flutterVersionName == null) {
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'dev.rikka.tools.refine'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
@ -32,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdkVersion 34
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
@ -52,8 +53,8 @@ android {
|
||||
applicationId "dev.imranr.obtainium"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
@ -90,6 +91,24 @@ flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
def shizuku_version = '13.1.5'
|
||||
implementation "dev.rikka.shizuku:api:$shizuku_version"
|
||||
implementation "dev.rikka.shizuku:provider:$shizuku_version"
|
||||
|
||||
def hidden_api_version = '4.1.0'
|
||||
// DO NOT UPDATE Hidden API without updating the Android tools
|
||||
// and do not update Android tools without updating the whole Flutter
|
||||
// (also in android/build.gradle)
|
||||
implementation "dev.rikka.tools.refine:runtime:$hidden_api_version"
|
||||
implementation "dev.rikka.hidden:compat:$hidden_api_version"
|
||||
compileOnly "dev.rikka.hidden:stub:$hidden_api_version"
|
||||
|
||||
implementation "com.github.topjohnwu.libsu:core:5.2.2"
|
||||
}
|
||||
|
@ -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,31 @@
|
||||
</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>
|
||||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
android:multiprocess="false"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
</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>
|
@ -1,6 +0,0 @@
|
||||
package dev.imranr.obtainium
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
171
android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt
Normal file
171
android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt
Normal file
@ -0,0 +1,171 @@
|
||||
package dev.imranr.obtainium
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.content.pm.IPackageInstaller
|
||||
import android.content.pm.IPackageInstallerSession
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import androidx.annotation.NonNull
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import dev.imranr.obtainium.util.IIntentSenderAdaptor
|
||||
import dev.imranr.obtainium.util.IntentSenderUtils
|
||||
import dev.imranr.obtainium.util.PackageInstallerUtils
|
||||
import dev.imranr.obtainium.util.ShizukuSystemServerApi
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import rikka.shizuku.Shizuku
|
||||
import rikka.shizuku.Shizuku.OnRequestPermissionResultListener
|
||||
import rikka.shizuku.ShizukuBinderWrapper
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
private var installersChannel: MethodChannel? = null
|
||||
private val SHIZUKU_PERMISSION_REQUEST_CODE = (10..200).random()
|
||||
|
||||
private fun shizukuCheckPermission(result: Result) {
|
||||
try {
|
||||
if (Shizuku.isPreV11()) { // Unsupported
|
||||
result.success(-1)
|
||||
} else if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
|
||||
result.success(1)
|
||||
} else if (Shizuku.shouldShowRequestPermissionRationale()) { // Deny and don't ask again
|
||||
result.success(0)
|
||||
} else {
|
||||
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||
result.success(-2)
|
||||
}
|
||||
} catch (_: Exception) { // If shizuku not running
|
||||
result.success(-1)
|
||||
}
|
||||
}
|
||||
|
||||
private val shizukuRequestPermissionResultListener = OnRequestPermissionResultListener {
|
||||
requestCode: Int, grantResult: Int ->
|
||||
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
|
||||
val res = if (grantResult == PackageManager.PERMISSION_GRANTED) 1 else 0
|
||||
installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to res))
|
||||
}
|
||||
}
|
||||
|
||||
private fun shizukuInstallApk(apkFileUri: String, result: Result) {
|
||||
val uri = Uri.parse(apkFileUri)
|
||||
var res = false
|
||||
var session: PackageInstaller.Session? = null
|
||||
try {
|
||||
val iPackageInstaller: IPackageInstaller =
|
||||
ShizukuSystemServerApi.PackageManager_getPackageInstaller()
|
||||
val isRoot = Shizuku.getUid() == 0
|
||||
// The reason for use "com.android.shell" as installer package under adb
|
||||
// is that getMySessions will check installer package's owner
|
||||
val installerPackageName = if (isRoot) packageName else "com.android.shell"
|
||||
var installerAttributionTag: String? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
installerAttributionTag = attributionTag
|
||||
}
|
||||
val userId = if (isRoot) Process.myUserHandle().hashCode() else 0
|
||||
val packageInstaller = PackageInstallerUtils.createPackageInstaller(
|
||||
iPackageInstaller, installerPackageName, installerAttributionTag, userId)
|
||||
val params =
|
||||
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
var installFlags: Int = PackageInstallerUtils.getInstallFlags(params)
|
||||
installFlags = installFlags or 0x00000004 // PackageManager.INSTALL_ALLOW_TEST
|
||||
PackageInstallerUtils.setInstallFlags(params, installFlags)
|
||||
val sessionId = packageInstaller.createSession(params)
|
||||
val iSession = IPackageInstallerSession.Stub.asInterface(
|
||||
ShizukuBinderWrapper(iPackageInstaller.openSession(sessionId).asBinder()))
|
||||
session = PackageInstallerUtils.createSession(iSession)
|
||||
val inputStream = contentResolver.openInputStream(uri)
|
||||
val openedSession = session.openWrite("apk.apk", 0, -1)
|
||||
val buffer = ByteArray(8192)
|
||||
var length: Int
|
||||
try {
|
||||
while (inputStream!!.read(buffer).also { length = it } > 0) {
|
||||
openedSession.write(buffer, 0, length)
|
||||
openedSession.flush()
|
||||
session.fsync(openedSession)
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
inputStream!!.close()
|
||||
openedSession.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
val results = arrayOf<Intent?>(null)
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
val intentSender: IntentSender =
|
||||
IntentSenderUtils.newInstance(object : IIntentSenderAdaptor() {
|
||||
override fun send(intent: Intent?) {
|
||||
results[0] = intent
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
})
|
||||
session.commit(intentSender)
|
||||
countDownLatch.await()
|
||||
res = results[0]!!.getIntExtra(
|
||||
PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) == 0
|
||||
} catch (_: Exception) {
|
||||
res = false
|
||||
} finally {
|
||||
if (session != null) {
|
||||
try {
|
||||
session.close()
|
||||
} catch (_: Exception) {
|
||||
res = false
|
||||
}
|
||||
}
|
||||
}
|
||||
result.success(res)
|
||||
}
|
||||
|
||||
private fun rootCheckPermission(result: Result) {
|
||||
Shell.getShell(Shell.GetShellCallback(
|
||||
fun(shell: Shell) {
|
||||
result.success(shell.isRoot)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
private fun rootInstallApk(apkFilePath: String, result: Result) {
|
||||
Shell.sh("pm install -R -t " + apkFilePath).submit { out ->
|
||||
val builder = StringBuilder()
|
||||
for (data in out.getOut()) { builder.append(data) }
|
||||
result.success(builder.toString().endsWith("Success"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener)
|
||||
installersChannel = MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger, "installers")
|
||||
installersChannel!!.setMethodCallHandler {
|
||||
call, result ->
|
||||
if (call.method == "checkPermissionShizuku") {
|
||||
shizukuCheckPermission(result)
|
||||
} else if (call.method == "checkPermissionRoot") {
|
||||
rootCheckPermission(result)
|
||||
} else if (call.method == "installWithShizuku") {
|
||||
val apkFileUri: String? = call.argument("apkFileUri")
|
||||
shizukuInstallApk(apkFileUri!!, result)
|
||||
} else if (call.method == "installWithRoot") {
|
||||
val apkFilePath: String? = call.argument("apkFilePath")
|
||||
rootInstallApk(apkFilePath!!, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Shizuku.removeRequestPermissionResultListener(shizukuRequestPermissionResultListener)
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.os.Build;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class ApplicationUtils {
|
||||
|
||||
private static Application application;
|
||||
|
||||
public static Application getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
public static void setApplication(Application application) {
|
||||
ApplicationUtils.application = application;
|
||||
}
|
||||
|
||||
public static String getProcessName() {
|
||||
if (Build.VERSION.SDK_INT >= 28)
|
||||
return Application.getProcessName();
|
||||
else {
|
||||
try {
|
||||
@SuppressLint("PrivateApi")
|
||||
Class<?> activityThread = Class.forName("android.app.ActivityThread");
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
Method method = activityThread.getDeclaredMethod("currentProcessName");
|
||||
return (String) method.invoke(null);
|
||||
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
import android.content.IIntentReceiver;
|
||||
import android.content.IIntentSender;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
||||
public abstract class IIntentSenderAdaptor extends IIntentSender.Stub {
|
||||
|
||||
public abstract void send(Intent intent);
|
||||
|
||||
@Override
|
||||
public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
|
||||
send(intent);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
|
||||
send(intent);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
import android.content.IIntentSender;
|
||||
import android.content.IntentSender;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
public class IntentSenderUtils {
|
||||
|
||||
public static IntentSender newInstance(IIntentSender binder) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
||||
//noinspection JavaReflectionMemberAccess
|
||||
return IntentSender.class.getConstructor(IIntentSender.class).newInstance(binder);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.IPackageInstaller;
|
||||
import android.content.pm.IPackageInstallerSession;
|
||||
import android.content.pm.PackageInstaller;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
@SuppressWarnings({"JavaReflectionMemberAccess"})
|
||||
public class PackageInstallerUtils {
|
||||
|
||||
public static PackageInstaller createPackageInstaller(IPackageInstaller installer, String installerPackageName, String installerAttributionTag, int userId) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, String.class, int.class)
|
||||
.newInstance(installer, installerPackageName, installerAttributionTag, userId);
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, int.class)
|
||||
.newInstance(installer, installerPackageName, userId);
|
||||
} else {
|
||||
return PackageInstaller.class.getConstructor(Context.class, PackageManager.class, IPackageInstaller.class, String.class, int.class)
|
||||
.newInstance(ApplicationUtils.getApplication(), ApplicationUtils.getApplication().getPackageManager(), installer, installerPackageName, userId);
|
||||
}
|
||||
}
|
||||
|
||||
public static PackageInstaller.Session createSession(IPackageInstallerSession session) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
||||
return PackageInstaller.Session.class.getConstructor(IPackageInstallerSession.class)
|
||||
.newInstance(session);
|
||||
|
||||
}
|
||||
|
||||
public static int getInstallFlags(PackageInstaller.SessionParams params) throws NoSuchFieldException, IllegalAccessException {
|
||||
return (int) PackageInstaller.SessionParams.class.getDeclaredField("installFlags").get(params);
|
||||
}
|
||||
|
||||
public static void setInstallFlags(PackageInstaller.SessionParams params, int newValue) throws NoSuchFieldException, IllegalAccessException {
|
||||
PackageInstaller.SessionParams.class.getDeclaredField("installFlags").set(params, newValue);
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.IPackageInstaller;
|
||||
import android.content.pm.IPackageManager;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.os.Build;
|
||||
import android.os.IUserManager;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import rikka.shizuku.ShizukuBinderWrapper;
|
||||
import rikka.shizuku.SystemServiceHelper;
|
||||
|
||||
public class ShizukuSystemServerApi {
|
||||
|
||||
private static final Singleton<IPackageManager> PACKAGE_MANAGER = new Singleton<IPackageManager>() {
|
||||
@Override
|
||||
protected IPackageManager create() {
|
||||
return IPackageManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")));
|
||||
}
|
||||
};
|
||||
|
||||
private static final Singleton<IUserManager> USER_MANAGER = new Singleton<IUserManager>() {
|
||||
@Override
|
||||
protected IUserManager create() {
|
||||
return IUserManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService(Context.USER_SERVICE)));
|
||||
}
|
||||
};
|
||||
|
||||
public static IPackageInstaller PackageManager_getPackageInstaller() throws RemoteException {
|
||||
IPackageInstaller packageInstaller = PACKAGE_MANAGER.get().getPackageInstaller();
|
||||
return IPackageInstaller.Stub.asInterface(new ShizukuBinderWrapper(packageInstaller.asBinder()));
|
||||
}
|
||||
|
||||
public static List<UserInfo> UserManager_getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated) throws RemoteException {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated);
|
||||
} else {
|
||||
try {
|
||||
return USER_MANAGER.get().getUsers(excludeDying);
|
||||
} catch (NoSuchFieldError e) {
|
||||
return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// method 2: use transactRemote directly
|
||||
/*public static List<UserInfo> UserManager_getUsers(boolean excludeDying) {
|
||||
Parcel data = SystemServiceHelper.obtainParcel(Context.USER_SERVICE, "android.os.IUserManager", "getUsers");
|
||||
Parcel reply = Parcel.obtain();
|
||||
data.writeInt(excludeDying ? 1 : 0);
|
||||
|
||||
List<UserInfo> res = null;
|
||||
try {
|
||||
ShizukuService.transactRemote(data, reply, 0);
|
||||
reply.readException();
|
||||
res = reply.createTypedArrayList(UserInfo.CREATOR);
|
||||
} catch (RemoteException e) {
|
||||
Log.e("ShizukuSample", "UserManager#getUsers", e);
|
||||
} finally {
|
||||
data.recycle();
|
||||
reply.recycle();
|
||||
}
|
||||
return res;
|
||||
}*/
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package dev.imranr.obtainium.util;
|
||||
|
||||
public abstract class Singleton<T> {
|
||||
|
||||
private T mInstance;
|
||||
|
||||
protected abstract T create();
|
||||
|
||||
public final T get() {
|
||||
synchronized (this) {
|
||||
if (mInstance == null) {
|
||||
mInstance = create();
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="142.129"
|
||||
android:viewportHeight="142.129"
|
||||
android:width="503.6066dp"
|
||||
android:height="503.6066dp">
|
||||
android:width="108dp"
|
||||
android:height="108dp">
|
||||
<group
|
||||
android:translateX="-30.39437"
|
||||
android:translateY="-54.68043">
|
||||
|
@ -8,6 +8,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'dev.rikka.tools.refine:gradle-plugin:4.1.0' // Do not update!
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@
|
||||
"importExport": "Uvoz/izvoz",
|
||||
"settings": "Postavke",
|
||||
"exportedTo": "Izvezeno u {}",
|
||||
"obtainiumExport": "Obtainium Export",
|
||||
"obtainiumExport": "Obtainium izvoz",
|
||||
"invalidInput": "Neispravan unos.",
|
||||
"importedX": "Uvezeno {}",
|
||||
"obtainiumImport": "Obtainium uvoz",
|
||||
@ -134,7 +134,7 @@
|
||||
"close": "Zatvori",
|
||||
"share": "Podijeli",
|
||||
"appNotFound": "Aplikacija nije pronađena",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-izvoz",
|
||||
"pickAnAPK": "Odaberite APK",
|
||||
"appHasMoreThanOnePackage": "{} ima više od jednog paketa:",
|
||||
"deviceSupportsXArch": "Vaš uređaj podržava {} arhitekturu procesora.",
|
||||
@ -231,52 +231,56 @@
|
||||
"checkUpdateOnDetailPage": "Provjerite ima li novosti pri otvaranju stranice s detaljima aplikacije",
|
||||
"disablePageTransitions": "Ugasite animaciju prijelaza stranice",
|
||||
"reversePageTransitions": "Reverzne animacije prijelaza stranice",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"addInfoBelow": "Add this info below.",
|
||||
"addInfoInSettings": "Add this info in the Settings.",
|
||||
"githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
|
||||
"gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
|
||||
"sortByFileNamesNotLinks": "Sort by file names instead of full links",
|
||||
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
|
||||
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
|
||||
"appsPossiblyUpdated": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
|
||||
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
|
||||
"enableBackgroundUpdates": "Enable background updates",
|
||||
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
|
||||
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
|
||||
"verifyLatestTag": "Verify the 'latest' tag",
|
||||
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
|
||||
"intermediateLinkNotFound": "Intermediate link not found",
|
||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||
"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",
|
||||
"dontSortReleasesList": "Retain release order from API",
|
||||
"reverseSort": "Reverse sorting",
|
||||
"debugMenu": "Debug Menu",
|
||||
"bgTaskStarted": "Background task started - check logs.",
|
||||
"runBgCheckNow": "Run Background Update Check Now",
|
||||
"versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
|
||||
"installing": "Installing",
|
||||
"skipUpdateNotifications": "Skip update notifications",
|
||||
"minStarCount": "Najmanji broj zvjezdica",
|
||||
"addInfoBelow": "Dodajte ove informacije ispod.",
|
||||
"addInfoInSettings": "Dodajte ove informacije u Postavkama.",
|
||||
"githubSourceNote": "GitHub ograničavanje se može izbjeći korišćenjem tokena za lični pristup.",
|
||||
"gitlabSourceNote": "GitLab APK preuzimanje možda neće raditi bez tokena za lični pristup.",
|
||||
"sortByFileNamesNotLinks": "Sortirajte po imenima datoteka umjesto po punim linkovima",
|
||||
"filterReleaseNotesByRegEx": "Filtirajte promjene u izdanju po regularnom izrazu",
|
||||
"customLinkFilterRegex": "Prilagođeni APK link filtrira se po regularnom izrazu (Zadano '.apk$')",
|
||||
"appsPossiblyUpdated": "Pokušano ažuriranje aplikacija",
|
||||
"appsPossiblyUpdatedNotifDescription": "Obavještava korisnika da je ažuriranje jedne ili više aplikacija potencijalno izvršeno u pozadini",
|
||||
"xWasPossiblyUpdatedToY": "{} aplikacija bi trebala biti ažurirana na {}.",
|
||||
"enableBackgroundUpdates": "Dozvolite ažuriranja u pozadini",
|
||||
"backgroundUpdateReqsExplanation": "Ažuriranja u pozadini možda neće raditi za sve aplikacije.",
|
||||
"backgroundUpdateLimitsExplanation": "Uspjeh ažuriranja u pozadini se može provjeriti tek kada otvorite Obtainium.",
|
||||
"verifyLatestTag": "Provjerite 'posljednu' ('latest') oznaku",
|
||||
"intermediateLinkRegex": "Filtrirajte da prvo posjetite 'Intemediate' link",
|
||||
"intermediateLinkNotFound": "Intermediate link nije nađen",
|
||||
"exemptFromBackgroundUpdates": "Izuzmi iz ažuriranja u pozadini (ako su uključeni)",
|
||||
"bgUpdatesOnWiFiOnly": "Isključite ažuriranje u pozadini kada niste na WiFi-ju",
|
||||
"autoSelectHighestVersionCode": "Automatski izaberite najveću (verziju) versionCode APK-a",
|
||||
"versionExtractionRegEx": "RegEx ekstrakcija verzije",
|
||||
"matchGroupToUse": "Podjesite grupu za upotebu",
|
||||
"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",
|
||||
"reverseSort": "Obrni redosled",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Meni za otkrivanje grešaka",
|
||||
"bgTaskStarted": "Rad u pozadini pokrenut - provjerite log-ove.",
|
||||
"runBgCheckNow": "Pokrenite pozadinsku provjeru ažuriranja sad",
|
||||
"versionExtractWholePage": "Primjenite Regex ekstrakciju verzije na cijelu stranicu",
|
||||
"installing": "Instaliranje",
|
||||
"skipUpdateNotifications": "Ne prikazujte obavještenja ažuriranja",
|
||||
"updatesAvailableNotifChannel": "Dostupna ažuriranja",
|
||||
"appsUpdatedNotifChannel": "Aplikacije su ažurirane",
|
||||
"appsPossiblyUpdatedNotifChannel": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifChannel": "Pokušano ažuriranje aplikacija",
|
||||
"errorCheckingUpdatesNotifChannel": "Greška pri provjeri ažuriranja",
|
||||
"appsRemovedNotifChannel": "Aplikacije su uklonjene",
|
||||
"downloadingXNotifChannel": "Preuzimanje {}",
|
||||
"completeAppInstallationNotifChannel": "Dovršite instalaciju aplikacije",
|
||||
"checkingForUpdatesNotifChannel": "Tražim moguće nadogradnje",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Isključivo provjerite ažuriranje za instalirane i aplikacije 'samo za praćenje'",
|
||||
"supportFixedAPKURL": "Podržite fiksne APK URL-ove",
|
||||
"selectX": "Izaberite {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "Želite li ukloniti aplikaciju?",
|
||||
"other": "Želite li ukloniti aplikacije?"
|
||||
@ -326,7 +330,7 @@
|
||||
"other": "{} i još {} aplikacija je ažurirano."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
"one": "{} i još jedna aplikacija je vjerovatno ažurirana.",
|
||||
"other": "{} i još {} aplikacija su vjerovatno ažurirane."
|
||||
}
|
||||
}
|
||||
|
@ -256,10 +256,13 @@
|
||||
"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",
|
||||
"reverseSort": "Reverse sorting",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Debug Menu",
|
||||
"bgTaskStarted": "Background task started - check logs.",
|
||||
"runBgCheckNow": "Run Background Update Check Now",
|
||||
@ -277,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "Odstranit Apku?",
|
||||
"other": "Odstranit Apky?"
|
||||
|
@ -256,10 +256,13 @@
|
||||
"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",
|
||||
"reverseSort": "Umgekehrtes Sortieren",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Debug-Menü",
|
||||
"bgTaskStarted": "Hintergrundaufgabe gestartet – Logs prüfen.",
|
||||
"runBgCheckNow": "Hintergrundaktualisierungsprüfung jetzt durchführen",
|
||||
@ -275,8 +278,9 @@
|
||||
"completeAppInstallationNotifChannel": "App Installation abschließen",
|
||||
"checkingForUpdatesNotifChannel": "Nach Aktualisierungen suchen",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Überprüfe nur installierte und mit „nur Nachverfolgen“ markierte Apps auf Aktualisierungen",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"supportFixedAPKURL": "neuere Version anhand der ersten dreißig Zahlen der Checksumme der APK URL erraten, wenn anderweitig nicht unterstützt",
|
||||
"selectX": "Wähle {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "App entfernen?",
|
||||
"other": "Apps entfernen?"
|
||||
@ -329,4 +333,4 @@
|
||||
"one": "{} und 1 weitere Anwendung wurden möglicherweise aktualisiert.",
|
||||
"other": "{} und {} weitere Anwendungen wurden möglicherweise aktualisiert."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -256,10 +256,13 @@
|
||||
"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",
|
||||
"reverseSort": "Reverse sorting",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Debug Menu",
|
||||
"bgTaskStarted": "Background task started - check logs.",
|
||||
"runBgCheckNow": "Run Background Update Check Now",
|
||||
@ -277,6 +280,12 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"installMethod": "Installation method",
|
||||
"normal": "Normal",
|
||||
"shizuku": "Shizuku",
|
||||
"root": "Root",
|
||||
"shizukuBinderNotFound": "Shizuku is not running",
|
||||
"removeAppQuestion": {
|
||||
"one": "Remove App?",
|
||||
"other": "Remove Apps?"
|
||||
|
@ -11,18 +11,18 @@
|
||||
"unexpectedError": "Error Inesperado",
|
||||
"ok": "Correcto",
|
||||
"and": "y",
|
||||
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
|
||||
"githubPATLabel": "Token Github de Acceso Personal\n(Reduce tiempos de espera)",
|
||||
"includePrereleases": "Incluir versiones preliminares",
|
||||
"fallbackToOlderReleases": "Retorceder a versiones previas",
|
||||
"filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
|
||||
"invalidRegEx": "Expresión regular inválida",
|
||||
"fallbackToOlderReleases": "Retroceder a versiones previas",
|
||||
"filterReleaseTitlesByRegEx": "Filtrar Títulos de Versiones",
|
||||
"invalidRegEx": "Expresión inválida",
|
||||
"noDescription": "Sin descripción",
|
||||
"cancel": "Cancelar",
|
||||
"continue": "Continuar",
|
||||
"requiredInBrackets": "(Requerido)",
|
||||
"dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
|
||||
"colour": "Color",
|
||||
"githubStarredRepos": "Repositorios favoritos de GitHub",
|
||||
"githubStarredRepos": "Repositorios favoritos GitHub",
|
||||
"uname": "Nombre de usuario",
|
||||
"wrongArgNum": "Número de argumentos provistos inválido",
|
||||
"xIsTrackOnly": "{} es de 'Solo Seguimiento'",
|
||||
@ -30,7 +30,7 @@
|
||||
"app": "Aplicación",
|
||||
"appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'Solo Seguimiento'.",
|
||||
"youPickedTrackOnly": "Debes seleccionar la opción de 'Solo Seguimiento'.",
|
||||
"trackOnlyAppDescription": "Se monitorizará la aplicación en busca de actualizaciones, pero Obtainium no será capaz de descargarla o acutalizarla.",
|
||||
"trackOnlyAppDescription": "Se hará el seguimiento de actualizaciones para la aplicación, pero Obtainium no será capaz de descargarla o actalizarla.",
|
||||
"cancelled": "Cancelado",
|
||||
"appAlreadyAdded": "Aplicación ya añadida",
|
||||
"alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
|
||||
@ -38,12 +38,12 @@
|
||||
"appSourceURL": "URL de Origen de la Aplicación",
|
||||
"error": "Error",
|
||||
"add": "Añadir",
|
||||
"searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
|
||||
"searchSomeSourcesLabel": "Buscar (solo algunas fuentes)",
|
||||
"search": "Buscar",
|
||||
"additionalOptsFor": "Opciones Adicionales para {}",
|
||||
"supportedSources": "Fuentes Soportadas",
|
||||
"trackOnlyInBrackets": "(Solo Seguimiento)",
|
||||
"searchableInBrackets": "(Soporta Búsquedas)",
|
||||
"searchableInBrackets": "(soporta búsqueda)",
|
||||
"appsString": "Aplicaciones",
|
||||
"noApps": "Sin Aplicaciones",
|
||||
"noAppsForFilter": "Sin Aplicaciones para Filtrar",
|
||||
@ -56,12 +56,12 @@
|
||||
"estimateInBrackets": "(Aproximado)",
|
||||
"selectAll": "Seleccionar Todo",
|
||||
"deselectX": "Deseleccionar {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.",
|
||||
"removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?",
|
||||
"removeSelectedApps": "Borrar Aplicaciones Seleccionadas",
|
||||
"xWillBeRemovedButRemainInstalled": "{} será eliminada de Obtainium pero continuará instalada en el dispositivo.",
|
||||
"removeSelectedAppsQuestion": "¿Eliminar aplicaciones seleccionadas?",
|
||||
"removeSelectedApps": "Eliminar Aplicaciones Seleccionadas",
|
||||
"updateX": "Actualizar {}",
|
||||
"installX": "Instalar {}",
|
||||
"markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimient)\ncomo Actualizada",
|
||||
"markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimiento)\ncomo Actualizada",
|
||||
"changeX": "Cambiar {}",
|
||||
"installUpdateApps": "Instalar/Actualizar Aplicaciones",
|
||||
"installUpdateSelectedApps": "Instalar/Actualizar Aplicaciones Seleccionadas",
|
||||
@ -72,7 +72,7 @@
|
||||
"pinToTop": "Fijar arriba",
|
||||
"unpinFromTop": "Desfijar de arriba",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "¿Restuarar Estado de Instalación para las Aplicaciones Seleccionadas?",
|
||||
"installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de utilidad cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
|
||||
"installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de útil cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
|
||||
"shareSelectedAppURLs": "Compartir URLs de las Aplicaciones Seleccionadas",
|
||||
"resetInstallStatus": "Restaurar Estado de Instalación",
|
||||
"more": "Más",
|
||||
@ -98,9 +98,9 @@
|
||||
"line": "Línea",
|
||||
"searchX": "Buscar {}",
|
||||
"noResults": "Resultados no encontrados",
|
||||
"importX": "Importar {}",
|
||||
"importedAppsIdDisclaimer": "Las Aplicaciones Importadas pueden mostrarse incorrectamente como \"No Instalada\".\nPara arreglar esto, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
|
||||
"importErrors": "Import Errors",
|
||||
"importX": "Importar desde {}",
|
||||
"importedAppsIdDisclaimer": "Las aplicaciones importadas podrían mostrarse incorrectamente como \"No Instalada\".\nPara solucionarlo, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
|
||||
"importErrors": "Errores de Importación",
|
||||
"importedXOfYApps": "{} de {} Aplicaciones importadas.",
|
||||
"followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:",
|
||||
"okay": "Correcto",
|
||||
@ -113,12 +113,12 @@
|
||||
"followSystem": "Seguir al Sistema",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Usar tema oscuro con negros puros",
|
||||
"appSortBy": "Ordenar Aplicaciones Por",
|
||||
"useBlackTheme": "Usar negros puros en tema oscuro",
|
||||
"appSortBy": "Ordenar Apps Por",
|
||||
"authorName": "Autor/Nombre",
|
||||
"nameAuthor": "Nombre/Autor",
|
||||
"asAdded": "Según se Añadieron",
|
||||
"appSortOrder": "Orden de Clasificación de Aplicaciones",
|
||||
"appSortOrder": "Orden de Clasificación",
|
||||
"ascending": "Ascendente",
|
||||
"descending": "Descendente",
|
||||
"bgUpdateCheckInterval": "Intervalo de Comprobación de Actualizaciones en Segundo Plano",
|
||||
@ -135,7 +135,7 @@
|
||||
"share": "Compartir",
|
||||
"appNotFound": "Aplicación no encontrada",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"pickAnAPK": "Elige una APK",
|
||||
"pickAnAPK": "Selecciona una APK",
|
||||
"appHasMoreThanOnePackage": "{} tiene más de un paquete:",
|
||||
"deviceSupportsXArch": "Tu dispositivo soporta las siguientes arquitecturas de procesador: {}.",
|
||||
"deviceSupportsFollowingArchs": "Tu dispositivo soporta las siguientes arquitecturas de procesador:",
|
||||
@ -154,8 +154,8 @@
|
||||
"appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas",
|
||||
"xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}",
|
||||
"completeAppInstallation": "Instalación Completa de la Aplicación",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierta para instalar aplicaciones",
|
||||
"completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para teminar de instalar una aplicación",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierto para instalar aplicaciones",
|
||||
"completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para terminar de instalar una aplicación",
|
||||
"checkingForUpdates": "Buscando Actualizaciones",
|
||||
"checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
|
||||
"pleaseAllowInstallPerm": "Por favor, permite a Obtainium instalar aplicaciones",
|
||||
@ -170,24 +170,24 @@
|
||||
"lastUpdateCheckX": "Última Comprobación: {}",
|
||||
"remove": "Eliminar",
|
||||
"yesMarkUpdated": "Sí, Marcar como Actualizada",
|
||||
"fdroid": "Repositorio oficial de F-Droid",
|
||||
"fdroid": "Repositorio oficial F-Droid",
|
||||
"appIdOrName": "ID o Nombre de la Aplicación",
|
||||
"appId": "ID de la Aplicación",
|
||||
"appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
|
||||
"reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
|
||||
"fdroidThirdPartyRepo": "Rpositorios de terceros de F-Droid",
|
||||
"fdroidThirdPartyRepo": "Rpositorios de terceros F-Droid",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"install": "Instalar",
|
||||
"markInstalled": "Marcar como Instalda",
|
||||
"markInstalled": "Marcar como Instalada",
|
||||
"update": "Actualizar",
|
||||
"markUpdated": "Marcar como Actualizada",
|
||||
"additionalOptions": "Opciones Adicionales",
|
||||
"disableVersionDetection": "Descativar Detección de Versiones",
|
||||
"noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda no funcionar correctamente.",
|
||||
"downloadingX": "Descargando {}",
|
||||
"downloadNotifDescription": "Notifica al usuario de progreso de descarga de una aplicación",
|
||||
"downloadNotifDescription": "Notifica al usuario del progreso de descarga de una aplicación",
|
||||
"noAPKFound": "APK no encontrada",
|
||||
"noVersionDetection": "Sin detección de versiones",
|
||||
"categorize": "Catogorizar",
|
||||
@ -195,15 +195,15 @@
|
||||
"category": "Categoría",
|
||||
"noCategory": "Sin Categoría",
|
||||
"noCategories": "Sin Categorías",
|
||||
"deleteCategoriesQuestion": "¿Borrar Categorías?",
|
||||
"categoryDeleteWarning": "Todas las aplicaciones en las categorías borradas serán margadas como 'Sin Categoría'.",
|
||||
"deleteCategoriesQuestion": "¿Eliminar Categorías?",
|
||||
"categoryDeleteWarning": "Todas las aplicaciones en las categorías eliminadas serán marcadas como 'Sin Categoría'.",
|
||||
"addCategory": "Añadir Categoría",
|
||||
"label": "Nombre",
|
||||
"language": "Idioma",
|
||||
"copiedToClipboard": "Copiado al Portapapeles",
|
||||
"storagePermissionDenied": "Permiso de Almacenamiento rechazado",
|
||||
"selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaicones seleccionadas.",
|
||||
"filterAPKsByRegEx": "Filtrar APKs mediante Expresiones Regulares",
|
||||
"selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaciones seleccionadas.",
|
||||
"filterAPKsByRegEx": "Filtrar por APKs",
|
||||
"removeFromObtainium": "Eliminar de Obtainium",
|
||||
"uninstallFromDevice": "Desinstalar del Dispositivo",
|
||||
"onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
|
||||
@ -211,72 +211,76 @@
|
||||
"releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.",
|
||||
"changes": "Cambios",
|
||||
"releaseDate": "Fecha de Publicación",
|
||||
"importFromURLsInFile": "Importar de URls en un Archivo (como OPML)",
|
||||
"importFromURLsInFile": "Importar URLs desde archivo (como OPML)",
|
||||
"versionDetection": "Detección de Versiones",
|
||||
"standardVersionDetection": "Detección de versiones estándar",
|
||||
"groupByCategory": "Agrupar por Categoría",
|
||||
"autoApkFilterByArch": "Tratar de filtrar las APKs mediante arquitecturas de procesador si es posible",
|
||||
"autoApkFilterByArch": "Filtrar las APKs mediante arquitecturas de procesador, si es posible",
|
||||
"overrideSource": "Sobrescribir Fuente",
|
||||
"dontShowAgain": "No mostrar de nuevo",
|
||||
"dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
|
||||
"dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks",
|
||||
"moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
|
||||
"about": "About",
|
||||
"requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)",
|
||||
"checkOnStart": "Check for updates on startup",
|
||||
"tryInferAppIdFromCode": "Try inferring App ID from source code",
|
||||
"removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
|
||||
"pickHighestVersionCode": "Auto-select highest version code APK",
|
||||
"checkUpdateOnDetailPage": "Check for updates on opening an App detail page",
|
||||
"disablePageTransitions": "Disable page transition animations",
|
||||
"reversePageTransitions": "Reverse page transition animations",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"addInfoBelow": "Add this info below.",
|
||||
"addInfoInSettings": "Add this info in the Settings.",
|
||||
"githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
|
||||
"gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
|
||||
"sortByFileNamesNotLinks": "Sort by file names instead of full links",
|
||||
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
|
||||
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
|
||||
"appsPossiblyUpdated": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
|
||||
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
|
||||
"enableBackgroundUpdates": "Enable background updates",
|
||||
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
|
||||
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
|
||||
"verifyLatestTag": "Verify the 'latest' tag",
|
||||
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
|
||||
"intermediateLinkNotFound": "Intermediate link not found",
|
||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||
"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",
|
||||
"dontSortReleasesList": "Retain release order from API",
|
||||
"reverseSort": "Reverse sorting",
|
||||
"debugMenu": "Debug Menu",
|
||||
"bgTaskStarted": "Background task started - check logs.",
|
||||
"runBgCheckNow": "Run Background Update Check Now",
|
||||
"versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
|
||||
"installing": "Installing",
|
||||
"skipUpdateNotifications": "Skip update notifications",
|
||||
"dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APKs",
|
||||
"moveNonInstalledAppsToBottom": "Mover Apps no instaladas en la Parte Inferior de la Vista de Aplicaciones",
|
||||
"gitlabPATLabel": "Token GitLab de Acceso Personal\n(Habilita la Búsqueda y Mejor Detección de APKs)",
|
||||
"about": "Acerca",
|
||||
"requiresCredentialsInSettings": "{}: Esto requiere credenciales adicionales (en Ajustes)",
|
||||
"checkOnStart": "Comprobar actualizaciones durante el inicio",
|
||||
"tryInferAppIdFromCode": "Intentar deducir la ID de la APP por el código fuente",
|
||||
"removeOnExternalUninstall": "Auto eliminar Apps desinstaladas externamente",
|
||||
"pickHighestVersionCode": "Auto selección versión superior del código APK",
|
||||
"checkUpdateOnDetailPage": "Comprobar actualizaciones al abrir detalles de la App",
|
||||
"disablePageTransitions": "Deshabilitar animaciones de transición de la página",
|
||||
"reversePageTransitions": "Invertir animaciones de transición de la página",
|
||||
"minStarCount": "Número Mínimo de Estrellas",
|
||||
"addInfoBelow": "Añadir esta información debajo.",
|
||||
"addInfoInSettings": "Añadir esta información en Ajustes.",
|
||||
"githubSourceNote": "La limitación de velocidad de GitHub puede evitarse con una clave API.",
|
||||
"gitlabSourceNote": "La extracción de APK de GitLab podría no funcionar sin una clave API.",
|
||||
"sortByFileNamesNotLinks": "Ordenar por nombres de fichero en vez de por enlaces completos",
|
||||
"filterReleaseNotesByRegEx": "Filtrar por Notas de Versión (Release Notes)",
|
||||
"customLinkFilterRegex": "Filtro personalizado de Enlace APK (por defecto '.apk$')",
|
||||
"appsPossiblyUpdated": "Actualización de Apps intentada",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifica al usuario que las actualizaciones en segundo plano podrían haberse realizado para una o más aplicaciones",
|
||||
"xWasPossiblyUpdatedToY": "{} podría estar actualizada a {}.",
|
||||
"enableBackgroundUpdates": "Habilitar actualizaciones en segundo plano",
|
||||
"backgroundUpdateReqsExplanation": "Las actualizaciones en segundo plano pueden no estar disponibles para todas las aplicaciones.",
|
||||
"backgroundUpdateLimitsExplanation": "El éxito de las instalaciones en segundo plano solo se puede comprobar con Obtainium abierto.",
|
||||
"verifyLatestTag": "Comprueba la etiqueta 'latest'",
|
||||
"intermediateLinkRegex": "Filtrar por Enlace 'Intermedio' para Visitar Primero",
|
||||
"intermediateLinkNotFound": "Enlace Intermedio no encontrado",
|
||||
"exemptFromBackgroundUpdates": "Exento de actualizciones en segundo plano (si están habilitadas)",
|
||||
"bgUpdatesOnWiFiOnly": "Deshabilitar las actualizaciones en segundo plano sin WiFi",
|
||||
"autoSelectHighestVersionCode": "Auto Selección de la versionCode APK superior",
|
||||
"versionExtractionRegEx": "Versión de Extracción de RegEx",
|
||||
"matchGroupToUse": "Coincidir en Grupo a Usar",
|
||||
"highlightTouchTargets": "Resaltar objetivos menos obvios",
|
||||
"pickExportDir": "Selecciona el Directorio para Exportar",
|
||||
"autoExportOnChanges": "Auto Exportar cuando haya cambios",
|
||||
"includeSettings": "Incluir ajustes",
|
||||
"filterVersionsByRegEx": "Filtrar por Versiones",
|
||||
"trySelectingSuggestedVersionCode": "Prueba seleccionando la versionCode APK sugerida",
|
||||
"dontSortReleasesList": "Mantener el order de publicación desde API",
|
||||
"reverseSort": "Orden inverso",
|
||||
"takeFirstLink": "Usar primer enlace",
|
||||
"skipSort": "Omitir orden",
|
||||
"debugMenu": "Menu Depurar",
|
||||
"bgTaskStarted": "Iniciada tarea en segundo plano - revisa los logs.",
|
||||
"runBgCheckNow": "Ejecutar verficiación de actualizaciones en segundo plano",
|
||||
"versionExtractWholePage": "Aplicar la Versión de Extracción Regex a la Página Entera",
|
||||
"installing": "Instalando",
|
||||
"skipUpdateNotifications": "Omitir notificaciones sobre actualizaciones",
|
||||
"updatesAvailableNotifChannel": "Actualizaciones Disponibles",
|
||||
"appsUpdatedNotifChannel": "Aplicaciones Actualizadas",
|
||||
"appsPossiblyUpdatedNotifChannel": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifChannel": "Se ha Intentado Actualizar la Aplicación",
|
||||
"errorCheckingUpdatesNotifChannel": "Error Buscando Actualizaciones",
|
||||
"appsRemovedNotifChannel": "Aplicaciones Eliminadas",
|
||||
"downloadingXNotifChannel": "Descargando {}",
|
||||
"completeAppInstallationNotifChannel": "Instalación Completa de la Aplicación",
|
||||
"checkingForUpdatesNotifChannel": "Buscando Actualizaciones",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Comprobar actualizaciones solo para apps instaladas y en seguimiento",
|
||||
"supportFixedAPKURL": "Soporte para URLs fijas de APK",
|
||||
"selectX": "Selecciona {}",
|
||||
"parallelDownloads": "Permitir descargas paralelas",
|
||||
"removeAppQuestion": {
|
||||
"one": "¿Eliminar Aplicación?",
|
||||
"other": "¿Eliminar Aplicaciones?"
|
||||
@ -314,19 +318,19 @@
|
||||
"other": "{} Días"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Borrado {n} log (previo a = {before}, posterior a = {after})",
|
||||
"other": "Borrados {n} logs (previos a = {before}, posteriores a = {after})"
|
||||
"one": "Eliminado {n} log (previo a = {before}, posterior a = {after})",
|
||||
"other": "Eliminados {n} logs (previos a = {before}, posteriores a = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} y 1 aplicación más tiene actualizaciones.",
|
||||
"other": "{} y {} aplicaciones más tiene actualizaciones."
|
||||
"other": "{} y {} aplicaciones más tienen actualizaciones."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} y 1 aplicación más han sido actualizadas.",
|
||||
"other": "{} y {} aplicaciones más han sido actualizadas."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
"one": "{} y 1 aplicación más podría haber sido actualizada.",
|
||||
"other": "{} y {} aplicaciones más podrían haber sido actualizadas."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -239,44 +239,48 @@
|
||||
"sortByFileNamesNotLinks": "مرتب سازی بر اساس نام فایل به جای پیوندهای کامل",
|
||||
"filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
|
||||
"customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیشفرض '.apk$')",
|
||||
"appsPossiblyUpdated": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
|
||||
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
|
||||
"enableBackgroundUpdates": "Enable background updates",
|
||||
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
|
||||
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
|
||||
"verifyLatestTag": "Verify the 'latest' tag",
|
||||
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
|
||||
"intermediateLinkNotFound": "Intermediate link not found",
|
||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||
"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",
|
||||
"dontSortReleasesList": "Retain release order from API",
|
||||
"reverseSort": "Reverse sorting",
|
||||
"debugMenu": "Debug Menu",
|
||||
"bgTaskStarted": "Background task started - check logs.",
|
||||
"runBgCheckNow": "Run Background Update Check Now",
|
||||
"versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
|
||||
"installing": "Installing",
|
||||
"skipUpdateNotifications": "Skip update notifications",
|
||||
"appsPossiblyUpdated": "بهروزرسانی برنامه انجام شد",
|
||||
"appsPossiblyUpdatedNotifDescription": "به کاربر اطلاع میدهد که بهروزرسانیهای یک یا چند برنامه به طور بالقوه در پسزمینه اعمال شده است",
|
||||
"xWasPossiblyUpdatedToY": "ممکن است {} به {} به روز شده باشد.",
|
||||
"enableBackgroundUpdates": "به روز رسانی پس زمینه را فعال کنید",
|
||||
"backgroundUpdateReqsExplanation": "به روز رسانی پس زمینه ممکن است برای همه برنامه ها امکان پذیر نباشد.",
|
||||
"backgroundUpdateLimitsExplanation": "موفقیت نصب پسزمینه تنها زمانی مشخص میشود که Obtainium باز شود.",
|
||||
"verifyLatestTag": "برچسب \"آخرین\" را تأیید کنید",
|
||||
"intermediateLinkRegex": "برای اولین بار بازدید از لینک \"متوسط\" را فیلتر کنید",
|
||||
"intermediateLinkNotFound": "لینک میانی پیدا نشد",
|
||||
"exemptFromBackgroundUpdates": "معاف از بهروزرسانیهای پسزمینه (در صورت فعال بودن)",
|
||||
"bgUpdatesOnWiFiOnly": "بهروزرسانیهای پسزمینه را در صورت عدم اتصال به WiFi غیرفعال کنید",
|
||||
"autoSelectHighestVersionCode": "انتخاب خودکار بالاترین نسخه کد APK",
|
||||
"versionExtractionRegEx": "نسخه استخراج RegEx",
|
||||
"matchGroupToUse": "گروه مورد استفاده را مطابقت دهید",
|
||||
"highlightTouchTargets": "اهداف لمسی کمتر واضح را برجسته کنید",
|
||||
"pickExportDir": "فهرست صادرات را انتخاب کنید",
|
||||
"autoExportOnChanges": "صادرات خودکار تغییرات",
|
||||
"includeSettings": "Include settings",
|
||||
"filterVersionsByRegEx": "فیلتر کردن نسخه ها با RegEx",
|
||||
"trySelectingSuggestedVersionCode": "نسخه پیشنهادی APK نسخه کد را انتخاب کنید",
|
||||
"dontSortReleasesList": "حفظ سفارش انتشار از API",
|
||||
"reverseSort": "مرتب سازی معکوس",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "منوی اشکال زدایی",
|
||||
"bgTaskStarted": "کار پس زمینه شروع شد - لاگ های مربوط را بررسی کنید.",
|
||||
"runBgCheckNow": "اکنون بهروزرسانی پسزمینه را بررسی کنید",
|
||||
"versionExtractWholePage": "نسخه Extraction Regex را در کل صفحه اعمال کنید",
|
||||
"installing": "در حال نصب",
|
||||
"skipUpdateNotifications": "رد شدن از اعلان های به روز رسانی",
|
||||
"updatesAvailableNotifChannel": "بروزرسانی در دسترس ",
|
||||
"appsUpdatedNotifChannel": "برنامه ها به روز شدند",
|
||||
"appsPossiblyUpdatedNotifChannel": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifChannel": "بهروزرسانی برنامه انجام شد",
|
||||
"errorCheckingUpdatesNotifChannel": "خطا در بررسی بهروزرسانیها",
|
||||
"appsRemovedNotifChannel": "برنامه ها حذف شدند",
|
||||
"downloadingXNotifChannel": "در حال دانلود {}",
|
||||
"completeAppInstallationNotifChannel": "نصب کامل برنامه",
|
||||
"checkingForUpdatesNotifChannel": "بررسی بهروزرسانیها",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "فقط برنامه های نصب شده و فقط ردیابی را برای به روز رسانی بررسی کنید",
|
||||
"supportFixedAPKURL": "پشتیبانی از URL های APK ثابت",
|
||||
"selectX": "انتخاب کنید {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "برنامه حذف شود؟",
|
||||
"other": "برنامه ها حذف شوند؟"
|
||||
@ -326,7 +330,7 @@
|
||||
"other": "{} و {} برنامه دیگر به روز شدند."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
"one": "{} و 1 برنامه دیگر ممکن است به روز شده باشند.",
|
||||
"other": "ممکن است {} و {} برنامه های دیگر به روز شده باشند."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -256,10 +256,13 @@
|
||||
"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",
|
||||
"reverseSort": "Reverse sorting",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Debug Menu",
|
||||
"bgTaskStarted": "Background task started - check logs.",
|
||||
"runBgCheckNow": "Run Background Update Check Now",
|
||||
@ -277,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "Supprimer l'application ?",
|
||||
"other": "Supprimer les applications ?"
|
||||
|
@ -215,7 +215,7 @@
|
||||
"versionDetection": "Verzió érzékelés",
|
||||
"standardVersionDetection": "Alapért. verzió érzékelés",
|
||||
"groupByCategory": "Csoportosítás Kategória alapján",
|
||||
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-okat",
|
||||
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-kat",
|
||||
"overrideSource": "Forrás felülbírálása",
|
||||
"dontShowAgain": "Ne mutassa ezt újra",
|
||||
"dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
|
||||
@ -255,10 +255,13 @@
|
||||
"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",
|
||||
"reverseSort": "Fordított rendezés",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Hibakereső menü",
|
||||
"bgTaskStarted": "A háttérfeladat elindult – ellenőrizze a naplókat.",
|
||||
"enableBackgroundUpdates": "Frissítések a háttérben",
|
||||
@ -275,8 +278,9 @@
|
||||
"completeAppInstallationNotifChannel": "Teljes app telepítés",
|
||||
"checkingForUpdatesNotifChannel": "Frissítések keresése",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Csak a telepített és a csak követhető appokat ellenőrizze frissítésekért",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"supportFixedAPKURL": "Támogatja a rögzített APK URL-eket",
|
||||
"selectX": "Kiválaszt {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "Eltávolítja az alkalmazást?",
|
||||
"other": "Eltávolítja az alkalmazást?"
|
||||
|
@ -256,10 +256,13 @@
|
||||
"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",
|
||||
"reverseSort": "Ordine inverso",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Menu di debug",
|
||||
"bgTaskStarted": "Attività in secondo piano iniziata - controllo log.",
|
||||
"runBgCheckNow": "Inizia aggiornamento in secondo piano ora",
|
||||
@ -277,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Cerca aggiornamenti solo per app installate e app in Solo-Monitoraggio",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "Rimuovere l'app?",
|
||||
"other": "Rimuovere le app?"
|
||||
|
@ -256,10 +256,13 @@
|
||||
"highlightTouchTargets": "目立たないタップ可能な対象をハイライトする",
|
||||
"pickExportDir": "エクスポートディレクトリを選択",
|
||||
"autoExportOnChanges": "変更があった際に自動でエクスポートする",
|
||||
"includeSettings": "Include settings",
|
||||
"filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする",
|
||||
"trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
|
||||
"dontSortReleasesList": "APIからのリリース順を保持する",
|
||||
"reverseSort": "逆順ソート",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "デバッグメニュー",
|
||||
"bgTaskStarted": "バックグラウンドタスクが開始されました - ログを確認してください。",
|
||||
"runBgCheckNow": "今すぐバックグラウンドでのアップデート確認を開始する",
|
||||
@ -277,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "インストール済みのアプリと「追跡のみ」のアプリのアップデートのみを確認する",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "アプリを削除しますか?",
|
||||
"other": "アプリを削除しますか?"
|
||||
|
@ -256,10 +256,13 @@
|
||||
"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",
|
||||
"reverseSort": "Sortering omkeren",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Debug menu",
|
||||
"bgTaskStarted": "Achtergrondtaak gestart - controleer de logs.",
|
||||
"runBgCheckNow": "Voer nu een achtergrondupdatecontrole uit",
|
||||
@ -277,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Alleen geïnstalleerde en Track-Only apps controleren op updates",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "App verwijderen?",
|
||||
"other": "Apps verwijderen?"
|
||||
|
@ -256,10 +256,13 @@
|
||||
"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",
|
||||
"reverseSort": "Odwrotne sortowanie",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Menu debugowania",
|
||||
"bgTaskStarted": "Uruchomiono zadanie w tle - sprawdź logi.",
|
||||
"runBgCheckNow": "Wymuś sprawdzenie aktualizacji w tle",
|
||||
@ -275,8 +278,9 @@
|
||||
"completeAppInstallationNotifChannel": "Ukończenie instalacji aplikacji",
|
||||
"checkingForUpdatesNotifChannel": "Sprawdzanie dostępności aktualizacji",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Sprawdzaj tylko zainstalowane i obserwowane aplikacje pod kątem aktualizacji",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"supportFixedAPKURL": "Obsługuj stałe adresy URL APK",
|
||||
"selectX": "Wybierz {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "Usunąć aplikację?",
|
||||
"few": "Usunąć aplikacje?",
|
||||
|
@ -256,10 +256,13 @@
|
||||
"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",
|
||||
"reverseSort": "Ordenação reversa",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Menu Debug",
|
||||
"bgTaskStarted": "Tarefa em segundo plano iniciada - verifique os logs.",
|
||||
"runBgCheckNow": "Execute a verificação de atualização em segundo plano agora",
|
||||
@ -277,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "Remover App?",
|
||||
"other": "Remover Apps?"
|
||||
|
@ -256,10 +256,13 @@
|
||||
"highlightTouchTargets": "Выделить менее очевидные элементы управления касанием",
|
||||
"pickExportDir": "Выбрать каталог для экспорта",
|
||||
"autoExportOnChanges": "Автоэкспорт при изменениях",
|
||||
"includeSettings": "Include settings",
|
||||
"filterVersionsByRegEx": "Фильтровать версии по регулярному выражению",
|
||||
"trySelectingSuggestedVersionCode": "Попробуйте выбрать предложенный код версии APK",
|
||||
"dontSortReleasesList": "Сохранить порядок релизов от API",
|
||||
"reverseSort": "Обратная сортировка",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Меню отладки",
|
||||
"bgTaskStarted": "Фоновая задача начата — проверьте журналы",
|
||||
"runBgCheckNow": "Запустить проверку фонового обновления сейчас",
|
||||
@ -274,9 +277,15 @@
|
||||
"downloadingXNotifChannel": "Загрузка {}",
|
||||
"completeAppInstallationNotifChannel": "Завершение установки приложения",
|
||||
"checkingForUpdatesNotifChannel": "Проверка обновлений",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений",
|
||||
"supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
|
||||
"selectX": "Выбрать {}",
|
||||
"parallelDownloads": "Разрешить параллельные загрузки",
|
||||
"installMethod": "Метод установки",
|
||||
"normal": "Нормальный",
|
||||
"shizuku": "Shizuku",
|
||||
"root": "Суперпользователь",
|
||||
"shizukuBinderNotFound": "Shizuku не запущен",
|
||||
"removeAppQuestion": {
|
||||
"one": "Удалить приложение?",
|
||||
"other": "Удалить приложения?"
|
||||
|
@ -256,13 +256,17 @@
|
||||
"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",
|
||||
"reverseSort": "Omvänd sortering",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Felsökningsmeny",
|
||||
"bgTaskStarted": "Background task started - check logs.",
|
||||
"runBgCheckNow": "Kör Bakgrundsuppdateringskoll Nu",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "Ta Bort App?",
|
||||
"other": "Ta Bort Appar?"
|
||||
|
@ -256,10 +256,13 @@
|
||||
"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",
|
||||
"reverseSort": "Ters sıralama",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Hata Ayıklama Menüsü",
|
||||
"bgTaskStarted": "Arka plan görevi başladı - günlükleri kontrol et.",
|
||||
"runBgCheckNow": "Arka Plan Güncelleme Kontrolünü Şimdi Çalıştır",
|
||||
@ -277,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Yalnızca yüklü ve Yalnızca İzleme Uygulamalarını güncelleme",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "Uygulamayı Kaldır?",
|
||||
"other": "Uygulamaları Kaldır?"
|
||||
|
@ -256,10 +256,13 @@
|
||||
"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",
|
||||
"reverseSort": "Sắp xếp ngược",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Danh sách gỡ lỗi",
|
||||
"bgTaskStarted": "Tác vụ nền đã bắt đầu - kiểm tra nhật ký.",
|
||||
"runBgCheckNow": "Chạy kiểm tra cập nhật nền ngay bây giờ",
|
||||
@ -277,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Chỉ kiểm tra các ứng dụng đã cài đặt và Chỉ-Theo dõi để biết các bản cập nhật",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion":{
|
||||
"one": "Gỡ ứng dụng?",
|
||||
"other": "Gỡ ứng dụng?"
|
||||
|
@ -256,10 +256,13 @@
|
||||
"highlightTouchTargets": "突出展示不明显的触摸区域",
|
||||
"pickExportDir": "选择导出文件夹",
|
||||
"autoExportOnChanges": "数据变更时自动导出",
|
||||
"includeSettings": "Include settings",
|
||||
"filterVersionsByRegEx": "筛选版本号(正则表达式)",
|
||||
"trySelectingSuggestedVersionCode": "尝试选择推荐版本的 APK 文件",
|
||||
"dontSortReleasesList": "保持来自 API 的发行顺序",
|
||||
"reverseSort": "反转排序",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "调试选项",
|
||||
"bgTaskStarted": "后台任务已启动 - 详见日志",
|
||||
"runBgCheckNow": "立即进行后台更新检查",
|
||||
@ -277,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "只对已安装和“仅追踪”的应用进行更新检查",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "是否删除应用?",
|
||||
"other": "是否删除应用?"
|
||||
|
@ -65,7 +65,7 @@ class FDroid extends AppSource {
|
||||
) async {
|
||||
String? appId = await tryInferringAppId(standardUrl);
|
||||
String host = Uri.parse(standardUrl).host;
|
||||
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
var details = getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
await sourceRequest('https://$host/api/v1/packages/$appId'),
|
||||
'https://$host/repo/$appId',
|
||||
standardUrl,
|
||||
@ -80,6 +80,23 @@ class FDroid extends AppSource {
|
||||
true
|
||||
? additionalSettings['filterVersionsByRegEx']
|
||||
: null);
|
||||
if (!hostChanged) {
|
||||
try {
|
||||
var res = await sourceRequest(
|
||||
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml');
|
||||
String author = res.body
|
||||
.split('\n')
|
||||
.where((l) => l.startsWith('AuthorName: '))
|
||||
.first
|
||||
.split(': ')
|
||||
.sublist(1)
|
||||
.join(': ');
|
||||
details.names.author = author;
|
||||
} catch (e) {
|
||||
// Fail silently
|
||||
}
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -111,79 +128,79 @@ class FDroid extends AppSource {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
Response res, String apkUrlPrefix, String standardUrl, String sourceName,
|
||||
{bool autoSelectHighestVersionCode = false,
|
||||
bool trySelectingSuggestedVersionCode = false,
|
||||
String? filterVersionsByRegEx}) {
|
||||
if (res.statusCode == 200) {
|
||||
var response = jsonDecode(res.body);
|
||||
List<dynamic> releases = response['packages'] ?? [];
|
||||
if (releases.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
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'];
|
||||
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
Response res, String apkUrlPrefix, String standardUrl, String sourceName,
|
||||
{bool autoSelectHighestVersionCode = false,
|
||||
bool trySelectingSuggestedVersionCode = false,
|
||||
String? filterVersionsByRegEx}) {
|
||||
if (res.statusCode == 200) {
|
||||
var response = jsonDecode(res.body);
|
||||
List<dynamic> releases = response['packages'] ?? [];
|
||||
if (releases.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
}
|
||||
// Apply the release filter if any
|
||||
if (filterVersionsByRegEx?.isNotEmpty == true) {
|
||||
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();
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
// 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) =>
|
||||
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?.isNotEmpty == true) {
|
||||
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();
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
// 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(version, getApkUrlsFromUrls(apkUrls.toSet().toList()),
|
||||
AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last));
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
if (releaseChoices.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
List<String> apkUrls = releaseChoices
|
||||
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
||||
.toList();
|
||||
return APKDetails(version, getApkUrlsFromUrls(apkUrls.toSet().toList()),
|
||||
AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last));
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
|
@ -234,7 +234,7 @@ class GitHub extends AppSource {
|
||||
bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true;
|
||||
bool dontSortReleasesList =
|
||||
additionalSettings['dontSortReleasesList'] == true;
|
||||
String? latestTag;
|
||||
dynamic latestRelease;
|
||||
if (verifyLatestTag) {
|
||||
var temp = requestUrl.split('?');
|
||||
Response res = await sourceRequest(
|
||||
@ -245,12 +245,20 @@ class GitHub extends AppSource {
|
||||
}
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var jsres = jsonDecode(res.body);
|
||||
latestTag = jsres['tag_name'] ?? jsres['name'];
|
||||
latestRelease = jsonDecode(res.body);
|
||||
}
|
||||
Response res = await sourceRequest(requestUrl);
|
||||
if (res.statusCode == 200) {
|
||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||
if (latestRelease != null) {
|
||||
var latestTag = latestRelease['tag_name'] ?? latestRelease['name'];
|
||||
if (releases
|
||||
.where((element) =>
|
||||
(element['tag_name'] ?? element['name']) == latestTag)
|
||||
.isEmpty) {
|
||||
releases = [latestRelease, ...releases];
|
||||
}
|
||||
}
|
||||
|
||||
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
|
||||
(release['assets'] as List<dynamic>?)
|
||||
@ -299,13 +307,13 @@ class GitHub extends AppSource {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (latestTag != null &&
|
||||
if (latestRelease != null &&
|
||||
releases.isNotEmpty &&
|
||||
latestTag !=
|
||||
latestRelease !=
|
||||
(releases[releases.length - 1]['tag_name'] ??
|
||||
releases[0]['name'])) {
|
||||
var ind = releases.indexWhere(
|
||||
(element) => latestTag == (element['tag_name'] ?? element['name']));
|
||||
var ind = releases.indexWhere((element) =>
|
||||
latestRelease == (element['tag_name'] ?? element['name']));
|
||||
if (ind >= 0) {
|
||||
releases.add(releases.removeAt(ind));
|
||||
}
|
||||
|
@ -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);
|
||||
@ -203,7 +187,7 @@ class GitLab extends AppSource {
|
||||
});
|
||||
}
|
||||
if (apkDetailsList.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
throw NoReleasesError(note: tr('gitlabSourceNote'));
|
||||
}
|
||||
if (fallbackToOlderReleases) {
|
||||
if (additionalSettings['trackOnly'] != true) {
|
||||
@ -211,7 +195,7 @@ class GitLab extends AppSource {
|
||||
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
|
||||
}
|
||||
if (apkDetailsList.isEmpty) {
|
||||
throw NoReleasesError();
|
||||
throw NoReleasesError(note: tr('gitlabSourceNote'));
|
||||
}
|
||||
}
|
||||
return apkDetailsList.first;
|
||||
|
@ -94,7 +94,8 @@ class HTML extends AppSource {
|
||||
GeneratedFormSwitch('sortByFileNamesNotLinks',
|
||||
label: tr('sortByFileNamesNotLinks'))
|
||||
],
|
||||
[GeneratedFormSwitch('reverseSort', label: tr('reverseSort'))],
|
||||
[GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
|
||||
[GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))],
|
||||
[
|
||||
GeneratedFormSwitch('supportFixedAPKURL',
|
||||
defaultValue: true, label: tr('supportFixedAPKURL')),
|
||||
@ -185,12 +186,15 @@ class HTML extends AppSource {
|
||||
.toList();
|
||||
}
|
||||
List<String> links = [];
|
||||
bool skipSort = additionalSettings['skipSort'] == true;
|
||||
if ((additionalSettings['intermediateLinkRegex'] as String?)
|
||||
?.isNotEmpty ==
|
||||
true) {
|
||||
var reg = RegExp(additionalSettings['intermediateLinkRegex']);
|
||||
links = allLinks.where((element) => reg.hasMatch(element)).toList();
|
||||
links.sort((a, b) => compareAlphaNumeric(a, b));
|
||||
if (!skipSort) {
|
||||
links.sort((a, b) => compareAlphaNumeric(a, b));
|
||||
}
|
||||
if (links.isEmpty) {
|
||||
throw ObtainiumError(tr('intermediateLinkNotFound'));
|
||||
}
|
||||
@ -211,10 +215,14 @@ class HTML extends AppSource {
|
||||
Uri.parse(element).path.toLowerCase().endsWith('.apk'))
|
||||
.toList();
|
||||
}
|
||||
links.sort((a, b) => additionalSettings['sortByFileNamesNotLinks'] == true
|
||||
? compareAlphaNumeric(a.split('/').where((e) => e.isNotEmpty).last,
|
||||
b.split('/').where((e) => e.isNotEmpty).last)
|
||||
: compareAlphaNumeric(a, b));
|
||||
if (!skipSort) {
|
||||
links.sort((a, b) =>
|
||||
additionalSettings['sortByFileNamesNotLinks'] == true
|
||||
? compareAlphaNumeric(
|
||||
a.split('/').where((e) => e.isNotEmpty).last,
|
||||
b.split('/').where((e) => e.isNotEmpty).last)
|
||||
: compareAlphaNumeric(a, b));
|
||||
}
|
||||
if (additionalSettings['reverseSort'] == true) {
|
||||
links = links.reversed.toList();
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class IzzyOnDroid extends AppSource {
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
String? appId = await tryInferringAppId(standardUrl);
|
||||
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
await sourceRequest(
|
||||
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
|
||||
'https://android.izzysoft.de/frepo/$appId',
|
||||
|
@ -34,7 +34,9 @@ class CredsNeededError extends ObtainiumError {
|
||||
}
|
||||
|
||||
class NoReleasesError extends ObtainiumError {
|
||||
NoReleasesError() : super(tr('noReleaseFound'));
|
||||
NoReleasesError({String? note})
|
||||
: super(
|
||||
'${tr('noReleaseFound')}${note?.isNotEmpty == true ? '\n\n$note' : ''}');
|
||||
}
|
||||
|
||||
class NoAPKError extends ObtainiumError {
|
||||
|
@ -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.35';
|
||||
const String currentVersion = '0.14.41';
|
||||
const String currentReleaseTag =
|
||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -145,6 +145,29 @@ class _AppPageState extends State<AppPage> {
|
||||
appsProvider.saveApps([app.app]);
|
||||
}
|
||||
}),
|
||||
if (app?.app.additionalSettings['about'] is String &&
|
||||
app?.app.additionalSettings['about'].isNotEmpty)
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
GestureDetector(
|
||||
onLongPress: () {
|
||||
Clipboard.setData(ClipboardData(
|
||||
text: app?.app.additionalSettings['about'] ?? ''));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(tr('copiedToClipboard')),
|
||||
));
|
||||
},
|
||||
child: Text(
|
||||
app?.app.additionalSettings['about'],
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -496,14 +496,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
var transparent =
|
||||
Theme.of(context).colorScheme.background.withAlpha(0).value;
|
||||
List<double> stops = [
|
||||
...listedApps[index]
|
||||
.app
|
||||
.categories
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) =>
|
||||
((e.key / (listedApps[index].app.categories.length - 1))))
|
||||
,
|
||||
...listedApps[index].app.categories.asMap().entries.map(
|
||||
(e) => ((e.key / (listedApps[index].app.categories.length - 1)))),
|
||||
1
|
||||
];
|
||||
if (stops.length == 2) {
|
||||
@ -516,13 +510,9 @@ class AppsPageState extends State<AppsPage> {
|
||||
begin: const Alignment(-1, 0),
|
||||
end: const Alignment(-0.97, 0),
|
||||
colors: [
|
||||
...listedApps[index]
|
||||
.app
|
||||
.categories
|
||||
.map((e) =>
|
||||
Color(settingsProvider.categories[e] ?? transparent)
|
||||
.withAlpha(255))
|
||||
,
|
||||
...listedApps[index].app.categories.map((e) =>
|
||||
Color(settingsProvider.categories[e] ?? transparent)
|
||||
.withAlpha(255)),
|
||||
Color(transparent)
|
||||
])),
|
||||
child: ListTile(
|
||||
@ -881,7 +871,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
onPressed: () {
|
||||
String urls = '';
|
||||
for (var a in selectedApps) {
|
||||
urls += '${a.url}\n';
|
||||
urls += 'obtainium://add/${a.url}\n';
|
||||
}
|
||||
urls = urls.substring(0, urls.length - 1);
|
||||
Share.share(urls,
|
||||
@ -981,10 +971,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
defaultValue: filter.sourceFilter,
|
||||
[
|
||||
MapEntry('', tr('none')),
|
||||
...sourceProvider.sources
|
||||
.map((e) =>
|
||||
MapEntry(e.runtimeType.toString(), e.name))
|
||||
|
||||
...sourceProvider.sources.map(
|
||||
(e) => MapEntry(e.runtimeType.toString(), e.name))
|
||||
])
|
||||
]
|
||||
],
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -344,7 +347,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
: () {
|
||||
runObtainiumExport(pickOnly: true);
|
||||
},
|
||||
child: Text(tr('pickExportDir')),
|
||||
child: Text(tr('pickExportDir'),
|
||||
textAlign: TextAlign.center),
|
||||
)),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
@ -357,7 +361,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
snapshot.data == null
|
||||
? null
|
||||
: runObtainiumExport,
|
||||
child: Text(tr('obtainiumExport')),
|
||||
child: Text(tr('obtainiumExport'),
|
||||
textAlign: TextAlign.center),
|
||||
)),
|
||||
],
|
||||
),
|
||||
@ -372,7 +377,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
onPressed: importInProgress
|
||||
? null
|
||||
: runObtainiumImport,
|
||||
child: Text(tr('obtainiumImport')))),
|
||||
child: Text(tr('obtainiumImport'),
|
||||
textAlign: TextAlign.center))),
|
||||
],
|
||||
),
|
||||
if (snapshot.data != null)
|
||||
@ -388,6 +394,14 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
defaultValue: settingsProvider
|
||||
.autoExportOnChanges,
|
||||
)
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch(
|
||||
'exportSettings',
|
||||
label: tr('includeSettings'),
|
||||
defaultValue: settingsProvider
|
||||
.exportSettings,
|
||||
)
|
||||
]
|
||||
],
|
||||
onValueChanges:
|
||||
@ -400,6 +414,12 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
'autoExportOnChanges'] ==
|
||||
true;
|
||||
}
|
||||
if (value['exportSettings'] !=
|
||||
null) {
|
||||
settingsProvider.exportSettings =
|
||||
value['exportSettings'] ==
|
||||
true;
|
||||
}
|
||||
}
|
||||
}),
|
||||
],
|
||||
@ -598,6 +618,7 @@ class SelectionModal extends StatefulWidget {
|
||||
|
||||
class _SelectionModalState extends State<SelectionModal> {
|
||||
Map<MapEntry<String, List<String>>, bool> entrySelections = {};
|
||||
String filterRegex = '';
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -618,11 +639,50 @@ class _SelectionModalState extends State<SelectionModal> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Map<MapEntry<String, List<String>>, bool> filteredEntrySelections = {};
|
||||
entrySelections.forEach((key, value) {
|
||||
var searchableText = key.value.isEmpty ? key.key : key.value[0];
|
||||
if (filterRegex.isEmpty || RegExp(filterRegex).hasMatch(searchableText)) {
|
||||
filteredEntrySelections.putIfAbsent(key, () => value);
|
||||
}
|
||||
});
|
||||
if (filterRegex.isNotEmpty && filteredEntrySelections.isEmpty) {
|
||||
entrySelections.forEach((key, value) {
|
||||
var searchableText = key.value.isEmpty ? key.key : key.value[0];
|
||||
if (filterRegex.isEmpty ||
|
||||
RegExp(filterRegex, caseSensitive: false)
|
||||
.hasMatch(searchableText)) {
|
||||
filteredEntrySelections.putIfAbsent(key, () => value);
|
||||
}
|
||||
});
|
||||
}
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(widget.title ?? tr('pick')),
|
||||
content: Column(children: [
|
||||
...entrySelections.keys.map((entry) {
|
||||
GeneratedForm(
|
||||
items: [
|
||||
[
|
||||
GeneratedFormTextField('filter',
|
||||
label: tr('filter'),
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
return regExValidator(value);
|
||||
}
|
||||
])
|
||||
]
|
||||
],
|
||||
onValueChanges: (value, valid, isBuilding) {
|
||||
if (valid && !isBuilding) {
|
||||
if (value['filter'] != null) {
|
||||
setState(() {
|
||||
filterRegex = value['filter'];
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
...filteredEntrySelections.keys.map((entry) {
|
||||
selectThis(bool? value) {
|
||||
setState(() {
|
||||
value ??= false;
|
||||
|
@ -30,6 +30,29 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
settingsProvider.initializeSettings();
|
||||
}
|
||||
|
||||
var installMethodDropdown = DropdownButtonFormField(
|
||||
decoration: InputDecoration(labelText: tr('installMethod')),
|
||||
value: settingsProvider.installMethod,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: InstallMethodSettings.normal,
|
||||
child: Text(tr('normal')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: InstallMethodSettings.shizuku,
|
||||
child: Text(tr('shizuku')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: InstallMethodSettings.root,
|
||||
child: Text(tr('root')),
|
||||
)
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsProvider.installMethod = value;
|
||||
}
|
||||
});
|
||||
|
||||
var themeDropdown = DropdownButtonFormField(
|
||||
decoration: InputDecoration(labelText: tr('theme')),
|
||||
value: settingsProvider.theme,
|
||||
@ -327,6 +350,21 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
})
|
||||
],
|
||||
),
|
||||
height16,
|
||||
installMethodDropdown,
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(child: Text(tr('parallelDownloads'))),
|
||||
Switch(
|
||||
value: settingsProvider.parallelDownloads,
|
||||
onChanged: (value) {
|
||||
settingsProvider.parallelDownloads =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height32,
|
||||
Text(
|
||||
tr('sourceSpecific'),
|
||||
|
@ -33,6 +33,7 @@ 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;
|
||||
import 'installers_provider.dart';
|
||||
|
||||
final pm = AndroidPackageManager();
|
||||
|
||||
@ -504,7 +505,8 @@ class AppsProvider with ChangeNotifier {
|
||||
!(await canDowngradeApps())) {
|
||||
throw DowngradeError();
|
||||
}
|
||||
if (needsBGWorkaround) {
|
||||
if (needsBGWorkaround &&
|
||||
settingsProvider.installMethod == InstallMethodSettings.normal) {
|
||||
// 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
|
||||
@ -515,8 +517,21 @@ class AppsProvider with ChangeNotifier {
|
||||
await saveApps([apps[file.appId]!.app],
|
||||
attemptToCorrectInstallStatus: false);
|
||||
}
|
||||
int? code =
|
||||
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
|
||||
int? code;
|
||||
switch (settingsProvider.installMethod) {
|
||||
case InstallMethodSettings.normal:
|
||||
code = await AndroidPackageInstaller.installApk(
|
||||
apkFilePath: file.file.path);
|
||||
case InstallMethodSettings.shizuku:
|
||||
code = (await Installers.installWithShizuku(
|
||||
apkFileUri: file.file.uri.toString()))
|
||||
? 0
|
||||
: 1;
|
||||
case InstallMethodSettings.root:
|
||||
code = (await Installers.installWithRoot(apkFilePath: file.file.path))
|
||||
? 0
|
||||
: 1;
|
||||
}
|
||||
bool installed = false;
|
||||
if (code != null && code != 0 && code != 3) {
|
||||
throw InstallError(code);
|
||||
@ -657,7 +672,7 @@ class AppsProvider with ChangeNotifier {
|
||||
appsToInstall =
|
||||
moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId);
|
||||
|
||||
for (var id in appsToInstall) {
|
||||
Future<void> updateFn(String id, {bool skipInstalls = false}) async {
|
||||
try {
|
||||
var downloadedArtifact =
|
||||
// ignore: use_build_context_synchronously
|
||||
@ -672,8 +687,23 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
var appId = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||
bool willBeSilent = await canInstallSilently(apps[appId]!.app);
|
||||
if (!(await settingsProvider.getInstallPermission(enforce: false))) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
switch (settingsProvider.installMethod) {
|
||||
case InstallMethodSettings.normal:
|
||||
if (!(await settingsProvider.getInstallPermission(
|
||||
enforce: false))) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
}
|
||||
case InstallMethodSettings.shizuku:
|
||||
int code = await Installers.checkPermissionShizuku();
|
||||
if (code == -1) {
|
||||
throw ObtainiumError(tr('shizukuBinderNotFound'));
|
||||
} else if (code == 0) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
}
|
||||
case InstallMethodSettings.root:
|
||||
if (!(await Installers.checkPermissionRoot())) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
}
|
||||
}
|
||||
if (!willBeSilent && context != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
@ -682,24 +712,26 @@ class AppsProvider with ChangeNotifier {
|
||||
apps[id]?.downloadProgress = -1;
|
||||
notifyListeners();
|
||||
try {
|
||||
if (downloadedFile != null) {
|
||||
if (willBeSilent && context == null) {
|
||||
installApk(downloadedFile, needsBGWorkaround: true);
|
||||
if (!skipInstalls) {
|
||||
if (downloadedFile != null) {
|
||||
if (willBeSilent && context == null) {
|
||||
installApk(downloadedFile, needsBGWorkaround: true);
|
||||
} else {
|
||||
await installApk(downloadedFile);
|
||||
}
|
||||
} else {
|
||||
await installApk(downloadedFile);
|
||||
if (willBeSilent && context == null) {
|
||||
installXApkDir(downloadedDir!, needsBGWorkaround: true);
|
||||
} else {
|
||||
await installXApkDir(downloadedDir!);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (willBeSilent && context == null) {
|
||||
installXApkDir(downloadedDir!, needsBGWorkaround: true);
|
||||
} else {
|
||||
await installXApkDir(downloadedDir!);
|
||||
notificationsProvider?.notify(SilentUpdateAttemptNotification(
|
||||
[apps[appId]!.app],
|
||||
id: appId.hashCode));
|
||||
}
|
||||
}
|
||||
if (willBeSilent && context == null) {
|
||||
notificationsProvider?.notify(SilentUpdateAttemptNotification(
|
||||
[apps[appId]!.app],
|
||||
id: appId.hashCode));
|
||||
}
|
||||
} finally {
|
||||
apps[id]?.downloadProgress = null;
|
||||
notifyListeners();
|
||||
@ -710,6 +742,20 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
if (!settingsProvider.parallelDownloads) {
|
||||
for (var id in appsToInstall) {
|
||||
await updateFn(id);
|
||||
}
|
||||
} else {
|
||||
await Future.wait(
|
||||
appsToInstall.map((id) => updateFn(id, skipInstalls: true)));
|
||||
for (var id in appsToInstall) {
|
||||
if (!errors.appIdNames.containsKey(id)) {
|
||||
await updateFn(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.idsByErrorString.isNotEmpty) {
|
||||
throw errors;
|
||||
}
|
||||
@ -726,12 +772,15 @@ class AppsProvider with ChangeNotifier {
|
||||
return appsDir;
|
||||
}
|
||||
|
||||
Future<PackageInfo?> getInstalledInfo(String? packageName) async {
|
||||
Future<PackageInfo?> getInstalledInfo(String? packageName,
|
||||
{bool printErr = true}) async {
|
||||
if (packageName != null) {
|
||||
try {
|
||||
return await pm.getPackageInfo(packageName: packageName);
|
||||
} catch (e) {
|
||||
print(e); // OK
|
||||
if (printErr) {
|
||||
print(e); // OK
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@ -974,7 +1023,7 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
exportApps(isAuto: true);
|
||||
export(isAuto: true);
|
||||
}
|
||||
|
||||
Future<void> removeApps(List<String> appIds) async {
|
||||
@ -996,7 +1045,7 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
if (appIds.isNotEmpty) {
|
||||
notifyListeners();
|
||||
exportApps(isAuto: true);
|
||||
export(isAuto: true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1173,7 +1222,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 +1252,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,21 +1277,36 @@ 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));
|
||||
}
|
||||
for (App a in importedApps) {
|
||||
if (apps[a.id]?.app.installedVersion != null) {
|
||||
a.installedVersion = apps[a.id]?.app.installedVersion;
|
||||
}
|
||||
a.installedVersion =
|
||||
(await getInstalledInfo(a.id, printErr: false))?.versionName;
|
||||
}
|
||||
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
|
||||
|
56
lib/providers/installers_provider.dart
Normal file
56
lib/providers/installers_provider.dart
Normal file
@ -0,0 +1,56 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class Installers {
|
||||
static const MethodChannel _channel = MethodChannel('installers');
|
||||
static bool _callbacksApplied = false;
|
||||
static int _resPermShizuku = -2; // not set
|
||||
|
||||
static Future waitWhile(bool Function() test,
|
||||
[Duration pollInterval = const Duration(milliseconds: 250)]) {
|
||||
var completer = Completer();
|
||||
check() {
|
||||
if (test()) {
|
||||
Timer(pollInterval, check);
|
||||
} else {
|
||||
completer.complete();
|
||||
}
|
||||
}
|
||||
check();
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future handleCalls(MethodCall call) async {
|
||||
if (call.method == 'resPermShizuku') {
|
||||
_resPermShizuku = call.arguments['res'];
|
||||
}
|
||||
}
|
||||
|
||||
static Future<int> checkPermissionShizuku() async {
|
||||
if (!_callbacksApplied) {
|
||||
_channel.setMethodCallHandler(handleCalls);
|
||||
_callbacksApplied = true;
|
||||
}
|
||||
int res = await _channel.invokeMethod('checkPermissionShizuku');
|
||||
if(res == -2) {
|
||||
await waitWhile(() => _resPermShizuku == -2);
|
||||
res = _resPermShizuku;
|
||||
_resPermShizuku = -2;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static Future<bool> checkPermissionRoot() async {
|
||||
return await _channel.invokeMethod('checkPermissionRoot');
|
||||
}
|
||||
|
||||
static Future<bool> installWithShizuku({required String apkFileUri}) async {
|
||||
return await _channel.invokeMethod(
|
||||
'installWithShizuku', {'apkFileUri': apkFileUri});
|
||||
}
|
||||
|
||||
static Future<bool> installWithRoot({required String apkFilePath}) async {
|
||||
return await _channel.invokeMethod(
|
||||
'installWithRoot', {'apkFilePath': apkFilePath});
|
||||
}
|
||||
}
|
@ -17,6 +17,8 @@ import 'package:shared_storage/shared_storage.dart' as saf;
|
||||
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
|
||||
String obtainiumId = 'dev.imranr.obtainium';
|
||||
|
||||
enum InstallMethodSettings { normal, shizuku, root }
|
||||
|
||||
enum ThemeSettings { system, light, dark }
|
||||
|
||||
enum ColourSettings { basic, materialYou }
|
||||
@ -49,6 +51,16 @@ class SettingsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
InstallMethodSettings get installMethod {
|
||||
return InstallMethodSettings
|
||||
.values[prefs?.getInt('installMethod') ?? InstallMethodSettings.normal.index];
|
||||
}
|
||||
|
||||
set installMethod(InstallMethodSettings t) {
|
||||
prefs?.setInt('installMethod', t.index);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
ThemeSettings get theme {
|
||||
return ThemeSettings
|
||||
.values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
|
||||
@ -213,7 +225,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 +428,22 @@ 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();
|
||||
}
|
||||
|
||||
bool get parallelDownloads {
|
||||
return prefs?.getBool('parallelDownloads') ?? false;
|
||||
}
|
||||
|
||||
set parallelDownloads(bool val) {
|
||||
prefs?.setBool('parallelDownloads', val);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -454,7 +454,8 @@ abstract class AppSource {
|
||||
[
|
||||
GeneratedFormSwitch('skipUpdateNotifications',
|
||||
label: tr('skipUpdateNotifications'))
|
||||
]
|
||||
],
|
||||
[GeneratedFormTextField('about', label: tr('about'), required: false)]
|
||||
];
|
||||
|
||||
// Previous 2 variables combined into one at runtime for convenient usage
|
||||
@ -683,8 +684,9 @@ class SourceProvider {
|
||||
name = name.isNotEmpty ? name : apk.names.name;
|
||||
App finalApp = App(
|
||||
currentApp?.id ??
|
||||
((!source.appIdInferIsOptional ||
|
||||
(source.appIdInferIsOptional && inferAppIdIfOptional))
|
||||
(!trackOnly &&
|
||||
(!source.appIdInferIsOptional ||
|
||||
(source.appIdInferIsOptional && inferAppIdIfOptional))
|
||||
? await source.tryInferringAppId(standardUrl,
|
||||
additionalSettings: additionalSettings)
|
||||
: null) ??
|
||||
@ -704,8 +706,9 @@ class SourceProvider {
|
||||
changeLog: apk.changeLog,
|
||||
overrideSource: overrideSource ?? currentApp?.overrideSource,
|
||||
allowIdChange: currentApp?.allowIdChange ??
|
||||
source.appIdInferIsOptional &&
|
||||
inferAppIdIfOptional // Optional ID inferring may be incorrect - allow correction on first install
|
||||
trackOnly ||
|
||||
(source.appIdInferIsOptional &&
|
||||
inferAppIdIfOptional) // Optional ID inferring may be incorrect - allow correction on first install
|
||||
);
|
||||
return source.endOfGetAppChanges(finalApp);
|
||||
}
|
||||
|
96
pubspec.lock
96
pubspec.lock
@ -5,10 +5,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: android_alarm_manager_plus
|
||||
sha256: "82fb28c867c4b3dd7e9157728e46426b8916362f977dbba46b949210f00099f4"
|
||||
sha256: "84720c8ad2758aabfbeafd24a8c355d8c8dd3aa52b01eaf3bb827c7210f61a91"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.4"
|
||||
android_intent_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -38,10 +38,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: animations
|
||||
sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70
|
||||
sha256: "708e4b68c23228c264b038fe7003a2f5d01ce85fc64d8cae090e86b27fcea6c5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
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:
|
||||
@ -142,10 +150,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "2f9d2cbccb76127ba28528cb3ae2c2326a122446a83de5a056aaa3880d3882c5"
|
||||
sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+7"
|
||||
version: "0.3.3+8"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -198,10 +206,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dynamic_color
|
||||
sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f"
|
||||
sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.8"
|
||||
version: "1.6.9"
|
||||
easy_localization:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -259,10 +267,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_archive
|
||||
sha256: aec85d1da65e5b33a529db00a86df0b8e92bda78088a7cfaeeba5187701d0d85
|
||||
sha256: "004132780d382df5171589ab793e2efc9c3eef570fe72d78b4ccfbfbe52762ae"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "6.0.0"
|
||||
flutter_fgbg:
|
||||
dependency: "direct main"
|
||||
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:
|
||||
@ -370,10 +386,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -562,10 +578,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: d96ff56a757b7f04fa825c469d296c5aebc55f743e87bd639fef91a466a24da8
|
||||
sha256: "11b762a8c123dced6461933a88ea1edbbe036078c3f9f41b08886e678e7864df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0+1"
|
||||
version: "0.1.0+2"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -586,10 +602,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6
|
||||
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
version: "6.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -767,10 +783,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
|
||||
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.0+1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
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,34 +919,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: "8326ee235f87605a2bfc444a4abc897f4abc78d83f054ba7d3d1074ce82b4fbf"
|
||||
sha256: b54c89fe14a6d26a2a46e24880da0441cdd2bf1f6d01a5b3e1d39558feb1de0b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.1"
|
||||
version: "3.13.1"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: adb8c03c2be231bea5a8ed0e9039e9d18dbb049603376beaefa15393ede468a5
|
||||
sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
version: "2.9.0"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: accdaaa49a2aca2dc3c3230907988954cdd23fed0a19525d6c9789d380f4dc76
|
||||
sha256: eebfabfa8a115b535b52031b8b26f7a4b58ceceab378bc9db8762b0fb46f7b5d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.9.4"
|
||||
version: "3.10.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f"
|
||||
sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
version: "5.1.1"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -951,10 +967,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.2"
|
||||
version: "6.5.0"
|
||||
yaml:
|
||||
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.14.35+229 # When changing this, update the tag in main() accordingly
|
||||
version: 0.14.41+235 # When changing this, update the tag in main() accordingly
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
@ -62,11 +62,12 @@ dependencies:
|
||||
easy_localization: ^3.0.1
|
||||
android_intent_plus: ^4.0.0
|
||||
flutter_markdown: ^0.6.14
|
||||
flutter_archive: ^5.0.0
|
||||
flutter_archive: ^6.0.0
|
||||
hsluv: ^1.1.3
|
||||
connectivity_plus: ^5.0.0
|
||||
shared_storage: ^0.8.0
|
||||
crypto: ^3.0.3
|
||||
app_links: ^3.5.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user