Compare commits

...

80 Commits

Author SHA1 Message Date
6e13457eb2 Merge pull request #1210 from iDazai/main
Update de.json
2023-12-29 12:44:09 -06:00
3b319ee19b Merge pull request #1206 from gidano/main
Update hu.json
2023-12-29 12:44:02 -06:00
8ea8daa811 Merge pull request #1212 from ImranR98/dev
HTML intermediate link bugfix (#1211)
2023-12-29 12:43:49 -06:00
eb29b908c2 Increment version 2023-12-29 13:43:06 -05:00
9935cb482e HTML intermediate link bugfix (#1211) 2023-12-29 13:39:56 -05:00
0d6e7181cf Update de.json 2023-12-29 17:35:25 +01:00
d225650e15 Update hu.json 2023-12-29 10:12:13 +01:00
bfe09791d5 Merge pull request #1205 from ImranR98/dev
Bugfix in HTML intermediate link UI (incomplete)
2023-12-28 18:24:03 -06:00
85d103f3f6 Bugfix in HTML intermediate link UI (incomplete) 2023-12-28 19:16:16 -05:00
9e6dbe2465 Merge pull request #1189 from iDazai/main
Update de.json
2023-12-28 16:09:12 -06:00
355e5ccda6 Merge branch 'main' into main 2023-12-28 16:09:03 -06:00
a5f6f05e10 Merge pull request #1190 from unbranched/patch-4
Update it.json
2023-12-28 16:08:29 -06:00
db0d35d80b Merge pull request #1196 from LucasTavaresA/main
Update pt.json
2023-12-28 16:08:21 -06:00
6fca2a3931 Merge pull request #1197 from CertainBot/main
Update Spanish 3rd pass
2023-12-28 16:08:11 -06:00
0305a42b02 Merge branch 'main' into main 2023-12-28 16:08:04 -06:00
77d81716ed Switch to a New Background Task Plugin (#608), Add Link Text Filter for HTML Links (#1182), Add Support for Multiple Intermediate Links to HTML Source (#1204)
- Switch to a New Background Task Plugin (#608)
- Add Link Text Filter for HTML Links (#1182)
- Add Support for Multiple Intermediate Links to HTML Source
2023-12-28 17:06:11 -05:00
3e54e80eb6 Update it.json
Added missing strings.
2023-12-27 09:15:22 +00:00
3c9bb63d32 Update es.json
- English: updated new strings, few minor corrections.
- Español: actualizacion de frases nuevas, correcciones menores.
2023-12-25 16:04:51 +01:00
617ab9efab Update pt.json 2023-12-25 10:45:12 -03:00
bc574097e2 Update it.json 2023-12-25 09:40:29 +00:00
4cc64dc233 Update de.json
translated three new strings, fixed on with hyphen missing
2023-12-25 10:30:44 +01:00
45fa0a165a Merge pull request #1188 from ImranR98/dev
Typo, minor error reporting bug
2023-12-24 21:08:06 -06:00
0e5c07a078 Update packages, increment version 2023-12-24 22:07:27 -05:00
601a742c71 Merge remote-tracking branch 'origin/main' into dev 2023-12-24 22:04:51 -05:00
c972401b6e Merge pull request #1186 from CertainBot/main
Update Spanish 2nd pass
2023-12-24 21:04:35 -06:00
024e81cf01 bug 2023-12-24 22:03:29 -05:00
975ed402d5 Merge remote-tracking branch 'origin/main' into dev 2023-12-24 22:03:05 -05:00
b9e8083744 Merge pull request #1184 from re7gog/re7gog
Shizuku and Root
2023-12-24 21:02:48 -06:00
bb859708bc Typo 2023-12-24 21:14:18 -05:00
3cf2c221ac Update Spanish 2nd pass
- English: update new strings, homogenize expressions.
-Español: actualizar frases nuevas, homogeneizar expresiones.
2023-12-24 21:00:37 +01:00
6edd7edcd2 No delay when already has permission 2023-12-24 19:43:34 +03:00
4e26a02d78 Merge 2023-12-24 18:49:33 +03:00
bb36a57053 Merge branch 'main' into re7gog 2023-12-24 18:37:11 +03:00
b291c800f1 Now it looks good 2023-12-24 18:26:11 +03:00
b63a798d86 Merge pull request #1180 from ImranR98/dev
- Fix installed version bug when importing apps (#1179)
- Share button now shares Obtainium "protocol" links
2023-12-22 20:14:07 -06:00
eacf3777a4 Update packages, increment version 2023-12-22 21:13:15 -05:00
a5a7436bb1 Share button now shares Obtainium "protocol" links 2023-12-22 21:12:07 -05:00
2a4cc35df7 Fix installed version bug when importing apps (#1179) 2023-12-22 21:10:11 -05:00
cdccf58b76 Merge remote-tracking branch 'origin/dev' 2023-12-22 21:00:09 -05:00
27300383a1 Add Wiki link 2023-12-22 20:59:49 -05:00
375b9bce30 Working shizuku installer, need refactor 2023-12-22 16:27:54 +03:00
b6b8db48df request Shizuku permission 2023-12-22 16:08:41 +03:00
36e6c267b9 Shizuku dependencies 2023-12-22 14:21:18 +03:00
13066b3b4a Merge pull request #1177 from ImranR98/dev
- Add "Don't Sort" option to the HTML Source (#1168 )
- Fix syntax error in translation file (#1169)
- Parallel download option (#1175)
2023-12-21 15:02:58 -06:00
ccbe9d00c8 Update packages, increment version 2023-12-21 16:01:42 -05:00
ce291582cb Add "Don't Sort" option to the HTML Source (#1168 ) 2023-12-21 15:57:59 -05:00
bb37bc3b51 Fix syntax error in translation file (#1169) 2023-12-21 15:46:38 -05:00
5a7747acd1 Merge remote-tracking branch 'origin/main' into dev 2023-12-21 15:45:03 -05:00
1bc2ec9461 Parallel download option (#1169) 2023-12-21 15:44:38 -05:00
2b977fc2b0 Merge pull request #1173 from drizzt/fix_1142
Resize adaptative icon to 108x108dp (#1142)
2023-12-21 14:24:21 -06:00
de60c4ee9e Root install 2023-12-20 16:21:12 +03:00
de67e40c00 Add installMethod in settings 2023-12-20 11:57:56 +03:00
cc4b016c64 Resize adaptative icon to 108x108dp (#1142)
Android documentation [1] explicitly specify that adaptive icons must be
108x108dp.
Having bigger Adaptive icons triggers a bug in Android 14 QPR1 that
prevent to install apps from apk directly when the icons are bigger.

[1] https://developer.android.com/develop/ui/views/launch/icon_design_adaptive
2023-12-20 00:00:45 +01:00
f64f561d6f Merge pull request #1167 from ImranR98/dev
Update issue templates
2023-12-17 16:00:25 -06:00
80bddf8a6b Update issue templates 2023-12-17 16:59:23 -05:00
cbaaec961c Merge pull request #1166 from ImranR98/dev
- Get real author name for F-Droid (#1076)
- Always add Track-only apps with random ID (#1138)
- Minor UI fix (#1162)
- Fix 'verify latest' option for GitHub (#1163)
2023-12-17 13:57:34 -06:00
5477b3f936 Update packages, increment version, run dart fix 2023-12-17 14:54:54 -05:00
fd59a93ede Get real author name for F-Droid (#1076) 2023-12-17 14:47:22 -05:00
cd316b7138 Always add Track-only apps with random ID (#1138) 2023-12-17 14:36:06 -05:00
d1955192ed Minor UI fix (#1162) 2023-12-17 14:14:46 -05:00
9beb839bf4 Fix 'verify latest' option for GitHub (#1163) 2023-12-17 14:13:26 -05:00
29ea303093 Merge pull request #1161 from ImranR98/dev
- Custom link support (#368, #918)
- Export settings (#1157)
- Use public GitLab search API (#1147)
- Fix unauthorized error (#1153)
2023-12-16 03:12:10 -06:00
feff6751ca Merge remote-tracking branch 'origin/main' into dev 2023-12-16 04:08:56 -05:00
ca33fdf752 Merge pull request #1133 from CertainBot/patch-1
Update Spanish translation
2023-12-16 03:07:03 -06:00
fdcdfe89d6 Merge pull request #1149 from gidano/main
Update hu.json
2023-12-16 03:06:53 -06:00
48ed2115a7 Merge pull request #1150 from Erudaro/main
Update bs.json
2023-12-16 03:06:44 -06:00
65988f4e08 Link UI bugfix 2023-12-16 04:02:10 -05:00
ede65eda6c bug 2023-12-16 03:47:29 -05:00
5da56acac8 bug 2023-12-16 03:45:15 -05:00
5720c55301 bugs 2023-12-16 03:43:55 -05:00
ffefa4b30e Update packages, increment version 2023-12-16 02:57:08 -05:00
80e4986b23 Export settings (#1157) 2023-12-16 02:55:05 -05:00
dc92ccda0a Use public GitLab search API (#1147) 2023-12-16 02:01:43 -05:00
f9bab18076 Fix unauthorized error (#1153) 2023-12-16 01:44:02 -05:00
2dec52e221 Bugfix 2023-12-16 01:24:43 -05:00
7413f693d7 JSON import link support (#368) 2023-12-16 00:31:32 -05:00
415460df75 Custom link support (#918) 2023-12-15 23:37:04 -05:00
125a194468 Update bs.json 2023-12-10 22:07:14 +01:00
32e9afbf36 Update hu.json 2023-12-10 17:16:08 +01:00
43616c566d Update es.json
- Spanish translation updated. Typos corrected.
- Traducción al castellano actualizada. Corrección de erratas.
2023-12-06 00:35:30 +01:00
52 changed files with 1880 additions and 862 deletions

View File

@ -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. -->

View File

@ -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.

View File

@ -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/)

View File

@ -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 rootProject.ext.compileSdkVersion
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 rootProject.ext.targetSdkVersion
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"
}

View File

@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="dev.imranr.obtainium">
<application
android:label="Obtainium"
@ -28,8 +29,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 +47,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 +60,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>

View File

@ -1,6 +0,0 @@
package dev.imranr.obtainium
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View 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)
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}*/
}

View File

@ -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;
}
}
}

View File

@ -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">

View File

@ -1,5 +1,10 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext {
compileSdkVersion = 34 // or latest
targetSdkVersion = 34 // or latest
appCompatVersion = "1.4.2" // or latest
}
repositories {
google()
mavenCentral()
@ -8,6 +13,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!
}
}
@ -15,6 +21,10 @@ allprojects {
repositories {
google()
mavenCentral()
maven {
// [required] background_fetch
url "${project(':background_fetch').projectDir}/libs"
}
}
}

View File

@ -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,58 @@
"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.",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"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": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Intermediate link nije nađen",
"intermediateLink": "Intermediate link",
"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 +332,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."
}
}

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "Přidat tuto informaci do nastavení.",
"githubSourceNote": "Omezení rychlosti GitHub lze obejít pomocí klíče API.",
"gitlabSourceNote": "Extrakce GitLab APK nemusí fungovat bez klíče API",
"sortByFileNamesNotLinks": "Řadit podle názvů souborů místo celých odkazů",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Filtrovat poznámky k vydání podle regulárního výrazu",
"customLinkFilterRegex": "Vlastní filtr odkazů APK podle regulárního výrazu (výchozí '.apk$')",
"appsPossiblyUpdated": "Byly provedeny pokusy o aktualizaci aplikací",
@ -246,8 +246,10 @@
"backgroundUpdateReqsExplanation": "Aktualizace na pozadí nemusí být možné pro všechny aplikace.",
"backgroundUpdateLimitsExplanation": "Úspěšnost instalace na pozadí lze určit pouze v případě, že je otevřen Obtainium.",
"verifyLatestTag": "Ověřit značku 'latest'",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Intermediate link not found",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Vyloučit aktualizace na pozadí (pokud jsou povoleny)",
"bgUpdatesOnWiFiOnly": "Zakázat aktualizace na pozadí, pokud není přítomna Wi-Fi",
"autoSelectHighestVersionCode": "Automatický výběr nejvyššího kódu verze APK",
@ -256,10 +258,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 +282,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?"

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "Fügen Sie diese Info in den Einstellungen hinzu.",
"githubSourceNote": "Die GitHub-Ratenbegrenzung kann mit einem API-Schlüssel umgangen werden.",
"gitlabSourceNote": "GitLab APK-Extraktion funktioniert möglicherweise nicht ohne API-Schlüssel",
"sortByFileNamesNotLinks": "Sortiere nach Dateinamen, anstelle von ganzen Links",
"sortByLastLinkSegment": "Sortiere nur nach dem letzten Teil des Links",
"filterReleaseNotesByRegEx": "Versionshinweise nach regulärem Ausdruck filtern",
"customLinkFilterRegex": "Benutzerdefinierter APK Link Filter nach Regulärem Ausdruck (Standard '.apk$')",
"appsPossiblyUpdated": "App Aktualisierungen wurden versucht",
@ -247,7 +247,9 @@
"backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.",
"verifyLatestTag": "Überprüfe das „latest“ Tag",
"intermediateLinkRegex": "Filter für einen „Zwischen“-Link, der zuerst besucht werden soll",
"intermediateLinkNotFound": "„Zwischen“link nicht gefunden",
"filterByLinkText": "Filtere Links durch Linktext",
"intermediateLinkNotFound": "„Zwischen“-Link nicht gefunden",
"intermediateLink": "„Zwischen“-Link",
"exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)",
"bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
"autoSelectHighestVersionCode": "Automatisch höchste APK-Version auswählen",
@ -256,10 +258,13 @@
"highlightTouchTargets": "Weniger offensichtliche Touch-Ziele hervorheben",
"pickExportDir": "Export-Verzeichnis wählen",
"autoExportOnChanges": "Automatischer Export bei Änderung(en)",
"includeSettings": "Einstellungen einbeziehen",
"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": "Verwende den ersten Link",
"skipSort": "Überspringe Sortieren",
"debugMenu": "Debug-Menü",
"bgTaskStarted": "Hintergrundaufgabe gestartet Logs prüfen.",
"runBgCheckNow": "Hintergrundaktualisierungsprüfung jetzt durchführen",
@ -277,6 +282,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": "Erlaube parallele Downloads",
"removeAppQuestion": {
"one": "App entfernen?",
"other": "Apps entfernen?"

View File

@ -236,7 +236,7 @@
"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",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
"appsPossiblyUpdated": "App Updates Attempted",
@ -246,8 +246,10 @@
"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",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Intermediate link not found",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
@ -256,10 +258,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 +282,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?"

View File

@ -9,28 +9,28 @@
"placeholder": "Espacio reservado",
"someErrors": "Han ocurrido algunos errores",
"unexpectedError": "Error Inesperado",
"ok": "Correcto",
"ok": "OK",
"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 por título de version",
"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'",
"source": "Origen",
"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.",
"youPickedTrackOnly": "Debe seleccionar la opción de 'Solo Seguimiento'.",
"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,16 +38,16 @@
"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",
"byX": "Por {}",
"noAppsForFilter": "Sin aplicaciones para filtrar",
"byX": "por: {}",
"percentProgress": "Progreso: {}%",
"pleaseWait": "Por favor, espere",
"updateAvailable": "Actualización Disponible",
@ -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,12 +98,12 @@
"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",
"followingURLsHadErrors": "Las siguientes URLs han tenido problemas:",
"okay": "Aceptar",
"selectURL": "Seleccionar URL",
"selectURLs": "Seleccionar URLs",
"pick": "Escoger",
@ -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": "Negro puro 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,10 +135,10 @@
"share": "Compartir",
"appNotFound": "Aplicación no encontrada",
"obtainiumExportHyphenatedLowercase": "obtainium-export",
"pickAnAPK": "Elige una APK",
"pickAnAPK": "Seleccione 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:",
"deviceSupportsXArch": "Su dispositivo soporta las siguientes arquitecturas de procesador: {}.",
"deviceSupportsFollowingArchs": "Su dispositivo soporta las siguientes arquitecturas de procesador:",
"warning": "Aviso",
"sourceIsXButPackageFromYPrompt": "La fuente de la aplicación es '{}' pero el paquete de la actualización viene de '{}'. ¿Desea continuar?",
"updatesAvailable": "Actualizaciones Disponibles",
@ -154,11 +154,11 @@
"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",
"pleaseAllowInstallPerm": "Por favor, permita que Obtainium instale aplicaciones",
"trackOnly": "Solo Seguimiento",
"errorWithHttpStatusCode": "Error {}",
"versionCorrectionDisabled": "Corrección de versiones desactivada (el plugin parece no funcionar)",
@ -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,88 +195,94 @@
"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.",
"releaseDateAsVersion": "Usar Fecha de Publicación como Versión",
"releaseDateAsVersion": "Por fecha de publicación",
"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",
"standardVersionDetection": "Por versión",
"groupByCategory": "Agrupar por Categoría",
"autoApkFilterByArch": "Tratar de filtrar las APKs mediante arquitecturas de procesador si es posible",
"autoApkFilterByArch": "Filtrar APKs por arquitectura del 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": "Puede 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.",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Filtrar por notas de nersió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",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Enlace intermedio no encontrado",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Exenta 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": "Pruebe 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 de 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 +320,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."
}
}
}

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.",
"githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.",
"gitlabSourceNote": "استخراج APK GitLab ممکن است بدون کلید API کار نکند.",
"sortByFileNamesNotLinks": "مرتب سازی بر اساس نام فایل به جای پیوندهای کامل",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
"customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیش‌فرض '.apk$')",
"appsPossiblyUpdated": "به‌روزرسانی برنامه انجام شد",
@ -245,9 +245,11 @@
"enableBackgroundUpdates": "به روز رسانی پس زمینه را فعال کنید",
"backgroundUpdateReqsExplanation": "به روز رسانی پس زمینه ممکن است برای همه برنامه ها امکان پذیر نباشد.",
"backgroundUpdateLimitsExplanation": "موفقیت نصب پس‌زمینه تنها زمانی مشخص می‌شود که Obtainium باز شود.",
"verifyLatestTag": "برچسب "آخرین" را تأیید کنید",
"intermediateLinkRegex": "برای اولین بار بازدید از لینک "متوسط" را فیلتر کنید",
"verifyLatestTag": "برچسب \"آخرین\" را تأیید کنید",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "لینک میانی پیدا نشد",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "معاف از به‌روزرسانی‌های پس‌زمینه (در صورت فعال بودن)",
"bgUpdatesOnWiFiOnly": "به‌روزرسانی‌های پس‌زمینه را در صورت عدم اتصال به WiFi غیرفعال کنید",
"autoSelectHighestVersionCode": "انتخاب خودکار بالاترین نسخه کد APK",
@ -256,10 +258,13 @@
"highlightTouchTargets": "اهداف لمسی کمتر واضح را برجسته کنید",
"pickExportDir": "فهرست صادرات را انتخاب کنید",
"autoExportOnChanges": "صادرات خودکار تغییرات",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "فیلتر کردن نسخه ها با RegEx",
"trySelectingSuggestedVersionCode": "نسخه پیشنهادی APK نسخه کد را انتخاب کنید",
"dontSortReleasesList": "حفظ سفارش انتشار از API",
"reverseSort": "مرتب سازی معکوس",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "منوی اشکال زدایی",
"bgTaskStarted": "کار پس زمینه شروع شد - لاگ های مربوط را بررسی کنید.",
"runBgCheckNow": "اکنون به‌روزرسانی پس‌زمینه را بررسی کنید",
@ -277,6 +282,7 @@
"onlyCheckInstalledOrTrackOnlyApps": "فقط برنامه های نصب شده و فقط ردیابی را برای به روز رسانی بررسی کنید",
"supportFixedAPKURL": "پشتیبانی از URL های APK ثابت",
"selectX": "انتخاب کنید {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": {
"one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟"

View File

@ -236,7 +236,7 @@
"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",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
"appsPossiblyUpdated": "App Updates Attempted",
@ -246,8 +246,10 @@
"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",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Intermediate link not found",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
@ -256,10 +258,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 +282,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 ?"

View File

@ -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",
@ -236,7 +236,7 @@
"addInfoInSettings": "Adja hozzá ezt az infót a Beállításokban.",
"githubSourceNote": "A GitHub sebességkorlátozás elkerülhető API-kulcs használatával.",
"gitlabSourceNote": "Előfordulhat, hogy a GitLab APK kibontása nem működik API-kulcs nélkül.",
"sortByFileNamesNotLinks": "Fájlnevek szerinti elrendezés teljes linkek helyett",
"sortByLastLinkSegment": "Rendezés csak a link utolsó szegmense szerint",
"filterReleaseNotesByRegEx": "Kiadási megjegyzések szűrése reguláris kifejezéssel",
"customLinkFilterRegex": "Egyéni APK hivatkozásszűrő reguláris kifejezéssel (Alapérték '.apk$')",
"appsPossiblyUpdated": "App frissítési kísérlet",
@ -245,8 +245,10 @@
"backgroundUpdateReqsExplanation": "Előfordulhat, hogy nem minden appnál lehetséges a háttérbeli frissítés.",
"backgroundUpdateLimitsExplanation": "A háttérben történő telepítés sikeressége csak az Obtainium megnyitásakor állapítható meg.",
"verifyLatestTag": "Ellenőrizze a „legújabb” címkét",
"intermediateLinkRegex": "Szűrés egy 'közvetítő' linkre, amelyet először meg kell látogatni",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Közvetítő link nem található",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)",
"bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
"autoSelectHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
@ -255,10 +257,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 +280,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?"

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "Aggiungi questa info nelle impostazioni.",
"githubSourceNote": "Il limite di ricerca GitHub può essere evitato usando una chiave API.",
"gitlabSourceNote": "L'estrazione di APK da GitLab potrebbe non funzionare senza chiave API.",
"sortByFileNamesNotLinks": "Ordina per nome del file invece dei link completi",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Filtra le note di rilascio con espressione regolare",
"customLinkFilterRegex": "Filtra link APK personalizzato con espressione regolare (predefinito '.apk$')",
"appsPossiblyUpdated": "Aggiornamenti app tentati",
@ -246,8 +246,10 @@
"backgroundUpdateReqsExplanation": "Gli aggiornamenti in secondo piano potrebbero non essere possibili per tutte le app.",
"backgroundUpdateLimitsExplanation": "La riuscita di un'installazione in secondo piano può essere determinata solo quando viene aperto Obtainium.",
"verifyLatestTag": "Verifica l'etichetta 'Latest'",
"intermediateLinkRegex": "Filtra un link 'Intermedio' da visitare prima",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Link intermedio non trovato",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Esente da aggiornamenti in secondo piano (se attivo)",
"bgUpdatesOnWiFiOnly": "Disattiva aggiornamenti in secondo piano quando non si usa il WiFi",
"autoSelectHighestVersionCode": "Auto-seleziona APK con versionCode più alto",
@ -256,10 +258,13 @@
"highlightTouchTargets": "Evidenzia elementi toccabili meno ovvi",
"pickExportDir": "Scegli cartella esp.",
"autoExportOnChanges": "Auto-esporta dopo modifiche",
"includeSettings": "Includi impostazioni",
"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": "Prendi il primo link",
"skipSort": "Salta ordinamento",
"debugMenu": "Menu di debug",
"bgTaskStarted": "Attività in secondo piano iniziata - controllo log.",
"runBgCheckNow": "Inizia aggiornamento in secondo piano ora",
@ -275,8 +280,14 @@
"completeAppInstallationNotifChannel": "Completa l'installazione dell'app",
"checkingForUpdatesNotifChannel": "Controllo degli aggiornamenti in corso",
"onlyCheckInstalledOrTrackOnlyApps": "Cerca aggiornamenti solo per app installate e app in Solo-Monitoraggio",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"supportFixedAPKURL": "Supporta URL fissi di APK",
"selectX": "Seleziona {}",
"parallelDownloads": "Permetti download paralleli",
"installMethod": "Metodo d'installazione",
"normal": "Normale",
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku non è in esecuzione",
"removeAppQuestion": {
"one": "Rimuovere l'app?",
"other": "Rimuovere le app?"

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "設定でこの情報を追加してください。",
"githubSourceNote": "GitHubのレート制限はAPIキーを使うことで回避できます。",
"gitlabSourceNote": "GitLabのAPK抽出はAPIキーがないと動作しない場合があります。",
"sortByFileNamesNotLinks": "フルのリンクではなくファイル名でソートする",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "正規表現でリリースノートをフィルタリングする",
"customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')",
"appsPossiblyUpdated": "アプリのアップデートを試行",
@ -246,8 +246,10 @@
"backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。",
"backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。",
"verifyLatestTag": "'latest'タグを確認する",
"intermediateLinkRegex": "最初にアクセスする「中間」リンクをフィルタリングする",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "中間リンクが見つかりませんでした",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
"bgUpdatesOnWiFiOnly": "WiFiを使用していない場合バックグラウンドアップデートを無効にする",
"autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する",
@ -256,10 +258,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 +282,7 @@
"onlyCheckInstalledOrTrackOnlyApps": "インストール済みのアプリと「追跡のみ」のアプリのアップデートのみを確認する",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": {
"one": "アプリを削除しますか?",
"other": "アプリを削除しますか?"

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "Voeg deze informatie toe in de instellingen.",
"githubSourceNote": "Beperkingen van GitHub kunnen worden vermeden door het gebruik van een API-sleutel.",
"gitlabSourceNote": "GitLab APK-extractie werkt mogelijk niet zonder een API-sleutel.",
"sortByFileNamesNotLinks": "Sorteren op bestandsnamen in plaats van volledige links.",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Filter release-opmerkingen met een reguliere expressie.",
"customLinkFilterRegex": "Aangepaste APK-linkfilter met een reguliere expressie (Standaard '.apk$').",
"appsPossiblyUpdated": "Poging tot app-updates",
@ -246,8 +246,10 @@
"backgroundUpdateReqsExplanation": "Achtergrondupdates zijn mogelijk niet voor alle apps mogelijk.",
"backgroundUpdateLimitsExplanation": "Het succes van een installatie in de achtergrond kan alleen worden bepaald wanneer Obtainium is geopend.",
"verifyLatestTag": "Verifieer de 'Laatste'-tag",
"intermediateLinkRegex": "Filter voor een 'tussenliggende' link om eerst te bezoeken",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Tussenliggende link niet gevonden",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Vrijgesteld van achtergrondupdates (indien ingeschakeld)",
"bgUpdatesOnWiFiOnly": "Achtergrondupdates uitschakelen wanneer niet verbonden met WiFi",
"autoSelectHighestVersionCode": "Automatisch de APK met de hoogste versiecode selecteren",
@ -256,10 +258,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 +282,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?"

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "Dodaj tę informację w Ustawieniach.",
"githubSourceNote": "Limit żądań GitHub można ominąć za pomocą klucza API.",
"gitlabSourceNote": "Pozyskiwanie pliku APK z GitLab może nie działać bez klucza API.",
"sortByFileNamesNotLinks": "Sortuj wg nazw plików zamiast pełnych linków",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego",
"customLinkFilterRegex": "Filtruj linki APK według wyrażenia regularnego (domyślnie \".apk$\")",
"appsPossiblyUpdated": "Aplikacje mogły zostać zaktualizowane",
@ -246,8 +246,10 @@
"backgroundUpdateReqsExplanation": "Aktualizacje w tle mogą nie być możliwe dla wszystkich aplikacji.",
"backgroundUpdateLimitsExplanation": "Powodzenie instalacji w tle można określić dopiero po otwarciu Obtainium.",
"verifyLatestTag": "Zweryfikuj najnowszy tag",
"intermediateLinkRegex": "Filtr linków \"pośrednich\" do odwiedzenia w pierwszej kolejności",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Nie znaleziono linku pośredniego",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)",
"bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi",
"autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK",
@ -256,10 +258,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",
@ -277,6 +282,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?",

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "Adicionar essa informação nas configurações.",
"githubSourceNote": "A limitação de taxa do GitHub pode ser evitada usando uma chave de API.",
"gitlabSourceNote": "A extração de APK do GitLab pode não funcionar sem uma chave de API.",
"sortByFileNamesNotLinks": "Classifique por nomes de arquivos em vez de links completos",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Filtrar Notas de Lançamento por Expressão Regular",
"customLinkFilterRegex": "Filtro de Link Personalizado por Expressão Regular (Padrão '.apk$')",
"appsPossiblyUpdated": "Tentativas de atualização de Apps",
@ -246,8 +246,10 @@
"backgroundUpdateReqsExplanation": "Atualizações em segundo plano podem não ser possíveis para todos os Apps.",
"backgroundUpdateLimitsExplanation": "O sucesso de uma instalação em segundo plano só pode ser determinado quando o Obtainium é aberto.",
"verifyLatestTag": "Verifique a 'ultima' etiqueta",
"intermediateLinkRegex": "Filtre por um Link 'Intermediário' para Visitar Primeiro",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Link intermediário não encontrado",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)",
"bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi",
"autoSelectHighestVersionCode": "Auto-selecionar o maior codigo de versão",
@ -256,10 +258,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",
@ -274,9 +279,15 @@
"downloadingXNotifChannel": "Baixando {}",
"completeAppInstallationNotifChannel": "Instalação completa do App",
"checkingForUpdatesNotifChannel": "Checando por Atualizações",
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"onlyCheckInstalledOrTrackOnlyApps": "Apenas checar apps instalados e 'Apenas Seguir' por updates",
"supportFixedAPKURL": "Suporte APK com URLs fixas",
"selectX": "Selecionar {}",
"parallelDownloads": "Permitir downloads paralelos",
"installMethod": "Método de instalação",
"normal": "Normal",
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku não esta rodando",
"removeAppQuestion": {
"one": "Remover App?",
"other": "Remover Apps?"

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "Добавьте эту информацию в Настройки",
"githubSourceNote": "Используя ключ API можно обойти лимит запросов GitHub",
"gitlabSourceNote": "Без ключа API может не работать извлечение APK с GitLab",
"sortByFileNamesNotLinks": "Сортировать по именам файлов, а не ссылкам целиком",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Фильтровать примечания к выпуску\n(регулярное выражение)",
"customLinkFilterRegex": "Пользовательский фильтр ссылок APK\n(регулярное выражение, по умолчанию: '.apk$')",
"appsPossiblyUpdated": "Попытки обновления приложений",
@ -246,8 +246,10 @@
"backgroundUpdateReqsExplanation": "Фоновые обновления могут быть возможны не для всех приложений",
"backgroundUpdateLimitsExplanation": "Успешность фоновой установки можно определить только после открытия Obtainium",
"verifyLatestTag": "Проверять тег 'latest'",
"intermediateLinkRegex": "Фильтр промежуточных ссылок для первоочередного посещения\n(регулярное выражение)",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Промежуточная ссылка не найдена",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Исключить из фоновых обновлений (если включено)",
"bgUpdatesOnWiFiOnly": "Отключить фоновые обновления, если нет соединения с Wi-Fi",
"autoSelectHighestVersionCode": "Автоматически выбирать APK с актуальной версией кода",
@ -256,10 +258,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 +279,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": "Удалить приложения?"

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "Lägg till denna information i Inställningar.",
"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",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
"appsPossiblyUpdated": "App Updates Attempted",
@ -246,8 +246,10 @@
"backgroundUpdateReqsExplanation": "Bakgrundsuppdateringar är inte möjligt för alla appar.",
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
"verifyLatestTag": "Verifiera 'senaste'-taggen",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Intermediate link not found",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Undta från bakgrundsuppdateringar (om aktiverad)",
"bgUpdatesOnWiFiOnly": "Inaktivera Bakgrundsuppdateringar utan WiFi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
@ -256,13 +258,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?"

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "Bu bilgiyi Ayarlar'da ekleyin.",
"githubSourceNote": "GitHub hız sınırlaması bir API anahtarı kullanılarak atlanabilir.",
"gitlabSourceNote": "GitLab APK çıkarma işlemi bir API anahtarı olmadan çalışmayabilir.",
"sortByFileNamesNotLinks": "Bağlantılar yerine dosya adlarına göre sırala",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Sürüm Notlarını Düzenli İfade ile Filtrele",
"customLinkFilterRegex": "Özel APK Bağlantı Filtresi Düzenli İfade ile (Varsayılan '.apk$')",
"appsPossiblyUpdated": "Uygulama Güncellemeleri Denendi",
@ -246,8 +246,10 @@
"backgroundUpdateReqsExplanation": "Arka plan güncellemeleri tüm uygulamalar için mümkün olmayabilir.",
"backgroundUpdateLimitsExplanation": "Arka plan kurulumunun başarısı, Obtainium'un açıldığında ancak belirlenebilir.",
"verifyLatestTag": "'latest' etiketini doğrula",
"intermediateLinkRegex": "İlk Ziyaret Edilecek 'Ara' Bağlantısını Filtrele",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Ara bağlantı bulunamadı",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Arka plan güncellemelerinden muaf tut (etkinse)",
"bgUpdatesOnWiFiOnly": "WiFi olmadığında arka plan güncellemelerini devre dışı bırak",
"autoSelectHighestVersionCode": "Otomatik olarak en yüksek sürüm kodunu seç",
@ -256,10 +258,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 +282,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?"

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "Thêm thông tin này vào Cài đặt.",
"githubSourceNote": "Có thể tránh được việc giới hạn tốc độ GitHub bằng cách sử dụng khóa API.",
"gitlabSourceNote": "Trích xuất APK GitLab có thể không hoạt động nếu không có khóa API.",
"sortByFileNamesNotLinks": "Sắp xếp theo tên tệp thay vì liên kết đầy đủ",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "Lọc ghi chú phát hành theo biểu thức chính quy",
"customLinkFilterRegex": "Bộ lọc liên kết APK tùy chỉnh theo biểu thức chính quy (Mặc định '.apk$')",
"appsPossiblyUpdated": "Đã cố gắng cập nhật ứng dụng",
@ -246,8 +246,10 @@
"backgroundUpdateReqsExplanation": "Có thể không thực hiện được cập nhật nền cho tất cả ứng dụng.",
"backgroundUpdateLimitsExplanation": "Sự thành công của cài đặt nền chỉ có thể được xác định khi mở Obtainium.",
"verifyLatestTag": "Xác minh thẻ 'mới nhất'",
"intermediateLinkRegex": "Lọc tìm liên kết 'Trung gian' để truy cập trước",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "Không tìm thấy liên kết trung gian",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "Miễn cập nhật nền (nếu được bật)",
"bgUpdatesOnWiFiOnly": "Tắt cập nhật nền khi không có WiFi",
"autoSelectHighestVersionCode": "Tự động chọn APK mã phiên bản cao nhất",
@ -256,10 +258,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 +282,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?"

View File

@ -236,7 +236,7 @@
"addInfoInSettings": "在“设置”中添加此凭据。",
"githubSourceNote": "使用访问令牌可避免触发 GitHub 的 API 请求限制。",
"gitlabSourceNote": "未使用访问令牌时可能无法从 GitLab 获取 APK 文件。",
"sortByFileNamesNotLinks": "使用文件名代替链接进行排序",
"sortByLastLinkSegment": "Sort by only the last segment of the link",
"filterReleaseNotesByRegEx": "筛选发行说明(正则表达式)",
"customLinkFilterRegex": "筛选自定义来源 APK 文件链接\n正则表达式默认匹配模式为“.apk$”)",
"appsPossiblyUpdated": "已尝试更新应用",
@ -246,8 +246,10 @@
"backgroundUpdateReqsExplanation": "后台更新未必适用于所有的应用。",
"backgroundUpdateLimitsExplanation": "只有在启动 Obtainium 时才能确认安装是否成功。",
"verifyLatestTag": "验证“Latest”标签",
"intermediateLinkRegex": "筛选首先访问的“中转”链接(正则表达式)",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
"filterByLinkText": "Filter links by link text",
"intermediateLinkNotFound": "未找到“中转”链接",
"intermediateLink": "Intermediate link",
"exemptFromBackgroundUpdates": "禁用后台更新\n如果已经全局启用",
"bgUpdatesOnWiFiOnly": "未连接 Wi-Fi 时禁用后台更新",
"autoSelectHighestVersionCode": "自动选择版本号最高的 APK 文件",
@ -256,10 +258,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 +282,7 @@
"onlyCheckInstalledOrTrackOnlyApps": "只对已安装和“仅追踪”的应用进行更新检查",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": {
"one": "是否删除应用?",
"other": "是否删除应用?"

View File

@ -7,7 +7,7 @@ import 'package:obtainium/providers/source_provider.dart';
class Aptoide extends AppSource {
Aptoide() {
host = 'aptoide.com';
name = tr('Aptoide');
name = 'Aptoide';
allowSubDomains = true;
naiveStandardVersionDetection = true;
}

View File

@ -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);
}
}

View File

@ -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));
}

View File

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

View File

@ -88,61 +88,77 @@ bool _isNumeric(String s) {
}
class HTML extends AppSource {
var finalStepFormitems = [
[
GeneratedFormTextField('customLinkFilterRegex',
label: tr('customLinkFilterRegex'),
hint: 'download/(.*/)?(android|apk|mobile)',
required: false,
additionalValidators: [
(value) {
return regExValidator(value);
}
])
],
[
GeneratedFormTextField('versionExtractionRegEx',
label: tr('versionExtractionRegEx'),
required: false,
additionalValidators: [(value) => regExValidator(value)]),
],
[
GeneratedFormTextField('matchGroupToUse',
label: tr('matchGroupToUse'),
required: false,
hint: '0',
textInputType: const TextInputType.numberWithOptions(),
additionalValidators: [
(value) {
if (value?.isEmpty == true) {
value = null;
}
value ??= '0';
return intValidator(value);
}
])
],
[
GeneratedFormSwitch('versionExtractWholePage',
label: tr('versionExtractWholePage'))
],
[
GeneratedFormSwitch('supportFixedAPKURL',
defaultValue: true, label: tr('supportFixedAPKURL')),
],
];
var commonFormItems = [
[GeneratedFormSwitch('filterByLinkText', label: tr('filterByLinkText'))],
[GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
[GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))],
[
GeneratedFormSwitch('sortByLastLinkSegment',
label: tr('sortByLastLinkSegment'))
],
];
var intermediateFormItems = [
[
GeneratedFormTextField('customLinkFilterRegex',
label: tr('intermediateLinkRegex'),
hint: '([0-9]+.)*[0-9]+/\$',
required: true,
additionalValidators: [(value) => regExValidator(value)])
],
];
HTML() {
additionalSourceAppSpecificSettingFormItems = [
[
GeneratedFormSwitch('sortByFileNamesNotLinks',
label: tr('sortByFileNamesNotLinks'))
GeneratedFormSubForm(
'intermediateLink', [...intermediateFormItems, ...commonFormItems],
label: tr('intermediateLink'))
],
[GeneratedFormSwitch('reverseSort', label: tr('reverseSort'))],
[
GeneratedFormSwitch('supportFixedAPKURL',
defaultValue: true, label: tr('supportFixedAPKURL')),
],
[
GeneratedFormTextField('customLinkFilterRegex',
label: tr('customLinkFilterRegex'),
hint: 'download/(.*/)?(android|apk|mobile)',
required: false,
additionalValidators: [
(value) {
return regExValidator(value);
}
])
],
[
GeneratedFormTextField('intermediateLinkRegex',
label: tr('intermediateLinkRegex'),
hint: '([0-9]+.)*[0-9]+/\$',
required: false,
additionalValidators: [(value) => regExValidator(value)])
],
[
GeneratedFormTextField('versionExtractionRegEx',
label: tr('versionExtractionRegEx'),
required: false,
additionalValidators: [(value) => regExValidator(value)]),
],
[
GeneratedFormTextField('matchGroupToUse',
label: tr('matchGroupToUse'),
required: false,
hint: '0',
textInputType: const TextInputType.numberWithOptions(),
additionalValidators: [
(value) {
if (value?.isEmpty == true) {
value = null;
}
value ??= '0';
return intValidator(value);
}
])
],
[
GeneratedFormSwitch('versionExtractWholePage',
label: tr('versionExtractWholePage'))
]
finalStepFormitems[0],
...commonFormItems,
...finalStepFormitems.sublist(1)
];
overrideVersionDetectionFormDefault('noVersionDetection',
disableStandard: false, disableRelDate: true);
@ -163,100 +179,120 @@ class HTML extends AppSource {
return url;
}
// Given an HTTP response, grab some links according to the common additional settings
// (those that apply to intermediate and final steps)
Future<List<MapEntry<String, String>>> grabLinksCommon(
Response res, Map<String, dynamic> additionalSettings) async {
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
var html = parse(res.body);
List<MapEntry<String, String>> allLinks = html
.querySelectorAll('a')
.map((element) => MapEntry(
element.attributes['href'] ?? '',
element.text.isNotEmpty
? element.text
: (element.attributes['href'] ?? '').split('/').last))
.where((element) => element.key.isNotEmpty)
.toList();
if (allLinks.isEmpty) {
allLinks = RegExp(
r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?')
.allMatches(res.body)
.map((match) =>
MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''))
.toList();
}
List<MapEntry<String, String>> links = [];
bool skipSort = additionalSettings['skipSort'] == true;
bool filterLinkByText = additionalSettings['filterByLinkText'] == true;
if ((additionalSettings['customLinkFilterRegex'] as String?)?.isNotEmpty ==
true) {
var reg = RegExp(additionalSettings['customLinkFilterRegex']);
links = allLinks
.where((element) =>
reg.hasMatch(filterLinkByText ? element.value : element.key))
.toList();
} else {
links = allLinks
.where((element) =>
Uri.parse(filterLinkByText ? element.value : element.key)
.path
.toLowerCase()
.endsWith('.apk'))
.toList();
}
if (!skipSort) {
links.sort((a, b) => additionalSettings['sortByLastLinkSegment'] == true
? compareAlphaNumeric(
a.key.split('/').where((e) => e.isNotEmpty).last,
b.key.split('/').where((e) => e.isNotEmpty).last)
: compareAlphaNumeric(a.key, b.key));
}
if (additionalSettings['reverseSort'] == true) {
links = links.reversed.toList();
}
return links;
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
var uri = Uri.parse(standardUrl);
Response res = await sourceRequest(standardUrl);
if (res.statusCode == 200) {
var html = parse(res.body);
List<String> allLinks = html
.querySelectorAll('a')
.map((element) => element.attributes['href'] ?? '')
.where((element) => element.isNotEmpty)
.toList();
if (allLinks.isEmpty) {
allLinks = RegExp(
r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?')
.allMatches(res.body)
.map((match) => match.group(0)!)
.toList();
}
List<String> links = [];
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 (links.isEmpty) {
throw ObtainiumError(tr('intermediateLinkNotFound'));
}
Map<String, dynamic> additionalSettingsTemp =
Map.from(additionalSettings);
additionalSettingsTemp['intermediateLinkRegex'] = null;
return getLatestAPKDetails(
ensureAbsoluteUrl(links.last, uri), additionalSettingsTemp);
}
if ((additionalSettings['customLinkFilterRegex'] as String?)
?.isNotEmpty ==
true) {
var reg = RegExp(additionalSettings['customLinkFilterRegex']);
links = allLinks.where((element) => reg.hasMatch(element)).toList();
} else {
links = allLinks
.where((element) =>
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 (additionalSettings['reverseSort'] == true) {
links = links.reversed.toList();
}
if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty ==
true) {
var reg = RegExp(additionalSettings['apkFilterRegEx']);
links = links.where((element) => reg.hasMatch(element)).toList();
}
if (links.isEmpty) {
var currentUrl = standardUrl;
for (int i = 0;
i < (additionalSettings['intermediateLink']?.length ?? 0);
i++) {
var intLinks = await grabLinksCommon(await sourceRequest(currentUrl),
additionalSettings['intermediateLink'][i]);
if (intLinks.isEmpty) {
throw NoReleasesError();
} else {
currentUrl = intLinks.last.key;
}
var rel = links.last;
String? version;
if (additionalSettings['supportFixedAPKURL'] != true) {
version = rel.hashCode.toString();
}
var versionExtractionRegEx =
additionalSettings['versionExtractionRegEx'] as String?;
if (versionExtractionRegEx?.isNotEmpty == true) {
var match = RegExp(versionExtractionRegEx!).allMatches(
additionalSettings['versionExtractWholePage'] == true
? res.body.split('\r\n').join('\n').split('\n').join('\\n')
: rel);
if (match.isEmpty) {
throw NoVersionError();
}
String matchGroupString =
(additionalSettings['matchGroupToUse'] as String).trim();
if (matchGroupString.isEmpty) {
matchGroupString = "0";
}
version = match.last.group(int.parse(matchGroupString));
if (version?.isEmpty == true) {
throw NoVersionError();
}
}
rel = ensureAbsoluteUrl(rel, uri);
version ??= (await checkDownloadHash(rel)).toString();
return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(),
AppNames(uri.host, tr('app')));
} else {
throw getObtainiumHttpError(res);
}
var uri = Uri.parse(currentUrl);
Response res = await sourceRequest(currentUrl);
var links = await grabLinksCommon(res, additionalSettings);
if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == true) {
var reg = RegExp(additionalSettings['apkFilterRegEx']);
links = links.where((element) => reg.hasMatch(element.key)).toList();
}
if (links.isEmpty) {
throw NoReleasesError();
}
var rel = links.last.key;
String? version;
if (additionalSettings['supportFixedAPKURL'] != true) {
version = rel.hashCode.toString();
}
var versionExtractionRegEx =
additionalSettings['versionExtractionRegEx'] as String?;
if (versionExtractionRegEx?.isNotEmpty == true) {
var match = RegExp(versionExtractionRegEx!).allMatches(
additionalSettings['versionExtractWholePage'] == true
? res.body.split('\r\n').join('\n').split('\n').join('\\n')
: rel);
if (match.isEmpty) {
throw NoVersionError();
}
String matchGroupString =
(additionalSettings['matchGroupToUse'] as String).trim();
if (matchGroupString.isEmpty) {
matchGroupString = "0";
}
version = match.last.group(int.parse(matchGroupString));
if (version?.isEmpty == true) {
throw NoVersionError();
}
}
rel = ensureAbsoluteUrl(rel, uri);
version ??= (await checkDownloadHash(rel)).toString();
return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(),
AppNames(uri.host, tr('app')));
}
}

View File

@ -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',

View File

@ -4,6 +4,7 @@ import 'package:hsluv/hsluv.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/providers/source_provider.dart';
abstract class GeneratedFormItem {
late String key;
@ -31,7 +32,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
{super.label,
super.belowWidgets,
String super.defaultValue = '',
List<String? Function(String? value)> super.additionalValidators = const [],
List<String? Function(String? value)> super.additionalValidators =
const [],
this.required = true,
this.max = 1,
this.hint,
@ -117,6 +119,18 @@ class GeneratedForm extends StatefulWidget {
State<GeneratedForm> createState() => _GeneratedFormState();
}
class GeneratedFormSubForm extends GeneratedFormItem {
final List<List<GeneratedFormItem>> items;
GeneratedFormSubForm(super.key, this.items,
{super.label, super.belowWidgets, super.defaultValue});
@override
ensureType(val) {
return val; // Not easy to validate List<Map<String, dynamic>>
}
}
// Generates a color in the HSLuv (Pastel) color space
// https://pub.dev/documentation/hsluv/latest/hsluv/Hsluv/hpluvToRgb.html
Color generateRandomLightColor() {
@ -133,28 +147,39 @@ Color generateRandomLightColor() {
return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
}
int generateRandomNumber(int seed1,
{int seed2 = 0, int seed3 = 0, max = 10000}) {
int combinedSeed = seed1.hashCode ^ seed2.hashCode ^ seed3.hashCode;
Random random = Random(combinedSeed);
int randomNumber = random.nextInt(max);
return randomNumber;
}
bool validateTextField(TextFormField tf) =>
(tf.key as GlobalKey<FormFieldState>).currentState?.isValid == true;
class _GeneratedFormState extends State<GeneratedForm> {
final _formKey = GlobalKey<FormState>();
Map<String, dynamic> values = {};
late List<List<Widget>> formInputs;
List<List<Widget>> rows = [];
String? initKey;
int forceUpdateKeyCount = 0;
// If any value changes, call this to update the parent with value and validity
void someValueChanged({bool isBuilding = false}) {
void someValueChanged({bool isBuilding = false, bool forceInvalid = false}) {
Map<String, dynamic> returnValues = values;
var valid = true;
for (int r = 0; r < widget.items.length; r++) {
for (int i = 0; i < widget.items[r].length; i++) {
if (formInputs[r][i] is TextFormField) {
var fieldState =
(formInputs[r][i].key as GlobalKey<FormFieldState>).currentState;
if (fieldState != null) {
valid = valid && fieldState.isValid;
}
valid = valid && validateTextField(formInputs[r][i] as TextFormField);
}
}
}
if (forceInvalid) {
valid = false;
}
widget.onValueChanges(returnValues, valid, isBuilding);
}
@ -229,6 +254,17 @@ class _GeneratedFormState extends State<GeneratedForm> {
someValueChanged();
});
});
} else if (formItem is GeneratedFormSubForm) {
values[formItem.key] = [];
for (Map<String, dynamic> v
in ((formItem.defaultValue ?? []) as List<dynamic>)) {
var fullDefaults = getDefaultValuesFromFormItems(formItem.items);
for (var element in v.entries) {
fullDefaults[element.key] = element.value;
}
values[formItem.key].add(fullDefaults);
}
return Container();
} else {
return Container(); // Some input types added in build
}
@ -250,6 +286,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
}
for (var r = 0; r < formInputs.length; r++) {
for (var e = 0; e < formInputs[r].length; e++) {
String fieldKey = widget.items[r][e].key;
if (widget.items[r][e] is GeneratedFormSwitch) {
formInputs[r][e] = Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -259,10 +296,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
width: 8,
),
Switch(
value: values[widget.items[r][e].key],
value: values[fieldKey],
onChanged: (value) {
setState(() {
values[widget.items[r][e].key] = value;
values[fieldKey] = value;
someValueChanged();
});
})
@ -271,8 +308,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
} else if (widget.items[r][e] is GeneratedFormTagInput) {
formInputs[r][e] =
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
if ((values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>?)
if ((values[fieldKey] as Map<String, MapEntry<int, bool>>?)
?.isNotEmpty ==
true &&
(widget.items[r][e] as GeneratedFormTagInput)
@ -295,8 +331,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
(widget.items[r][e] as GeneratedFormTagInput).alignment,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
(values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>?)
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
?.isEmpty ==
true
? Text(
@ -304,8 +339,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
.emptyMessage,
)
: const SizedBox.shrink(),
...(values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>?)
...(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
?.entries
.map((e2) {
return Padding(
@ -318,11 +352,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
selected: e2.value.value,
onSelected: (value) {
setState(() {
(values[widget.items[r][e].key] as Map<String,
(values[fieldKey] as Map<String,
MapEntry<int, bool>>)[e2.key] =
MapEntry(
(values[widget.items[r][e].key] as Map<
String,
(values[fieldKey] as Map<String,
MapEntry<int, bool>>)[e2.key]!
.key,
value);
@ -330,22 +363,18 @@ class _GeneratedFormState extends State<GeneratedForm> {
as GeneratedFormTagInput)
.singleSelect &&
value == true) {
for (var key in (values[
widget.items[r][e].key]
for (var key in (values[fieldKey]
as Map<String, MapEntry<int, bool>>)
.keys) {
if (key != e2.key) {
(values[widget.items[r][e].key] as Map<
String,
MapEntry<int, bool>>)[key] =
MapEntry(
(values[widget.items[r][e].key]
as Map<
String,
MapEntry<int,
bool>>)[key]!
.key,
false);
(values[fieldKey] as Map<
String,
MapEntry<int,
bool>>)[key] = MapEntry(
(values[fieldKey] as Map<String,
MapEntry<int, bool>>)[key]!
.key,
false);
}
}
}
@ -355,8 +384,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
));
}) ??
[const SizedBox.shrink()],
(values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>?)
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
?.values
.where((e) => e.value)
.length ==
@ -366,7 +394,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
child: IconButton(
onPressed: () {
setState(() {
var temp = values[widget.items[r][e].key]
var temp = values[fieldKey]
as Map<String, MapEntry<int, bool>>;
// get selected category str where bool is true
final oldEntry = temp.entries
@ -379,7 +407,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
// Update entry with new color, remain selected
temp.update(oldEntry.key,
(old) => MapEntry(newColor, old.value));
values[widget.items[r][e].key] = temp;
values[fieldKey] = temp;
someValueChanged();
});
},
@ -388,8 +416,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
tooltip: tr('colour'),
))
: const SizedBox.shrink(),
(values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>?)
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
?.values
.where((e) => e.value)
.isNotEmpty ==
@ -400,10 +427,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
onPressed: () {
fn() {
setState(() {
var temp = values[widget.items[r][e].key]
var temp = values[fieldKey]
as Map<String, MapEntry<int, bool>>;
temp.removeWhere((key, value) => value.value);
values[widget.items[r][e].key] = temp;
values[fieldKey] = temp;
someValueChanged();
});
}
@ -454,7 +481,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
String? label = value?['label'];
if (label != null) {
setState(() {
var temp = values[widget.items[r][e].key]
var temp = values[fieldKey]
as Map<String, MapEntry<int, bool>>?;
temp ??= {};
if (temp[label] == null) {
@ -467,7 +494,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
temp[label] = MapEntry(
generateRandomLightColor().value,
!(someSelected && singleSelect));
values[widget.items[r][e].key] = temp;
values[fieldKey] = temp;
someValueChanged();
}
});
@ -481,6 +508,93 @@ class _GeneratedFormState extends State<GeneratedForm> {
],
)
]);
} else if (widget.items[r][e] is GeneratedFormSubForm) {
List<Widget> subformColumn = [];
for (int i = 0; i < values[fieldKey].length; i++) {
var items = (widget.items[r][e] as GeneratedFormSubForm)
.items
.map((x) => x.map((y) {
y.defaultValue = values[fieldKey]?[i]?[y.key];
return y;
}).toList())
.toList();
var internalFormKey = ValueKey(generateRandomNumber(
values[fieldKey].length,
seed2: i,
seed3: forceUpdateKeyCount));
subformColumn.add(Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
const SizedBox(
height: 16,
),
Text(
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
style: const TextStyle(fontWeight: FontWeight.bold),
),
GeneratedForm(
key: internalFormKey,
items: items,
onValueChanges: (values, valid, isBuilding) {
if (valid) {
this.values[fieldKey]?[i] = values;
}
someValueChanged(
isBuilding: isBuilding, forceInvalid: !valid);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.error),
onPressed: (values[fieldKey].length > 0)
? () {
var temp = List.from(values[fieldKey]);
temp.removeAt(i);
values[fieldKey] = List.from(temp);
forceUpdateKeyCount++;
someValueChanged();
}
: null,
label: Text(
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
),
icon: const Icon(
Icons.delete_outline_rounded,
))
],
),
],
));
}
subformColumn.add(Padding(
padding: EdgeInsets.only(
bottom: values[fieldKey].length > 0 ? 24 : 0, top: 8),
child: Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
values[fieldKey].add(getDefaultValuesFromFormItems(
(widget.items[r][e] as GeneratedFormSubForm)
.items));
forceUpdateKeyCount++;
someValueChanged();
},
icon: const Icon(Icons.add),
label: Text((widget.items[r][e] as GeneratedFormSubForm)
.label))),
],
),
));
if (values[fieldKey].length > 0) {
subformColumn.add(const Divider());
}
formInputs[r][e] = Column(children: subformColumn);
}
}
}

View File

@ -12,14 +12,14 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:background_fetch/background_fetch.dart';
import 'package:easy_localization/easy_localization.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.14.36';
const String currentVersion = '0.15.1';
const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
@ -76,6 +76,19 @@ Future<void> loadTranslations() async {
fallbackTranslations: controller.fallbackTranslations);
}
@pragma('vm:entry-point')
void backgroundFetchHeadlessTask(HeadlessTask task) async {
String taskId = task.taskId;
bool isTimeout = task.timeout;
if (isTimeout) {
print('BG update task timed out.');
BackgroundFetch.finish(taskId);
return;
}
await bgUpdateCheck(taskId, null);
BackgroundFetch.finish(taskId);
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
@ -93,7 +106,6 @@ void main() async {
);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
await AndroidAlarmManager.initialize();
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => AppsProvider()),
@ -108,6 +120,7 @@ void main() async {
useOnlyLangCode: true,
child: const Obtainium()),
));
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
}
var defaultThemeColour = Colors.deepPurple;
@ -122,6 +135,33 @@ class Obtainium extends StatefulWidget {
class _ObtainiumState extends State<Obtainium> {
var existingUpdateInterval = -1;
@override
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
await BackgroundFetch.configure(
BackgroundFetchConfig(
minimumFetchInterval: 15,
stopOnTerminate: false,
enableHeadless: true,
requiresBatteryNotLow: false,
requiresCharging: false,
requiresStorageNotLow: false,
requiresDeviceIdle: false,
requiredNetworkType: NetworkType.ANY), (String taskId) async {
// We don't want periodic tasks in the foreground - ignore
await bgUpdateCheck(taskId, null);
BackgroundFetch.finish(taskId);
}, (String taskId) async {
context.read<LogsProvider>().add('BG update task timed out.');
BackgroundFetch.finish(taskId);
});
if (!mounted) return;
}
@override
Widget build(BuildContext context) {
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
@ -161,30 +201,6 @@ class _ObtainiumState extends State<Obtainium> {
context.locale.languageCode)) {
settingsProvider.resetLocaleSafe(context);
}
// Register the background update task according to the user's setting
var actualUpdateInterval = settingsProvider.updateInterval;
if (existingUpdateInterval != actualUpdateInterval) {
if (actualUpdateInterval == 0) {
AndroidAlarmManager.cancel(bgUpdateCheckAlarmId);
} else {
var settingChanged = existingUpdateInterval != -1;
var lastCheckWasTooLongAgo = actualUpdateInterval != 0 &&
settingsProvider.lastBGCheckTime
.add(Duration(minutes: actualUpdateInterval + 60))
.isBefore(DateTime.now());
if (settingChanged || lastCheckWasTooLongAgo) {
logs.add(
'Update interval was set to ${actualUpdateInterval.toString()} (reason: ${settingChanged ? 'setting changed' : 'last check was ${settingsProvider.lastBGCheckTime.toLocal().toString()}'}).');
AndroidAlarmManager.periodic(
Duration(minutes: actualUpdateInterval),
bgUpdateCheckAlarmId,
bgUpdateCheck,
rescheduleOnReboot: true,
wakeup: true);
}
}
existingUpdateInterval = actualUpdateInterval;
}
}
return DynamicColorBuilder(

View File

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

View File

@ -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))
])
]
],

View File

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

View File

@ -106,7 +106,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
runObtainiumExport({bool pickOnly = false}) async {
HapticFeedback.selectionClick();
appsProvider
.exportApps(
.export(
pickOnly:
pickOnly || (await settingsProvider.getExportDir()) == null,
sp: settingsProvider)
@ -132,7 +132,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
} catch (e) {
throw ObtainiumError(tr('invalidInput'));
}
appsProvider.importApps(data).then((value) {
appsProvider.import(data).then((value) {
var cats = settingsProvider.categories;
appsProvider.apps.forEach((key, value) {
for (var c in value.app.categories) {
@ -143,7 +143,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
});
appsProvider.addMissingCategories(settingsProvider);
showMessage(
tr('importedX', args: [plural('apps', value)]), context);
'${tr('importedX', args: [
plural('apps', value.key)
])}${value.value ? ' + ${tr('settings')}' : ''}',
context);
});
} else {
// User canceled the picker
@ -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;
}
}
}),
],

