diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtensionFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtensionFile.kt new file mode 100644 index 00000000..a4f78b55 --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtensionFile.kt @@ -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() + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/service/ExtensionRepository.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/service/ExtensionRepository.kt index 4aec748e..5b6bd4d3 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/service/ExtensionRepository.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/service/ExtensionRepository.kt @@ -6,19 +6,36 @@ package ca.gosyer.jui.domain.extension.service +import ca.gosyer.jui.core.io.SYSTEM import ca.gosyer.jui.domain.extension.model.Extension 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.ReqBuilder import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.forms.formData 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 kotlinx.coroutines.flow.Flow +import okio.FileSystem +import okio.buffer interface ExtensionRepository { @GET("api/v1/extension/list") fun getExtensionList(): Flow> + @Multipart + @POST("api/v1/extension/install") + fun installExtension( + @Part("") formData: List, + ): Flow + @GET("api/v1/extension/install/{pkgName}") fun installExtension( @Path("pkgName") pkgName: String @@ -39,4 +56,17 @@ interface ExtensionRepository { @Path("apkName") apkName: String, @ReqBuilder block: HttpRequestBuilder.() -> Unit ): Flow + + 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") + } + ) + } + } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreen.kt index 1be6c9ce..db43dc02 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreen.kt @@ -30,6 +30,7 @@ class ExtensionsScreen : Screen { enabledLangs = vm.enabledLangs.collectAsState().value, availableLangs = vm.availableLangs.collectAsState().value, setEnabledLanguages = vm::setEnabledLanguages, + installExtensionFile = vm::install, installExtension = vm::install, updateExtension = vm::update, uninstallExtension = vm::uninstall diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt index 775179a6..9d64d358 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt @@ -8,9 +8,12 @@ package ca.gosyer.jui.ui.extensions import androidx.compose.runtime.Immutable 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.throwIfCancellation import ca.gosyer.jui.domain.extension.interactor.GetExtensionList 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.UpdateExtension 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.launch import me.tatarka.inject.annotations.Inject +import okio.FileSystem +import okio.Source import org.lighthousegames.logging.logging +import kotlin.random.Random class ExtensionsScreenViewModel @Inject constructor( private val getExtensionList: GetExtensionList, + private val installExtensionFile: InstallExtensionFile, private val installExtension: InstallExtension, private val updateExtension: UpdateExtension, private val uninstallExtension: UninstallExtension, @@ -77,6 +84,26 @@ class ExtensionsScreenViewModel @Inject constructor( _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) { log.info { "Install clicked" } scope.launch { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt index c648e955..0784f6c5 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/components/ExtensionsScreenContent.kt @@ -33,6 +33,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Translate import androidx.compose.runtime.Composable 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.presentation.build.BuildKonfig 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.Toolbar 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.ImmutableSet import kotlinx.collections.immutable.toImmutableList +import okio.Source @Composable fun ExtensionsScreenContent( @@ -84,11 +87,13 @@ fun ExtensionsScreenContent( enabledLangs: ImmutableSet, availableLangs: ImmutableList, setEnabledLanguages: (Set) -> Unit, + installExtensionFile: (Source) -> Unit, installExtension: (Extension) -> Unit, updateExtension: (Extension) -> Unit, uninstallExtension: (Extension) -> Unit ) { val languageDialogState = rememberMaterialDialogState() + val chooser = rememberFileChooser(installExtensionFile) Scaffold( modifier = Modifier.windowInsetsPadding( WindowInsets.statusBars.add( @@ -97,9 +102,12 @@ fun ExtensionsScreenContent( ), topBar = { ExtensionsToolbar( - query, - setQuery, - languageDialogState::show + searchText = query, + search = setQuery, + openLanguageDialog = languageDialogState::show, + openInstallExtensionFile = { + chooser.launch("apk") + } ) } ) { padding -> @@ -176,14 +184,18 @@ fun ExtensionsScreenContent( fun ExtensionsToolbar( searchText: String?, search: (String) -> Unit, - openLanguageDialog: () -> Unit + openLanguageDialog: () -> Unit, + openInstallExtensionFile: () -> Unit, ) { Toolbar( stringResource(MR.strings.location_extensions), searchText = searchText, search = search, actions = { - getActionItems(openLanguageDialog) + getActionItems( + openLanguageDialog, + openInstallExtensionFile + ) } ) } @@ -317,13 +329,19 @@ fun LanguageDialog( @Stable @Composable private fun getActionItems( - openLanguageDialog: () -> Unit + openLanguageDialog: () -> Unit, + openInstallExtensionFile: () -> Unit, ): ImmutableList { return listOf( ActionItem( stringResource(MR.strings.enabled_languages), Icons.Rounded.Translate, doAction = openLanguageDialog + ), + ActionItem( + stringResource(MR.strings.action_install), + Icons.Rounded.Add, + doAction = openInstallExtensionFile ) ).toImmutableList() }