Skip to content

Commit

Permalink
Merge pull request #2 from dalafiarisamuel/feature/image-download
Browse files Browse the repository at this point in the history
added platform specific download implementations
  • Loading branch information
dalafiarisamuel committed Sep 5, 2024
2 parents 9ce4b52 + 1208d87 commit f51a1c1
Show file tree
Hide file tree
Showing 25 changed files with 498 additions and 127 deletions.
9 changes: 9 additions & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ kotlin {
}
}

targets.configureEach {
compilations.configureEach {
compileTaskProvider.get().compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
}

jvm("desktop")

listOf(
Expand Down Expand Up @@ -77,6 +85,7 @@ kotlin {
implementation(libs.material3.window.size.multiplatform)
api(libs.datastore.preferences)
api(libs.datastore)
implementation(libs.calf.permissions)
}
commonTest.dependencies {
implementation(libs.koin.test)
Expand Down
3 changes: 2 additions & 1 deletion composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>

<application
android:name=".MyApplication"
Expand Down
3 changes: 3 additions & 0 deletions composeApp/src/androidMain/kotlin/Platform.android.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
actual fun getPlatform(): Platform {
return Platform.Android
}
14 changes: 14 additions & 0 deletions composeApp/src/androidMain/kotlin/di/PlatformModule.android.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package di

import org.koin.android.ext.koin.androidApplication
import org.koin.core.module.Module
import org.koin.dsl.module
import ui.download.PlatformDownloadImage

actual fun platformModule(): Module {
return module {
single {
PlatformDownloadImage(androidApplication())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ui.download

import android.app.DownloadManager
import android.content.Context
import android.net.Uri
import android.os.Environment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import ui.state.ImageDownloadState
import kotlin.time.Duration.Companion.seconds

actual class PlatformDownloadImage(private val context: Context) {
actual suspend fun downloadImage(imageLink: String): ImageDownloadState {
withContext(Dispatchers.Default) {
delay(3.seconds)
}
return withContext(Dispatchers.Main) {
try {
val uri = Uri.parse(imageLink)
val fileName = "${uri.pathSegments[0]}.jpeg"

val request = DownloadManager.Request(uri)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)

(context.getSystemService(Context.DOWNLOAD_SERVICE)
as DownloadManager).enqueue(request)
ImageDownloadState.Success
} catch (e: Exception) {
ImageDownloadState.Failure(e)
}
}
}
}
4 changes: 2 additions & 2 deletions composeApp/src/commonMain/kotlin/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import data.repository.SharedRepository
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.koinInject
import ui.navigation.PhotoScreen
import ui.screen.HomeScreenEntryPoint
import ui.screen.PhotoDetailScreenEntryPoint
import ui.theme.LocalNavController
import ui.theme.UnsplashKMPTheme

@OptIn(ExperimentalFoundationApi::class)
Expand All @@ -28,7 +28,7 @@ fun App() {

UnsplashKMPTheme(darkTheme = sharedRepository.isDarkThemeEnabled) {

val navController = rememberNavController()
val navController = LocalNavController.current

NavHost(navController = navController, startDestination = PhotoScreen.HomeScreen) {

Expand Down
9 changes: 9 additions & 0 deletions composeApp/src/commonMain/kotlin/Platform.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
sealed class Platform {
data object Android : Platform()

data object Apple : Platform()

data object Desktop : Platform()
}

expect fun getPlatform(): Platform
3 changes: 2 additions & 1 deletion composeApp/src/commonMain/kotlin/di/Koin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ fun initKoin(appDeclaration: KoinAppDeclaration = {}) =
networkModule(),
mapperModule(),
repositoryModule(),
viewModelModule()
viewModelModule(),
platformModule()
)
}

Expand Down
5 changes: 5 additions & 0 deletions composeApp/src/commonMain/kotlin/di/PlatformModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package di

import org.koin.core.module.Module

expect fun platformModule(): Module
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ui.download

import ui.state.ImageDownloadState

expect class PlatformDownloadImage {
suspend fun downloadImage(imageLink: String): ImageDownloadState
}
22 changes: 22 additions & 0 deletions composeApp/src/commonMain/kotlin/ui/navigation/Util.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ui.navigation

import com.mohamedrejeb.calf.permissions.ExperimentalPermissionsApi
import com.mohamedrejeb.calf.permissions.PermissionState
import com.mohamedrejeb.calf.permissions.PermissionStatus

@OptIn(ExperimentalPermissionsApi::class, ExperimentalPermissionsApi::class)
fun runWithPermission(permissionState: PermissionState, ifGranted: () -> Unit) {
when (val status = permissionState.status) {
is PermissionStatus.Denied -> {
if (status.shouldShowRationale) {
permissionState.launchPermissionRequest()
} else {
permissionState.openAppSettings()
}
}

is PermissionStatus.Granted -> {
ifGranted()
}
}
}
154 changes: 82 additions & 72 deletions composeApp/src/commonMain/kotlin/ui/screen/PhotoDetail.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ui.screen
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
Expand Down Expand Up @@ -53,6 +54,7 @@ fun PhotoDetail(
modifier: Modifier = Modifier,
state: PhotoDetailState = PhotoDetailState(),
onBackPressed: () -> Unit = {},
onDownloadImageClicked: (String?) -> Unit = {},
) {

val scrollableState = rememberScrollState()
Expand All @@ -62,94 +64,102 @@ fun PhotoDetail(
modifier = Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize()
.verticalScroll(scrollableState)
.padding(horizontal = 12.dp, vertical = 10.dp + getEdgeToEdgeTopPadding())
.padding(top = 10.dp + getEdgeToEdgeTopPadding(), start = 12.dp, end = 12.dp)
.then(modifier)
) {

NavBar(modifier = Modifier.fillMaxWidth(), onBackPressed = onBackPressed)

when {
state.isLoading -> {
LinearProgressIndicator(
color = appWhite,
modifier = Modifier
.fillMaxWidth()
.height(2.dp)
)
}
NavBar(
modifier = Modifier
.defaultMinSize(minWidth = 25.dp)
.fillMaxWidth(),
onBackPressed = onBackPressed
)

Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(scrollableState)
) {
when {
state.isLoading -> {
LinearProgressIndicator(
color = appWhite,
modifier = Modifier
.fillMaxWidth()
.height(2.dp)
)
}

state.error != null -> {
ErrorComponent(
modifier = Modifier
.fillMaxSize()
.testTag("error_view"),
message = state.error.message ?: stringResource(
Res.string.an_unknown_error_occurred
state.error != null -> {
ErrorComponent(
modifier = Modifier
.fillMaxSize()
.testTag("error_view"),
message = state.error.message ?: stringResource(
Res.string.an_unknown_error_occurred
)
)
)
}
}

else -> {
else -> {

if (state.photo != null) {
Spacer(modifier = Modifier.padding(top = 10.dp))
ArtistCard(unsplashUser = state.photo.user)
}

if (state.photo != null) {
Spacer(modifier = Modifier.padding(top = 10.dp))
ArtistCard(unsplashUser = state.photo.user)
}

Spacer(modifier = Modifier.padding(top = 10.dp))

PhotoLargeDisplay(
modifier = Modifier
.fillMaxWidth()
.zIndex(2f),
imageUrl = state.photo?.urls?.full.orEmpty(),
imageColor = state.photo?.color,
imageSize = Size(
(state.photo?.width ?: 1).toFloat(),
(state.photo?.height ?: 1).toFloat()
PhotoLargeDisplay(
modifier = Modifier
.fillMaxWidth()
.zIndex(2f),
imageUrl = state.photo?.urls?.full.orEmpty(),
imageColor = state.photo?.color,
imageSize = Size(
(state.photo?.width ?: 1).toFloat(),
(state.photo?.height ?: 1).toFloat()
)
)
)

if (state.photo?.description != null || state.photo?.alternateDescription != null) {
Spacer(modifier = Modifier.padding(top = 16.dp))
if (state.photo?.description != null || state.photo?.alternateDescription != null) {
Spacer(modifier = Modifier.padding(top = 16.dp))

val content = state.photo.description ?: state.photo.alternateDescription

Text(
text = content.orEmpty().capitalize(locale),
color = appWhite,
fontSize = 13.sp,
fontStyle = FontStyle.Normal,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center,
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
}

val content = state.photo.description ?: state.photo.alternateDescription
Spacer(modifier = Modifier.padding(top = 20.dp))

Text(
text = content.orEmpty().capitalize(locale),
color = appWhite,
fontSize = 15.sp,
fontStyle = FontStyle.Normal,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Start,
letterSpacing = 0.5.sp,
OutlinedButton(
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
}

Spacer(modifier = Modifier.padding(top = 20.dp))

OutlinedButton(
modifier = Modifier
.height(height = 45.dp)
.width(width = 200.dp)
.align(Alignment.CenterHorizontally),
elevation = ButtonDefaults.elevation(0.dp),
shape = RoundedCornerShape(10.dp),
onClick = {
//TODO: implement OS specific download action
.height(height = 45.dp)
.width(width = 200.dp)
.align(Alignment.CenterHorizontally),
elevation = ButtonDefaults.elevation(0.dp),
shape = RoundedCornerShape(10.dp),
onClick = { onDownloadImageClicked(state.photo?.urls?.raw) }
) {
Icon(
painterResource(Res.drawable.round_downloading),
tint = appWhite,
contentDescription = null,
)
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Text(text = stringResource(Res.string.download_image), fontSize = 12.sp)
}
) {

Icon(
painterResource(Res.drawable.round_downloading),
tint = appWhite,
contentDescription = null,
)
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Text(text = stringResource(Res.string.download_image), fontSize = 12.sp)
Spacer(modifier = Modifier.padding(top = 30.dp))
}
}
}
Expand Down
Loading

0 comments on commit f51a1c1

Please sign in to comment.