View File

@ -1,4 +1,3 @@
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -30,6 +29,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 +349,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'),
@ -570,38 +607,35 @@ class _SettingsPageState extends State<SettingsPage> {
const Divider(
height: 32,
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(tr('debugMenu'))),
Switch(
value: settingsProvider.showDebugOpts,
onChanged: (value) {
settingsProvider.showDebugOpts = value;
})
],
),
if (settingsProvider.showDebugOpts)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
height16,
TextButton(
onPressed: () {
AndroidAlarmManager.oneShot(
const Duration(seconds: 0),
bgUpdateCheckAlarmId + 200,
bgUpdateCheck);
showMessage(tr('bgTaskStarted'), context);
},
child: Text(tr('runBgCheckNow')))
],
),
]),
),
// Padding(
// padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
// child: Column(children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Flexible(child: Text(tr('debugMenu'))),
// Switch(
// value: settingsProvider.showDebugOpts,
// onChanged: (value) {
// settingsProvider.showDebugOpts = value;
// })
// ],
// ),
// if (settingsProvider.showDebugOpts)
// Column(
// crossAxisAlignment: CrossAxisAlignment.stretch,
// children: [
// height16,
// TextButton(
// onPressed: () {
// bgUpdateCheck('taskId', null);
// showMessage(tr('bgTaskStarted'), context);
// },
// child: Text(tr('runBgCheckNow')))
// ],
// ),
// ]),
// ),
],
),
)

