Skip to content

Commit

Permalink
Merge pull request #29 from Yoon-Chan/list/create
Browse files Browse the repository at this point in the history
Feat:[snsproject] 게시글 사진 선택 기능 구현
  • Loading branch information
Yoon-Chan authored Apr 27, 2024
2 parents f85ed7f + 30f6356 commit 011d48f
Show file tree
Hide file tree
Showing 11 changed files with 446 additions and 3 deletions.
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET"/>
<!-- Devices running Android 12L (API level 32) or lower -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

<!-- Devices running Android 13 (API level 33) or higher -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<!-- To handle the reselection within the app on devices running Android 14
or higher if your app targets Android 14 (API level 34) or higher. -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

<application
android:name="com.example.app.App"
Expand Down
16 changes: 16 additions & 0 deletions data/src/main/java/com/example/data/di/WritingModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.data.di

import com.example.data.usecase.main.writing.GetImageListUseCaseImpl
import com.example.domain.usecase.main.writing.GetImageListUseCase
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent

@Module
@InstallIn(ActivityRetainedComponent::class)
abstract class WritingModule {

@Binds
abstract fun bindGetImageListUseCase(getImageListUseCaseImpl: GetImageListUseCaseImpl) : GetImageListUseCase
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ class GetMyUserUseCaseImpl @Inject constructor(
userDto.toUser()
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.example.data.usecase.main.writing

import android.content.ContentUris
import android.content.Context
import android.os.Build
import android.provider.MediaStore
import android.provider.MediaStore.Images
import com.example.domain.model.Image
import com.example.domain.usecase.main.writing.GetImageListUseCase
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class GetImageListUseCaseImpl @Inject constructor(
private val context: Context
) : GetImageListUseCase {
override suspend fun invoke(): List<Image> = withContext(Dispatchers.IO) {
val contentResolver = context.contentResolver
val projection = arrayOf(
Images.Media._ID,
Images.Media.DISPLAY_NAME,
Images.Media.SIZE,
Images.Media.MIME_TYPE,
)

val collectionUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Query all the device storage volumes instead of the primary only
Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
} else {
Images.Media.EXTERNAL_CONTENT_URI
}

val images = mutableListOf<Image>()

contentResolver.query(
collectionUri,
projection,
null,
null,
"${Images.Media.DATE_ADDED} DESC"
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(Images.Media._ID)
val displayNameColumn = cursor.getColumnIndexOrThrow(Images.Media.DISPLAY_NAME)
val sizeColumn = cursor.getColumnIndexOrThrow(Images.Media.SIZE)
val mimeTypeColumn = cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE)

while (cursor.moveToNext()) {
val uri = ContentUris.withAppendedId(collectionUri, cursor.getLong(idColumn))
val name = cursor.getString(displayNameColumn)
val size = cursor.getLong(sizeColumn)
val mimeType = cursor.getString(mimeTypeColumn)

val image = Image(uri.toString(), name, size, mimeType)
images.add(image)
}
}

return@withContext images
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.domain.usecase.main.writing

import com.example.domain.model.Image

interface GetImageListUseCase {
suspend operator fun invoke() : List<Image>
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
package com.example.presentation.main

import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.READ_MEDIA_IMAGES
import android.Manifest.permission.READ_MEDIA_VIDEO
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -21,25 +36,89 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.example.presentation.main.writing.WritingActivity
import com.example.presentation.ui.theme.SnsProjectTheme

fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
throw IllegalStateException("no activity")
}

@Composable
fun MainBottomBar(navController: NavController) {
val context = LocalContext.current
val navBackStackEntry by navController.currentBackStackEntryAsState()
var isShowDialog by remember {
mutableStateOf(false)
}
val currentRoute =
navBackStackEntry?.destination?.route?.let { currentRoute ->
MainRoute.entries.find { route -> currentRoute == route.route }
} ?: MainRoute.BOARD

if (isShowDialog) {
AlertDialog(
onDismissRequest = { isShowDialog = !isShowDialog },
title = {
Text(text = "현재 권한요청이 거부되었습니다. 권한요청을 변경하시겠습니까?")
},
confirmButton = {
Text(
text = "설정하러 가기",
modifier =
Modifier.clickable {
context.startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:${context.packageName}")))
isShowDialog = !isShowDialog
},
)
},
dismissButton = {
Text(text = "다음에", modifier = Modifier.clickable { isShowDialog = !isShowDialog })
},
)
}

val permissionLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions(),
) { grant ->

val areGranted = grant.values.reduce { acc, next -> acc && next }

if (areGranted) {
context.startActivity(
Intent(context, WritingActivity::class.java),
)
} else {
Toast.makeText(context, "권한 요청을 등록해야 게시글을 작성할 수 있습니다.", Toast.LENGTH_SHORT).show()
isShowDialog = !isShowDialog
}
}

MainBottomBar(currentRoute = currentRoute, onItemClick = { newRoute ->
if (newRoute == MainRoute.WRITING) {
context.startActivity(
Intent(context, WritingActivity::class.java),
)
val array =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO)
} else {
arrayOf(READ_EXTERNAL_STORAGE)
}

if (array.all { ContextCompat.checkSelfPermission(context, it) == PermissionChecker.PERMISSION_GRANTED }) {
context.startActivity(
Intent(context, WritingActivity::class.java),
)
} else {
permissionLauncher.launch(array)
}
} else {
navController.navigate(route = newRoute.route) {
navController.graph.startDestinationRoute?.let {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package com.example.presentation.main.writing

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import com.example.domain.model.Image
import com.example.presentation.ui.theme.SnsProjectTheme
import org.orbitmvi.orbit.compose.collectAsState

@Composable
fun ImageSelectScreen(viewModel: WritingViewModel) {
val state = viewModel.collectAsState().value

ImageSelectScreen(
selectImages = state.selectedImages,
images = state.images,
onBackClick = {},
onNextClick = {},
onItemClick = viewModel::onItemClick,
)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ImageSelectScreen(
selectImages: List<Image>,
images: List<Image>,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
onItemClick: (Image) -> Unit,
) {
Surface {
Scaffold(
topBar =
{
CenterAlignedTopAppBar(
title = {
Text(text = "새 게시물", style = MaterialTheme.typography.headlineSmall)
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "뒤로가기")
}
},
actions = {
TextButton(onClick = onNextClick) {
Text(text = "다음", style = MaterialTheme.typography.bodyMedium, color = Color.Black)
}
},
)
},
content = { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center,
) {
Image(
modifier = Modifier.fillMaxSize(),
painter =
rememberAsyncImagePainter(
model = selectImages.lastOrNull()?.uri,
),
contentScale = ContentScale.Crop,
contentDescription = null,
)

if (selectImages.isEmpty()) {
Text(text = "선택된 이미지가 없습니다.")
}
}
LazyVerticalGrid(
modifier =
Modifier
.weight(1f)
.fillMaxWidth()
.background(Color.White).padding(top = 2.dp),
columns = GridCells.Adaptive(110.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
items(
count = images.size,
key = { index: Int -> images[index].uri },
) { index ->
val image = images[index]
Box(modifier = Modifier.clickable { onItemClick(image) }) {
Image(
modifier =
Modifier
.fillMaxWidth()
.aspectRatio(1f),
painter =
rememberAsyncImagePainter(
model = image.uri,
contentScale = ContentScale.Crop,
),
contentDescription = null,
contentScale = ContentScale.Crop,
)

if (selectImages.contains(image)) {
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = "이미지 체크",
modifier =
Modifier
.align(Alignment.TopStart)
.padding(4.dp),
)
}
}
}
}
}
},
)
}
}

@Preview
@Composable
private fun ImageSelectScreenPreiview() {
SnsProjectTheme {
ImageSelectScreen(
selectImages = listOf(),
images = listOf(),
onBackClick = {},
onNextClick = {},
onItemClick = {},
)
}
}
Loading

0 comments on commit 011d48f

Please sign in to comment.