mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-23 21:12:34 +01:00
Add install extension file from filesystem
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.jui.domain.extension.interactor
|
||||||
|
|
||||||
|
import ca.gosyer.jui.domain.extension.service.ExtensionRepository
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import me.tatarka.inject.annotations.Inject
|
||||||
|
import okio.Path
|
||||||
|
import org.lighthousegames.logging.logging
|
||||||
|
|
||||||
|
class InstallExtensionFile @Inject constructor(private val extensionRepository: ExtensionRepository) {
|
||||||
|
|
||||||
|
suspend fun await(path: Path) = asFlow(path)
|
||||||
|
.catch { log.warn(it) { "Failed to install extension from $path" } }
|
||||||
|
.collect()
|
||||||
|
|
||||||
|
fun asFlow(path: Path) = extensionRepository.installExtension(ExtensionRepository.buildExtensionFormData(path))
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = logging()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,19 +6,36 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.domain.extension.service
|
package ca.gosyer.jui.domain.extension.service
|
||||||
|
|
||||||
|
import ca.gosyer.jui.core.io.SYSTEM
|
||||||
import ca.gosyer.jui.domain.extension.model.Extension
|
import ca.gosyer.jui.domain.extension.model.Extension
|
||||||
import de.jensklingenberg.ktorfit.http.GET
|
import de.jensklingenberg.ktorfit.http.GET
|
||||||
|
import de.jensklingenberg.ktorfit.http.Multipart
|
||||||
|
import de.jensklingenberg.ktorfit.http.POST
|
||||||
|
import de.jensklingenberg.ktorfit.http.Part
|
||||||
import de.jensklingenberg.ktorfit.http.Path
|
import de.jensklingenberg.ktorfit.http.Path
|
||||||
import de.jensklingenberg.ktorfit.http.ReqBuilder
|
import de.jensklingenberg.ktorfit.http.ReqBuilder
|
||||||
import io.ktor.client.request.HttpRequestBuilder
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
|
import io.ktor.client.request.forms.formData
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.Headers
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.http.content.PartData
|
||||||
import io.ktor.utils.io.ByteReadChannel
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.buffer
|
||||||
|
|
||||||
interface ExtensionRepository {
|
interface ExtensionRepository {
|
||||||
@GET("api/v1/extension/list")
|
@GET("api/v1/extension/list")
|
||||||
fun getExtensionList(): Flow<List<Extension>>
|
fun getExtensionList(): Flow<List<Extension>>
|
||||||
|
|
||||||
|
@Multipart
|
||||||
|
@POST("api/v1/extension/install")
|
||||||
|
fun installExtension(
|
||||||
|
@Part("") formData: List<PartData>,
|
||||||
|
): Flow<HttpResponse>
|
||||||
|
|
||||||
@GET("api/v1/extension/install/{pkgName}")
|
@GET("api/v1/extension/install/{pkgName}")
|
||||||
fun installExtension(
|
fun installExtension(
|
||||||
@Path("pkgName") pkgName: String
|
@Path("pkgName") pkgName: String
|
||||||
@@ -39,4 +56,17 @@ interface ExtensionRepository {
|
|||||||
@Path("apkName") apkName: String,
|
@Path("apkName") apkName: String,
|
||||||
@ReqBuilder block: HttpRequestBuilder.() -> Unit
|
@ReqBuilder block: HttpRequestBuilder.() -> Unit
|
||||||
): Flow<ByteReadChannel>
|
): Flow<ByteReadChannel>
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun buildExtensionFormData(file: okio.Path) = formData {
|
||||||
|
append(
|
||||||
|
"file",
|
||||||
|
FileSystem.SYSTEM.source(file).buffer().readByteArray(),
|
||||||
|
Headers.build {
|
||||||
|
append(HttpHeaders.ContentType, ContentType.MultiPart.FormData.toString())
|
||||||
|
append(HttpHeaders.ContentDisposition, "filename=file")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class ExtensionsScreen : Screen {
|
|||||||
enabledLangs = vm.enabledLangs.collectAsState().value,
|
enabledLangs = vm.enabledLangs.collectAsState().value,
|
||||||
availableLangs = vm.availableLangs.collectAsState().value,
|
availableLangs = vm.availableLangs.collectAsState().value,
|
||||||
setEnabledLanguages = vm::setEnabledLanguages,
|
setEnabledLanguages = vm::setEnabledLanguages,
|
||||||
|
installExtensionFile = vm::install,
|
||||||
installExtension = vm::install,
|
installExtension = vm::install,
|
||||||
updateExtension = vm::update,
|
updateExtension = vm::update,
|
||||||
uninstallExtension = vm::uninstall
|
uninstallExtension = vm::uninstall
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ package ca.gosyer.jui.ui.extensions
|
|||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
|
import ca.gosyer.jui.core.io.saveTo
|
||||||
import ca.gosyer.jui.core.lang.displayName
|
import ca.gosyer.jui.core.lang.displayName
|
||||||
|
import ca.gosyer.jui.core.lang.throwIfCancellation
|
||||||
import ca.gosyer.jui.domain.extension.interactor.GetExtensionList
|
import ca.gosyer.jui.domain.extension.interactor.GetExtensionList
|
||||||
import ca.gosyer.jui.domain.extension.interactor.InstallExtension
|
import ca.gosyer.jui.domain.extension.interactor.InstallExtension
|
||||||
|
import ca.gosyer.jui.domain.extension.interactor.InstallExtensionFile
|
||||||
import ca.gosyer.jui.domain.extension.interactor.UninstallExtension
|
import ca.gosyer.jui.domain.extension.interactor.UninstallExtension
|
||||||
import ca.gosyer.jui.domain.extension.interactor.UpdateExtension
|
import ca.gosyer.jui.domain.extension.interactor.UpdateExtension
|
||||||
import ca.gosyer.jui.domain.extension.model.Extension
|
import ca.gosyer.jui.domain.extension.model.Extension
|
||||||
@@ -32,10 +35,14 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.tatarka.inject.annotations.Inject
|
import me.tatarka.inject.annotations.Inject
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.Source
|
||||||
import org.lighthousegames.logging.logging
|
import org.lighthousegames.logging.logging
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
class ExtensionsScreenViewModel @Inject constructor(
|
class ExtensionsScreenViewModel @Inject constructor(
|
||||||
private val getExtensionList: GetExtensionList,
|
private val getExtensionList: GetExtensionList,
|
||||||
|
private val installExtensionFile: InstallExtensionFile,
|
||||||
private val installExtension: InstallExtension,
|
private val installExtension: InstallExtension,
|
||||||
private val updateExtension: UpdateExtension,
|
private val updateExtension: UpdateExtension,
|
||||||
private val uninstallExtension: UninstallExtension,
|
private val uninstallExtension: UninstallExtension,
|
||||||
@@ -77,6 +84,26 @@ class ExtensionsScreenViewModel @Inject constructor(
|
|||||||
_isLoading.value = false
|
_isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun install(source: Source) {
|
||||||
|
log.info { "Install file clicked" }
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val file = FileSystem.SYSTEM_TEMPORARY_DIRECTORY
|
||||||
|
.resolve("tachidesk.${Random.nextLong()}.proto.gz")
|
||||||
|
.also { file ->
|
||||||
|
source.saveTo(file)
|
||||||
|
}
|
||||||
|
installExtensionFile.await(file)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn(e) { "Error creating apk file" }
|
||||||
|
// todo toast if error
|
||||||
|
e.throwIfCancellation()
|
||||||
|
}
|
||||||
|
|
||||||
|
getExtensions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun install(extension: Extension) {
|
fun install(extension: Extension) {
|
||||||
log.info { "Install clicked" }
|
log.info { "Install clicked" }
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.compose.material.MaterialTheme
|
|||||||
import androidx.compose.material.Scaffold
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.Translate
|
import androidx.compose.material.icons.rounded.Translate
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
@@ -54,6 +55,7 @@ import ca.gosyer.jui.domain.extension.model.Extension
|
|||||||
import ca.gosyer.jui.i18n.MR
|
import ca.gosyer.jui.i18n.MR
|
||||||
import ca.gosyer.jui.presentation.build.BuildKonfig
|
import ca.gosyer.jui.presentation.build.BuildKonfig
|
||||||
import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties
|
import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties
|
||||||
|
import ca.gosyer.jui.ui.base.file.rememberFileChooser
|
||||||
import ca.gosyer.jui.ui.base.navigation.ActionItem
|
import ca.gosyer.jui.ui.base.navigation.ActionItem
|
||||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||||
import ca.gosyer.jui.ui.extensions.ExtensionUI
|
import ca.gosyer.jui.ui.extensions.ExtensionUI
|
||||||
@@ -74,6 +76,7 @@ import com.vanpra.composematerialdialogs.title
|
|||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import okio.Source
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionsScreenContent(
|
fun ExtensionsScreenContent(
|
||||||
@@ -84,11 +87,13 @@ fun ExtensionsScreenContent(
|
|||||||
enabledLangs: ImmutableSet<String>,
|
enabledLangs: ImmutableSet<String>,
|
||||||
availableLangs: ImmutableList<String>,
|
availableLangs: ImmutableList<String>,
|
||||||
setEnabledLanguages: (Set<String>) -> Unit,
|
setEnabledLanguages: (Set<String>) -> Unit,
|
||||||
|
installExtensionFile: (Source) -> Unit,
|
||||||
installExtension: (Extension) -> Unit,
|
installExtension: (Extension) -> Unit,
|
||||||
updateExtension: (Extension) -> Unit,
|
updateExtension: (Extension) -> Unit,
|
||||||
uninstallExtension: (Extension) -> Unit
|
uninstallExtension: (Extension) -> Unit
|
||||||
) {
|
) {
|
||||||
val languageDialogState = rememberMaterialDialogState()
|
val languageDialogState = rememberMaterialDialogState()
|
||||||
|
val chooser = rememberFileChooser(installExtensionFile)
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.windowInsetsPadding(
|
modifier = Modifier.windowInsetsPadding(
|
||||||
WindowInsets.statusBars.add(
|
WindowInsets.statusBars.add(
|
||||||
@@ -97,9 +102,12 @@ fun ExtensionsScreenContent(
|
|||||||
),
|
),
|
||||||
topBar = {
|
topBar = {
|
||||||
ExtensionsToolbar(
|
ExtensionsToolbar(
|
||||||
query,
|
searchText = query,
|
||||||
setQuery,
|
search = setQuery,
|
||||||
languageDialogState::show
|
openLanguageDialog = languageDialogState::show,
|
||||||
|
openInstallExtensionFile = {
|
||||||
|
chooser.launch("apk")
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
@@ -176,14 +184,18 @@ fun ExtensionsScreenContent(
|
|||||||
fun ExtensionsToolbar(
|
fun ExtensionsToolbar(
|
||||||
searchText: String?,
|
searchText: String?,
|
||||||
search: (String) -> Unit,
|
search: (String) -> Unit,
|
||||||
openLanguageDialog: () -> Unit
|
openLanguageDialog: () -> Unit,
|
||||||
|
openInstallExtensionFile: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Toolbar(
|
Toolbar(
|
||||||
stringResource(MR.strings.location_extensions),
|
stringResource(MR.strings.location_extensions),
|
||||||
searchText = searchText,
|
searchText = searchText,
|
||||||
search = search,
|
search = search,
|
||||||
actions = {
|
actions = {
|
||||||
getActionItems(openLanguageDialog)
|
getActionItems(
|
||||||
|
openLanguageDialog,
|
||||||
|
openInstallExtensionFile
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -317,13 +329,19 @@ fun LanguageDialog(
|
|||||||
@Stable
|
@Stable
|
||||||
@Composable
|
@Composable
|
||||||
private fun getActionItems(
|
private fun getActionItems(
|
||||||
openLanguageDialog: () -> Unit
|
openLanguageDialog: () -> Unit,
|
||||||
|
openInstallExtensionFile: () -> Unit,
|
||||||
): ImmutableList<ActionItem> {
|
): ImmutableList<ActionItem> {
|
||||||
return listOf(
|
return listOf(
|
||||||
ActionItem(
|
ActionItem(
|
||||||
stringResource(MR.strings.enabled_languages),
|
stringResource(MR.strings.enabled_languages),
|
||||||
Icons.Rounded.Translate,
|
Icons.Rounded.Translate,
|
||||||
doAction = openLanguageDialog
|
doAction = openLanguageDialog
|
||||||
|
),
|
||||||
|
ActionItem(
|
||||||
|
stringResource(MR.strings.action_install),
|
||||||
|
Icons.Rounded.Add,
|
||||||
|
doAction = openInstallExtensionFile
|
||||||
)
|
)
|
||||||
).toImmutableList()
|
).toImmutableList()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user