diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eb8c050..df66e6c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,7 +14,7 @@ android { applicationId = "com.d4rk.cleaner" minSdk = 26 targetSdk = 34 - versionCode = 92 + versionCode = 93 versionName = "2.0.0" archivesName = "${applicationId}-v${versionName}" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/kotlin/com/d4rk/cleaner/adapters/ImageOptimizationPagerAdapter.kt b/app/src/main/kotlin/com/d4rk/cleaner/adapters/ImageOptimizationPagerAdapter.kt deleted file mode 100644 index 7878137..0000000 --- a/app/src/main/kotlin/com/d4rk/cleaner/adapters/ImageOptimizationPagerAdapter.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.d4rk.cleaner.adapters - -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.viewpager2.adapter.FragmentStateAdapter -import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs.FileSizeFragment -import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs.ManualModeFragment -import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs.QuickCompressFragment - -class ImageOptimizationPagerAdapter(fragmentActivity: FragmentActivity) : - FragmentStateAdapter(fragmentActivity) { - override fun getItemCount(): Int { - return 3 - } - - override fun createFragment(position: Int): Fragment { - return when (position) { - 0 -> QuickCompressFragment() - 1 -> FileSizeFragment() - 2 -> ManualModeFragment() - else -> throw IllegalArgumentException("Invalid position") - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/imageoptimizer/CompressionLevel.kt b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/imageoptimizer/CompressionLevel.kt new file mode 100644 index 0000000..7dc973d --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/imageoptimizer/CompressionLevel.kt @@ -0,0 +1,9 @@ +package com.d4rk.cleaner.data.model.ui.imageoptimizer + +import com.d4rk.cleaner.R + +enum class CompressionLevel(val stringRes: Int, val defaultPercentage: Int) { + LOW(R.string.low, 25), + MEDIUM(R.string.medium, 50), + HIGH(R.string.high, 75) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/imageoptimizer/ImageOptimizerState.kt b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/imageoptimizer/ImageOptimizerState.kt new file mode 100644 index 0000000..853b12b --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/model/ui/imageoptimizer/ImageOptimizerState.kt @@ -0,0 +1,15 @@ +package com.d4rk.cleaner.data.model.ui.imageoptimizer + +import android.net.Uri + +data class ImageOptimizerState( + val selectedImageUri: Uri? = null, + val compressedImageUri: Uri? = null, + val isLoading: Boolean = false, + val quickCompressValue: Int = 50, + val fileSizeKB: Int = 0, + val manualWidth: Int = 0, + val manualHeight: Int = 0, + val manualQuality: Int = 50, + val currentTab: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/ImageOptimizerActivity.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/ImageOptimizerActivity.kt index 80e565b..9c345ba 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/ImageOptimizerActivity.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/ImageOptimizerActivity.kt @@ -1,247 +1,40 @@ package com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer -import android.content.Context -import android.graphics.Bitmap -import android.media.MediaScannerConnection import android.net.Uri import android.os.Bundle -import android.os.Environment -import android.provider.DocumentsContract -import android.provider.MediaStore -import android.view.View +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModelProvider +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope -import com.bumptech.glide.Glide -import com.d4rk.cleaner.R -import com.d4rk.cleaner.adapters.ImageOptimizationPagerAdapter -import com.d4rk.cleaner.databinding.ActivityImageOptimizerBinding -import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs.FileSizeFragment -import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs.ManualModeFragment -import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs.QuickCompressFragment -import com.google.android.gms.ads.AdRequest -import com.google.android.gms.ads.MobileAds -import com.google.android.material.snackbar.Snackbar -import com.google.android.material.tabs.TabLayoutMediator -import id.zelory.compressor.Compressor -import id.zelory.compressor.constraint.format -import id.zelory.compressor.constraint.quality -import id.zelory.compressor.constraint.resolution -import id.zelory.compressor.constraint.size -import kotlinx.coroutines.Dispatchers +import com.d4rk.cleaner.ui.settings.display.theme.style.AppTheme import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File class ImageOptimizerActivity : AppCompatActivity() { - private lateinit var binding: ActivityImageOptimizerBinding - private var actualImageFile: File? = null - private lateinit var viewModel: ImageOptimizerViewModel - private var compressedImageFile: File? = null - private var isCompressing = false - private val optimizedPicturesDirectory = - File(Environment.getExternalStorageDirectory(), "Pictures/Optimized Pictures").apply { - if (!exists()) { - mkdirs() - } - } + private val viewModel: ImageOptimizerViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityImageOptimizerBinding.inflate(layoutInflater) - setContentView(binding.root) - val adapter = ImageOptimizationPagerAdapter(this) - binding.viewPager.adapter = adapter - binding.progressBar.alpha = 0f - MobileAds.initialize(this) - binding.adView.loadAd(AdRequest.Builder().build()) - if (!intent.hasExtra("imageUri")) { - Snackbar.make(binding.root, getString(R.string.snack_no_image), Snackbar.LENGTH_SHORT) - .show() - finish() - return - } - val imageUri = Uri.parse(intent.getStringExtra("imageUri")) - Glide.with(this).load(imageUri).into(binding.imageView) - actualImageFile = getPath(this@ImageOptimizerActivity, imageUri)?.let { File(it) } - viewModel = ViewModelProvider(this)[ImageOptimizerViewModel::class.java] - viewModel.compressionLevelLiveData.observe(this) { compressionLevel -> - compressImageQuickCompress(actualImageFile, compressionLevel) - } - binding.buttonCompressImage.setOnClickListener { - when (binding.viewPager.currentItem) { - 0 -> { - val quickCompressFragment = - supportFragmentManager.findFragmentByTag("f0") as? QuickCompressFragment - val compressionLevel = quickCompressFragment?.getCurrentCompressionLevel() ?: 50 - compressImageQuickCompress(actualImageFile, compressionLevel) - } - - 1 -> { - val fileSizeFragment = - supportFragmentManager.findFragmentByTag("f1") as? FileSizeFragment - val targetSizeKB = fileSizeFragment?.getCurrentFileSizeKB() ?: -1 - if (targetSizeKB > 0) { - compressImageByFileSize(actualImageFile, targetSizeKB) - } else { - val snackbar = Snackbar.make( - binding.root, - getString(R.string.snack_validate_file), - Snackbar.LENGTH_LONG - ) - snackbar.setAction(android.R.string.ok) { - snackbar.dismiss() - } - snackbar.show() - } - } - - 2 -> { - val manualModeFragment = - supportFragmentManager.findFragmentByTag("f2") as? ManualModeFragment - val (width, height, quality) = manualModeFragment?.getCurrentCompressionSettings() - ?: Triple(0, 0, 0) - compressImageManualMode(width, height, quality) - } - } - } - binding.viewPager.adapter = adapter - TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position -> - when (position) { - 0 -> tab.text = getString(R.string.quick_compress) - 1 -> tab.text = getString(R.string.file_size) - 2 -> tab.text = getString(R.string.manual) - } - }.attach() - } - - private fun compressImageManualMode(width: Int, height: Int, quality: Int) { - if (compressedImageFile != null || isCompressing) { - return - } - binding.progressBar.visibility = View.VISIBLE - lifecycleScope.launch(Dispatchers.IO) { - val compressedImageFile = withContext(Dispatchers.IO) { - if (actualImageFile != null) { - Compressor.compress(this@ImageOptimizerActivity, actualImageFile!!) { - resolution(width, height) - quality(quality) - } - } else { - null - } - } - withContext(Dispatchers.Main) { - updateImageView(compressedImageFile) - binding.progressBar.visibility = View.GONE - compressedImageFile?.let { saveImage(it) } - isCompressing = false - } - } - } - - private fun compressImageByFileSize(imageFile: File?, targetSizeKB: Int) { - if (compressedImageFile != null || isCompressing) { - return - } - binding.progressBar.visibility = View.VISIBLE - lifecycleScope.launch(Dispatchers.IO) { - val compressedImageFile = withContext(Dispatchers.IO) { - if (imageFile != null) { - Compressor.compress(this@ImageOptimizerActivity, imageFile) { - format(Bitmap.CompressFormat.JPEG) - size((targetSizeKB * 1024).toLong()) - } - } else { - null - } - } - withContext(Dispatchers.Main) { - updateImageView(compressedImageFile) - binding.progressBar.visibility = View.GONE - compressedImageFile?.let { saveImage(it) } - isCompressing = false + enableEdgeToEdge() + val selectedImageUriString = intent.getStringExtra("selectedImageUri") + if (!selectedImageUriString.isNullOrEmpty()) { + lifecycleScope.launch { + viewModel.onImageSelected(Uri.parse(selectedImageUriString)) } } - } - private fun compressImageQuickCompress(actualImageFile: File?, compressionLevel: Int) { - if (compressedImageFile != null || isCompressing) { - return - } - binding.progressBar.visibility = View.VISIBLE - lifecycleScope.launch(Dispatchers.IO) { - val compressedImageFile = withContext(Dispatchers.IO) { - if (actualImageFile != null) { - Compressor.compress(this@ImageOptimizerActivity, actualImageFile) { - format(Bitmap.CompressFormat.JPEG) - quality(compressionLevel) - } - } else { - null + setContent { + AppTheme { + Surface( + modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background + ) { + ImageOptimizerComposable(this@ImageOptimizerActivity, viewModel) } } - withContext(Dispatchers.Main) { - updateImageView(compressedImageFile) - binding.progressBar.visibility = View.GONE - compressedImageFile?.let { saveImage(it) } - isCompressing = false - } - } - } - - private fun updateImageView(compressedImageFile: File?) { - compressedImageFile?.let { - val uri = Uri.fromFile(it) - Glide.with(this).load(uri).into(binding.imageView) - } - } - - private fun saveImage(file: File) { - lifecycleScope.launch(Dispatchers.Main) { - optimizedPicturesDirectory.mkdirs() - val savedFile = withContext(Dispatchers.IO) { - val newFile = File(optimizedPicturesDirectory, "${System.currentTimeMillis()}.jpg") - file.copyTo(newFile, overwrite = true) - newFile - } - MediaScannerConnection.scanFile( - applicationContext, - arrayOf(savedFile.path), - arrayOf("image/jpeg"), - null - ) - val snackbar = Snackbar.make( - binding.root, - getString(R.string.image_saved) + savedFile.path, - Snackbar.LENGTH_LONG - ) - snackbar.setAction(android.R.string.ok) { - snackbar.dismiss() - } - snackbar.show() - } - } - - private fun getPath(context: Context, uri: Uri): String? { - if (DocumentsContract.isDocumentUri(context, uri)) { - val docId = DocumentsContract.getDocumentId(uri) - val split = docId.split(":") - val type = split[0] - if ("primary".equals(type, ignoreCase = true)) { - return Environment.getExternalStorageDirectory().toString() + "/" + split[1] - } - } else { - val projection = arrayOf(MediaStore.Images.Media.DATA) - val cursor = context.contentResolver.query(uri, projection, null, null, null) - cursor?.let { - val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) - it.moveToFirst() - val path = it.getString(columnIndex) - it.close() - return path - } } - return null } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/ImageOptimizerComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/ImageOptimizerComposable.kt new file mode 100644 index 0000000..30ebae8 --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/ImageOptimizerComposable.kt @@ -0,0 +1,198 @@ +package com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import coil.compose.AsyncImage +import com.d4rk.cleaner.R +import com.d4rk.cleaner.ads.BannerAdsComposable +import com.d4rk.cleaner.data.datastore.DataStore +import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs.FileSizeScreen +import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs.ManualModeScreen +import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs.QuickCompressScreen +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +fun ImageOptimizerComposable(activity: ImageOptimizerActivity, viewModel: ImageOptimizerViewModel) { + val context = LocalContext.current + val dataStore = DataStore.getInstance(context) + val coroutineScope = rememberCoroutineScope() + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) + val adsState = dataStore.ads.collectAsState(initial = true) + val tabs = listOf( + stringResource(R.string.quick_compress), + stringResource(R.string.file_size), + stringResource(R.string.manual), + ) + val pagerState = rememberPagerState(pageCount = { tabs.size }) + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { Text(stringResource(R.string.image_optimizer)) }, + navigationIcon = { + IconButton(onClick = { + activity.finish() + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + scrollBehavior = scrollBehavior + ) + } + ) { paddingValues -> + ConstraintLayout( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + val ( + imageCardView, tabLayout, viewPager, + compressButton, adView + ) = createRefs() + + Card( + modifier = Modifier + .fillMaxWidth() + .constrainAs(imageCardView) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + bottom.linkTo(tabLayout.top) + } + .padding(24.dp), + ) { + ImageDisplay(viewModel) + } + + TabRow( + selectedTabIndex = pagerState.currentPage, + modifier = Modifier + .constrainAs(tabLayout) { + top.linkTo(imageCardView.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { + tabs.forEachIndexed { index, title -> + Tab( + text = { Text(title) }, + selected = pagerState.currentPage == index, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + } + ) + } + } + + HorizontalPager( + state = pagerState, + modifier = Modifier + .constrainAs(viewPager) { + top.linkTo(tabLayout.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + bottom.linkTo(compressButton.top) + height = Dimension.fillToConstraints + } + ) { page -> + when (page) { + 0 -> QuickCompressScreen(viewModel) + 1 -> FileSizeScreen(viewModel) + 2 -> ManualModeScreen(viewModel) + } + } + + Button( + onClick = { /* Handle Compress button click */ }, + modifier = Modifier + .constrainAs(compressButton) { + start.linkTo(parent.start) + end.linkTo(parent.end) + if (adsState.value) { + bottom.linkTo(adView.top) + } else { + bottom.linkTo(parent.bottom) + } + } + .padding(12.dp) + ) { + Text(stringResource(R.string.optimize_image)) + } + + BannerAdsComposable( + modifier = Modifier + .constrainAs(adView) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + dataStore = dataStore + ) + } + } +} + + +@Composable +fun ImageDisplay(viewModel: ImageOptimizerViewModel) { + val state by viewModel.uiState.collectAsState() + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f), + contentAlignment = Alignment.Center + ) { + if (state.isLoading) { + CircularProgressIndicator() + } else { + state.compressedImageUri?.let { imageUri -> + AsyncImage( + model = imageUri, + contentDescription = "Selected Image", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + placeholder = painterResource(id = R.drawable.ic_image), + error = painterResource(id = R.drawable.ic_image) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/ImageOptimizerViewModel.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/ImageOptimizerViewModel.kt index f7dd441..a7a38d2 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/ImageOptimizerViewModel.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/ImageOptimizerViewModel.kt @@ -1,11 +1,136 @@ package com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer + +import android.app.Application +import android.content.Context +import android.database.Cursor +import android.graphics.Bitmap +import android.net.Uri +import android.provider.OpenableColumns +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.d4rk.cleaner.data.model.ui.imageoptimizer.ImageOptimizerState +import id.zelory.compressor.Compressor +import id.zelory.compressor.constraint.format +import id.zelory.compressor.constraint.quality +import id.zelory.compressor.constraint.resolution +import id.zelory.compressor.constraint.size +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream + +class ImageOptimizerViewModel(application: Application) : AndroidViewModel(application) { + + val _uiState = MutableStateFlow(ImageOptimizerState()) + val uiState = _uiState.asStateFlow() + private val compressionLevelLiveData = MutableLiveData() + + private fun getAppContext(): Context = getApplication().applicationContext + -class ImageOptimizerViewModel : ViewModel() { - val compressionLevelLiveData = MutableLiveData() fun setCompressionLevel(compressionLevel: Int) { compressionLevelLiveData.value = compressionLevel } -} + + suspend fun setQuickCompressValue(value: Int) { + _uiState.emit(_uiState.value.copy(quickCompressValue = value)) + compressImage() + } + + suspend fun setFileSize(size: Int) { + _uiState.emit(_uiState.value.copy(fileSizeKB = size)) + compressImage() + } + + suspend fun setManualCompressSettings(width: Int, height: Int, quality: Int) { + _uiState.emit( + _uiState.value.copy(manualWidth = width, manualHeight = height, manualQuality = quality) + ) + compressImage() + } + + suspend fun onImageSelected(uri: Uri) { + _uiState.emit( + _uiState.value.copy( + selectedImageUri = uri, + compressedImageUri = uri, // Initially show original image + ) + ) + + } + + + private fun compressImage() = viewModelScope.launch { + _uiState.emit(_uiState.value.copy(isLoading = true)) + val context = getAppContext() + val originalFile = _uiState.value.selectedImageUri?.let { getRealFileFromUri(context, it) } + + // Fetch the current tab index BEFORE the suspension point + val currentTab = _uiState.value.currentTab // Access _uiState.value here + val compressedFile = originalFile?.let { file -> + withContext(Dispatchers.IO) { + try { + Compressor.compress(context, file) { + when (currentTab) { // Use the fetched currentTab value + 0 -> { + quality(_uiState.value.quickCompressValue) + format(Bitmap.CompressFormat.JPEG) + } + + 1 -> { + size(_uiState.value.fileSizeKB * 1024L) + format(Bitmap.CompressFormat.JPEG) + } + + 2 -> { + resolution( + _uiState.value.manualWidth, + _uiState.value.manualHeight + ) + quality(_uiState.value.manualQuality) + } + } + } + } catch (e: Exception) { + null + } + } + } + + _uiState.emit( + _uiState.value.copy( + isLoading = false, + compressedImageUri = compressedFile?.let { Uri.fromFile(it) } + ?: _uiState.value.selectedImageUri + ) + ) + } + + // Now a member function of the ViewModel + private fun getRealFileFromUri(context: Context, uri: Uri): File? { + if (uri.scheme == "content") { + val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val nameIndex: Int = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + val fileName: String = it.getString(nameIndex) + val file = File(context.cacheDir, fileName) + val inputStream = context.contentResolver.openInputStream(uri) + inputStream?.use { stream -> + val outputStream = FileOutputStream(file) + stream.copyTo(outputStream) + } + return file + } + } + } else if (uri.scheme == "file") { + return File(uri.path!!) + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/FileSizeComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/FileSizeComposable.kt new file mode 100644 index 0000000..1f0cbab --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/FileSizeComposable.kt @@ -0,0 +1,85 @@ +package com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.d4rk.cleaner.R +import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.ImageOptimizerViewModel +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FileSizeScreen(viewModel: ImageOptimizerViewModel) { + val state = viewModel.uiState.collectAsState() + var fileSizeText by remember { mutableStateOf(state.value.fileSizeKB.toString()) } + var expanded by remember { mutableStateOf(false) } + val presetSizes = stringArrayResource(R.array.file_sizes).toList() + var selectedPresetSize by remember { mutableStateOf("") } + val coroutineScope = rememberCoroutineScope() + Column(modifier = Modifier.padding(16.dp)) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded } + ) { + OutlinedTextField( + value = fileSizeText, + onValueChange = { newValue -> + fileSizeText = newValue + coroutineScope.launch { + viewModel.setFileSize(newValue.toIntOrNull() ?: 0) + } + }, + label = { Text(stringResource(R.string.file_size)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + supportingText = { + Text(text = stringResource(R.string.enter_a_value)) + }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + isError = fileSizeText.isNotEmpty() && fileSizeText.toFloatOrNull() == null, + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + .padding(top = 12.dp) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + presetSizes.forEach { size -> + DropdownMenuItem( + text = { Text("$size KB") }, + onClick = { + selectedPresetSize = size + fileSizeText = size + coroutineScope.launch { + viewModel.setFileSize(size.toIntOrNull() ?: 0) + } + expanded = false + } + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/FileSizeFragment.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/FileSizeFragment.kt deleted file mode 100644 index da65a96..0000000 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/FileSizeFragment.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import androidx.fragment.app.Fragment -import com.d4rk.cleaner.R -import com.d4rk.cleaner.databinding.FragmentFileSizeBinding - -class FileSizeFragment : Fragment() { - private lateinit var binding: FragmentFileSizeBinding - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentFileSizeBinding.inflate(inflater, container, false) - val fileSizeArray = resources.getStringArray(R.array.file_sizes) - val adapter = ArrayAdapter( - requireContext(), - android.R.layout.simple_dropdown_item_1line, - fileSizeArray - ) - binding.autoCompleteTextViewFileSize.setAdapter(adapter) - return binding.root - } - - fun getCurrentFileSizeKB(): Int { - val selectedValue = binding.autoCompleteTextViewFileSize.text.toString() - return try { - selectedValue.toInt() - } catch (e: NumberFormatException) { - -1 - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/ManualModeComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/ManualModeComposable.kt new file mode 100644 index 0000000..8592163 --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/ManualModeComposable.kt @@ -0,0 +1,108 @@ +package com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs + +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.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.d4rk.cleaner.R +import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.ImageOptimizerViewModel +import kotlinx.coroutines.launch + +@Composable +fun ManualModeScreen(viewModel: ImageOptimizerViewModel) { + val state = viewModel.uiState.collectAsState() + var widthText by remember { mutableStateOf(state.value.manualWidth.toString()) } + var heightText by remember { mutableStateOf(state.value.manualHeight.toString()) } + var qualityValue by remember { mutableFloatStateOf(state.value.manualQuality.toFloat()) } + val coroutineScope = rememberCoroutineScope() + Column(modifier = Modifier.padding(16.dp)) { + Row(modifier = Modifier.fillMaxWidth()) { + OutlinedTextField( + value = widthText, + onValueChange = { newValue -> + widthText = newValue + coroutineScope.launch { + viewModel.setManualCompressSettings( + newValue.toIntOrNull() ?: 0, + heightText.toIntOrNull() ?: 0, + qualityValue.toInt() + ) + } + }, + label = { Text(stringResource(R.string.width)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier + .weight(1f) + .padding(end = 8.dp) + ) + + OutlinedTextField( + value = heightText, + onValueChange = { newValue -> + heightText = newValue + coroutineScope.launch { + viewModel.setManualCompressSettings( + widthText.toIntOrNull() ?: 0, + newValue.toIntOrNull() ?: 0, + qualityValue.toInt() + ) + } + }, + label = { Text(stringResource(R.string.height)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.weight(1f) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.quality), + style = MaterialTheme.typography.bodyLarge + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Row(modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = stringResource(R.string.image_compressor_percentage_format, qualityValue.toInt())) + Slider( + value = qualityValue, + onValueChange = { newValue -> + coroutineScope.launch { + qualityValue = newValue + viewModel.setManualCompressSettings( + widthText.toIntOrNull() ?: 0, + heightText.toIntOrNull() ?: 0, + newValue.toInt() + ) + } + }, + // ... rest of the Slider parameters ... + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/ManualModeFragment.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/ManualModeFragment.kt deleted file mode 100644 index af211e9..0000000 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/ManualModeFragment.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.d4rk.cleaner.R -import com.d4rk.cleaner.databinding.FragmentManualModeBinding - -class ManualModeFragment : Fragment() { - private lateinit var binding: FragmentManualModeBinding - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentManualModeBinding.inflate(inflater, container, false) - binding.sliderManualMode.value = 50f - binding.textViewManualModePercentage.text = - getString(R.string.image_compressor_percentage_format, 50) - binding.sliderManualMode.addOnChangeListener { slider, _, _ -> - val compressionLevel = slider.value.toInt() - updateUI(compressionLevel) - } - return binding.root - } - - private fun updateUI(percentage: Int) { - updatePercentageText(percentage) - } - - private fun updatePercentageText(percentage: Int) { - binding.textViewManualModePercentage.text = getString( - R.string.image_compressor_percentage_format, percentage - ) - } - - fun getCurrentCompressionSettings(): Triple { - val width = binding.editTextWidth.text.toString().toIntOrNull() ?: 0 - val height = binding.editTextHeight.text.toString().toIntOrNull() ?: 0 - val quality = binding.sliderManualMode.value.toInt() - return Triple(width, height, quality) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/QuickCompressComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/QuickCompressComposable.kt new file mode 100644 index 0000000..ccafda1 --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/QuickCompressComposable.kt @@ -0,0 +1,76 @@ +package com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs + +import androidx.compose.foundation.BorderStroke +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.width +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.d4rk.cleaner.data.model.ui.imageoptimizer.CompressionLevel +import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.ImageOptimizerViewModel +import com.d4rk.cleaner.utils.imageoptimizer.getCompressionLevelFromSliderValue +import kotlinx.coroutines.launch + +@Composable +fun QuickCompressScreen(viewModel: ImageOptimizerViewModel) { + var sliderValue by remember { mutableFloatStateOf(50f) } + val selectedCompression = getCompressionLevelFromSliderValue(sliderValue) + val coroutineScope = rememberCoroutineScope() + Column(modifier = Modifier.padding(16.dp)) { + Row(modifier = Modifier.fillMaxWidth()) { + for (compressionLevel in CompressionLevel.entries) { + OutlinedButton( + onClick = { + coroutineScope.launch { + sliderValue = compressionLevel.defaultPercentage.toFloat() + viewModel.setQuickCompressValue(sliderValue.toInt()) + } + }, + modifier = Modifier.weight(1f), + border = BorderStroke( + width = 1.dp, + color = if (selectedCompression == compressionLevel) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.outline + ), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = if (selectedCompression == compressionLevel) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurface + ) + ) { + Text(text = stringResource(compressionLevel.stringRes)) + } + Spacer(modifier = Modifier.width(8.dp)) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Slider( + value = sliderValue, + onValueChange = { newValue -> + coroutineScope.launch { + sliderValue = newValue + viewModel.setQuickCompressValue(newValue.toInt()) + } + }, + valueRange = 0f..100f, + steps = 99 + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/QuickCompressFragment.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/QuickCompressFragment.kt deleted file mode 100644 index 2f33e57..0000000 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imageoptimizer/tabs/QuickCompressFragment.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.tabs - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import com.d4rk.cleaner.R -import com.d4rk.cleaner.databinding.FragmentQuickCompressBinding -import com.d4rk.cleaner.ui.imageoptimizer.imageoptimizer.ImageOptimizerViewModel - -class QuickCompressFragment : Fragment() { - private lateinit var binding: FragmentQuickCompressBinding - private lateinit var viewModel: ImageOptimizerViewModel - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentQuickCompressBinding.inflate(inflater, container, false) - viewModel = ViewModelProvider(this)[ImageOptimizerViewModel::class.java] - setCompressionLevel(50) - binding.toggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> - if (isChecked) { - val compressionLevel = when (checkedId) { - R.id.button_low -> 30 - R.id.button_medium -> 50 - R.id.button_high -> 70 - else -> { - binding.toggleGroup.clearChecked() - return@addOnButtonCheckedListener - } - } - setCompressionLevel(compressionLevel) - } - } - binding.sliderQuickCompress.addOnChangeListener { slider, _, _ -> - val percentage = slider.value.toInt() - updateUI(percentage) - viewModel.setCompressionLevel(percentage) - } - - return binding.root - } - - private fun setCompressionLevel(level: Int) { - binding.sliderQuickCompress.value = level.toFloat() - updateUI(level) - } - - private fun updateUI(percentage: Int) { - updatePercentageText(percentage) - setButtonFromSliderValue(percentage) - } - - private fun updatePercentageText(percentage: Int) { - binding.textViewQuickCompressPercentage.text = - getString(R.string.image_compressor_percentage_format, percentage) - } - - private fun setButtonFromSliderValue(percentage: Int) { - val buttonId = when (percentage) { - 30 -> R.id.button_low - 50 -> R.id.button_medium - 70 -> R.id.button_high - else -> { - binding.toggleGroup.clearChecked() - return - } - } - binding.toggleGroup.check(buttonId) - } - - fun getCurrentCompressionLevel(): Int { - return binding.sliderQuickCompress.value.toInt() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imagepicker/ImagePickerActivity.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imagepicker/ImagePickerActivity.kt index 40d664c..078e56d 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imagepicker/ImagePickerActivity.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imagepicker/ImagePickerActivity.kt @@ -6,22 +6,19 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier -import androidx.lifecycle.ViewModelProvider import com.d4rk.cleaner.R import com.d4rk.cleaner.ui.settings.display.theme.style.AppTheme class ImagePickerActivity : AppCompatActivity() { - private lateinit var viewModel: ImagePickerViewModel + private val viewModel: ImagePickerViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - viewModel = ViewModelProvider(this)[ImagePickerViewModel::class.java] - enableEdgeToEdge() setContent { AppTheme { diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imagepicker/ImagePickerComposable.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imagepicker/ImagePickerComposable.kt index 677ac3b..5c13f91 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imagepicker/ImagePickerComposable.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/imageoptimizer/imagepicker/ImagePickerComposable.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -39,6 +40,16 @@ fun ImagePickerComposable( val dataStore = DataStore.getInstance(context) val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) val adsState = dataStore.ads.collectAsState(initial = true) + + LaunchedEffect(key1 = viewModel.selectedImageUri) { + viewModel.selectedImageUri?.let { uri -> + val intent = Intent(context, ImageOptimizerActivity::class.java) + intent.putExtra("selectedImageUri", uri.toString()) + activity.startActivity(intent) + viewModel.setSelectedImageUri(null) + } + } + Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { LargeTopAppBar( title = { Text(stringResource(R.string.image_optimizer)) }, @@ -93,11 +104,4 @@ fun ImagePickerComposable( ) } } - - viewModel.selectedImageUri?.let { uri -> - val intent = Intent(context, ImageOptimizerActivity::class.java) - intent.putExtra("imageUri", uri.toString()) - activity.startActivity(intent) - viewModel.setSelectedImageUri(null) - } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/utils/imageoptimizer/ImageOptimizerUtils.kt b/app/src/main/kotlin/com/d4rk/cleaner/utils/imageoptimizer/ImageOptimizerUtils.kt new file mode 100644 index 0000000..7137fdb --- /dev/null +++ b/app/src/main/kotlin/com/d4rk/cleaner/utils/imageoptimizer/ImageOptimizerUtils.kt @@ -0,0 +1,11 @@ +package com.d4rk.cleaner.utils.imageoptimizer + +import com.d4rk.cleaner.data.model.ui.imageoptimizer.CompressionLevel + +fun getCompressionLevelFromSliderValue(sliderValue: Float): CompressionLevel { + return when { + sliderValue < 33f -> CompressionLevel.LOW + sliderValue < 66f -> CompressionLevel.MEDIUM + else -> CompressionLevel.HIGH + } +} \ No newline at end of file