View File

@ -8,7 +8,6 @@ import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:crypto/crypto.dart';
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:android_intent_plus/flag.dart';
import 'package:android_package_installer/android_package_installer.dart';
import 'package:android_package_manager/android_package_manager.dart';
@ -33,6 +32,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 +504,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 +516,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);
@ -606,7 +620,8 @@ class AppsProvider with ChangeNotifier {
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
Future<List<String>> downloadAndInstallLatestApps(
List<String> appIds, BuildContext? context,
{NotificationsProvider? notificationsProvider}) async {
{NotificationsProvider? notificationsProvider,
bool forceParallelDownloads = false}) async {
notificationsProvider =
notificationsProvider ?? context?.read<NotificationsProvider>();
List<String> appsToInstall = [];
@ -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 (forceParallelDownloads || !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
@ -1374,19 +1448,17 @@ class _APKOriginWarningDialogState extends State<APKOriginWarningDialog> {
/// When toCheck is empty, the function is in "install mode" (else it is in "update mode").
/// In update mode, all apps in toCheck are checked for updates (in parallel).
/// If an update is available and it cannot be installed silently, the user is notified of the available update.
/// If there are any errors, the task is run again for the remaining apps after a few minutes (based on the error with the longest retry interval).
/// Any app that has reached it's retry limit, the user is notified that it could not be checked.
/// If there are any errors, we recursively call the same function with retry count for the relevant apps decremented (if zero, the user is notified).
///
/// Once all update checks are complete, the task is run again in install mode.
/// In this mode, all pending silent updates are downloaded and installed in the background (serially - one at a time).
/// If there is an error, the offending app is moved to the back of the line of remaining apps, and the task is retried.
/// If an app repeatedly fails to install up to its retry limit, the user is notified.
/// In this mode, all pending silent updates are downloaded (in parallel) and installed in the background.
/// If there is an error, the user is notified.
///
@pragma('vm:entry-point')
Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
// ignore: avoid_print
print('Started $taskId: ${params.toString()}');
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
await AndroidAlarmManager.initialize();
await loadTranslations();
LogsProvider logs = LogsProvider();
@ -1395,11 +1467,20 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
await appsProvider.loadApps();
int maxAttempts = 4;
int maxRetryWaitSeconds = 5;
var netResult = await (Connectivity().checkConnectivity());
if (netResult == ConnectivityResult.none) {
logs.add('BG update task: No network.');
return;
}
params ??= {};
if (params['toCheck'] == null) {
appsProvider.settingsProvider.lastBGCheckTime = DateTime.now();
}
bool firstEverUpdateTask = DateTime.fromMillisecondsSinceEpoch(0)
.compareTo(appsProvider.settingsProvider.lastCompletedBGCheckTime) ==
0;
List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
...(params['toCheck']
?.map((entry) => MapEntry<String, int>(
@ -1407,6 +1488,11 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
.toList() ??
appsProvider
.getAppsSortedByUpdateCheckTime(
ignoreAppsCheckedAfter: params['toCheck'] == null
? firstEverUpdateTask
? null
: appsProvider.settingsProvider.lastCompletedBGCheckTime
: null,
onlyCheckInstalledOrTrackOnlyApps: appsProvider
.settingsProvider.onlyCheckInstalledOrTrackOnlyApps)
.map((e) => MapEntry(e, 0)))
@ -1419,51 +1505,34 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
(<List<MapEntry<String, int>>>[]))
];
var netResult = await (Connectivity().checkConnectivity());
if (netResult == ConnectivityResult.none) {
var networkBasedRetryInterval = 15;
var nextRegularCheck = appsProvider.settingsProvider.lastBGCheckTime
.add(Duration(minutes: appsProvider.settingsProvider.updateInterval));
var potentialNetworkRetryCheck =
DateTime.now().add(Duration(minutes: networkBasedRetryInterval));
var shouldRetry = potentialNetworkRetryCheck.isBefore(nextRegularCheck);
logs.add(
'BG update task $taskId: No network. Will ${shouldRetry ? 'retry in $networkBasedRetryInterval minutes' : 'not retry'}.');
AndroidAlarmManager.oneShot(
const Duration(minutes: 15), taskId + 1, bgUpdateCheck,
params: {
'toCheck': toCheck
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList(),
'toInstall': toInstall
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList(),
});
return;
}
var networkRestricted = false;
if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
networkRestricted = (netResult != ConnectivityResult.wifi) &&
(netResult != ConnectivityResult.ethernet);
}
bool installMode =
toCheck.isEmpty; // Task is either in update mode or install mode
logs.add(
'BG ${installMode ? 'install' : 'update'} task $taskId: Started (${installMode ? toInstall.length : toCheck.length}).');
if (!installMode) {
if (toCheck.isNotEmpty) {
// Task is either in update mode or install mode
// If in update mode, we check for updates.
// We divide the results into 4 groups:
// - toNotify - Apps with updates that the user will be notified about (can't be silently installed)
// - toRetry - Apps with update check errors that will be retried in a while
// - toThrow - Apps with update check errors that the user will be notified about (no retry)
// After grouping the updates, we take care of toNotify and toThrow first
// Then if toRetry is not empty, we schedule another update task to run in a while
// If toRetry is empty, we take care of schedule another task that will run in install mode (toCheck is empty)
// Then we run the function again in install mode (toCheck is empty)
var enoughTimePassed = appsProvider.settingsProvider.updateInterval != 0 &&
appsProvider.settingsProvider.lastCompletedBGCheckTime
.add(
Duration(minutes: appsProvider.settingsProvider.updateInterval))
.isBefore(DateTime.now());
if (!enoughTimePassed) {
// ignore: avoid_print
print(
'BG update task: Too early for another check (last check was ${appsProvider.settingsProvider.lastCompletedBGCheckTime.toIso8601String()}, interval is ${appsProvider.settingsProvider.updateInterval}).');
return;
}
logs.add('BG update task: Started (${toCheck.length}).');
// Init. vars.
List<App> updates = []; // All updates found (silent and non-silent)
@ -1471,8 +1540,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
[]; // All non-silent updates that the user will be notified about
List<MapEntry<String, int>> toRetry =
[]; // All apps that got errors while checking
var retryAfterXSeconds =
0; // How long to wait until the next attempt (if there are errors)
var retryAfterXSeconds = 0;
MultiAppMultiError?
errors; // All errors including those that will lead to a retry
MultiAppMultiError toThrow =
@ -1495,27 +1563,32 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
specificIds: toCheck.map((e) => e.key).toList(),
sp: appsProvider.settingsProvider);
} catch (e) {
// If there were errors, group them into toRetry and toThrow based on max retry count per app
if (e is Map) {
updates = e['updates'];
errors = e['errors'];
errors!.rawErrors.forEach((key, err) {
logs.add(
'BG update task $taskId: Got error on checking for $key \'${err.toString()}\'.');
'BG update task: Got error on checking for $key \'${err.toString()}\'.');
var toCheckApp = toCheck.where((element) => element.key == key).first;
if (toCheckApp.value < maxAttempts) {
toRetry.add(MapEntry(toCheckApp.key, toCheckApp.value + 1));
// Next task interval is based on the error with the longest retry time
var minRetryIntervalForThisApp = err is RateLimitError
int minRetryIntervalForThisApp = err is RateLimitError
? (err.remainingMinutes * 60)
: e is ClientException
? (15 * 60)
: pow(toCheckApp.value + 1, 2).toInt();
: (toCheckApp.value + 1);
if (minRetryIntervalForThisApp > maxRetryWaitSeconds) {
minRetryIntervalForThisApp = maxRetryWaitSeconds;
}
if (minRetryIntervalForThisApp > retryAfterXSeconds) {
retryAfterXSeconds = minRetryIntervalForThisApp;
}
} else {
toThrow.add(key, err, appName: errors?.appIdNames[key]);
if (err is! RateLimitError) {
toThrow.add(key, err, appName: errors?.appIdNames[key]);
}
}
});
} else {
@ -1550,37 +1623,32 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
id: Random().nextInt(10000)));
}
}
// if there are update checks to retry, schedule a retry task
logs.add('BG update task: Done checking for updates.');
if (toRetry.isNotEmpty) {
logs.add(
'BG update task $taskId: Will retry in $retryAfterXSeconds seconds.');
AndroidAlarmManager.oneShot(
Duration(seconds: retryAfterXSeconds), taskId + 1, bgUpdateCheck,
params: {
'toCheck': toRetry
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList(),
'toInstall': toInstall
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList(),
});
return await bgUpdateCheck(taskId, {
'toCheck': toRetry
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList(),
'toInstall': toInstall
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList(),
});
} else {
// If there are no more update checks, schedule an install task
logs.add(
'BG update task $taskId: Done. Scheduling install task to run immediately.');
AndroidAlarmManager.oneShot(
const Duration(minutes: 0), taskId + 1, bgUpdateCheck,
params: {
'toCheck': [],
'toInstall': toInstall
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList()
});
// If there are no more update checks, call the function in install mode
logs.add('BG update task: Done checking for updates.');
return await bgUpdateCheck(taskId, {
'toCheck': [],
'toInstall': toInstall
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList()
});
}
} else {
// In install mode...
// If you haven't explicitly been given updates to install (which is the case for new tasks), grab all available silent updates
// If you haven't explicitly been given updates to install, grab all available silent updates
if (toInstall.isEmpty && !networkRestricted) {
var temp = appsProvider.findExistingUpdates(installedOnly: true);
for (var i = 0; i < temp.length; i++) {
@ -1590,60 +1658,34 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
}
}
}
var didCompleteInstalling = false;
var tempObtArr = toInstall.where((element) => element.key == obtainiumId);
if (tempObtArr.isNotEmpty) {
// Move obtainium to the end of the list as it must always install last
var obt = tempObtArr.first;
toInstall = moveStrToEndMapEntryWithCount(toInstall, obt);
}
// Loop through all updates and install each
for (var i = 0; i < toInstall.length; i++) {
var appId = toInstall[i].key;
var retryCount = toInstall[i].value;
if (toInstall.isNotEmpty) {
logs.add('BG install task: Started (${toInstall.length}).');
var tempObtArr = toInstall.where((element) => element.key == obtainiumId);
if (tempObtArr.isNotEmpty) {
// Move obtainium to the end of the list as it must always install last
var obt = tempObtArr.first;
toInstall = moveStrToEndMapEntryWithCount(toInstall, obt);
}
// Loop through all updates and install each
try {
logs.add(
'BG install task $taskId: Attempting to update $appId in the background.');
await appsProvider.downloadAndInstallLatestApps([appId], null,
notificationsProvider: notificationsProvider);
await Future.delayed(const Duration(
seconds:
5)); // Just in case task ending causes install fail (not clear)
if (i == (toCheck.length - 1)) {
didCompleteInstalling = true;
}
await appsProvider.downloadAndInstallLatestApps(
toInstall.map((e) => e.key).toList(), null,
notificationsProvider: notificationsProvider,
forceParallelDownloads: true);
} catch (e) {
// If you got an error, move the offender to the back of the line (increment their fail count) and schedule another task to continue installing shortly
logs.add(
'BG install task $taskId: Got error on updating $appId \'${e.toString()}\'.');
if (retryCount < maxAttempts) {
var remainingSeconds = retryCount;
logs.add(
'BG install task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).');
var remainingToInstall = moveStrToEndMapEntryWithCount(
toInstall.sublist(i), MapEntry(appId, retryCount + 1));
AndroidAlarmManager.oneShot(
Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck,
params: {
'toCheck': toCheck
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList(),
'toInstall': remainingToInstall
.map((entry) => {'key': entry.key, 'value': entry.value})
.toList(),
});
break;
if (e is MultiAppMultiError) {
e.idsByErrorString.forEach((key, value) {
notificationsProvider.notify(ErrorCheckingUpdatesNotification(
e.errorsAppsString(key, value)));
});
} else {
// If the offender has reached its fail limit, notify the user and remove it from the list (task can continue)
toInstall.removeAt(i);
i--;
notificationsProvider
.notify(ErrorCheckingUpdatesNotification(e.toString()));
// We don't expect to ever get here in any situation so no need to catch (but log it in case)
logs.add('Fatal error in BG install task: ${e.toString()}');
rethrow;
}
}
}
if (didCompleteInstalling || toInstall.isEmpty) {
logs.add('BG install task $taskId: Done.');
logs.add('BG install task: Done installing updates.');
}
}
appsProvider.settingsProvider.lastCompletedBGCheckTime = DateTime.now();
}

