mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
feat: Final touch before merge
* Trust Extension rework * Deep link
This commit is contained in:
parent
67f32d400d
commit
addb84ee1e
12 changed files with 100 additions and 48 deletions
|
@ -60,6 +60,16 @@
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- Deep link to add repos -->
|
||||||
|
<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="tachiyomi" />
|
||||||
|
<data android:host="add-repo" />
|
||||||
|
</intent-filter>
|
||||||
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
|
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
@ -270,4 +280,4 @@
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -0,0 +1,29 @@
|
||||||
|
package dev.yokai.domain.extension
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
|
import dev.yokai.domain.source.SourcePreferences
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class TrustExtension(
|
||||||
|
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||||
|
) {
|
||||||
|
fun isTrusted(pkgInfo: PackageInfo, signatureHash: String): Boolean {
|
||||||
|
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:$signatureHash"
|
||||||
|
return key in sourcePreferences.trustedExtensions().get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
|
||||||
|
sourcePreferences.trustedExtensions().let { exts ->
|
||||||
|
val removed = exts.get().filterNot { it.startsWith("$pkgName:") }.toMutableSet()
|
||||||
|
|
||||||
|
removed += "$pkgName:$versionCode:$signatureHash"
|
||||||
|
exts.set(removed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun revokeAll() {
|
||||||
|
sourcePreferences.trustedExtensions().delete()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,23 @@
|
||||||
package dev.yokai.presentation.extension.repo
|
package dev.yokai.presentation.extension.repo
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
|
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
|
||||||
|
|
||||||
class ExtensionRepoController :
|
class ExtensionRepoController() :
|
||||||
BaseComposeController() {
|
BaseComposeController() {
|
||||||
|
|
||||||
@Preview
|
private var repoUrl: String? = null
|
||||||
|
|
||||||
|
constructor(repoUrl: String) : this() {
|
||||||
|
this.repoUrl = repoUrl
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ScreenContent() {
|
override fun ScreenContent() {
|
||||||
ExtensionRepoScreen(
|
ExtensionRepoScreen(
|
||||||
title = "Extension Repos",
|
title = "Extension Repos",
|
||||||
onBackPress = router::handleBack,
|
onBackPress = router::handleBack,
|
||||||
|
repoUrl = repoUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.ExtensionOff
|
import androidx.compose.material.icons.filled.ExtensionOff
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
@ -31,6 +27,7 @@ fun ExtensionRepoScreen(
|
||||||
title: String,
|
title: String,
|
||||||
onBackPress: () -> Unit,
|
onBackPress: () -> Unit,
|
||||||
viewModel: ExtensionRepoViewModel = viewModel(),
|
viewModel: ExtensionRepoViewModel = viewModel(),
|
||||||
|
repoUrl: String? = null,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val repoState = viewModel.repoState.collectAsState()
|
val repoState = viewModel.repoState.collectAsState()
|
||||||
|
@ -39,14 +36,6 @@ fun ExtensionRepoScreen(
|
||||||
YokaiScaffold(
|
YokaiScaffold(
|
||||||
onNavigationIconClicked = onBackPress,
|
onNavigationIconClicked = onBackPress,
|
||||||
title = title,
|
title = title,
|
||||||
fab = {
|
|
||||||
FloatingActionButton(
|
|
||||||
containerColor = MaterialTheme.colorScheme.secondary,
|
|
||||||
onClick = { context.toast("Test") },
|
|
||||||
) {
|
|
||||||
Icon(Icons.Filled.Add, "Add repo")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
appBarType = AppBarType.SMALL,
|
appBarType = AppBarType.SMALL,
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
if (repoState.value is ExtensionRepoState.Loading) return@YokaiScaffold
|
if (repoState.value is ExtensionRepoState.Loading) return@YokaiScaffold
|
||||||
|
@ -61,6 +50,7 @@ fun ExtensionRepoScreen(
|
||||||
item {
|
item {
|
||||||
ExtensionRepoItem(
|
ExtensionRepoItem(
|
||||||
inputText = inputText,
|
inputText = inputText,
|
||||||
|
// TODO: i18n
|
||||||
inputHint = "Add new repo",
|
inputHint = "Add new repo",
|
||||||
onInputChange = { inputText = it },
|
onInputChange = { inputText = it },
|
||||||
onAddClick = { viewModel.addRepo(it) },
|
onAddClick = { viewModel.addRepo(it) },
|
||||||
|
@ -72,6 +62,7 @@ fun ExtensionRepoScreen(
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
modifier = Modifier.fillParentMaxSize(),
|
modifier = Modifier.fillParentMaxSize(),
|
||||||
image = Icons.Filled.ExtensionOff,
|
image = Icons.Filled.ExtensionOff,
|
||||||
|
// TODO: i18n
|
||||||
message = "No extension repo found",
|
message = "No extension repo found",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -82,12 +73,17 @@ fun ExtensionRepoScreen(
|
||||||
item {
|
item {
|
||||||
ExtensionRepoItem(
|
ExtensionRepoItem(
|
||||||
repoUrl = repo,
|
repoUrl = repo,
|
||||||
|
// TODO: Confirmation dialog
|
||||||
onDeleteClick = { viewModel.deleteRepo(it) },
|
onDeleteClick = { viewModel.deleteRepo(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(repoUrl) {
|
||||||
|
repoUrl?.let { viewModel.addRepo(repoUrl) }
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.event.collectLatest { event ->
|
viewModel.event.collectLatest { event ->
|
||||||
|
|
|
@ -255,6 +255,11 @@ object Migrations {
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 112) {
|
||||||
|
prefs.edit {
|
||||||
|
remove("trusted_signatures")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -343,9 +343,6 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
|
||||||
|
|
||||||
fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
|
fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
|
||||||
|
|
||||||
// TODO: SourcePref
|
|
||||||
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet())
|
|
||||||
|
|
||||||
// using string instead of set so it is ordered
|
// using string instead of set so it is ordered
|
||||||
// TODO: SourcePref
|
// TODO: SourcePref
|
||||||
fun migrationSources() = preferenceStore.getString("migrate_sources", "")
|
fun migrationSources() = preferenceStore.getString("migrate_sources", "")
|
||||||
|
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.di
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import dev.yokai.domain.extension.TrustExtension
|
||||||
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
@ -61,6 +62,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||||
|
|
||||||
addSingletonFactory { MangaShortcutManager() }
|
addSingletonFactory { MangaShortcutManager() }
|
||||||
|
|
||||||
|
addSingletonFactory { TrustExtension() }
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
|
|
||||||
ContextCompat.getMainExecutor(app).execute {
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
|
|
|
@ -4,9 +4,8 @@ import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import dev.yokai.domain.source.SourcePreferences
|
import dev.yokai.domain.extension.TrustExtension
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.minusAssign
|
|
||||||
import eu.kanade.tachiyomi.data.preference.plusAssign
|
import eu.kanade.tachiyomi.data.preference.plusAssign
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
|
@ -23,7 +22,6 @@ import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -43,6 +41,7 @@ import java.util.Locale
|
||||||
class ExtensionManager(
|
class ExtensionManager(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val preferences: PreferencesHelper = Injekt.get(),
|
private val preferences: PreferencesHelper = Injekt.get(),
|
||||||
|
private val trustExtension: TrustExtension = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -307,34 +306,30 @@ class ExtensionManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the given signature to the list of trusted signatures. It also loads in background the
|
* Adds the given extension to the list of trusted extensions. It also loads in background the
|
||||||
* extensions that match this signature.
|
* now trusted extensions.
|
||||||
*
|
*
|
||||||
* @param signature The signature to whitelist.
|
* @param pkgName the package name of the extension
|
||||||
|
* @param versionCode the version code of the extension
|
||||||
|
* @param signatureHash the signature hash of the extension
|
||||||
*/
|
*/
|
||||||
fun trustSignature(signature: String) {
|
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
|
||||||
val untrustedSignatures = untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
|
val untrustedPkgName = untrustedExtensionsFlow.value.map { it.pkgName }.toSet()
|
||||||
if (signature !in untrustedSignatures) return
|
if (pkgName !in untrustedPkgName) return
|
||||||
|
|
||||||
ExtensionLoader.trustedSignatures += signature
|
trustExtension.trust(pkgName, versionCode, signatureHash)
|
||||||
val preference = preferences.trustedSignatures()
|
|
||||||
preference.set(preference.get() + signature)
|
|
||||||
|
|
||||||
val nowTrustedExtensions = untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
|
val nowTrustedExtensions = untrustedExtensionsFlow.value
|
||||||
|
.filter { it.pkgName == pkgName && it.versionCode == versionCode }
|
||||||
_untrustedExtensionsFlow.value -= nowTrustedExtensions
|
_untrustedExtensionsFlow.value -= nowTrustedExtensions
|
||||||
|
|
||||||
val ctx = context
|
|
||||||
launchNow {
|
launchNow {
|
||||||
nowTrustedExtensions
|
nowTrustedExtensions
|
||||||
.map { extension ->
|
.map { extension ->
|
||||||
async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) }
|
async { ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName) }.await()
|
||||||
}
|
|
||||||
.map { it.await() }
|
|
||||||
.forEach { result ->
|
|
||||||
if (result is LoadResult.Success) {
|
|
||||||
registerNewExtension(result.extension)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.filterIsInstance<LoadResult.Success>()
|
||||||
|
.forEach { registerNewExtension(it.extension) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -274,7 +274,7 @@ class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trustSignature(signatureHash: String) {
|
fun trustExtension(pkgName: String, versionCode: Long, signatureHash: String) {
|
||||||
extensionManager.trustSignature(signatureHash)
|
extensionManager.trust(pkgName, versionCode, signatureHash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -323,7 +323,7 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openTrustDialog(extension: Extension.Untrusted) {
|
private fun openTrustDialog(extension: Extension.Untrusted) {
|
||||||
ExtensionTrustDialog(this, extension.signatureHash, extension.pkgName)
|
ExtensionTrustDialog(this, extension.signatureHash, extension.pkgName, extension.versionCode)
|
||||||
.showDialog(controller.router)
|
.showDialog(controller.router)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,9 +407,10 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||||
extAdapter?.updateItem(updateHeader)
|
extAdapter?.updateItem(updateHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun trustSignature(signatureHash: String) {
|
override fun trustExtension(pkgName: String, versionCode: Long, signatureHash: String) {
|
||||||
presenter.trustSignature(signatureHash)
|
presenter.trustExtension(pkgName, versionCode, signatureHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun uninstallExtension(pkgName: String) {
|
override fun uninstallExtension(pkgName: String) {
|
||||||
presenter.uninstallExtension(pkgName)
|
presenter.uninstallExtension(pkgName)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,11 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||||
where T : ExtensionTrustDialog.Listener {
|
where T : ExtensionTrustDialog.Listener {
|
||||||
|
|
||||||
lateinit var listener: Listener
|
lateinit var listener: Listener
|
||||||
constructor(target: T, signatureHash: String, pkgName: String) : this(
|
constructor(target: T, signatureHash: String, pkgName: String, versionCode: Long) : this(
|
||||||
Bundle().apply {
|
Bundle().apply {
|
||||||
putString(SIGNATURE_KEY, signatureHash)
|
putString(SIGNATURE_KEY, signatureHash)
|
||||||
putString(PKGNAME_KEY, pkgName)
|
putString(PKGNAME_KEY, pkgName)
|
||||||
|
putLong(VERSION_CODE, versionCode)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
listener = target
|
listener = target
|
||||||
|
@ -24,7 +25,7 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||||
.setTitle(R.string.untrusted_extension)
|
.setTitle(R.string.untrusted_extension)
|
||||||
.setMessage(R.string.untrusted_extension_message)
|
.setMessage(R.string.untrusted_extension_message)
|
||||||
.setPositiveButton(R.string.trust) { _, _ ->
|
.setPositiveButton(R.string.trust) { _, _ ->
|
||||||
listener.trustSignature(args.getString(SIGNATURE_KEY)!!)
|
listener.trustExtension(args.getString(PKGNAME_KEY)!!, args.getLong(VERSION_CODE), args.getString(SIGNATURE_KEY)!!)
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.uninstall) { _, _ ->
|
.setNegativeButton(R.string.uninstall) { _, _ ->
|
||||||
listener.uninstallExtension(args.getString(PKGNAME_KEY)!!)
|
listener.uninstallExtension(args.getString(PKGNAME_KEY)!!)
|
||||||
|
@ -34,10 +35,11 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||||
private companion object {
|
private companion object {
|
||||||
const val SIGNATURE_KEY = "signature_key"
|
const val SIGNATURE_KEY = "signature_key"
|
||||||
const val PKGNAME_KEY = "pkgname_key"
|
const val PKGNAME_KEY = "pkgname_key"
|
||||||
|
const val VERSION_CODE = "version_code"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun trustSignature(signatureHash: String)
|
fun trustExtension(pkgName: String, versionCode: Long, signatureHash: String)
|
||||||
fun uninstallExtension(pkgName: String)
|
fun uninstallExtension(pkgName: String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
||||||
import com.google.common.primitives.Floats.max
|
import com.google.common.primitives.Floats.max
|
||||||
import com.google.common.primitives.Ints.max
|
import com.google.common.primitives.Ints.max
|
||||||
|
import dev.yokai.presentation.extension.repo.ExtensionRepoController
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.Migrations
|
import eu.kanade.tachiyomi.Migrations
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
@ -1053,6 +1054,14 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
||||||
controller?.showSheet()
|
controller?.showSheet()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Intent.ACTION_VIEW -> {
|
||||||
|
if (intent.scheme == "tachiyomi" && intent.data?.host == "add-repo") {
|
||||||
|
intent.data?.getQueryParameter("url")?.let { repoUrl ->
|
||||||
|
router.popToRoot()
|
||||||
|
router.pushController(ExtensionRepoController(repoUrl).withFadeTransaction())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue