diff --git a/app/build.gradle.kts b/app/build.gradle.kts index efb2e51..1ad6168 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,6 +100,9 @@ dependencies { implementation(libs.retrofit) implementation(libs.converter.gson) + // Jsoup HTML Parser Library + implementation(libs.jsoup) + } // Allow references to generated code diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b013ae0..78309cb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:roundIcon="@mipmap/ic_launcher_round" + android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" android:theme="@style/Theme.KNUTICE" tools:targetApi="31"> diff --git a/app/src/main/java/com/doyoonkim/knutice/data/NoticeLocalRepository.kt b/app/src/main/java/com/doyoonkim/knutice/data/NoticeLocalRepository.kt index 6ebb2a1..4c83f40 100644 --- a/app/src/main/java/com/doyoonkim/knutice/data/NoticeLocalRepository.kt +++ b/app/src/main/java/com/doyoonkim/knutice/data/NoticeLocalRepository.kt @@ -5,12 +5,14 @@ import com.doyoonkim.knutice.model.NoticeCategory import com.doyoonkim.knutice.model.NoticesPerPage import com.doyoonkim.knutice.model.TopThreeNotices import dagger.hilt.android.scopes.ActivityRetainedScoped +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch import javax.inject.Inject /* @@ -59,4 +61,10 @@ class NoticeLocalRepository @Inject constructor( } }.flowOn(Dispatchers.IO) } + + fun getFullNoticeContent(url: String): Flow { + return flow { + emit(remoteSource.getFullNoticeContent(url).await()) + }.flowOn(Dispatchers.IO) + } } \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/data/NoticeRemoteSource.kt b/app/src/main/java/com/doyoonkim/knutice/data/NoticeRemoteSource.kt index 4ca6d9c..868a8a5 100644 --- a/app/src/main/java/com/doyoonkim/knutice/data/NoticeRemoteSource.kt +++ b/app/src/main/java/com/doyoonkim/knutice/data/NoticeRemoteSource.kt @@ -5,6 +5,11 @@ import com.doyoonkim.knutice.model.NoticeCategory import com.doyoonkim.knutice.model.NoticesPerPage import com.doyoonkim.knutice.model.TopThreeNotices import com.example.knutice.BuildConfig +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import org.jsoup.Jsoup import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET @@ -36,6 +41,14 @@ class NoticeRemoteSource @Inject constructor() { } } + suspend fun getFullNoticeContent(url: String): Deferred = + CoroutineScope(Dispatchers.IO).async { + Jsoup.connect(url) + .get() + .getElementsByClass("bbs-view-content bbs-view-content-skin05") + .text() ?: "Unable to receive full notice content" + } + } interface KnuticeService { diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContent.kt b/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContent.kt new file mode 100644 index 0000000..4f144a5 --- /dev/null +++ b/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContent.kt @@ -0,0 +1,10 @@ +package com.doyoonkim.knutice.domain + +import com.doyoonkim.knutice.viewModel.DetailedContentState +import kotlinx.coroutines.flow.Flow + +interface CrawlFullContent { + + fun getFullContentFromSource(title: String, info: String, url: String): Flow + +} \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContentImpl.kt b/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContentImpl.kt new file mode 100644 index 0000000..e48a5f6 --- /dev/null +++ b/app/src/main/java/com/doyoonkim/knutice/domain/CrawlFullContentImpl.kt @@ -0,0 +1,27 @@ +package com.doyoonkim.knutice.domain + +import com.doyoonkim.knutice.data.NoticeLocalRepository +import com.doyoonkim.knutice.viewModel.DetailedContentState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class CrawlFullContentImpl @Inject constructor( + private val repository: NoticeLocalRepository +): CrawlFullContent { + override fun getFullContentFromSource( + title: String, + info: String, + url: String + ): Flow { + return repository.getFullNoticeContent(url) + .map { + DetailedContentState( + title = title, + info = info, + fullContent = it, + fullContentUrl = url + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/navigation/MainNavigator.kt b/app/src/main/java/com/doyoonkim/knutice/navigation/MainNavigator.kt index 3412bfd..e65da38 100644 --- a/app/src/main/java/com/doyoonkim/knutice/navigation/MainNavigator.kt +++ b/app/src/main/java/com/doyoonkim/knutice/navigation/MainNavigator.kt @@ -1,7 +1,9 @@ package com.doyoonkim.knutice.navigation +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -10,6 +12,7 @@ import com.doyoonkim.knutice.model.Destination import com.doyoonkim.knutice.model.NoticeCategory import com.doyoonkim.knutice.presentation.CategorizedNotification import com.doyoonkim.knutice.presentation.MoreCategorizedNotification +import com.doyoonkim.knutice.presentation.UserPreference import com.doyoonkim.knutice.viewModel.MainActivityViewModel @Composable @@ -58,5 +61,14 @@ fun MainNavigator( ) MoreCategorizedNotification(category = NoticeCategory.EVENT_NEWS) } + + composable(Destination.SETTINGS.name) { + viewModel.updateState( + updatedCurrentLocation = Destination.SETTINGS + ) + UserPreference( + Modifier.padding(top = 20.dp, start = 10.dp, end = 10.dp) + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/CategorizedNoficiation.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/CategorizedNoficiation.kt index b84d4bc..0d83ba6 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/CategorizedNoficiation.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/CategorizedNoficiation.kt @@ -1,6 +1,7 @@ package com.doyoonkim.knutice.presentation import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -34,6 +35,7 @@ import com.doyoonkim.knutice.ui.theme.notificationType3 import com.doyoonkim.knutice.ui.theme.notificationType4 import com.doyoonkim.knutice.ui.theme.subTitle import com.doyoonkim.knutice.viewModel.CategorizedNotificationViewModel +import com.doyoonkim.knutice.viewModel.DetailedContentState import com.example.knutice.R @Composable @@ -54,33 +56,51 @@ fun CategorizedNotification( NotificationPreviewList ( listTitle = stringResource(R.string.general_news), titleColor = MaterialTheme.colorScheme.notificationType1, - contents = uiState.notificationGeneral - ) { - navController.navigate(Destination.MORE_GENERAL.name) + contents = uiState.notificationGeneral, + onMoreClicked = { navController.navigate(Destination.MORE_GENERAL.name) } + ) { title, info, url -> + viewModel.getFullNoticeContent(title, info, url) } NotificationPreviewList( listTitle = stringResource(R.string.academic_news), titleColor = MaterialTheme.colorScheme.notificationType2, - contents = uiState.notificationAcademic - ) { - navController.navigate(Destination.MORE_ACADEMIC.name) + contents = uiState.notificationAcademic, + onMoreClicked = { navController.navigate(Destination.MORE_ACADEMIC.name) } + ) { title, info, url -> + viewModel.getFullNoticeContent(title, info, url) } NotificationPreviewList( listTitle = stringResource(R.string.scholarship_news), titleColor = MaterialTheme.colorScheme.notificationType3, - contents = uiState.notificationScholarship - ) { - navController.navigate(Destination.MORE_SCHOLARSHIP.name) + contents = uiState.notificationScholarship, + onMoreClicked = { navController.navigate(Destination.MORE_SCHOLARSHIP.name) } + ) { title, info, url -> + viewModel.getFullNoticeContent(title, info, url) } NotificationPreviewList( listTitle = stringResource(R.string.event_news), titleColor = MaterialTheme.colorScheme.notificationType4, - contents = uiState.notificationEvent + contents = uiState.notificationEvent, + onMoreClicked = { navController.navigate(Destination.MORE_EVENT.name) } + ) { title, info, url -> + viewModel.getFullNoticeContent(title, info, url) + } + } + + AnimatedVisibility( + visible = uiState.isDetailedViewOpened + ) { + DetailedNoticeContent( + modifier = Modifier.padding(10.dp), + requested = uiState.requestedContent ) { - navController.navigate(Destination.MORE_EVENT) + viewModel.updateState( + updatedIsDetailedViewOpened = false, + updatedRequestedContent = DetailedContentState() + ) } } } @@ -91,7 +111,8 @@ fun NotificationPreviewList( listTitle: String = "List Title goes here", titleColor: Color = Color.Unspecified, contents: List = listOf(), - onMoreClicked: () -> Unit = { } + onMoreClicked: () -> Unit = { }, + onNoticeClicked: (String, String, String) -> Unit ) { Column( modifier = Modifier.fillMaxWidth() @@ -126,7 +147,9 @@ fun NotificationPreviewList( NotificationPreviewCard( notificationTitle = content.title, notificationInfo = "[${content.departName}] ${content.timestamp}" - ) { } + ) { + onNoticeClicked(content.title, content.departName, content.url) + } } } } diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/DetailedNoticeContent.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/DetailedNoticeContent.kt new file mode 100644 index 0000000..5a76a8f --- /dev/null +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/DetailedNoticeContent.kt @@ -0,0 +1,139 @@ +package com.doyoonkim.knutice.presentation + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.Image +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.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.doyoonkim.knutice.ui.theme.buttonContainer +import com.doyoonkim.knutice.ui.theme.containerBackground +import com.doyoonkim.knutice.ui.theme.title +import com.doyoonkim.knutice.viewModel.DetailedContentState +import com.example.knutice.R + +@Composable +fun DetailedNoticeContent( + modifier: Modifier = Modifier, + requested: DetailedContentState = DetailedContentState(), + onCloseRequested: () -> Unit = { } +) { + val localContext = LocalContext.current + + Surface( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(10.dp), + color = MaterialTheme.colorScheme.containerBackground + ) { + Column( + Modifier.padding(15.dp) + ) { + Row( + Modifier.fillMaxWidth() + .weight(0.5f), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Bottom + ) { + Text( + modifier = Modifier.weight(5f).wrapContentHeight(), + text = requested.title, + fontSize = 28.sp, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + IconButton( + modifier = Modifier.weight(1f).wrapContentSize(), + onClick = { onCloseRequested() } + ) { + Image( + painter = painterResource(R.drawable.baseline_close_24), + contentDescription = "Close Button", + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.title) + ) + } + } + + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .weight(0.5f), + text = requested.info, + fontSize = 16.sp, + fontWeight = FontWeight.Normal + ) + + Text( + modifier = Modifier.fillMaxWidth() + .weight(8f), + text = requested.fullContent, + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + ) + + Button( + onClick = { + val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse(requested.fullContentUrl)) + localContext.startActivity(webIntent) + }, + shape = RoundedCornerShape(15.dp), + modifier = Modifier.fillMaxWidth() + .padding(start = 15.dp, end = 15.dp, top = 5.dp, bottom = 5.dp) + .weight(0.7f), + colors = ButtonColors( + containerColor = MaterialTheme.colorScheme.buttonContainer, + contentColor = Color.White, + disabledContentColor = Color.Unspecified, + disabledContainerColor = Color.Unspecified + ) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.btn_more_on_browser), + textAlign = TextAlign.Center, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold + ) + } + } + } +} + + +@Preview +@Composable +fun DetailedNoticeContent_Preview() { + DetailedNoticeContent( + requested = DetailedContentState( + title = "Test", + info = "Test", + fullContent = "Full Content" + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt index 6f7c6d5..13df48d 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt @@ -96,6 +96,7 @@ fun MainServiceScreen( Destination.MORE_ACADEMIC -> R.string.academic_news Destination.MORE_SCHOLARSHIP -> R.string.scholarship_news Destination.MORE_EVENT -> R.string.event_news + Destination.SETTINGS -> R.string.title_preference else -> R.string.app_name }), fontSize = 20.sp, @@ -110,7 +111,9 @@ fun MainServiceScreen( actions = { if (mainAppState.currentLocation == Destination.MAIN) { IconButton( - onClick = { } + onClick = { + navController.navigate(Destination.SETTINGS.name) + } ) { Image( painter = painterResource(R.drawable.baseline_settings_24), diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt index 33cfaa5..1038b2a 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt @@ -1,12 +1,17 @@ package com.doyoonkim.knutice.presentation import android.util.Log +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn +//noinspection UsingMaterialAndMaterial3Libraries import androidx.compose.material.Divider import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator @@ -69,12 +74,25 @@ fun MoreCategorizedNotification( color = MaterialTheme.colorScheme.containerBackground ) } - NotificationPreview( - notificationTitle = notice.title, - notificationInfo = "[${notice.departName}] ${notice.timestamp}", - isImageContained = notice.imageUrl != "Unknown", - imageUrl = notice.imageUrl - ) + Row( + modifier = Modifier.wrapContentSize() + .clickable { + viewModel.updatedDetailedContentRequest( + true, + notice.title, + "[${notice.departName}] ${notice.timestamp}", + notice.url + ) + } + ) { + NotificationPreview( + notificationTitle = notice.title, + notificationInfo = "[${notice.departName}] ${notice.timestamp}", + isImageContained = notice.imageUrl != "Unknown", + imageUrl = notice.imageUrl + ) + } + } } } @@ -85,5 +103,16 @@ fun MoreCategorizedNotification( state = pullRefreshState ) } + + AnimatedVisibility( + uiState.isDetailedContentVisible + ) { + DetailedNoticeContent( + modifier = Modifier.padding(15.dp), + requested = uiState.detailedContentState + ) { + viewModel.updatedDetailedContentRequest(false) + } + } } diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/UserPreference.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/UserPreference.kt new file mode 100644 index 0000000..6352631 --- /dev/null +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/UserPreference.kt @@ -0,0 +1,170 @@ +package com.doyoonkim.knutice.presentation + +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.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +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.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.doyoonkim.knutice.ui.theme.subTitle +import com.example.knutice.R + +// TODO: Apply Color Theme on HorizontalDivider. + +@Composable +fun UserPreference( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + Text( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + text = stringResource(R.string.pref_notification_title), + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + textAlign = TextAlign.Start + ) + + HorizontalDivider( + Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.subTitle + ) + + Column( + modifier = Modifier.fillMaxWidth().wrapContentSize() + .padding(top = 15.dp, bottom = 15.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.wrapContentHeight().weight(5f), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + Text( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + text = stringResource(R.string.enable_notification_title), + fontWeight = FontWeight.Medium, + fontSize = 18.sp, + textAlign = TextAlign.Start + ) + + Text( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + text = stringResource(R.string.enable_service_notification_sub), + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + textAlign = TextAlign.Start + ) + } + + Switch( + checked = false, + onCheckedChange = { }, + enabled = false + ) + } + } + + HorizontalDivider( + Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.subTitle + ) + + Text( + modifier = Modifier.fillMaxWidth().wrapContentHeight() + .padding(top = 20.dp), + text = stringResource(R.string.about_title), + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + textAlign = TextAlign.Start + ) + + HorizontalDivider( + Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.subTitle + ) + + Row( + modifier = Modifier.fillMaxWidth().wrapContentSize() + .padding(top = 15.dp, bottom = 15.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.wrapContentHeight().weight(3f), + text = stringResource(R.string.about_version), + fontWeight = FontWeight.Medium, + fontSize = 18.sp, + textAlign = TextAlign.Start + ) + + Text( + modifier = Modifier.wrapContentHeight().weight(3f), + text = stringResource(R.string.version_code), + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + textAlign = TextAlign.End + ) + } + + HorizontalDivider( + Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.subTitle + ) + + Row( + modifier = Modifier.fillMaxWidth().wrapContentSize() + .padding(top = 15.dp, bottom = 15.dp), + verticalAlignment = Alignment.Bottom + ) { + Text( + modifier = Modifier.wrapContentHeight().weight(5f), + text = stringResource(R.string.about_oss), + fontWeight = FontWeight.Medium, + fontSize = 18.sp, + textAlign = TextAlign.Start + ) + + //TODO: Should be replaced with Actual Icon Button. + Text( + modifier = Modifier.wrapContentHeight().weight(2f), + text = ">", + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + textAlign = TextAlign.End + ) + } + + HorizontalDivider( + Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.subTitle + ) + + } +} + + +@Preview(showSystemUi = true, locale = "KO") +@Composable +fun UserPreference_Preview() { + UserPreference(Modifier.padding(top = 20.dp, start = 10.dp, end = 10.dp)) +} \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreview.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreview.kt index 04a7bec..55575a3 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreview.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreview.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -65,7 +66,9 @@ fun NotificationPreview( textAlign = TextAlign.Start, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.title + color = MaterialTheme.colorScheme.title, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) Text( modifier = Modifier.fillMaxWidth() diff --git a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Color.kt b/app/src/main/java/com/doyoonkim/knutice/ui/theme/Color.kt index 873a762..263e2b1 100644 --- a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Color.kt +++ b/app/src/main/java/com/doyoonkim/knutice/ui/theme/Color.kt @@ -27,5 +27,5 @@ val TitleWhite = Color(0xFFFFFFFF) val SubtitleAny = Color(0xFF787879) -val backgroundGray = Color(0xFF3C3C3C) -val backgroundLightGray = Color(0xFF888888) \ No newline at end of file +val ButtonLight = Color(0xFF3C3C3C) +val ButtonDark = Color(0xFF888888) \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Theme.kt b/app/src/main/java/com/doyoonkim/knutice/ui/theme/Theme.kt index 71ee967..f63013f 100644 --- a/app/src/main/java/com/doyoonkim/knutice/ui/theme/Theme.kt +++ b/app/src/main/java/com/doyoonkim/knutice/ui/theme/Theme.kt @@ -67,6 +67,10 @@ val ColorScheme.subTitle: Color @Composable get() = SubtitleAny +val ColorScheme.buttonContainer: Color + @Composable + get() = if(isSystemInDarkTheme()) ButtonDark else ButtonLight + @Composable fun KNUTICETheme( diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/CategorizedNotificationViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/CategorizedNotificationViewModel.kt index f4a2cc1..bde6521 100644 --- a/app/src/main/java/com/doyoonkim/knutice/viewModel/CategorizedNotificationViewModel.kt +++ b/app/src/main/java/com/doyoonkim/knutice/viewModel/CategorizedNotificationViewModel.kt @@ -2,9 +2,9 @@ package com.doyoonkim.knutice.viewModel import android.util.Log import androidx.lifecycle.ViewModel +import com.doyoonkim.knutice.domain.CrawlFullContentImpl import com.doyoonkim.knutice.domain.FetchTopThreeNoticeByCategory import com.doyoonkim.knutice.model.Notice -import com.doyoonkim.knutice.model.NoticeCategory import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -20,7 +20,8 @@ import javax.inject.Inject @HiltViewModel class CategorizedNotificationViewModel @Inject constructor( - private val fetchTopThreeNoticeUseCase: FetchTopThreeNoticeByCategory + private val fetchTopThreeNoticeUseCase: FetchTopThreeNoticeByCategory, + private val crawlFullContentUseCase: CrawlFullContentImpl ) : ViewModel() { init { CoroutineScope(Dispatchers.IO).launch { @@ -35,13 +36,13 @@ class CategorizedNotificationViewModel @Inject constructor( private val _uiState = MutableStateFlow(CategorizedNotificationState()) var uiState: StateFlow = _uiState.asStateFlow() - private fun updateState ( + fun updateState ( updatedNotificationGeneral: List = _uiState.value.notificationGeneral, updatedNotificationAcademic: List = _uiState.value.notificationAcademic, updatedNotificationScholarship: List = _uiState.value.notificationScholarship, updatedNotificationEvent: List = _uiState.value.notificationEvent, - updatedIsMoreOptionSelected: Boolean = _uiState.value.isMoreOptionSelected, - updatedCategoryForMoreNotice: NoticeCategory = _uiState.value.categoryForMoreNotice + updatedIsDetailedViewOpened: Boolean = _uiState.value.isDetailedViewOpened, + updatedRequestedContent: DetailedContentState = _uiState.value.requestedContent ) { CoroutineScope(Dispatchers.Default).launch { _uiState.update { @@ -50,8 +51,8 @@ class CategorizedNotificationViewModel @Inject constructor( notificationAcademic = updatedNotificationAcademic, notificationScholarship = updatedNotificationScholarship, notificationEvent = updatedNotificationEvent, - isMoreOptionSelected = updatedIsMoreOptionSelected, - categoryForMoreNotice = updatedCategoryForMoreNotice + isDetailedViewOpened = updatedIsDetailedViewOpened, + requestedContent = updatedRequestedContent ) } } @@ -137,7 +138,29 @@ class CategorizedNotificationViewModel @Inject constructor( } } - + fun getFullNoticeContent(title: String, info: String, url: String) { + CoroutineScope(Dispatchers.IO).launch { + crawlFullContentUseCase.getFullContentFromSource(title, info, url) + .map { Result.success(it) } + .catch { emit(Result.failure(it)) } + .collectLatest { result -> + result.fold( + onSuccess = { content -> + Log.d("Received Full Text: ", content.toString()) + _uiState.update { + it.copy( + isDetailedViewOpened = true, + requestedContent = content + ) + } + }, + onFailure = { + Log.d("Failed to received full text", it.message ?: "") + } + ) + } + } + } } @@ -146,6 +169,13 @@ data class CategorizedNotificationState( val notificationAcademic: List = listOf(Notice(), Notice(), Notice()), val notificationScholarship: List = listOf(Notice(), Notice(), Notice()), val notificationEvent: List = listOf(Notice(), Notice(), Notice()), - val isMoreOptionSelected: Boolean = false, - val categoryForMoreNotice: NoticeCategory = NoticeCategory.Unspecified + val isDetailedViewOpened: Boolean = false, + val requestedContent: DetailedContentState = DetailedContentState() +) + +data class DetailedContentState( + val title: String = "", + val info: String = "", + val fullContent: String = "", + val fullContentUrl: String = "" ) \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/MoreCategorizedNotificationViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/MoreCategorizedNotificationViewModel.kt index c7c36f0..1e506b5 100644 --- a/app/src/main/java/com/doyoonkim/knutice/viewModel/MoreCategorizedNotificationViewModel.kt +++ b/app/src/main/java/com/doyoonkim/knutice/viewModel/MoreCategorizedNotificationViewModel.kt @@ -2,6 +2,7 @@ package com.doyoonkim.knutice.viewModel import android.util.Log import androidx.lifecycle.ViewModel +import com.doyoonkim.knutice.domain.CrawlFullContentImpl import com.doyoonkim.knutice.domain.FetchNoticesPerPageInCategory import com.doyoonkim.knutice.model.Notice import com.doyoonkim.knutice.model.NoticeCategory @@ -12,6 +13,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -19,7 +21,8 @@ import javax.inject.Inject @HiltViewModel class MoreCategorizedNotificationViewModel @Inject constructor( - private val fetchListOfNoticesUseCase: FetchNoticesPerPageInCategory + private val fetchListOfNoticesUseCase: FetchNoticesPerPageInCategory, + private val crawlFullContentUseCase: CrawlFullContentImpl ): ViewModel() { private val filename = "MoreCategorizedNotificationViewModel" @@ -43,6 +46,24 @@ class MoreCategorizedNotificationViewModel @Inject constructor( } } + fun updatedDetailedContentRequest( + isDetailedContentVisible: Boolean, + title: String = "", + info: String = "", + url: String = "" + ) { + if (isDetailedContentVisible) { + getFullContent(title, info, url) + } else { + _uiState.update { + it.copy( + isDetailedContentVisible = false, + detailedContentState = DetailedContentState() + ) + } + } + } + fun fetchNotificationPerPage() { _uiState.update { it.copy(isLoading = true) @@ -80,6 +101,30 @@ class MoreCategorizedNotificationViewModel @Inject constructor( } } + private fun getFullContent(title: String, info: String, url: String) { + CoroutineScope(Dispatchers.IO).launch { + crawlFullContentUseCase.getFullContentFromSource(title, info, url) + .map { Result.success(it) } + .catch { emit(Result.failure(it)) } + .flowOn(Dispatchers.IO) + .collectLatest { result -> + result.fold( + onSuccess = { content -> + _uiState.update { + it.copy( + isDetailedContentVisible = true, + detailedContentState = content + ) + } + }, + onFailure = { + Log.d("MoreCategorizedNotificationViewModel", it.stackTraceToString()) + } + ) + } + } + } + private fun List.addAll(additionalElements: List): List { return List(this.size + additionalElements.size) { if (it in indices) this[it] @@ -94,5 +139,7 @@ data class MoreNotificationListState( val notificationCategory: NoticeCategory = NoticeCategory.Unspecified, val notices: List = List(20) { Notice() }, val isLoading: Boolean = false, - val isRefreshRequested: Boolean = false + val isRefreshRequested: Boolean = false, + val isDetailedContentVisible: Boolean = false, + val detailedContentState: DetailedContentState = DetailedContentState() ) \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_close_24.xml b/app/src/main/res/drawable/baseline_close_24.xml new file mode 100644 index 0000000..f8ca0c6 --- /dev/null +++ b/app/src/main/res/drawable/baseline_close_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100755 index 0000000..345888d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000..e489eba Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100755 index 0000000..08751a9 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100755 index 0000000..e9e8c94 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100755 index 0000000..e9e8c94 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 0000000..094bf13 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100755 index 0000000..2257090 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100755 index 0000000..ee4cc30 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png new file mode 100755 index 0000000..ee4cc30 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 0000000..56a7cf7 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100755 index 0000000..afd4662 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100755 index 0000000..0495400 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png new file mode 100755 index 0000000..0495400 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 0000000..11e4723 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100755 index 0000000..6be412a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100755 index 0000000..e7141d5 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png new file mode 100755 index 0000000..e7141d5 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000..38bea8d Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100755 index 0000000..eddfa1e Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100755 index 0000000..9a0d31a Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png new file mode 100755 index 0000000..9a0d31a Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 219e78b..c4999d7 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -6,4 +6,13 @@ 장학안내 행사안내 더보기 + 브라우저에서 더보기 + 서비스 알림 + 새로운 공지사항이 등록되면 푸시 알림으로 알려드려요. + 더보기 + 버전정보 + 오픈소스 라이센스 + 1.0.0 알파 (내부테스트용) + 알림 + 설정 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 669e095..4990963 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,13 @@ Scholarship Event More + View More on Browser + Enable service notification + Allow push notification when a new notice is posted. + About + Version + Open Source License + 1.0.0 Alpha + Notification + Preference \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7b322b..cc1524a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ converterMoshi = "2.9.0" gson = "2.11.0" hiltAndroidCompiler = "2.51.1" hiltNavigationCompose = "1.2.0" +jsoup = "1.18.1" kotlin = "1.9.0" coreKtx = "1.10.1" junit = "4.13.2" @@ -34,6 +35,7 @@ converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.r gson = { module = "com.google.code.gson:gson", version.ref = "gson" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroidCompiler" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" } +jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }