chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.image.toolbox.library)
|
||||
alias(libs.plugins.image.toolbox.feature)
|
||||
alias(libs.plugins.image.toolbox.hilt)
|
||||
alias(libs.plugins.image.toolbox.compose)
|
||||
}
|
||||
|
||||
android.namespace = "com.t8rin.imagetoolbox.feature.cipher"
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ ImageToolbox is an image editor for android
|
||||
~ Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
~
|
||||
~ You should have received a copy of the Apache License
|
||||
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
-->
|
||||
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
@file:Suppress("PrivatePropertyName", "SpellCheckingInspection")
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.cipher.data
|
||||
|
||||
import com.t8rin.imagetoolbox.core.data.saving.io.StringReadable
|
||||
import com.t8rin.imagetoolbox.core.data.utils.computeBytesFromReadable
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.domain.model.CipherType
|
||||
import com.t8rin.imagetoolbox.core.domain.model.HashingType
|
||||
import com.t8rin.imagetoolbox.feature.cipher.domain.CryptographyManager
|
||||
import com.t8rin.imagetoolbox.feature.cipher.domain.WrongKeyException
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.security.Key
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import javax.crypto.spec.PBEParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AndroidCryptographyManager @Inject constructor(
|
||||
private val dispatchersHolder: DispatchersHolder
|
||||
) : CryptographyManager, DispatchersHolder by dispatchersHolder {
|
||||
|
||||
private val CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
private fun createKey(
|
||||
password: String,
|
||||
type: CipherType
|
||||
): Key = if ("PBE" in type.name) {
|
||||
SecretKeyFactory
|
||||
.getInstance(type.cipher)
|
||||
.generateSecret(
|
||||
PBEKeySpec(password.toCharArray())
|
||||
)
|
||||
} else {
|
||||
hashingType(type.name)
|
||||
.computeBytesFromReadable(StringReadable(password))
|
||||
.let { key ->
|
||||
SecretKeySpec(key, type.cipher)
|
||||
}
|
||||
}
|
||||
|
||||
override fun generateRandomString(length: Int): String {
|
||||
val sr = SecureRandom()
|
||||
return buildString {
|
||||
repeat(length) {
|
||||
append(CHARS[sr.nextInt(CHARS.length)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun decrypt(
|
||||
data: ByteArray,
|
||||
key: String,
|
||||
type: CipherType
|
||||
): ByteArray = withContext(defaultDispatcher) {
|
||||
val cipher = type.toCipher(
|
||||
keySpec = createKey(
|
||||
password = key,
|
||||
type = type
|
||||
),
|
||||
isEncrypt = false
|
||||
)
|
||||
|
||||
cipher.doOrThrow(data)
|
||||
}
|
||||
|
||||
override suspend fun encrypt(
|
||||
data: ByteArray,
|
||||
key: String,
|
||||
type: CipherType
|
||||
): ByteArray = withContext(defaultDispatcher) {
|
||||
val cipher = type.toCipher(
|
||||
keySpec = createKey(
|
||||
password = key,
|
||||
type = type
|
||||
),
|
||||
isEncrypt = true
|
||||
)
|
||||
|
||||
cipher.doOrThrow(data)
|
||||
}
|
||||
|
||||
private fun Cipher.init(
|
||||
keySpec: Key,
|
||||
isEncrypt: Boolean,
|
||||
type: CipherType
|
||||
) {
|
||||
val mode = if (isEncrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE
|
||||
try {
|
||||
val encodedKey = keySpec.encoded
|
||||
when {
|
||||
"PBE" in type.name -> {
|
||||
init(
|
||||
mode,
|
||||
keySpec,
|
||||
PBEParameterSpec(encodedKey, encodedKey.size)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
init(
|
||||
mode,
|
||||
keySpec,
|
||||
IvParameterSpec(encodedKey, 0, blockSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
runCatching {
|
||||
init(
|
||||
mode,
|
||||
keySpec,
|
||||
generateIV(ivSize(type.name))
|
||||
)
|
||||
}.getOrElse {
|
||||
init(
|
||||
mode,
|
||||
keySpec
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CipherType.toCipher(
|
||||
keySpec: Key,
|
||||
isEncrypt: Boolean
|
||||
): Cipher = Cipher.getInstance(cipher).apply {
|
||||
init(
|
||||
keySpec = keySpec,
|
||||
isEncrypt = isEncrypt,
|
||||
type = this@toCipher
|
||||
)
|
||||
}
|
||||
|
||||
private fun Cipher.doOrThrow(data: ByteArray): ByteArray {
|
||||
return try {
|
||||
doFinal(data)
|
||||
} catch (e: Throwable) {
|
||||
throw if (e.message?.contains("mac") == true && e.message?.contains("failed") == true) {
|
||||
WrongKeyException()
|
||||
} else e
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateIV(size: Int): IvParameterSpec {
|
||||
val iv = ByteArray(size)
|
||||
SecureRandom().nextBytes(iv)
|
||||
return IvParameterSpec(iv)
|
||||
}
|
||||
|
||||
private fun ivSize(name: String): Int = when {
|
||||
name == "AESRFC5649WRAP"
|
||||
|| name == "AESWRAPPAD"
|
||||
|| name == "AES_128/KWP/NoPadding"
|
||||
|| name == "AES_192/KWP/NoPadding"
|
||||
|| name == "AES_256/KWP/NoPadding"
|
||||
|| name == "ARIAWRAPPAD" -> 4
|
||||
|
||||
name == "AESWRAP"
|
||||
|| name == "AES_128/KW/NoPadding"
|
||||
|| name == "AES_192/KW/NoPadding"
|
||||
|| name == "CAMELLIAWRAP"
|
||||
|| name == "AES_256/KW/NoPadding"
|
||||
|| name == "ARIAWRAP"
|
||||
|| name == "CHACHA"
|
||||
|| "CBC" in name && name != "SEED/CBC"
|
||||
|| name == "DESEDEWRAP"
|
||||
|| name == "GRAINV1"
|
||||
|| name == "SALSA20"
|
||||
|| name.contains("wrap", true) -> 8
|
||||
|
||||
name == "AES/ECB/PKCS7PADDING"
|
||||
|| name == "AES/GCM-SIV/NOPADDING"
|
||||
|| name == "AES128/CCM"
|
||||
|| name == "AES192/CCM"
|
||||
|| name == "AES256/CCM"
|
||||
|| "CCM" in name
|
||||
|| "CHACHA" in name
|
||||
|| name == "AES_256/GCM-SIV/NOPADDING"
|
||||
|| name == "AES_256/GCM/NOPADDING"
|
||||
|| name == "GRAIN128"
|
||||
|| name == "AES_128/CBC/NOPADDING"
|
||||
|| name == "AES_128/CBC/PKCS5PADDING"
|
||||
|| name == "AES_128/GCM/NOPADDING" -> 12
|
||||
|
||||
name == "XSALSA20" -> 24
|
||||
name == "ZUC-256" -> 25
|
||||
|
||||
"DSTU7624" in name && "512" in name -> 64
|
||||
|
||||
else -> 16
|
||||
}
|
||||
|
||||
private val MD5List = listOf(
|
||||
"SEED",
|
||||
"NOEKEON",
|
||||
"HC128",
|
||||
"AES_128/CBC/NOPADDING",
|
||||
"AES_128/CBC/PKCS5PADDING",
|
||||
"AES_128/GCM/NOPADDING",
|
||||
"DESEDE",
|
||||
"GRAIN128",
|
||||
"SM4",
|
||||
"TEA",
|
||||
"ZUC-128",
|
||||
"AES_128/ECB/NOPADDING",
|
||||
"AES_128/ECB/PKCS5PADDING",
|
||||
"AES_128/GCM/NOPADDING"
|
||||
)
|
||||
|
||||
private fun hashingType(name: String): HashingType = when {
|
||||
MD5List.any { name.contains(it, true) } -> HashingType.MD5
|
||||
|
||||
else -> HashingType.SHA_256
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.cipher.data
|
||||
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.RandomStringGenerator
|
||||
import com.t8rin.imagetoolbox.feature.cipher.domain.CryptographyManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AndroidRandomStringGenerator @Inject constructor(
|
||||
private val cryptographyManager: CryptographyManager
|
||||
) : RandomStringGenerator {
|
||||
|
||||
override fun generate(length: Int): String = cryptographyManager.generateRandomString(length)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.cipher.di
|
||||
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.RandomStringGenerator
|
||||
import com.t8rin.imagetoolbox.feature.cipher.data.AndroidCryptographyManager
|
||||
import com.t8rin.imagetoolbox.feature.cipher.data.AndroidRandomStringGenerator
|
||||
import com.t8rin.imagetoolbox.feature.cipher.domain.CryptographyManager
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface CipherModule {
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun cryptographyManager(
|
||||
impl: AndroidCryptographyManager
|
||||
): CryptographyManager
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideRandomStringGenerator(
|
||||
impl: AndroidRandomStringGenerator
|
||||
): RandomStringGenerator
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.cipher.domain
|
||||
|
||||
import com.t8rin.imagetoolbox.core.domain.model.CipherType
|
||||
|
||||
interface CryptographyManager {
|
||||
|
||||
fun generateRandomString(
|
||||
length: Int
|
||||
): String
|
||||
|
||||
suspend fun decrypt(
|
||||
data: ByteArray,
|
||||
key: String,
|
||||
type: CipherType
|
||||
): ByteArray
|
||||
|
||||
suspend fun encrypt(
|
||||
data: ByteArray,
|
||||
key: String,
|
||||
type: CipherType
|
||||
): ByteArray
|
||||
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.cipher.domain
|
||||
|
||||
internal class WrongKeyException : Throwable()
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.cipher.presentation
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.domain.model.CipherType
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.FileOpen
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.KeyVertical
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Lock
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.rememberFilePicker
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.AppToastHost
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.isPortraitOrientationAsState
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.AdaptiveLayoutScreen
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.buttons.BottomButtonsBlock
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.dialogs.ExitWithoutSavingDialog
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.dialogs.LoadingDialog
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedBadge
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.AutoFilePicker
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.FileNotPickedWidget
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.scaleOnTap
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.TopAppBarEmoji
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.marquee
|
||||
import com.t8rin.imagetoolbox.feature.cipher.domain.WrongKeyException
|
||||
import com.t8rin.imagetoolbox.feature.cipher.presentation.components.CipherControls
|
||||
import com.t8rin.imagetoolbox.feature.cipher.presentation.components.CipherTipSheet
|
||||
import com.t8rin.imagetoolbox.feature.cipher.presentation.screenLogic.CipherComponent
|
||||
|
||||
|
||||
@Composable
|
||||
fun CipherContent(
|
||||
component: CipherComponent
|
||||
) {
|
||||
val showTip = component.showTip
|
||||
|
||||
var showExitDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val canGoBack = component.canGoBack
|
||||
val key = component.key
|
||||
|
||||
val onBack = {
|
||||
if (!canGoBack) showExitDialog = true
|
||||
else component.onGoBack()
|
||||
}
|
||||
|
||||
val filePicker = rememberFilePicker(onSuccess = component::setUri)
|
||||
|
||||
AutoFilePicker(
|
||||
onAutoPick = filePicker::pickFile,
|
||||
isPickedAlready = component.initialUri != null
|
||||
)
|
||||
|
||||
val isPortrait by isPortraitOrientationAsState()
|
||||
|
||||
AdaptiveLayoutScreen(
|
||||
title = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.marquee()
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = component.uri to component.isEncrypt,
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
||||
) { (uri, isEncrypt) ->
|
||||
Text(
|
||||
if (uri == null) {
|
||||
stringResource(R.string.cipher)
|
||||
} else {
|
||||
listOf(
|
||||
stringResource(R.string.encryption),
|
||||
stringResource(R.string.decryption)
|
||||
)[if (isEncrypt) 0 else 1]
|
||||
},
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
EnhancedBadge(
|
||||
content = {
|
||||
Text(
|
||||
text = CipherType.entries.size.toString()
|
||||
)
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.tertiary,
|
||||
contentColor = MaterialTheme.colorScheme.onTertiary,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
.scaleOnTap {
|
||||
AppToastHost.showConfetti()
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
onGoBack = onBack,
|
||||
shouldDisableBackHandler = canGoBack,
|
||||
topAppBarPersistentActions = {
|
||||
TopAppBarEmoji()
|
||||
},
|
||||
actions = {},
|
||||
buttons = {
|
||||
BottomButtonsBlock(
|
||||
isNoData = component.uri == null,
|
||||
onSecondaryButtonClick = filePicker::pickFile,
|
||||
secondaryButtonIcon = Icons.Rounded.FileOpen,
|
||||
secondaryButtonText = stringResource(R.string.pick_file),
|
||||
onPrimaryButtonClick = {
|
||||
component.startCryptography {
|
||||
if (it is WrongKeyException) {
|
||||
AppToastHost.showFailureToast(R.string.invalid_password_or_not_encrypted)
|
||||
} else if (it != null) {
|
||||
AppToastHost.showFailureToast(
|
||||
throwable = it
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
primaryButtonIcon = if (component.isEncrypt) {
|
||||
Icons.Rounded.Lock
|
||||
} else {
|
||||
Icons.Rounded.KeyVertical
|
||||
},
|
||||
primaryButtonText = if (isPortrait) {
|
||||
if (component.isEncrypt) {
|
||||
stringResource(R.string.encrypt)
|
||||
} else {
|
||||
stringResource(R.string.decrypt)
|
||||
}
|
||||
} else "",
|
||||
isPrimaryButtonEnabled = key.isNotEmpty(),
|
||||
actions = {}
|
||||
)
|
||||
},
|
||||
canShowScreenData = component.uri != null,
|
||||
noDataControls = {
|
||||
FileNotPickedWidget(onPickFile = filePicker::pickFile)
|
||||
},
|
||||
controls = {
|
||||
CipherControls(component)
|
||||
},
|
||||
imagePreview = {},
|
||||
showImagePreviewAsStickyHeader = false,
|
||||
placeImagePreview = false,
|
||||
showActionsInTopAppBar = false,
|
||||
addHorizontalCutoutPaddingIfNoPreview = component.uri != null,
|
||||
)
|
||||
|
||||
ExitWithoutSavingDialog(
|
||||
onExit = component.onGoBack,
|
||||
onDismiss = { showExitDialog = false },
|
||||
visible = showExitDialog
|
||||
)
|
||||
|
||||
CipherTipSheet(
|
||||
visible = showTip,
|
||||
onDismiss = component::hideTip
|
||||
)
|
||||
|
||||
LoadingDialog(
|
||||
visible = component.isSaving,
|
||||
onCancelLoading = component::cancelSaving
|
||||
)
|
||||
|
||||
}
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.cipher.presentation.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.t8rin.imagetoolbox.core.domain.model.CipherType
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.toInt
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Cancel
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.CheckCircle
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Download
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.File
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.HashTag
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Help
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Share
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Shuffle
|
||||
import com.t8rin.imagetoolbox.core.settings.presentation.provider.LocalSettingsState
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.Green
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.outlineVariant
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.rememberFileCreator
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.ContextUtils.rememberFilename
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.ImageUtils.rememberHumanFileSize
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.isPortraitOrientationAsState
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.controls.selection.DataSelector
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedButtonGroup
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedIconButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.preferences.PreferenceItem
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.AutoSizeText
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.RoundedTextField
|
||||
import com.t8rin.imagetoolbox.feature.cipher.presentation.screenLogic.CipherComponent
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
internal fun CipherControls(component: CipherComponent) {
|
||||
val settingsState = LocalSettingsState.current
|
||||
val isPortrait by isPortraitOrientationAsState()
|
||||
|
||||
val saveLauncher = rememberFileCreator(
|
||||
onSuccess = component::saveCryptographyTo
|
||||
)
|
||||
|
||||
val filename = component.uri?.let {
|
||||
rememberFilename(it)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
if (isPortrait) Spacer(Modifier.height(20.dp))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.container(ShapeDefaults.circle)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val items = listOf(
|
||||
stringResource(R.string.encryption),
|
||||
stringResource(R.string.decryption)
|
||||
)
|
||||
EnhancedButtonGroup(
|
||||
enabled = true,
|
||||
itemCount = items.size,
|
||||
selectedIndex = (!component.isEncrypt).toInt(),
|
||||
onIndexChange = {
|
||||
component.setIsEncrypt(it == 0)
|
||||
},
|
||||
itemContent = {
|
||||
Text(
|
||||
text = items[it],
|
||||
fontSize = 12.sp
|
||||
)
|
||||
},
|
||||
isScrollable = false,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
EnhancedIconButton(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
onClick = component::showTip
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Help,
|
||||
contentDescription = "Info"
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
PreferenceItem(
|
||||
modifier = Modifier,
|
||||
title = filename ?: stringResource(R.string.something_went_wrong),
|
||||
onClick = null,
|
||||
titleFontStyle = LocalTextStyle.current.copy(
|
||||
lineHeight = 16.sp,
|
||||
fontSize = 15.sp
|
||||
),
|
||||
subtitle = component.uri?.let {
|
||||
stringResource(
|
||||
id = R.string.size,
|
||||
rememberHumanFileSize(it)
|
||||
)
|
||||
},
|
||||
startIcon = Icons.Rounded.File
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
RoundedTextField(
|
||||
modifier = Modifier
|
||||
.container(
|
||||
shape = MaterialTheme.shapes.large,
|
||||
resultPadding = 8.dp
|
||||
),
|
||||
value = component.key,
|
||||
startIcon = {
|
||||
EnhancedIconButton(
|
||||
onClick = {
|
||||
component.updateKey(component.generateRandomPassword())
|
||||
},
|
||||
modifier = Modifier.padding(start = 4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Shuffle,
|
||||
contentDescription = stringResource(R.string.shuffle),
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
},
|
||||
endIcon = {
|
||||
EnhancedIconButton(
|
||||
onClick = { component.updateKey("") },
|
||||
modifier = Modifier.padding(end = 4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Cancel,
|
||||
contentDescription = stringResource(R.string.cancel),
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
singleLine = false,
|
||||
onValueChange = component::updateKey,
|
||||
label = {
|
||||
Text(stringResource(R.string.key))
|
||||
}
|
||||
)
|
||||
AnimatedVisibility(visible = component.byteArray != null) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp)
|
||||
.container(
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
color = MaterialTheme
|
||||
.colorScheme
|
||||
.surfaceContainerHighest,
|
||||
resultPadding = 0.dp
|
||||
)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = Green,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
shape = ShapeDefaults.circle
|
||||
)
|
||||
.border(
|
||||
width = settingsState.borderWidth,
|
||||
color = MaterialTheme.colorScheme.outlineVariant(),
|
||||
shape = ShapeDefaults.circle
|
||||
)
|
||||
.padding(4.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
stringResource(R.string.file_proceed),
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.store_file_desc),
|
||||
fontSize = 13.sp,
|
||||
color = LocalContentColor.current.copy(alpha = 0.7f),
|
||||
lineHeight = 14.sp,
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
)
|
||||
var name by rememberSaveable(component.byteArray, filename) {
|
||||
mutableStateOf(
|
||||
if (component.isEncrypt) {
|
||||
"enc-"
|
||||
} else {
|
||||
"dec-"
|
||||
} + (filename ?: Random.nextInt())
|
||||
)
|
||||
}
|
||||
RoundedTextField(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.container(
|
||||
shape = MaterialTheme.shapes.large,
|
||||
resultPadding = 8.dp
|
||||
),
|
||||
value = name,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
singleLine = false,
|
||||
onValueChange = { name = it },
|
||||
label = {
|
||||
Text(stringResource(R.string.filename))
|
||||
}
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
EnhancedButton(
|
||||
onClick = {
|
||||
saveLauncher.make(name)
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.fillMaxWidth(0.5f)
|
||||
.height(50.dp),
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Download,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
AutoSizeText(
|
||||
text = stringResource(id = R.string.save),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
EnhancedButton(
|
||||
onClick = {
|
||||
component.byteArray?.let {
|
||||
component.shareFile(
|
||||
it = it,
|
||||
filename = name
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Share,
|
||||
contentDescription = stringResource(
|
||||
R.string.share
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
AutoSizeText(
|
||||
text = stringResource(id = R.string.share),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
DataSelector(
|
||||
modifier = Modifier,
|
||||
value = component.cipherType,
|
||||
containerColor = Color.Unspecified,
|
||||
spanCount = 5,
|
||||
selectedItemColor = MaterialTheme.colorScheme.secondary,
|
||||
onValueChange = component::updateCipherType,
|
||||
entries = CipherType.entries,
|
||||
title = stringResource(R.string.algorithms),
|
||||
titleIcon = Icons.Rounded.HashTag,
|
||||
itemContentText = {
|
||||
it.name
|
||||
},
|
||||
initialExpanded = true
|
||||
)
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.cipher.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Extension
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.File
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Functions
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Interface
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Security
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedModalBottomSheet
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.enhancedVerticalScroll
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.AutoSizeText
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.TitleItem
|
||||
|
||||
@Composable
|
||||
fun CipherTipSheet(
|
||||
visible: Boolean,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
EnhancedModalBottomSheet(
|
||||
sheetContent = {
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.enhancedVerticalScroll(rememberScrollState())
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.container(
|
||||
shape = ShapeDefaults.top
|
||||
)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
TitleItem(
|
||||
text = stringResource(R.string.features),
|
||||
icon = Icons.Rounded.Functions
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.features_sub),
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.container(
|
||||
shape = ShapeDefaults.center
|
||||
)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
TitleItem(
|
||||
text = stringResource(R.string.implementation),
|
||||
icon = Icons.Filled.Interface
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.implementation_sub),
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.container(
|
||||
shape = ShapeDefaults.center
|
||||
)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
TitleItem(
|
||||
text = stringResource(R.string.file_size),
|
||||
icon = Icons.Outlined.File
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.file_size_sub),
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.container(
|
||||
shape = ShapeDefaults.bottom
|
||||
)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
TitleItem(
|
||||
text = stringResource(R.string.compatibility),
|
||||
icon = Icons.Outlined.Extension
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.compatibility_sub),
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
visible = visible,
|
||||
onDismiss = {
|
||||
if (!it) onDismiss()
|
||||
},
|
||||
title = {
|
||||
TitleItem(
|
||||
text = stringResource(R.string.cipher),
|
||||
icon = Icons.Rounded.Security
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
EnhancedButton(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
onClick = onDismiss
|
||||
) {
|
||||
AutoSizeText(stringResource(R.string.close))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.cipher.presentation.screenLogic
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ShareProvider
|
||||
import com.t8rin.imagetoolbox.core.domain.model.CipherType
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.FileController
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.smartJob
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.update
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.BaseComponent
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.AppToastHost
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.state.savable
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.state.update
|
||||
import com.t8rin.imagetoolbox.feature.cipher.domain.CryptographyManager
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
class CipherComponent @AssistedInject internal constructor(
|
||||
@Assisted componentContext: ComponentContext,
|
||||
@Assisted val initialUri: Uri?,
|
||||
@Assisted val onGoBack: () -> Unit,
|
||||
private val cryptographyManager: CryptographyManager,
|
||||
private val shareProvider: ShareProvider,
|
||||
private val fileController: FileController,
|
||||
dispatchersHolder: DispatchersHolder
|
||||
) : BaseComponent(dispatchersHolder, componentContext) {
|
||||
|
||||
private val _cipherType = fileController.savable(
|
||||
scope = componentScope,
|
||||
initial = CipherType.entries.first()
|
||||
)
|
||||
val cipherType: CipherType by _cipherType
|
||||
|
||||
private val _showTip: MutableState<Boolean> = mutableStateOf(false)
|
||||
val showTip by _showTip
|
||||
|
||||
private val _key: MutableState<String> = mutableStateOf("")
|
||||
val key by _key
|
||||
|
||||
val canGoBack: Boolean
|
||||
get() = uri == null || (key.isEmpty() && byteArray == null)
|
||||
|
||||
fun showTip() = _showTip.update { true }
|
||||
fun hideTip() = _showTip.update { false }
|
||||
|
||||
init {
|
||||
debounce {
|
||||
initialUri?.let(::setUri)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateKey(newKey: String) {
|
||||
_key.update { newKey }
|
||||
resetCalculatedData()
|
||||
}
|
||||
|
||||
|
||||
private val _uri = mutableStateOf<Uri?>(null)
|
||||
val uri by _uri
|
||||
|
||||
private val _isEncrypt = mutableStateOf(true)
|
||||
val isEncrypt by _isEncrypt
|
||||
|
||||
private val _byteArray = mutableStateOf<ByteArray?>(null)
|
||||
val byteArray by _byteArray
|
||||
|
||||
private val _isSaving: MutableState<Boolean> = mutableStateOf(false)
|
||||
val isSaving by _isSaving
|
||||
|
||||
fun setUri(newUri: Uri) {
|
||||
_uri.value = newUri
|
||||
resetCalculatedData()
|
||||
}
|
||||
|
||||
private var savingJob: Job? by smartJob {
|
||||
_isSaving.update { false }
|
||||
}
|
||||
|
||||
fun startCryptography(
|
||||
onComplete: (Throwable?) -> Unit
|
||||
) {
|
||||
savingJob = trackProgress {
|
||||
_isSaving.value = true
|
||||
val uri = uri
|
||||
|
||||
if (uri == null) {
|
||||
onComplete(null)
|
||||
return@trackProgress
|
||||
}
|
||||
runSuspendCatching {
|
||||
_byteArray.update {
|
||||
fileController.readBytes(uri.toString()).let { file ->
|
||||
if (isEncrypt) {
|
||||
cryptographyManager.encrypt(
|
||||
data = file,
|
||||
key = key,
|
||||
type = cipherType
|
||||
)
|
||||
} else {
|
||||
cryptographyManager.decrypt(
|
||||
data = file,
|
||||
key = key,
|
||||
type = cipherType
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.exceptionOrNull().let(onComplete)
|
||||
_isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCipherType(type: CipherType) {
|
||||
_cipherType.update { type }
|
||||
resetCalculatedData()
|
||||
}
|
||||
|
||||
fun setIsEncrypt(isEncrypt: Boolean) {
|
||||
_isEncrypt.value = isEncrypt
|
||||
resetCalculatedData()
|
||||
}
|
||||
|
||||
fun resetCalculatedData() {
|
||||
_byteArray.value = null
|
||||
}
|
||||
|
||||
fun saveCryptographyTo(uri: Uri) {
|
||||
savingJob = trackProgress {
|
||||
_isSaving.value = true
|
||||
byteArray?.let { byteArray ->
|
||||
fileController.writeBytes(
|
||||
uri = uri.toString(),
|
||||
block = { it.writeBytes(byteArray) }
|
||||
).also(::parseFileSaveResult).onSuccess(::registerSave)
|
||||
}
|
||||
_isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun generateRandomPassword(): String = cryptographyManager.generateRandomString(18)
|
||||
|
||||
fun shareFile(
|
||||
it: ByteArray,
|
||||
filename: String,
|
||||
) {
|
||||
savingJob = trackProgress {
|
||||
_isSaving.value = true
|
||||
shareProvider.shareByteArray(
|
||||
byteArray = it,
|
||||
filename = filename,
|
||||
onComplete = {
|
||||
_isSaving.value = false
|
||||
AppToastHost.showConfetti()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelSaving() {
|
||||
savingJob?.cancel()
|
||||
savingJob = null
|
||||
_isSaving.value = false
|
||||
}
|
||||
|
||||
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
operator fun invoke(
|
||||
componentContext: ComponentContext,
|
||||
initialUri: Uri?,
|
||||
onGoBack: () -> Unit,
|
||||
): CipherComponent
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user