diff --git a/feature/addlink/.gitignore b/feature/addlink/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/addlink/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/addlink/build.gradle.kts b/feature/addlink/build.gradle.kts new file mode 100644 index 00000000..d37b8318 --- /dev/null +++ b/feature/addlink/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + alias(libs.plugins.com.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) +} + +android { + namespace = "com.strayalpaca.addlink" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + implementation(libs.orbit.compose) + implementation(libs.orbit.core) + implementation(libs.orbit.viewmodel) + + implementation(project(":core:ui")) +} diff --git a/feature/addlink/consumer-rules.pro b/feature/addlink/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/addlink/proguard-rules.pro b/feature/addlink/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/addlink/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/addlink/src/androidTest/java/com/strayalpaca/addlink/ExampleInstrumentedTest.kt b/feature/addlink/src/androidTest/java/com/strayalpaca/addlink/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..8675bf96 --- /dev/null +++ b/feature/addlink/src/androidTest/java/com/strayalpaca/addlink/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.strayalpaca.addlink + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.strayalpaca.addlink.test", appContext.packageName) + } +} diff --git a/feature/addlink/src/main/AndroidManifest.xml b/feature/addlink/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/addlink/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt new file mode 100644 index 00000000..10e36ede --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt @@ -0,0 +1,341 @@ +package com.strayalpaca.addlink + +import android.widget.Toast +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.strayalpaca.addlink.components.block.Link +import com.strayalpaca.addlink.components.block.Toolbar +import com.strayalpaca.addlink.model.AddLinkScreenSideEffect +import com.strayalpaca.addlink.model.AddLinkScreenState +import com.strayalpaca.addlink.model.Pokit +import com.strayalpaca.addlink.model.ScreenStep +import com.strayalpaca.addlink.utils.BackPressHandler +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect +import pokitmons.pokit.core.ui.components.atom.button.PokitButton +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIcon +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIconPosition +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize +import pokitmons.pokit.core.ui.components.atom.inputarea.PokitInputArea +import pokitmons.pokit.core.ui.components.block.labeledinput.LabeledInput +import pokitmons.pokit.core.ui.components.block.pokitlist.PokitList +import pokitmons.pokit.core.ui.components.block.pokitlist.attributes.PokitListState +import pokitmons.pokit.core.ui.components.block.select.PokitSelect +import pokitmons.pokit.core.ui.components.block.switchradio.PokitSwitchRadio +import pokitmons.pokit.core.ui.components.block.switchradio.attributes.PokitSwitchRadioStyle +import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +fun AddLinkScreenContainer( + linkId: String?, + viewModel: AddLinkViewModel, + onBackPressed: () -> Unit, +) { + val state by viewModel.collectAsState() + val context = LocalContext.current + + BackPressHandler(onBackPressed = viewModel::onBackPressed) + + LaunchedEffect(Unit) { + linkId?.let { + viewModel.loadPokitLink(it) + } + } + + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + AddLinkScreenSideEffect.AddLinkSuccess -> { + onBackPressed() + } + AddLinkScreenSideEffect.OnNavigationBack -> { + onBackPressed() + } + is AddLinkScreenSideEffect.ToastMessage -> { + Toast.makeText(context, context.getString(sideEffect.toastMessageEvent.stringResourceId), Toast.LENGTH_SHORT).show() + } + } + } + + val url by viewModel.linkUrl.collectAsState() + val title by viewModel.title.collectAsState() + val memo by viewModel.memo.collectAsState() + val pokitName by viewModel.pokitName.collectAsState() + + AddLinkScreen( + isModifyLink = (linkId != null), + url = url, + title = title, + memo = memo, + state = state, + pokitName = pokitName, + inputUrl = viewModel::inputLinkUrl, + inputTitle = viewModel::inputTitle, + inputMemo = viewModel::inputMemo, + inputNewPokitName = viewModel::inputNewPokitName, + onClickAddPokit = viewModel::showAddPokitBottomSheet, + onClickSavePokit = viewModel::savePokit, + dismissPokitAddBottomSheet = viewModel::hideAddPokitBottomSheet, + onClickSelectPokit = viewModel::showSelectPokitBottomSheet, + onClickSelectPokitItem = viewModel::selectPokit, + dismissPokitSelectBottomSheet = viewModel::hideSelectPokitBottomSheet, + toggleRemindRadio = viewModel::setRemind, + onBackPressed = viewModel::onBackPressed, + onClickSaveButton = viewModel::saveLink + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun AddLinkScreen( + isModifyLink: Boolean, + url: String, + title: String, + memo: String, + pokitName: String, + state: AddLinkScreenState, + inputUrl: (String) -> Unit, + inputTitle: (String) -> Unit, + inputMemo: (String) -> Unit, + inputNewPokitName: (String) -> Unit, + onClickAddPokit: () -> Unit, + onClickSavePokit: () -> Unit, + dismissPokitAddBottomSheet: () -> Unit, + onClickSelectPokit: () -> Unit, + onClickSelectPokitItem: (Pokit) -> Unit, + dismissPokitSelectBottomSheet: () -> Unit, + toggleRemindRadio: (Boolean) -> Unit, + onBackPressed: () -> Unit, + onClickSaveButton: () -> Unit, +) { + val scrollState = rememberScrollState() + val enable = remember(state.step) { + !( + state.step == ScreenStep.SAVE_LOADING || + state.step == ScreenStep.LOADING || + state.step == ScreenStep.POKIT_ADD_LOADING || + state.step == ScreenStep.LINK_LOADING + ) + } + + Column( + modifier = Modifier.fillMaxSize() + ) { + Toolbar( + modifier = Modifier.fillMaxWidth(), + onClickBack = onBackPressed, + title = if (isModifyLink) stringResource(id = R.string.modify_link) else stringResource(id = R.string.add_link) + ) + + CompositionLocalProvider( + LocalOverscrollConfiguration provides null + ) { + Column( + modifier = Modifier + .padding(vertical = 16.dp, horizontal = 20.dp) + .verticalScroll( + state = scrollState, + flingBehavior = null + ) + ) { + if (state.link != null) { + Link(state.link) + } + + Spacer(modifier = Modifier.height(16.dp)) + + LabeledInput( + label = stringResource(id = R.string.link), + sub = "", + maxLength = null, + inputText = url, + hintText = stringResource(id = R.string.placeholder_link), + onChangeText = inputUrl, + enable = enable + ) + + Spacer(modifier = Modifier.height(24.dp)) + + LabeledInput( + label = stringResource(id = R.string.title), + sub = "", + maxLength = 20, + inputText = title, + hintText = stringResource(id = R.string.placeholder_title), + onChangeText = inputTitle, + enable = enable + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Bottom + ) { + PokitSelect( + text = if (state.currentPokit == null) stringResource(id = R.string.uncategorized) else state.currentPokit.title, + hintText = stringResource(id = R.string.uncategorized), + label = stringResource(id = R.string.pokit), + modifier = Modifier.weight(1f), + onClick = onClickSelectPokit, + enable = enable + ) + + Spacer(modifier = Modifier.width(8.dp)) + + PokitButton( + text = null, + icon = PokitButtonIcon( + resourceId = pokitmons.pokit.core.ui.R.drawable.icon_24_plus, + position = PokitButtonIconPosition.LEFT + ), + size = PokitButtonSize.LARGE, + onClick = onClickAddPokit, + enable = enable + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(id = R.string.memo), + style = PokitTheme.typography.body2Medium.copy(color = PokitTheme.colors.textSecondary) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + PokitInputArea( + text = memo, + hintText = stringResource(id = R.string.placeholder_memo), + onChangeText = inputMemo, + enable = enable + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + modifier = Modifier.fillMaxWidth(), + text = "${memo.length}/100", + style = PokitTheme.typography.detail1.copy(color = PokitTheme.colors.textTertiary), + textAlign = TextAlign.End + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(id = R.string.title_remind), + style = PokitTheme.typography.body2Medium.copy(color = PokitTheme.colors.textSecondary) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + PokitSwitchRadio( + modifier = Modifier.fillMaxWidth(), + itemList = listOf( + Pair(stringResource(id = R.string.reject_remind), false), + Pair(stringResource(id = R.string.accept_remind), true) + ), + style = PokitSwitchRadioStyle.STROKE, + selectedItem = if (state.useRemind) { + Pair(stringResource(id = R.string.accept_remind), true) + } else { + Pair(stringResource(id = R.string.reject_remind), false) + }, + onClickItem = { + toggleRemindRadio(it.second) + }, + getTitleFromItem = { it.first }, + enabled = enable + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(id = R.string.sub_remind), + style = PokitTheme.typography.detail1.copy(color = PokitTheme.colors.textTertiary) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + PokitButton( + text = stringResource(id = R.string.save), + icon = null, + onClick = onClickSaveButton, + modifier = Modifier.fillMaxWidth(), + size = PokitButtonSize.LARGE + ) + } + } + + if (state.step == ScreenStep.POKIT_SELECT) { + PokitBottomSheet(onHideBottomSheet = dismissPokitSelectBottomSheet) { + LazyColumn { + items( + items = state.pokitList + ) { + PokitList( + item = it, + title = it.title, + sub = stringResource(id = R.string.count_format, it.count), + onClickKebab = onClickSelectPokitItem, + onClickItem = onClickSelectPokitItem, + state = PokitListState.ACTIVE + ) + } + } + } + } + + if (state.step == ScreenStep.POKIT_ADD) { + PokitBottomSheet(onHideBottomSheet = dismissPokitAddBottomSheet) { + Column( + modifier = Modifier.padding(horizontal = 20.dp) + ) { + LabeledInput( + label = "", + inputText = pokitName, + hintText = stringResource(id = R.string.placeholder_input_pokit_name), + onChangeText = inputNewPokitName, + maxLength = 10 + ) + + Spacer(modifier = Modifier.height(12.dp)) + + PokitButton( + text = stringResource(id = R.string.add), + icon = null, + onClick = onClickSavePokit, + modifier = Modifier.fillMaxWidth(), + size = PokitButtonSize.LARGE, + enable = pokitName.isNotEmpty() + ) + } + } + } + } +} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt new file mode 100644 index 00000000..8f607b45 --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt @@ -0,0 +1,158 @@ +package com.strayalpaca.addlink + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.strayalpaca.addlink.model.AddLinkScreenSideEffect +import com.strayalpaca.addlink.model.AddLinkScreenState +import com.strayalpaca.addlink.model.Pokit +import com.strayalpaca.addlink.model.ScreenStep +import com.strayalpaca.addlink.model.sampleLink +import com.strayalpaca.addlink.model.samplePokitList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container + +class AddLinkViewModel : ContainerHost, ViewModel() { + override val container: Container = container(AddLinkScreenState()) + + private val _linkUrl = MutableStateFlow("") + val linkUrl: StateFlow = _linkUrl.asStateFlow() + + private val _title = MutableStateFlow("") + val title: StateFlow = _title.asStateFlow() + + private val _memo = MutableStateFlow("") + val memo: StateFlow = _memo.asStateFlow() + + private val _pokitName = MutableStateFlow("") + val pokitName: StateFlow = _pokitName.asStateFlow() + + init { + loadPokitList() + } + + private var inputLinkJob: Job? = null + + private fun loadPokitList() = intent { + viewModelScope.launch(Dispatchers.IO) { + reduce { state.copy(step = ScreenStep.LOADING) } + // todo 포킷 목록 가져오기 api 연결 + delay(1000L) + reduce { + state.copy( + step = ScreenStep.IDLE, + pokitList = samplePokitList + ) + } + } + } + + fun loadPokitLink(linkId: String) = intent { + viewModelScope.launch(Dispatchers.IO) { + reduce { state.copy(step = ScreenStep.LOADING) } + // todo 포킷 링크 가져오기 api 연결 + delay(1000L) + reduce { + state.copy( + step = ScreenStep.IDLE + ) + } + } + } + + fun inputLinkUrl(linkUrl: String) { + this._linkUrl.update { linkUrl } + + intent { + inputLinkJob?.cancel() + inputLinkJob = viewModelScope.launch(Dispatchers.IO) { + delay(1000L) + reduce { state.copy(step = ScreenStep.LINK_LOADING) } + // todo 링크 카드 정보 가져오기 api 연결 + delay(1000L) + reduce { state.copy(step = ScreenStep.IDLE, link = sampleLink.copy(url = linkUrl)) } + } + } + } + + fun inputTitle(title: String) { + _title.update { title } + } + + fun inputMemo(memo: String) { + _memo.update { memo } + } + + fun showAddPokitBottomSheet() = intent { + reduce { state.copy(step = ScreenStep.POKIT_ADD) } + } + + fun hideAddPokitBottomSheet() = intent { + reduce { state.copy(step = ScreenStep.IDLE) } + } + + fun showSelectPokitBottomSheet() = intent { + reduce { state.copy(step = ScreenStep.POKIT_SELECT) } + } + + fun hideSelectPokitBottomSheet() = intent { + reduce { state.copy(step = ScreenStep.IDLE) } + } + + fun selectPokit(pokit: Pokit) = intent { + reduce { state.copy(currentPokit = pokit, step = ScreenStep.IDLE) } + } + + fun onBackPressed() = intent { + val currentStep = container.stateFlow.value.step + when (currentStep) { + is ScreenStep.POKIT_ADD_LOADING -> {} // discard + + is ScreenStep.SAVE_LOADING -> {} // discard + + ScreenStep.POKIT_SELECT -> reduce { state.copy(step = ScreenStep.IDLE) } + + is ScreenStep.POKIT_ADD -> reduce { state.copy(step = ScreenStep.IDLE) } + + else -> postSideEffect(AddLinkScreenSideEffect.OnNavigationBack) + } + } + + fun inputNewPokitName(pokitName: String) { + _pokitName.update { pokitName } + } + + fun savePokit() = intent { + viewModelScope.launch(Dispatchers.IO) { + reduce { state.copy(step = ScreenStep.POKIT_ADD_LOADING) } + // todo 포킷 저장 useCase 연결 + delay(1000L) + reduce { state.copy(step = ScreenStep.IDLE) } + } + } + + fun saveLink() = intent { + viewModelScope.launch(Dispatchers.IO) { + reduce { state.copy(step = ScreenStep.LINK_LOADING) } + // todo 링크 저장 useCase 연결 + delay(1000L) + reduce { state.copy(step = ScreenStep.IDLE) } + postSideEffect(AddLinkScreenSideEffect.AddLinkSuccess) + } + } + + fun setRemind(remind: Boolean) = intent { + reduce { state.copy(useRemind = remind) } + } +} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/Preview.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/Preview.kt new file mode 100644 index 00000000..869196a4 --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/Preview.kt @@ -0,0 +1,42 @@ +package com.strayalpaca.addlink + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.strayalpaca.addlink.model.AddLinkScreenState +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Preview(showBackground = true) +@Composable +fun AddLinkScreenPreview() { + PokitTheme { + Column( + modifier = Modifier.fillMaxSize().background(PokitTheme.colors.backgroundBase) + ) { + AddLinkScreen( + isModifyLink = false, + url = "", + title = "", + memo = "", + pokitName = "", + state = AddLinkScreenState(), + inputUrl = {}, + inputTitle = {}, + inputMemo = {}, + inputNewPokitName = {}, + onClickAddPokit = {}, + onClickSavePokit = {}, + dismissPokitAddBottomSheet = {}, + onClickSelectPokit = {}, + onClickSelectPokitItem = {}, + dismissPokitSelectBottomSheet = {}, + toggleRemindRadio = {}, + onBackPressed = {}, + onClickSaveButton = {} + ) + } + } +} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/components/block/Link.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/components/block/Link.kt new file mode 100644 index 00000000..821e6614 --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/components/block/Link.kt @@ -0,0 +1,69 @@ +package com.strayalpaca.addlink.components.block + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +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.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.strayalpaca.addlink.model.Link +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +internal fun Link( + link: Link, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .height(IntrinsicSize.Min) + .border( + width = 1.dp, + color = PokitTheme.colors.borderTertiary, + shape = RoundedCornerShape(12.dp) + ) + ) { + Image( + painter = painterResource(id = pokitmons.pokit.core.ui.R.drawable.icon_24_google), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.width(124.dp) + ) + + Column( + modifier = Modifier + .padding(start = 16.dp, end = 20.dp, top = 16.dp, bottom = 16.dp) + .weight(1f) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = link.title, + maxLines = 2, + style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.textSecondary) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier.fillMaxWidth(), + text = link.url, + maxLines = 2, + style = PokitTheme.typography.detail2.copy(color = PokitTheme.colors.textTertiary) + ) + } + } +} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/components/block/Toolbar.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/components/block/Toolbar.kt new file mode 100644 index 00000000..5a5c5254 --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/components/block/Toolbar.kt @@ -0,0 +1,53 @@ +package com.strayalpaca.addlink.components.block + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +internal fun Toolbar( + onClickBack: () -> Unit, + title: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .height(56.dp) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + IconButton( + modifier = Modifier.size(48.dp), + onClick = onClickBack + ) { + Icon( + painter = painterResource(id = pokitmons.pokit.core.ui.R.drawable.icon_24_arrow_left), + contentDescription = "back button" + ) + } + + Text( + modifier = Modifier.weight(1f), + text = title, + style = PokitTheme.typography.title3.copy(color = PokitTheme.colors.textPrimary), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.width(48.dp)) + } +} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/model/AddLinkScreenState.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/AddLinkScreenState.kt new file mode 100644 index 00000000..c6a722eb --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/AddLinkScreenState.kt @@ -0,0 +1,31 @@ +package com.strayalpaca.addlink.model + +import com.strayalpaca.addlink.R + +data class AddLinkScreenState( + val link: Link? = null, + val currentPokit: Pokit? = null, + val pokitList: List = emptyList(), + val useRemind: Boolean = false, + val step: ScreenStep = ScreenStep.IDLE, +) + +sealed class ScreenStep { + data object LOADING : ScreenStep() + data object IDLE : ScreenStep() + data object LINK_LOADING : ScreenStep() + data object POKIT_SELECT : ScreenStep() + data object POKIT_ADD : ScreenStep() + data object POKIT_ADD_LOADING : ScreenStep() + data object SAVE_LOADING : ScreenStep() +} + +sealed class AddLinkScreenSideEffect() { + data object AddLinkSuccess : AddLinkScreenSideEffect() + data class ToastMessage(val toastMessageEvent: ToastMessageEvent) : AddLinkScreenSideEffect() + data object OnNavigationBack : AddLinkScreenSideEffect() +} + +enum class ToastMessageEvent(val stringResourceId: Int) { + NETWORK_ERROR(R.string.network_error), +} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Link.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Link.kt new file mode 100644 index 00000000..b05521d4 --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Link.kt @@ -0,0 +1,9 @@ +package com.strayalpaca.addlink.model + +data class Link( + val url: String, + val title: String, + val imageUrl: String?, +) + +internal val sampleLink = Link(url = "https://pokit.com/watch?v=xSTwqkUyM8k", title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", imageUrl = null) diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Pokit.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Pokit.kt new file mode 100644 index 00000000..0088afe6 --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Pokit.kt @@ -0,0 +1,15 @@ +package com.strayalpaca.addlink.model + +data class Pokit( + val title: String, + val id: String, + val count: Int, +) + +internal val samplePokitList = listOf( + Pokit(title = "안드로이드", id = "1", count = 2), + Pokit(title = "IOS", id = "2", count = 2), + Pokit(title = "디자인", id = "3", count = 2), + Pokit(title = "PM", id = "4", count = 1), + Pokit(title = "서버", id = "5", count = 2) +) diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/utils/BackPressHandler.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/utils/BackPressHandler.kt new file mode 100644 index 00000000..dc0796d5 --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/utils/BackPressHandler.kt @@ -0,0 +1,35 @@ +package com.strayalpaca.addlink.utils + +import androidx.activity.OnBackPressedCallback +import androidx.activity.OnBackPressedDispatcher +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState + +@Composable +internal fun BackPressHandler( + backPressedDispatcher: OnBackPressedDispatcher? = + LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher, + onBackPressed: () -> Unit, +) { + val currentOnBackPressed by rememberUpdatedState(newValue = onBackPressed) + + val backCallback = remember { + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + currentOnBackPressed() + } + } + } + + DisposableEffect(backPressedDispatcher) { + backPressedDispatcher?.addCallback(backCallback) + + onDispose { + backCallback.remove() + } + } +} diff --git a/feature/addlink/src/main/res/values/string.xml b/feature/addlink/src/main/res/values/string.xml new file mode 100644 index 00000000..cf43735d --- /dev/null +++ b/feature/addlink/src/main/res/values/string.xml @@ -0,0 +1,24 @@ + + + 링크 추가 + 링크 수정 + 링크 + 내용을 입력해주세요. + 제목 + 내용을 입력해주세요. + 포킷 + 메모 + 내용을 입력해주세요. + 리마인드 알림을 보내드릴까요? + 안받을래요 + 받을래요 + 일주일 후에 알림을 전송해드립니다. + 저장하기 + 미분류 + + 포킷명을 입력해주세요. + 추가하기 + 링크 %d개 + + 네트워크 에러가 발생했습니다. 네트워크 환경을 확인해주세요. + \ No newline at end of file diff --git a/feature/addlink/src/test/java/com/strayalpaca/addlink/ExampleUnitTest.kt b/feature/addlink/src/test/java/com/strayalpaca/addlink/ExampleUnitTest.kt new file mode 100644 index 00000000..5093231d --- /dev/null +++ b/feature/addlink/src/test/java/com/strayalpaca/addlink/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.strayalpaca.addlink + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 995d8d42..04541faf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,3 +25,4 @@ include(":domain") include(":data") include(":feature:login") include(":core:ui") +include(":feature:addlink")