View 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});
}
}

View File

@ -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) {
@ -332,15 +345,15 @@ class SettingsProvider with ChangeNotifier {
notifyListeners();
}
DateTime get lastBGCheckTime {
int? temp = prefs?.getInt('lastBGCheckTime');
DateTime get lastCompletedBGCheckTime {
int? temp = prefs?.getInt('lastCompletedBGCheckTime');
return temp != null
? DateTime.fromMillisecondsSinceEpoch(temp)
: DateTime.fromMillisecondsSinceEpoch(0);
}
set lastBGCheckTime(DateTime val) {
prefs?.setInt('lastBGCheckTime', val.millisecondsSinceEpoch);
set lastCompletedBGCheckTime(DateTime val) {
prefs?.setInt('lastCompletedBGCheckTime', val.millisecondsSinceEpoch);
notifyListeners();
}
@ -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();
}
}

View File

@ -135,10 +135,34 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
if (additionalSettings['autoApkFilterByArch'] == null) {
additionalSettings['autoApkFilterByArch'] = false;
}
// HTML 'fixed URL' support should be disabled if it previously did not exist
if (source.runtimeType == HTML().runtimeType &&
originalAdditionalSettings['supportFixedAPKURL'] == null) {
additionalSettings['supportFixedAPKURL'] = false;
if (source.runtimeType == HTML().runtimeType) {
// HTML 'fixed URL' support should be disabled if it previously did not exist
if (originalAdditionalSettings['supportFixedAPKURL'] == null) {
additionalSettings['supportFixedAPKURL'] = false;
}
// HTML key rename
if (originalAdditionalSettings['sortByFileNamesNotLinks'] != null) {
additionalSettings['sortByLastLinkSegment'] =
originalAdditionalSettings['sortByFileNamesNotLinks'];
}
// HTML single 'intermediate link' should be converted to multi-support version
if (originalAdditionalSettings['intermediateLinkRegex'] != null &&
additionalSettings['intermediateLinkRegex']?.isNotEmpty != true) {
additionalSettings['intermediateLink'] = [
{
'customLinkFilterRegex':
originalAdditionalSettings['intermediateLinkRegex'],
'filterByLinkText':
originalAdditionalSettings['intermediateLinkByText']
}
];
}
if ((additionalSettings['intermediateLink']?.length ?? 0) > 0) {
additionalSettings['intermediateLink'] =
additionalSettings['intermediateLink'].where((e) {
return e['intermediateLinkRegex']?.isNotEmpty == true;
}).toList();
}
}
json['additionalSettings'] = jsonEncode(additionalSettings);
// F-Droid no longer needs cloudflare exception since override can be used - migrate apps appropriately
@ -684,8 +708,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 +730,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);
}

