Proposal of Audio cover export by #1742

This commit is contained in:
T8RIN
2025-03-12 03:54:57 +03:00
parent a6d34ad79b
commit cb0c754556
23 changed files with 643 additions and 161 deletions

View File

@ -1,5 +1,3 @@
import dev.iurysouza.modulegraph.Theme
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
@ -17,10 +15,36 @@ import dev.iurysouza.modulegraph.Theme
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
/** Added if needed to regenerate module graph
import dev.iurysouza.modulegraph.Theme
plugins {
alias(libs.plugins.dev.iurysouza.modulegraph) apply true
}
moduleGraphConfig {
readmePath.set("./ARCHITECTURE.md")
heading = "# 📐 Modules Graph"
theme.set(
Theme.BASE(
mapOf(
"primaryColor" to "#00381a",
"primaryTextColor" to "#d4fcb1",
"primaryBorderColor" to "#14b800",
"lineColor" to "#15c400",
"secondaryColor" to "#283b26",
"tertiaryColor" to "#355238",
"nodeTextColor" to "#e0ffd6",
"edgeLabelBackground" to "#1a1a1a",
"fontSize" to "28px"
)
)
)
}
**/
buildscript {
repositories {
gradlePluginPortal()
@ -46,24 +70,4 @@ buildscript {
tasks.register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
}
moduleGraphConfig {
readmePath.set("./ARCHITECTURE.md")
heading = "# 📐 Modules Graph"
theme.set(
Theme.BASE(
mapOf(
"primaryColor" to "#00381a",
"primaryTextColor" to "#d4fcb1",
"primaryBorderColor" to "#14b800",
"lineColor" to "#15c400",
"secondaryColor" to "#283b26",
"tertiaryColor" to "#355238",
"nodeTextColor" to "#e0ffd6",
"edgeLabelBackground" to "#1a1a1a",
"fontSize" to "28px"
)
)
)
}

View File

@ -1602,4 +1602,6 @@
<string name="barcodes_sub">Scan any Barcode (QR, EAN, AZTEC, …) and get it\'s content or paste your text to generate new one</string>
<string name="no_barcode_found">No Barcode Found</string>
<string name="generated_barcode_will_be_here">Generated Barcode Will Be Here</string>
<string name="audio_cover_extractor">Audio Cover Extractor</string>
<string name="audio_cover_extractor_sub">Extract album cover images from audio files, most common formats are supported</string>
</resources>

View File

@ -199,7 +199,7 @@ object ContextUtils {
onShowToast: (message: String, icon: ImageVector) -> Unit,
onNavigate: (Screen) -> Unit,
onGetUris: (List<Uri>) -> Unit,
onHasExtraImageType: (String) -> Unit,
onHasExtraImageType: (String) -> Unit, //TODO: Add normal sealed class instead of string
isHasUris: Boolean,
onWantGithubReview: () -> Unit,
isOpenEditInsteadOfPreview: Boolean,
@ -294,10 +294,17 @@ object ContextUtils {
onHasExtraImageType(text)
onGetUris(listOf())
} else {
val isAudio = intent.type?.startsWith("audio/") == true
when (intent.action) {
Intent.ACTION_SEND_MULTIPLE -> {
intent.parcelableArrayList<Uri>(Intent.EXTRA_STREAM)?.let {
onNavigate(Screen.Zip(it))
if (isAudio) {
onHasExtraImageType("audio")
onGetUris(it)
} else {
onNavigate(Screen.Zip(it))
}
}
}
@ -307,7 +314,12 @@ object ContextUtils {
onHasExtraImageType("$BackupFileExtension $it")
return
}
onHasExtraImageType("file")
if (isAudio) {
onHasExtraImageType("audio")
} else {
onHasExtraImageType("file")
}
onGetUris(listOf(it))
}
}
@ -326,10 +338,20 @@ object ContextUtils {
return
}
onHasExtraImageType("file")
if (isAudio) {
onHasExtraImageType("audio")
} else {
onHasExtraImageType("file")
}
onGetUris(uris)
} else if (uris.isNotEmpty()) {
onNavigate(Screen.Zip(uris))
if (isAudio) {
onHasExtraImageType("audio")
onGetUris(uris)
} else {
onNavigate(Screen.Zip(uris))
}
} else {
Unit
}

View File

@ -726,6 +726,15 @@ sealed class Screen(
subtitle = R.string.image_cutting_sub
)
@Serializable
data class AudioCoverExtractor(
val uris: List<Uri>? = null
) : Screen(
id = 39,
title = R.string.audio_cover_extractor,
subtitle = R.string.audio_cover_extractor_sub
)
companion object : ScreenConstants by ScreenConstantsImpl
}

View File

@ -34,6 +34,7 @@ import androidx.compose.material.icons.outlined.Grain
import androidx.compose.material.icons.outlined.Photo
import androidx.compose.material.icons.outlined.PictureAsPdf
import androidx.compose.material.icons.outlined.QrCode
import androidx.compose.material.icons.rounded.Album
import androidx.compose.material.icons.rounded.Compare
import androidx.compose.material.icons.rounded.ContentCut
import androidx.compose.material.icons.rounded.Tag
@ -71,7 +72,51 @@ import ru.tech.imageresizershrinker.core.resources.icons.Stack
import ru.tech.imageresizershrinker.core.resources.icons.Toolbox
import ru.tech.imageresizershrinker.core.resources.icons.VectorPolyline
import ru.tech.imageresizershrinker.core.resources.icons.WebpBox
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.*
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.ApngTools
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.AudioCoverExtractor
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.Base64Tools
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.ChecksumTools
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.Cipher
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.CollageMaker
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.ColorTools
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.Compare
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.Crop
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.DeleteExif
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.DocumentScanner
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.Draw
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.EasterEgg
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.EditExif
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.EraseBackground
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.Filter
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.FormatConversion
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.GeneratePalette
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.GifTools
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.GradientMaker
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.ImageCutter
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.ImagePreview
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.ImageSplitting
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.ImageStacking
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.ImageStitching
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.JxlTools
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.LibrariesInfo
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.LimitResize
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.LoadNetImage
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.Main
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.MarkupLayers
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.MeshGradients
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.NoiseGeneration
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.PdfTools
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.PickColorFromImage
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.RecognizeText
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.ResizeAndConvert
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.ScanQrCode
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.Settings
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.SingleEdit
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.SvgMaker
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.Watermarking
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.WebpTools
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.WeightResize
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen.Zip
import android.net.Uri as AndroidUri
internal fun Screen.isBetaFeature(): Boolean = when (this) {
@ -82,7 +127,7 @@ internal fun Screen.isBetaFeature(): Boolean = when (this) {
internal fun Screen.simpleName(): String? = when (this) {
is ApngTools -> "APNG_Tools"
is Cipher -> "Cipher"
is Screen.Compare -> "Compare"
is Compare -> "Compare"
is Crop -> "Crop"
is DeleteExif -> "Delete_Exif"
is Draw -> "Draw"
@ -109,7 +154,7 @@ internal fun Screen.simpleName(): String? = when (this) {
is Zip -> "Zip"
is SvgMaker -> "Svg"
is FormatConversion -> "Convert"
is Screen.DocumentScanner -> "Document_Scanner"
is DocumentScanner -> "Document_Scanner"
is ScanQrCode -> "QR_Code"
is ImageStacking -> "Image_Stacking"
is ImageSplitting -> "Image_Splitting"
@ -124,6 +169,7 @@ internal fun Screen.simpleName(): String? = when (this) {
is MeshGradients -> "Mesh_Gradients"
is EditExif -> "Edit_EXIF"
is ImageCutter -> "Image_Cutting"
is AudioCoverExtractor -> "Audio_Cover_Extractor"
}
internal fun Screen.icon(): ImageVector? = when (this) {
@ -136,7 +182,7 @@ internal fun Screen.icon(): ImageVector? = when (this) {
is SingleEdit -> Icons.Outlined.ImageEdit
is ApngTools -> Icons.Rounded.ApngBox
is Cipher -> Icons.Outlined.Encrypted
is Screen.Compare -> Icons.Rounded.Compare
is Compare -> Icons.Rounded.Compare
is Crop -> Icons.Rounded.CropSmall
is DeleteExif -> Icons.Outlined.Exif
is Draw -> Icons.Outlined.Draw
@ -159,7 +205,7 @@ internal fun Screen.icon(): ImageVector? = when (this) {
is Zip -> Icons.Outlined.FolderZip
is SvgMaker -> Icons.Outlined.VectorPolyline
is FormatConversion -> Icons.Outlined.ImageConvert
is Screen.DocumentScanner -> Icons.Outlined.DocumentScanner
is DocumentScanner -> Icons.Outlined.DocumentScanner
is ScanQrCode -> Icons.Outlined.QrCode
is ImageStacking -> Icons.Outlined.ImageOverlay
is ImageSplitting -> Icons.Outlined.SplitAlt
@ -172,6 +218,7 @@ internal fun Screen.icon(): ImageVector? = when (this) {
is ChecksumTools -> Icons.Rounded.Tag
is EditExif -> Icons.Outlined.ExifEdit
is ImageCutter -> Icons.Rounded.ContentCut
is AudioCoverExtractor -> Icons.Rounded.Album
}
internal object UriSerializer : KSerializer<AndroidUri> {
@ -261,7 +308,8 @@ internal object ScreenConstantsImpl : ScreenConstants {
Zip(),
JxlTools(),
ApngTools(),
WebpTools()
WebpTools(),
AudioCoverExtractor()
),
title = R.string.tools,
selectedIcon = Icons.Rounded.Toolbox,
@ -274,5 +322,5 @@ internal object ScreenConstantsImpl : ScreenConstants {
typedEntries.flatMap { it.entries }.sortedBy { it.id }
}
override val FEATURES_COUNT = 66
override val FEATURES_COUNT = 67
}

View File

@ -35,7 +35,7 @@ import java.util.Locale
@Composable
internal fun List<Uri>.screenList(
extraImageType: String?
extraImageType: String? //TODO: Add normal sealed class instead of string
): State<List<Screen>> {
val uris = this
val context = LocalContext.current
@ -59,6 +59,17 @@ internal fun List<Uri>.screenList(
)
}
}
val audioAvailableScreens by remember(uris) {
derivedStateOf {
listOf(
Screen.AudioCoverExtractor(uris)
) + if (uris.size > 1) {
filesAvailableScreens
} else {
listOf(Screen.Zip(uris))
}
}
}
val gifAvailableScreens by remember(uris) {
derivedStateOf {
listOf(
@ -272,6 +283,7 @@ internal fun List<Uri>.screenList(
) {
derivedStateOf {
when {
extraImageType == "audio" -> audioAvailableScreens
extraImageType == "pdf" -> pdfAvailableScreens
extraImageType == "gif" -> gifAvailableScreens
extraImageType == "file" -> filesAvailableScreens

View File

@ -0,0 +1 @@
/build

View File

@ -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>.
*/
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 = "ru.tech.imageresizershrinker.feature.audio_cover_extractor"
dependencies {
implementation(libs.ffmpeg.metadata.retriever.core)
implementation(libs.ffmpeg.metadata.retriever.native)
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>

View File

@ -0,0 +1,92 @@
/*
* 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 ru.tech.imageresizershrinker.feature.audio_cover_extractor.data
import android.content.Context
import android.graphics.Bitmap
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface
import dagger.hilt.android.qualifiers.ApplicationContext
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.domain.image.ImageCompressor
import ru.tech.imageresizershrinker.core.domain.image.ImageGetter
import ru.tech.imageresizershrinker.core.domain.image.ShareProvider
import ru.tech.imageresizershrinker.core.domain.image.model.ImageFormat
import ru.tech.imageresizershrinker.core.domain.image.model.Quality
import ru.tech.imageresizershrinker.core.domain.resource.ResourceManager
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.feature.audio_cover_extractor.domain.AudioCoverRetriever
import wseemann.media.FFmpegMediaMetadataRetriever
import javax.inject.Inject
internal class AndroidAudioCoverRetriever @Inject constructor(
@ApplicationContext private val context: Context,
private val imageCompressor: ImageCompressor<Bitmap>,
private val shareProvider: ShareProvider<Bitmap>,
private val imageGetter: ImageGetter<Bitmap, ExifInterface>,
dispatchersHolder: DispatchersHolder,
resourceManager: ResourceManager
) : AudioCoverRetriever,
DispatchersHolder by dispatchersHolder,
ResourceManager by resourceManager {
override suspend fun loadCover(
audioUri: String
): Result<String> {
val pictureData = FFmpegMediaMetadataRetriever().apply {
setDataSource(
context,
audioUri.toUri()
)
}.embeddedPicture
return imageGetter.getImage(
data = pictureData,
originalSize = true
)?.let { bitmap ->
shareProvider.cacheData(
writeData = {
it.writeBytes(
imageCompressor.compress(
image = bitmap,
imageFormat = ImageFormat.Png.Lossless,
quality = Quality.Base()
)
)
},
filename = "${audioUri.substringBeforeLast('.')}.png"
)?.let(Result.Companion::success)
} ?: Result.failure(NullPointerException(getString(R.string.no_image)))
}
override suspend fun loadCover(
audioData: ByteArray
): Result<String> {
return loadCover(
shareProvider.cacheData(
writeData = {
it.writeBytes(audioData)
},
filename = "Audio_data_${System.currentTimeMillis()}.mp3"
)
?: return Result.failure(NullPointerException(getString(R.string.filename_is_not_set)))
)
}
}

View File

@ -0,0 +1,38 @@
/*
* 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 ru.tech.imageresizershrinker.feature.audio_cover_extractor.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ru.tech.imageresizershrinker.feature.audio_cover_extractor.data.AndroidAudioCoverRetriever
import ru.tech.imageresizershrinker.feature.audio_cover_extractor.domain.AudioCoverRetriever
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AudioCoverExtractorModule {
@Binds
@Singleton
fun extractor(
impl: AndroidAudioCoverRetriever
): AudioCoverRetriever
}

View File

@ -0,0 +1,30 @@
/*
* 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 ru.tech.imageresizershrinker.feature.audio_cover_extractor.domain
interface AudioCoverRetriever {
suspend fun loadCover(
audioUri: String
): Result<String>
suspend fun loadCover(
audioData: ByteArray
): Result<String>
}

View File

@ -0,0 +1,28 @@
/*
* 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 ru.tech.imageresizershrinker.feature.audio_cover_extractor.ui
import androidx.compose.runtime.Composable
import ru.tech.imageresizershrinker.feature.audio_cover_extractor.ui.screenLogic.AudioCoverExtractorComponent
@Composable
fun AudioCoverExtractorContent(
component: AudioCoverExtractorComponent
) {
}

View File

@ -0,0 +1,26 @@
/*
* 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 ru.tech.imageresizershrinker.feature.audio_cover_extractor.ui.components
import android.net.Uri
data class AudioWithCover(
val audioUri: Uri,
val imageCoverUri: Uri?,
val isLoading: Boolean
)

View File

@ -0,0 +1,109 @@
/*
* 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 ru.tech.imageresizershrinker.feature.audio_cover_extractor.ui.screenLogic
import android.net.Uri
import androidx.core.net.toUri
import com.arkivanov.decompose.ComponentContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.ui.utils.BaseComponent
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
import ru.tech.imageresizershrinker.feature.audio_cover_extractor.domain.AudioCoverRetriever
import ru.tech.imageresizershrinker.feature.audio_cover_extractor.ui.components.AudioWithCover
class AudioCoverExtractorComponent @AssistedInject constructor(
@Assisted componentContext: ComponentContext,
@Assisted val initialUris: List<Uri>?,
@Assisted val onGoBack: () -> Unit,
@Assisted val onNavigate: (Screen) -> Unit,
private val audioCoverRetriever: AudioCoverRetriever,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {
init {
debounce {
initialUris?.let(::updateCovers)
}
}
private val _covers: MutableStateFlow<List<AudioWithCover>> = MutableStateFlow(emptyList())
val covers: StateFlow<List<AudioWithCover>> = _covers.asStateFlow()
fun updateCovers(uris: List<Uri>) {
val audioUris = uris.distinct()
componentScope.launch {
_covers.update {
audioUris.map {
AudioWithCover(
audioUri = it,
imageCoverUri = null,
isLoading = true
)
}
}
val newCovers = audioUris.map { audioUri ->
async {
val coverUri = audioCoverRetriever.loadCover(audioUri.toString()).getOrNull()
val newCover = AudioWithCover(
audioUri = audioUri,
imageCoverUri = coverUri?.toUri(),
isLoading = false
)
_covers.update { covers ->
covers.toMutableList().apply {
val index = indexOfFirst { it.audioUri == audioUri }.takeIf { it >= 0 }
?: return@update covers
set(index, newCover)
}
}
newCover
}
}
_covers.update {
newCovers.awaitAll().filter { it.imageCoverUri != null }
}
}
}
@AssistedFactory
fun interface Factory {
operator fun invoke(
componentContext: ComponentContext,
initialUris: List<Uri>?,
onGoBack: () -> Unit,
onNavigate: (Screen) -> Unit,
): AudioCoverExtractorComponent
}
}

View File

@ -31,9 +31,7 @@ import coil3.request.ImageRequest
import coil3.size.Size
import coil3.toBitmap
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ru.tech.imageresizershrinker.core.data.utils.aspectRatio
import ru.tech.imageresizershrinker.core.data.utils.getSuitableConfig
@ -108,17 +106,16 @@ internal class AndroidPdfManager @Inject constructor(
}
}
override fun convertPdfToImages(
override suspend fun convertPdfToImages(
pdfUri: String,
pages: List<Int>?,
preset: Preset.Percentage,
onGetPagesCount: suspend (Int) -> Unit,
onProgressChange: suspend (Int, Bitmap) -> Unit,
onComplete: suspend () -> Unit
) = CoroutineScope(ioDispatcher).launch {
): Unit = withContext(ioDispatcher) {
context.contentResolver.openFileDescriptor(
pdfUri.toUri(),
"r"
pdfUri.toUri(), "r"
)?.use { fileDescriptor ->
withContext(defaultDispatcher) {
val pdfRenderer = PdfRenderer(fileDescriptor)

View File

@ -17,7 +17,6 @@
package ru.tech.imageresizershrinker.feature.pdf_tools.domain
import kotlinx.coroutines.Job
import ru.tech.imageresizershrinker.core.domain.image.model.Preset
import ru.tech.imageresizershrinker.core.domain.model.IntegerSize
@ -34,13 +33,13 @@ interface PdfManager<I> {
preset: Preset.Percentage
): ByteArray
fun convertPdfToImages(
suspend fun convertPdfToImages(
pdfUri: String,
pages: List<Int>?,
preset: Preset.Percentage,
onGetPagesCount: suspend (Int) -> Unit,
onProgressChange: suspend (Int, I) -> Unit,
onComplete: suspend () -> Unit = {}
): Job
)
}

View File

@ -215,44 +215,46 @@ class PdfToolsComponent @AssistedInject internal constructor(
_done.value = 0
_left.value = 1
val results = mutableListOf<SaveResult>()
savingJob = pdfManager.convertPdfToImages(
pdfUri = _pdfToImageState.value?.uri.toString(),
pages = _pdfToImageState.value?.selectedPages,
preset = presetSelected,
onProgressChange = { _, bitmap ->
val imageInfo = imageTransformer.applyPresetBy(
image = bitmap,
preset = _presetSelected.value,
currentInfo = imageInfo
)
results.add(
fileController.save(
saveTarget = ImageSaveTarget(
imageInfo = imageInfo,
metadata = null,
originalUri = _pdfToImageState.value?.uri.toString(),
sequenceNumber = _done.value + 1,
data = imageCompressor.compressAndTransform(
image = bitmap,
imageInfo = imageInfo
)
),
keepOriginalMetadata = false,
oneTimeSaveLocationUri = oneTimeSaveLocationUri
savingJob = componentScope.launch {
pdfManager.convertPdfToImages(
pdfUri = _pdfToImageState.value?.uri.toString(),
pages = _pdfToImageState.value?.selectedPages,
preset = presetSelected,
onProgressChange = { _, bitmap ->
val imageInfo = imageTransformer.applyPresetBy(
image = bitmap,
preset = _presetSelected.value,
currentInfo = imageInfo
)
)
_done.value += 1
},
onGetPagesCount = { size ->
_left.update { size }
_isSaving.value = true
},
onComplete = {
_isSaving.value = false
onComplete(results.onSuccess(::registerSave))
}
)
results.add(
fileController.save(
saveTarget = ImageSaveTarget(
imageInfo = imageInfo,
metadata = null,
originalUri = _pdfToImageState.value?.uri.toString(),
sequenceNumber = _done.value + 1,
data = imageCompressor.compressAndTransform(
image = bitmap,
imageInfo = imageInfo
)
),
keepOriginalMetadata = false,
oneTimeSaveLocationUri = oneTimeSaveLocationUri
)
)
_done.value += 1
},
onGetPagesCount = { size ->
_left.update { size }
_isSaving.value = true
},
onComplete = {
_isSaving.value = false
onComplete(results.onSuccess(::registerSave))
}
)
}
}
fun convertImagesToPdf(onComplete: () -> Unit) {
@ -315,39 +317,41 @@ class PdfToolsComponent @AssistedInject internal constructor(
_left.value = 1
_isSaving.value = false
val uris: MutableList<String?> = mutableListOf()
savingJob = pdfManager.convertPdfToImages(
pdfUri = _pdfToImageState.value?.uri.toString(),
pages = _pdfToImageState.value?.selectedPages,
onProgressChange = { _, bitmap ->
imageInfo.copy(
originalUri = _pdfToImageState.value?.uri?.toString()
).let {
imageTransformer.applyPresetBy(
image = bitmap,
preset = _presetSelected.value,
currentInfo = it
)
}.apply {
uris.add(
shareProvider.cacheImage(
imageInfo = this,
image = bitmap
savingJob = componentScope.launch {
pdfManager.convertPdfToImages(
pdfUri = _pdfToImageState.value?.uri.toString(),
pages = _pdfToImageState.value?.selectedPages,
onProgressChange = { _, bitmap ->
imageInfo.copy(
originalUri = _pdfToImageState.value?.uri?.toString()
).let {
imageTransformer.applyPresetBy(
image = bitmap,
preset = _presetSelected.value,
currentInfo = it
)
)
}.apply {
uris.add(
shareProvider.cacheImage(
imageInfo = this,
image = bitmap
)
)
}
_done.value += 1
},
preset = presetSelected,
onGetPagesCount = { size ->
_left.update { size }
_isSaving.value = true
},
onComplete = {
_isSaving.value = false
shareProvider.shareUris(uris.filterNotNull())
onComplete()
}
_done.value += 1
},
preset = presetSelected,
onGetPagesCount = { size ->
_left.update { size }
_isSaving.value = true
},
onComplete = {
_isSaving.value = false
shareProvider.shareUris(uris.filterNotNull())
onComplete()
}
)
)
}
}
is Screen.PdfTools.Type.Preview -> {

View File

@ -69,4 +69,5 @@ dependencies {
implementation(projects.feature.meshGradients)
implementation(projects.feature.editExif)
implementation(projects.feature.imageCutting)
implementation(projects.feature.audioCoverExtractor)
}

View File

@ -22,6 +22,7 @@ import ru.tech.imageresizershrinker.collage_maker.presentation.screenLogic.Colla
import ru.tech.imageresizershrinker.color_tools.presentation.screenLogic.ColorToolsComponent
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
import ru.tech.imageresizershrinker.feature.apng_tools.presentation.screenLogic.ApngToolsComponent
import ru.tech.imageresizershrinker.feature.audio_cover_extractor.ui.screenLogic.AudioCoverExtractorComponent
import ru.tech.imageresizershrinker.feature.base64_tools.presentation.screenLogic.Base64ToolsComponent
import ru.tech.imageresizershrinker.feature.checksum_tools.presentation.screenLogic.ChecksumToolsComponent
import ru.tech.imageresizershrinker.feature.cipher.presentation.screenLogic.CipherComponent
@ -53,6 +54,7 @@ import ru.tech.imageresizershrinker.feature.pick_color.presentation.screenLogic.
import ru.tech.imageresizershrinker.feature.recognize.text.presentation.screenLogic.RecognizeTextComponent
import ru.tech.imageresizershrinker.feature.resize_convert.presentation.screenLogic.ResizeAndConvertComponent
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ApngTools
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.AudioCoverExtractor
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Base64Tools
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.ChecksumTools
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.Cipher
@ -154,7 +156,8 @@ internal class ChildProvider @Inject constructor(
private val checksumToolsComponentFactory: ChecksumToolsComponent.Factory,
private val meshGradientsComponentFactory: MeshGradientsComponent.Factory,
private val editExifComponentFactory: EditExifComponent.Factory,
private val imageCutterComponentFactory: ImageCutterComponent.Factory
private val imageCutterComponentFactory: ImageCutterComponent.Factory,
private val audioCoverExtractorComponentFactory: AudioCoverExtractorComponent.Factory
) {
fun RootComponent.createChild(
config: Screen,
@ -541,5 +544,14 @@ internal class ChildProvider @Inject constructor(
onNavigate = ::navigateTo
)
)
is Screen.AudioCoverExtractor -> AudioCoverExtractor(
audioCoverExtractorComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
onGoBack = ::navigateBack,
onNavigate = ::navigateTo
)
)
}
}

View File

@ -24,6 +24,8 @@ import ru.tech.imageresizershrinker.color_tools.presentation.ColorToolsContent
import ru.tech.imageresizershrinker.color_tools.presentation.screenLogic.ColorToolsComponent
import ru.tech.imageresizershrinker.feature.apng_tools.presentation.ApngToolsContent
import ru.tech.imageresizershrinker.feature.apng_tools.presentation.screenLogic.ApngToolsComponent
import ru.tech.imageresizershrinker.feature.audio_cover_extractor.ui.AudioCoverExtractorContent
import ru.tech.imageresizershrinker.feature.audio_cover_extractor.ui.screenLogic.AudioCoverExtractorComponent
import ru.tech.imageresizershrinker.feature.base64_tools.presentation.Base64ToolsContent
import ru.tech.imageresizershrinker.feature.base64_tools.presentation.screenLogic.Base64ToolsComponent
import ru.tech.imageresizershrinker.feature.checksum_tools.presentation.ChecksumToolsContent
@ -108,230 +110,237 @@ import ru.tech.imageresizershrinker.noise_generation.presentation.NoiseGeneratio
import ru.tech.imageresizershrinker.noise_generation.presentation.screenLogic.NoiseGenerationComponent
internal sealed class NavigationChild {
internal sealed interface NavigationChild {
@Composable
abstract fun Content()
fun Content()
class ApngTools(val component: ApngToolsComponent) : NavigationChild() {
class ApngTools(private val component: ApngToolsComponent) : NavigationChild {
@Composable
override fun Content() = ApngToolsContent(component)
}
class Cipher(val component: CipherComponent) : NavigationChild() {
class Cipher(private val component: CipherComponent) : NavigationChild {
@Composable
override fun Content() = CipherContent(component)
}
class CollageMaker(val component: CollageMakerComponent) : NavigationChild() {
class CollageMaker(private val component: CollageMakerComponent) : NavigationChild {
@Composable
override fun Content() = CollageMakerContent(component)
}
class ColorTools(val component: ColorToolsComponent) : NavigationChild() {
class ColorTools(private val component: ColorToolsComponent) : NavigationChild {
@Composable
override fun Content() = ColorToolsContent(component)
}
class Compare(val component: CompareComponent) : NavigationChild() {
class Compare(private val component: CompareComponent) : NavigationChild {
@Composable
override fun Content() = CompareContent(component)
}
class Crop(val component: CropComponent) : NavigationChild() {
class Crop(private val component: CropComponent) : NavigationChild {
@Composable
override fun Content() = CropContent(component)
}
class DeleteExif(val component: DeleteExifComponent) : NavigationChild() {
class DeleteExif(private val component: DeleteExifComponent) : NavigationChild {
@Composable
override fun Content() = DeleteExifContent(component)
}
class DocumentScanner(val component: DocumentScannerComponent) : NavigationChild() {
class DocumentScanner(private val component: DocumentScannerComponent) : NavigationChild {
@Composable
override fun Content() = DocumentScannerContent(component)
}
class Draw(val component: DrawComponent) : NavigationChild() {
class Draw(private val component: DrawComponent) : NavigationChild {
@Composable
override fun Content() = DrawContent(component)
}
class EasterEgg(val component: EasterEggComponent) : NavigationChild() {
class EasterEgg(private val component: EasterEggComponent) : NavigationChild {
@Composable
override fun Content() = EasterEggContent(component)
}
class EraseBackground(val component: EraseBackgroundComponent) : NavigationChild() {
class EraseBackground(private val component: EraseBackgroundComponent) : NavigationChild {
@Composable
override fun Content() = EraseBackgroundContent(component)
}
class Filter(val component: FiltersComponent) : NavigationChild() {
class Filter(private val component: FiltersComponent) : NavigationChild {
@Composable
override fun Content() = FiltersContent(component)
}
class FormatConversion(val component: FormatConversionComponent) : NavigationChild() {
class FormatConversion(private val component: FormatConversionComponent) : NavigationChild {
@Composable
override fun Content() = FormatConversionContent(component)
}
class GeneratePalette(val component: GeneratePaletteComponent) : NavigationChild() {
class GeneratePalette(private val component: GeneratePaletteComponent) : NavigationChild {
@Composable
override fun Content() = GeneratePaletteContent(component)
}
class GifTools(val component: GifToolsComponent) : NavigationChild() {
class GifTools(private val component: GifToolsComponent) : NavigationChild {
@Composable
override fun Content() = GifToolsContent(component)
}
class GradientMaker(val component: GradientMakerComponent) : NavigationChild() {
class GradientMaker(private val component: GradientMakerComponent) : NavigationChild {
@Composable
override fun Content() = GradientMakerContent(component)
}
class ImagePreview(val component: ImagePreviewComponent) : NavigationChild() {
class ImagePreview(private val component: ImagePreviewComponent) : NavigationChild {
@Composable
override fun Content() = ImagePreviewContent(component)
}
class ImageSplitting(val component: ImageSplitterComponent) : NavigationChild() {
class ImageSplitting(private val component: ImageSplitterComponent) : NavigationChild {
@Composable
override fun Content() = ImageSplitterContent(component)
}
class ImageStacking(val component: ImageStackingComponent) : NavigationChild() {
class ImageStacking(private val component: ImageStackingComponent) : NavigationChild {
@Composable
override fun Content() = ImageStackingContent(component)
}
class ImageStitching(val component: ImageStitchingComponent) : NavigationChild() {
class ImageStitching(private val component: ImageStitchingComponent) : NavigationChild {
@Composable
override fun Content() = ImageStitchingContent(component)
}
class JxlTools(val component: JxlToolsComponent) : NavigationChild() {
class JxlTools(private val component: JxlToolsComponent) : NavigationChild {
@Composable
override fun Content() = JxlToolsContent(component)
}
class LimitResize(val component: LimitsResizeComponent) : NavigationChild() {
class LimitResize(private val component: LimitsResizeComponent) : NavigationChild {
@Composable
override fun Content() = LimitsResizeContent(component)
}
class LoadNetImage(val component: LoadNetImageComponent) : NavigationChild() {
class LoadNetImage(private val component: LoadNetImageComponent) : NavigationChild {
@Composable
override fun Content() = LoadNetImageContent(component)
}
class Main(val component: MainComponent) : NavigationChild() {
class Main(private val component: MainComponent) : NavigationChild {
@Composable
override fun Content() = MainContent(component)
}
class NoiseGeneration(val component: NoiseGenerationComponent) : NavigationChild() {
class NoiseGeneration(private val component: NoiseGenerationComponent) : NavigationChild {
@Composable
override fun Content() = NoiseGenerationContent(component)
}
class PdfTools(val component: PdfToolsComponent) : NavigationChild() {
class PdfTools(private val component: PdfToolsComponent) : NavigationChild {
@Composable
override fun Content() = PdfToolsContent(component)
}
class PickColorFromImage(val component: PickColorFromImageComponent) : NavigationChild() {
class PickColorFromImage(private val component: PickColorFromImageComponent) : NavigationChild {
@Composable
override fun Content() = PickColorFromImageContent(component)
}
class RecognizeText(val component: RecognizeTextComponent) : NavigationChild() {
class RecognizeText(private val component: RecognizeTextComponent) : NavigationChild {
@Composable
override fun Content() = RecognizeTextContent(component)
}
class ResizeAndConvert(val component: ResizeAndConvertComponent) : NavigationChild() {
class ResizeAndConvert(private val component: ResizeAndConvertComponent) : NavigationChild {
@Composable
override fun Content() = ResizeAndConvertContent(component)
}
class ScanQrCode(val component: ScanQrCodeComponent) : NavigationChild() {
class ScanQrCode(private val component: ScanQrCodeComponent) : NavigationChild {
@Composable
override fun Content() = ScanQrCodeContent(component)
}
class Settings(val component: SettingsComponent) : NavigationChild() {
class Settings(private val component: SettingsComponent) : NavigationChild {
@Composable
override fun Content() = SettingsContent(component)
}
class SingleEdit(val component: SingleEditComponent) : NavigationChild() {
class SingleEdit(private val component: SingleEditComponent) : NavigationChild {
@Composable
override fun Content() = SingleEditContent(component)
}
class SvgMaker(val component: SvgMakerComponent) : NavigationChild() {
class SvgMaker(private val component: SvgMakerComponent) : NavigationChild {
@Composable
override fun Content() = SvgMakerContent(component)
}
class Watermarking(val component: WatermarkingComponent) : NavigationChild() {
class Watermarking(private val component: WatermarkingComponent) : NavigationChild {
@Composable
override fun Content() = WatermarkingContent(component)
}
class WebpTools(val component: WebpToolsComponent) : NavigationChild() {
class WebpTools(private val component: WebpToolsComponent) : NavigationChild {
@Composable
override fun Content() = WebpToolsContent(component)
}
class WeightResize(val component: WeightResizeComponent) : NavigationChild() {
class WeightResize(private val component: WeightResizeComponent) : NavigationChild {
@Composable
override fun Content() = WeightResizeContent(component)
}
class Zip(val component: ZipComponent) : NavigationChild() {
class Zip(private val component: ZipComponent) : NavigationChild {
@Composable
override fun Content() = ZipContent(component)
}
class LibrariesInfo(val component: LibrariesInfoComponent) : NavigationChild() {
class LibrariesInfo(private val component: LibrariesInfoComponent) : NavigationChild {
@Composable
override fun Content() = LibrariesInfoContent(component)
}
class MarkupLayers(val component: MarkupLayersComponent) : NavigationChild() {
class MarkupLayers(private val component: MarkupLayersComponent) : NavigationChild {
@Composable
override fun Content() = MarkupLayersContent(component)
}
class Base64Tools(val component: Base64ToolsComponent) : NavigationChild() {
class Base64Tools(private val component: Base64ToolsComponent) : NavigationChild {
@Composable
override fun Content() = Base64ToolsContent(component)
}
class ChecksumTools(val component: ChecksumToolsComponent) : NavigationChild() {
class ChecksumTools(private val component: ChecksumToolsComponent) : NavigationChild {
@Composable
override fun Content() = ChecksumToolsContent(component)
}
class MeshGradients(val component: MeshGradientsComponent) : NavigationChild() {
class MeshGradients(private val component: MeshGradientsComponent) : NavigationChild {
@Composable
override fun Content() = MeshGradientsContent(component)
}
class EditExif(val component: EditExifComponent) : NavigationChild() {
class EditExif(private val component: EditExifComponent) : NavigationChild {
@Composable
override fun Content() = EditExifContent(component)
}
class ImageCutter(val component: ImageCutterComponent) : NavigationChild() {
class ImageCutter(private val component: ImageCutterComponent) : NavigationChild {
@Composable
override fun Content() = ImageCutterContent(component)
}
class AudioCoverExtractor(
private val component: AudioCoverExtractorComponent
) : NavigationChild {
@Composable
override fun Content() = AudioCoverExtractorContent(component)
}
}

View File

@ -32,7 +32,6 @@ konfettiCompose = "2.0.5"
shadowsPlus = "1.0.4"
exifinterface = "1.4.0"
firebaseAnalyticsKtx = "22.3.0"
firebaseCrashlyticsGradle = "3.0.3"
google-segmentationSelfie = "16.0.0-beta6"
google-subjectSegmentation = "16.0.0-beta1"
detekt = "1.23.8"
@ -75,13 +74,18 @@ zxingAndroidEmbedded = "4.3.0"
capturable = "3.0.1"
moshi = "1.15.2"
aboutlibraries = "12.0.0-a02+compose_1_8"
aboutlibrariesGradle = "12.0.0-a04"
junit = "4.13.2"
bouncycastle = "1.80"
evaluator = "1.0.0"
ffmpeg-metadata-retriever = "1.0.19"
firebaseCrashlyticsGradle = "3.0.3"
aboutlibrariesGradle = "12.0.0-a04"
moduleGraphGradle = "0.12.0"
[libraries]
ffmpeg-metadata-retriever-core = { module = "com.github.wseemann:FFmpegMediaMetadataRetriever-core", version.ref = "ffmpeg-metadata-retriever" }
ffmpeg-metadata-retriever-native = { module = "com.github.wseemann:FFmpegMediaMetadataRetriever-native", version.ref = "ffmpeg-metadata-retriever" }
evaluator = { module = "com.github.T8RIN:KotlinEvaluator", version.ref = "evaluator" }
aboutlibraries-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" }
moshi = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }

View File

@ -109,6 +109,7 @@ include(":feature:checksum-tools")
include(":feature:mesh-gradients")
include(":feature:edit-exif")
include(":feature:image-cutting")
include(":feature:audio-cover-extractor")
include(":feature:root")