mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Compare commits
40 Commits
v0.14.37-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 |
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"
|
||||
}
|
||||
|
@ -66,6 +66,13 @@
|
||||
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" />
|
||||
|
@ -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!
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -261,6 +261,8 @@
|
||||
"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",
|
||||
@ -278,6 +280,7 @@
|
||||
"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?"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"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",
|
||||
@ -278,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?"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"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",
|
||||
@ -278,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Überprüfe nur installierte und mit „nur Nachverfolgen“ markierte Apps auf Aktualisierungen",
|
||||
"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?"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"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",
|
||||
@ -278,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,9 +11,9 @@
|
||||
"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",
|
||||
"fallbackToOlderReleases": "Retroceder a versiones previas",
|
||||
"filterReleaseTitlesByRegEx": "Filtrar Títulos de Versiones",
|
||||
"invalidRegEx": "Expresión inválida",
|
||||
"noDescription": "Sin descripción",
|
||||
@ -22,7 +22,7 @@
|
||||
"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 actalizarla.",
|
||||
"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,9 +56,9 @@
|
||||
"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 Seguimiento)\ncomo Actualizada",
|
||||
@ -98,8 +98,8 @@
|
||||
"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.",
|
||||
"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:",
|
||||
@ -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",
|
||||
@ -170,12 +170,12 @@
|
||||
"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",
|
||||
@ -195,8 +195,8 @@
|
||||
"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 marcadas 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",
|
||||
@ -211,7 +211,7 @@
|
||||
"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 desde 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",
|
||||
@ -219,8 +219,8 @@
|
||||
"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": "Mover las Apps no instaladas al final de la vista de Apps",
|
||||
"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)",
|
||||
@ -230,7 +230,7 @@
|
||||
"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 las 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.",
|
||||
@ -244,23 +244,25 @@
|
||||
"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 verificar con Obtainium abierto.",
|
||||
"verifyLatestTag": "Verifica la etiqueta 'latest'",
|
||||
"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": "Match Group to Use",
|
||||
"matchGroupToUse": "Coincidir en Grupo a Usar",
|
||||
"highlightTouchTargets": "Resaltar objetivos menos obvios",
|
||||
"pickExportDir": "Selecciona el Directorio para Exportar",
|
||||
"autoExportOnChanges": "Auto Exportar cuando haya cambios",
|
||||
"includeSettings": "Include settings",
|
||||
"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",
|
||||
@ -275,9 +277,10 @@
|
||||
"downloadingXNotifChannel": "Descargando {}",
|
||||
"completeAppInstallationNotifChannel": "Instalación Completa de la Aplicación",
|
||||
"checkingForUpdatesNotifChannel": "Buscando Actualizaciones",
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
|
||||
"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?"
|
||||
@ -315,8 +318,8 @@
|
||||
"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.",
|
||||
|
@ -245,8 +245,8 @@
|
||||
"enableBackgroundUpdates": "به روز رسانی پس زمینه را فعال کنید",
|
||||
"backgroundUpdateReqsExplanation": "به روز رسانی پس زمینه ممکن است برای همه برنامه ها امکان پذیر نباشد.",
|
||||
"backgroundUpdateLimitsExplanation": "موفقیت نصب پسزمینه تنها زمانی مشخص میشود که Obtainium باز شود.",
|
||||
"verifyLatestTag": "برچسب "آخرین" را تأیید کنید",
|
||||
"intermediateLinkRegex": "برای اولین بار بازدید از لینک "متوسط" را فیلتر کنید",
|
||||
"verifyLatestTag": "برچسب \"آخرین\" را تأیید کنید",
|
||||
"intermediateLinkRegex": "برای اولین بار بازدید از لینک \"متوسط\" را فیلتر کنید",
|
||||
"intermediateLinkNotFound": "لینک میانی پیدا نشد",
|
||||
"exemptFromBackgroundUpdates": "معاف از بهروزرسانیهای پسزمینه (در صورت فعال بودن)",
|
||||
"bgUpdatesOnWiFiOnly": "بهروزرسانیهای پسزمینه را در صورت عدم اتصال به WiFi غیرفعال کنید",
|
||||
@ -261,6 +261,8 @@
|
||||
"trySelectingSuggestedVersionCode": "نسخه پیشنهادی APK نسخه کد را انتخاب کنید",
|
||||
"dontSortReleasesList": "حفظ سفارش انتشار از API",
|
||||
"reverseSort": "مرتب سازی معکوس",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "منوی اشکال زدایی",
|
||||
"bgTaskStarted": "کار پس زمینه شروع شد - لاگ های مربوط را بررسی کنید.",
|
||||
"runBgCheckNow": "اکنون بهروزرسانی پسزمینه را بررسی کنید",
|
||||
@ -278,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "فقط برنامه های نصب شده و فقط ردیابی را برای به روز رسانی بررسی کنید",
|
||||
"supportFixedAPKURL": "پشتیبانی از URL های APK ثابت",
|
||||
"selectX": "انتخاب کنید {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "برنامه حذف شود؟",
|
||||
"other": "برنامه ها حذف شوند؟"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"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",
|
||||
@ -278,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 ?"
|
||||
|
@ -260,6 +260,8 @@
|
||||
"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",
|
||||
@ -278,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Csak a telepített és a csak követhető appokat ellenőrizze frissítésekért",
|
||||
"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?"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"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",
|
||||
@ -278,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?"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
|
||||
"dontSortReleasesList": "APIからのリリース順を保持する",
|
||||
"reverseSort": "逆順ソート",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "デバッグメニュー",
|
||||
"bgTaskStarted": "バックグラウンドタスクが開始されました - ログを確認してください。",
|
||||
"runBgCheckNow": "今すぐバックグラウンドでのアップデート確認を開始する",
|
||||
@ -278,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "インストール済みのアプリと「追跡のみ」のアプリのアップデートのみを確認する",
|
||||
"supportFixedAPKURL": "Support fixed APK URLs",
|
||||
"selectX": "Select {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "アプリを削除しますか?",
|
||||
"other": "アプリを削除しますか?"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"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",
|
||||
@ -278,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?"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"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",
|
||||
@ -278,6 +280,7 @@
|
||||
"onlyCheckInstalledOrTrackOnlyApps": "Sprawdzaj tylko zainstalowane i obserwowane aplikacje pod kątem aktualizacji",
|
||||
"supportFixedAPKURL": "Obsługuj stałe adresy URL APK",
|
||||
"selectX": "Wybierz {}",
|
||||
"parallelDownloads": "Allow parallel downloads",
|
||||
"removeAppQuestion": {
|
||||
"one": "Usunąć aplikację?",
|
||||
"few": "Usunąć aplikacje?",
|
||||
|
@ -261,6 +261,8 @@
|
||||
"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",
|
||||
@ -278,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?"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"trySelectingSuggestedVersionCode": "Попробуйте выбрать предложенный код версии APK",
|
||||
"dontSortReleasesList": "Сохранить порядок релизов от API",
|
||||
"reverseSort": "Обратная сортировка",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "Меню отладки",
|
||||
"bgTaskStarted": "Фоновая задача начата — проверьте журналы",
|
||||
"runBgCheckNow": "Запустить проверку фонового обновления сейчас",
|
||||
@ -275,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": "Удалить приложения?"
|
||||
|
@ -261,9 +261,12 @@
|
||||
"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?"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"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",
|
||||
@ -278,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?"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"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ờ",
|
||||
@ -278,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?"
|
||||
|
@ -261,6 +261,8 @@
|
||||
"trySelectingSuggestedVersionCode": "尝试选择推荐版本的 APK 文件",
|
||||
"dontSortReleasesList": "保持来自 API 的发行顺序",
|
||||
"reverseSort": "反转排序",
|
||||
"takeFirstLink": "Take first link",
|
||||
"skipSort": "Skip sorting",
|
||||
"debugMenu": "调试选项",
|
||||
"bgTaskStarted": "后台任务已启动 - 详见日志",
|
||||
"runBgCheckNow": "立即进行后台更新检查",
|
||||
@ -278,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));
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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.37';
|
||||
const String currentVersion = '0.14.41';
|
||||
const String currentReleaseTag =
|
||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||
|
||||
|
@ -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))
|
||||
])
|
||||
]
|
||||
],
|
||||
|
@ -347,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,
|
||||
@ -360,7 +361,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
snapshot.data == null
|
||||
? null
|
||||
: runObtainiumExport,
|
||||
child: Text(tr('obtainiumExport')),
|
||||
child: Text(tr('obtainiumExport'),
|
||||
textAlign: TextAlign.center),
|
||||
)),
|
||||
],
|
||||
),
|
||||
@ -375,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)
|
||||
|
@ -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;
|
||||
@ -1230,7 +1279,7 @@ class AppsProvider with ChangeNotifier {
|
||||
|
||||
Future<MapEntry<int, bool>> import(String appsJSON) async {
|
||||
var decodedJSON = jsonDecode(appsJSON);
|
||||
var newFormat = !(decodedJSON is List);
|
||||
var newFormat = decodedJSON is! List;
|
||||
List<App> importedApps =
|
||||
((newFormat ? decodedJSON['apps'] : decodedJSON) as List<dynamic>)
|
||||
.map((e) => App.fromJson(e))
|
||||
@ -1239,9 +1288,8 @@ class AppsProvider with ChangeNotifier {
|
||||
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();
|
||||
|
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];
|
||||
@ -425,4 +437,13 @@ class SettingsProvider with ChangeNotifier {
|
||||
prefs?.setBool('exportSettings', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get parallelDownloads {
|
||||
return prefs?.getBool('parallelDownloads') ?? false;
|
||||
}
|
||||
|
||||
set parallelDownloads(bool val) {
|
||||
prefs?.setBool('parallelDownloads', val);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -684,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) ??
|
||||
@ -705,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);
|
||||
}
|
||||
|
24
pubspec.lock
24
pubspec.lock
@ -206,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:
|
||||
@ -267,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:
|
||||
@ -783,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:
|
||||
@ -919,26 +919,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: e313dcdf45d4c95bcb8960351ef2389b7f0687b90bc92483f7f7983ae5758456
|
||||
sha256: b54c89fe14a6d26a2a46e24880da0441cdd2bf1f6d01a5b3e1d39558feb1de0b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.13.0"
|
||||
version: "3.13.1"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: "68e86162aa8fc646ae859e1585995c096c95fc2476881fa0c4a8d10f56013a5a"
|
||||
sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.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:
|
||||
|
@ -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.37+231 # 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,7 +62,7 @@ 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
|
||||
|
Reference in New Issue
Block a user