View File

@ -1,14 +1,6 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
android_alarm_manager_plus:
dependency: "direct main"
description:
name: android_alarm_manager_plus
sha256: "84720c8ad2758aabfbeafd24a8c355d8c8dd3aa52b01eaf3bb827c7210f61a91"
url: "https://pub.dev"
source: hosted
version: "3.0.4"
android_intent_plus:
dependency: "direct main"
description:
@ -42,6 +34,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.10"
app_links:
dependency: "direct main"
description:
name: app_links
sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb"
url: "https://pub.dev"
source: hosted
version: "3.5.0"
archive:
dependency: transitive
description:
@ -66,6 +66,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
background_fetch:
dependency: "direct main"
description:
name: background_fetch
sha256: f70b28a0f7a3156195e9742229696f004ea3bf10f74039b7bf4c78a74fbda8a4
url: "https://pub.dev"
source: hosted
version: "1.2.1"
boolean_selector:
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:
@ -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:
@ -291,10 +299,10 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: bb5cd63ff7c91d6efe452e41d0d0ae6348925c82eafd10ce170ef585ea04776e
sha256: "892ada16046d641263f30c72e7432397088810a84f34479f6677494802a2b535"
url: "https://pub.dev"
source: hosted
version: "16.2.0"
version: "16.3.0"
flutter_local_notifications_linux:
dependency: transitive
description:
@ -350,6 +358,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.2.4"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
url: "https://pub.dev"
source: hosted
version: "2.1.0"
hsluv:
dependency: "direct main"
description:
@ -498,10 +514,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
path_provider_foundation:
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,26 +919,26 @@ 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: "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:

View File

@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.14.36+230 # When changing this, update the tag in main() accordingly
version: 0.15.1+237 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=3.0.0 <4.0.0'
@ -57,16 +57,17 @@ dependencies:
ref: main
android_package_manager: ^0.6.0
share_plus: ^7.0.0
android_alarm_manager_plus: ^3.0.0
sqflite: ^2.2.0+3
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
background_fetch: ^1.2.1
dev_dependencies:
flutter_test: