diff --git a/.github/workflows/GenerateBuildArtifact.yml b/.github/workflows/GenerateBuildArtifact.yml index 4a0a137e..0c81bd26 100644 --- a/.github/workflows/GenerateBuildArtifact.yml +++ b/.github/workflows/GenerateBuildArtifact.yml @@ -2,7 +2,7 @@ name: Generate Build Artifact on: pull_request: - branches: ["main", "release"] + branches: ["release"] jobs: build: @@ -37,10 +37,10 @@ jobs: run: ./gradlew clean - name: Build with Gradle - run: ./gradlew + run: ./gradlew build - - name: Generate and upload a Build Artifact - uses: actions/upload-artifact@v3.1.3 - with: - name: KNUTICE_RC.apk - path: app/build/outputs/apk/debug/app-debug.apk \ No newline at end of file +# - name: Generate and upload a Build Artifact +# uses: actions/upload-artifact@v3.1.3 +# with: +# name: KNUTICE_RC.apk +# path: app/build/outputs/apk/debug/app-debug.apk \ No newline at end of file diff --git a/.github/workflows/ValidateDevelopmentForRelease.yml b/.github/workflows/ValidateDevelopmentForRelease.yml deleted file mode 100644 index 88984bd3..00000000 --- a/.github/workflows/ValidateDevelopmentForRelease.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Validate Development for Release - -on: - push: - branches: ["development"] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4.1.1 - - - name: Setup Java JDk - uses: actions/setup-java@v3.0.0 - with: - java-version: '17' - distribution: 'adopt' - - - name: Change permissions of Wrapper - run: chmod +x ./gradlew - - - name: Decode BASEURL - env: - BASEURL: ${{ secrets.BASEURL }} - run: echo BASEURL="$BASEURL" > ./local.properties - - - name: Create Necessary File for Building Project - run: cat /home/runner/work/KNUTICE-Android/KNUTICE-Android/app/google-services.json | base64 - - - name: Putting Data into Created File. - env: - DATA: ${{ secrets.GOOGLE_SERVICES_JSON }} - run: echo $DATA > /home/runner/work/KNUTICE-Android/KNUTICE-Android/app/google-services.json - - - name: Clean Project - run: ./gradlew clean - - - name: Build with Gradle - run: ./gradlew build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 36c4c5ba..4035c501 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,6 +38,12 @@ android { } buildConfigField("String", "API_ROOT", "\"$apiRoot\"") + + javaCompileOptions { + annotationProcessorOptions { + arguments["room.schemaLocation"] = "$projectDir/schemas" + } + } } buildTypes { 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 b20f5584..7bd060ae 100644 --- a/app/src/main/java/com/doyoonkim/knutice/data/NoticeLocalRepository.kt +++ b/app/src/main/java/com/doyoonkim/knutice/data/NoticeLocalRepository.kt @@ -1,22 +1,16 @@ package com.doyoonkim.knutice.data -import android.content.Context import androidx.annotation.WorkerThread import com.doyoonkim.knutice.data.local.KnuticeLocalSource -import com.doyoonkim.knutice.data.local.LocalDatabase -import com.doyoonkim.knutice.domain.NoticeDummySource import com.doyoonkim.knutice.model.Bookmark import com.doyoonkim.knutice.model.Notice import com.doyoonkim.knutice.model.NoticeCategory +import com.doyoonkim.knutice.model.NoticeEntity import com.doyoonkim.knutice.model.NoticesPerPage -import com.doyoonkim.knutice.model.TopThreeNotices -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ActivityRetainedScoped import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import javax.inject.Inject @@ -34,8 +28,12 @@ class NoticeLocalRepository @Inject constructor( private val localSource: KnuticeLocalSource ) { // Local - fun createBookmark(bookmark: Bookmark) { - localSource.createBookmark(bookmark) + fun createBookmark(bookmark: Bookmark): Result { + return localSource.createBookmark(bookmark) + } + + fun createBookmark(bookmark: Bookmark, targetNotice: Notice): Result { + return localSource.createBookmark(bookmark, targetNotice) } fun updateBookmark(bookmark: Bookmark) { @@ -46,19 +44,29 @@ class NoticeLocalRepository @Inject constructor( localSource.deleteBookmark(bookmark) } - fun getAllBookmarks(): Flow> { + fun deleteNoticeEntity(entity: NoticeEntity) { + localSource.deleteNoticeEntity(entity) + } + + fun getAllBookmarks(): Flow { return flow { - localSource.getAllBookmarks().fold( - onSuccess = { - emit(it) - }, - onFailure = { - emit(emptyList()) - } - ) + runCatching { + localSource.getAllBookmarks().forEach { emit(it) } + }.onFailure { emit(Bookmark(-1)) } } } + fun getNoticeByNttId(nttId: Int): Notice { + val noticeEntity = localSource.getNoticeByNttId(nttId) + return noticeEntity.toNotice() + } + + fun getBookmarkByNttID(nttId: Int): Result { + return runCatching { + localSource.getBookmarkByNttId(nttId) ?: Bookmark(-1) + }.onFailure { throw it } + } + // Remote @WorkerThread fun getTopThreeNotice(category: NoticeCategory): Flow { diff --git a/app/src/main/java/com/doyoonkim/knutice/data/local/KnuticeLocalSource.kt b/app/src/main/java/com/doyoonkim/knutice/data/local/KnuticeLocalSource.kt index 36f7d9c2..95ac77e0 100644 --- a/app/src/main/java/com/doyoonkim/knutice/data/local/KnuticeLocalSource.kt +++ b/app/src/main/java/com/doyoonkim/knutice/data/local/KnuticeLocalSource.kt @@ -2,8 +2,9 @@ package com.doyoonkim.knutice.data.local import android.content.Context import com.doyoonkim.knutice.model.Bookmark +import com.doyoonkim.knutice.model.Notice +import com.doyoonkim.knutice.model.NoticeEntity import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -25,19 +26,27 @@ class KnuticeLocalSource @Inject constructor( } } - fun updateBookmark(bookmark: Bookmark): Result { + fun createBookmark(bookmark: Bookmark, targetNotice: Notice): Result { return runCatching { - localDatabase.getDao().updateBookmark(bookmark) + // Insert Notice First. + localDatabase.getDao().createNoticeEntity(targetNotice.toNoticeEntity()) + // Insert Bookmark Entity + localDatabase.getDao().createBookmark(bookmark) true }.onFailure { throw it } } - fun getAllBookmarks(): Result> { + fun updateBookmark(bookmark: Bookmark): Result { return runCatching { - localDatabase.getDao().getAllBookmarks() + localDatabase.getDao().updateBookmark(bookmark) + true }.onFailure { throw it } } + fun getAllBookmarks(): List { + return localDatabase.getDao().getAllBookmarks() + } + fun deleteBookmark(bookmark: Bookmark): Result { return runCatching { localDatabase.getDao().deleteBookmark(bookmark) @@ -45,4 +54,19 @@ class KnuticeLocalSource @Inject constructor( }.onFailure { throw it } } + fun deleteNoticeEntity(entity: NoticeEntity): Result { + return runCatching { + localDatabase.getDao().deleteNoticeEntity(entity) + true + }.onFailure { throw it } + } + + fun getNoticeByNttId(nttId: Int): NoticeEntity { + return localDatabase.getDao().getNoticeByNttId(nttId) + } + + fun getBookmarkByNttId(nttId: Int): Bookmark? { + return localDatabase.getDao().getBookmarkByNttId(nttId) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/data/local/MainDatabaseDao.kt b/app/src/main/java/com/doyoonkim/knutice/data/local/MainDatabaseDao.kt index bf58d603..c8606812 100644 --- a/app/src/main/java/com/doyoonkim/knutice/data/local/MainDatabaseDao.kt +++ b/app/src/main/java/com/doyoonkim/knutice/data/local/MainDatabaseDao.kt @@ -7,6 +7,7 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update import com.doyoonkim.knutice.model.Bookmark +import com.doyoonkim.knutice.model.NoticeEntity @Dao interface MainDatabaseDao { @@ -16,9 +17,22 @@ interface MainDatabaseDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun createBookmark(entity: Bookmark) + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun createNoticeEntity(entity: NoticeEntity) + @Update fun updateBookmark(updated: Bookmark) @Delete fun deleteBookmark(target: Bookmark) + + @Delete + fun deleteNoticeEntity(target: NoticeEntity) + + @Query("SELECT * FROM NoticeEntity WHERE ntt_id=:nttId") + fun getNoticeByNttId(nttId: Int): NoticeEntity + + @Query("SELECT * FROM Bookmark WHERE target_ntt_id=:nttId") + fun getBookmarkByNttId(nttId: Int): Bookmark? + } \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/FetchBookmarkFromDatabase.kt b/app/src/main/java/com/doyoonkim/knutice/domain/FetchBookmarkFromDatabase.kt new file mode 100644 index 00000000..fe17d422 --- /dev/null +++ b/app/src/main/java/com/doyoonkim/knutice/domain/FetchBookmarkFromDatabase.kt @@ -0,0 +1,20 @@ +package com.doyoonkim.knutice.domain + +import com.doyoonkim.knutice.data.NoticeLocalRepository +import com.doyoonkim.knutice.model.Bookmark +import com.doyoonkim.knutice.model.Notice +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class FetchBookmarkFromDatabase @Inject constructor( + private val repository: NoticeLocalRepository +) { + + fun getAllBookmarkWithNotices(): Flow> { + return repository.getAllBookmarks().map { + Pair(it, repository.getNoticeByNttId(it.nttId)) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/domain/FetchTopThreeNoticeByCategory.kt b/app/src/main/java/com/doyoonkim/knutice/domain/FetchTopThreeNoticeByCategory.kt index c7e092c6..c55e4689 100644 --- a/app/src/main/java/com/doyoonkim/knutice/domain/FetchTopThreeNoticeByCategory.kt +++ b/app/src/main/java/com/doyoonkim/knutice/domain/FetchTopThreeNoticeByCategory.kt @@ -55,6 +55,7 @@ class FetchTopThreeNoticeByCategory @Inject constructor ( private fun ArrayList.toNotice(): List { return List(3) { index -> Notice( + nttId = this[index].nttId ?: -1, title = this[index].title ?: "Unknown", url = this[index].contentUrl ?: "Unknown", departName = this[index].departName ?: "Unknown", diff --git a/app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt b/app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt index 5c856979..a9efebb3 100644 --- a/app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt +++ b/app/src/main/java/com/doyoonkim/knutice/model/DataWrappers.kt @@ -2,6 +2,7 @@ package com.doyoonkim.knutice.model import androidx.room.Entity import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable data class Result( @@ -81,6 +82,7 @@ data class ManageTopicRequest( // Data class to be applied to uiState. // Universal +@Serializable data class Notice( val nttId: Int = -1, val title: String = "Unknown", @@ -88,7 +90,28 @@ data class Notice( val imageUrl: String = "", val departName: String = "Unknown", val timestamp: String = "Unknown" -) +) { + fun toFullContent(): FullContent { + return FullContent( + title, + "[$departName] $timestamp", + url, + imageUrl + ) + } + + fun toNoticeEntity(): NoticeEntity { + return NoticeEntity( + noticeEntityId = 0, + nttId = nttId, + title = title, + url = url, + imageUrl = imageUrl, + departName = departName, + timestamp = timestamp + ) + } +} // DetailedNoticeContent data class DetailedContentState( @@ -105,6 +128,7 @@ data class DetailedContentState( data class CustomerServiceReportState( val userReport: String = "", val reachedMaxCharacters: Boolean = false, + val exceedMinCharacters: Boolean = false, val isSubmissionFailed: Boolean = false, val isSubmissionCompleted: Boolean = false ) @@ -125,8 +149,18 @@ data class NotificationPreferenceStatus( // BookmarkComposable data class BookmarkComposableState( - val bookmarks: List = emptyList(), + val bookmarks: List> = emptyList(), val isRefreshing: Boolean = false, val isRefreshRequested: Boolean = false ) +// EditBookmark +data class EditBookmarkState( + val bookmarkId: Int = 0, + val targetNotice: Notice = Notice(), + val isReminderRequested: Boolean = false, + val timeForRemind: Long = 0, // Should be replaced with an appropriate data type later. + val bookmarkNote: String = "", + val requireCreation: Boolean = true +) + diff --git a/app/src/main/java/com/doyoonkim/knutice/model/RoomEntities.kt b/app/src/main/java/com/doyoonkim/knutice/model/RoomEntities.kt index c09ac46d..1c18f1ea 100644 --- a/app/src/main/java/com/doyoonkim/knutice/model/RoomEntities.kt +++ b/app/src/main/java/com/doyoonkim/knutice/model/RoomEntities.kt @@ -6,22 +6,26 @@ import androidx.room.PrimaryKey @Entity data class NoticeEntity( - @PrimaryKey(autoGenerate = true) val id: Int, + @PrimaryKey(autoGenerate = true) val noticeEntityId: Int, @ColumnInfo("ntt_id") val nttId: Int = -1, @ColumnInfo("notice_title") val title: String, @ColumnInfo("notice_url") val url: String, @ColumnInfo("notice_image") val imageUrl: String, @ColumnInfo("info_dept") val departName: String, @ColumnInfo("info_timestamp") val timestamp: String -) +) { + fun toNotice(): Notice { + return Notice(nttId, title, url, imageUrl, departName, timestamp) + } +} @Entity data class Bookmark( @PrimaryKey(autoGenerate = true) val bookmarkId: Int, - @ColumnInfo("bookmarked_notice") val noticeNttId: String, - @ColumnInfo("isScheduled") val isScheduled: Boolean, - @ColumnInfo("remind_schedule") val reminderSchedule: String, - @ColumnInfo("bookmark_note") val note: String + @ColumnInfo("isScheduled") val isScheduled: Boolean = false, + @ColumnInfo("remind_schedule") val reminderSchedule: Long = 0, + @ColumnInfo("bookmark_note") val note: String = "", + @ColumnInfo("target_ntt_id") val nttId: Int = -1 ) /* diff --git a/app/src/main/java/com/doyoonkim/knutice/model/Types.kt b/app/src/main/java/com/doyoonkim/knutice/model/Types.kt index 1528e123..08dee5c5 100644 --- a/app/src/main/java/com/doyoonkim/knutice/model/Types.kt +++ b/app/src/main/java/com/doyoonkim/knutice/model/Types.kt @@ -4,7 +4,8 @@ import kotlinx.serialization.Serializable enum class NoticeCategory { GENERAL_NEWS, ACADEMIC_NEWS, SCHOLARSHIP_NEWS, EVENT_NEWS, Unspecified } -enum class Destination { MAIN, MORE_GENERAL, MORE_ACADEMIC, MORE_SCHOLARSHIP, MORE_EVENT, DETAILED, SETTINGS, OSS, CS, SEARCH, NOTIFICATION, BOOKMARKS, Unspecified } +enum class Destination { MAIN, MORE_GENERAL, MORE_ACADEMIC, MORE_SCHOLARSHIP, MORE_EVENT, DETAILED, + SETTINGS, OSS, CS, SEARCH, NOTIFICATION, BOOKMARKS, EDIT_BOOKMARK, Unspecified } // Navigation Destinations @Serializable 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 1b0d1c30..d25f0026 100644 --- a/app/src/main/java/com/doyoonkim/knutice/navigation/MainNavigator.kt +++ b/app/src/main/java/com/doyoonkim/knutice/navigation/MainNavigator.kt @@ -1,5 +1,7 @@ package com.doyoonkim.knutice.navigation +import android.util.Log +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height @@ -15,15 +17,17 @@ import androidx.navigation.toRoute import com.doyoonkim.knutice.model.Destination import com.doyoonkim.knutice.model.FullContent import com.doyoonkim.knutice.model.NavDestination +import com.doyoonkim.knutice.model.Notice import com.doyoonkim.knutice.presentation.BookmarkComposable import com.doyoonkim.knutice.presentation.CategorizedNotification import com.doyoonkim.knutice.presentation.CustomerService import com.doyoonkim.knutice.presentation.DetailedNoticeContent +import com.doyoonkim.knutice.presentation.EditBookmark import com.doyoonkim.knutice.presentation.MoreCategorizedNotification import com.doyoonkim.knutice.presentation.NotificationPreferences import com.doyoonkim.knutice.presentation.OpenSourceLicenseNotice import com.doyoonkim.knutice.presentation.UserPreference -import com.doyoonkim.knutice.presentation.component.SearchNotice +import com.doyoonkim.knutice.presentation.SearchNotice import com.doyoonkim.knutice.viewModel.MainActivityViewModel @Composable @@ -44,12 +48,28 @@ fun MainNavigator( updatedCurrentLocation = destination.arrived ) - when (destination.arrived) { - Destination.MAIN -> CategorizedNotification( - onGoBackAction = { navController.popBackStack() }, - onMoreNoticeRequested = { navController.navigate(NavDestination(arrived = it)) }, - onFullContentRequested = { navController.navigate(it) } + if (destination.arrived == Destination.CS + || destination.arrived == Destination.OSS) { + viewModel.updateState( + updatedBottomNavBarVisibility = false + ) + } else { + viewModel.updateState( + updatedBottomNavBarVisibility = true ) + } + + when (destination.arrived) { + Destination.MAIN -> { + CategorizedNotification( + onGoBackAction = { /* Disable Swipe-to-Back on Main Page of the App */ }, + onMoreNoticeRequested = { navController.navigate(NavDestination(arrived = it)) }, + onFullContentRequested = { + viewModel.updateState(updatedTempReservedNoticeForBookmark = it) + navController.navigate(it.toFullContent()) + } + ) + } Destination.SETTINGS -> UserPreference( Modifier.padding(top = 20.dp, start = 10.dp, end = 10.dp).fillMaxSize(), onCustomerServiceClicked = { navController.navigate(NavDestination(it))}, @@ -57,33 +77,55 @@ fun MainNavigator( navController.navigate(NavDestination(it)) } Destination.OSS -> OpenSourceLicenseNotice() - Destination.CS -> CustomerService(Modifier.padding(15.dp)) + Destination.CS -> { CustomerService(Modifier.padding(15.dp)) } Destination.SEARCH -> SearchNotice( onBackClicked = { navController.popBackStack() }, - onNoticeClicked = { navController.navigate(it) } + onNoticeClicked = { + viewModel.updateState(updatedTempReservedNoticeForBookmark = it) + navController.navigate(it.toFullContent()) + } ) Destination.NOTIFICATION -> NotificationPreferences( onBackClicked = { navController.popBackStack() }, onMainNotificationSwitchToggled = { } ) Destination.BOOKMARKS -> BookmarkComposable( - Modifier.fillMaxSize() + modifier =Modifier.fillMaxSize(), + onEachItemClicked = { navController.navigate(it) }, + onBackPressed = { /* Disable swipe-to-back on BOOKMARKS composable of the app */ } ) else -> MoreCategorizedNotification( backButtonHandler = { navController.popBackStack() }, - onNoticeSelected = { navController.navigate(it) } + onNoticeSelected = { + viewModel.updateState(updatedTempReservedNoticeForBookmark = it) + navController.navigate(it.toFullContent()) + } ) } } composable { backStackEntry -> - val scaffoldTitle = backStackEntry.toRoute().title + val requestedNotice = backStackEntry.toRoute() + val scaffoldTitle = requestedNotice.title viewModel.updateState( updatedCurrentLocation = Destination.DETAILED, - updatedCurrentScaffoldTitle = scaffoldTitle ?: "Full Content" + updatedCurrentScaffoldTitle = scaffoldTitle ?: "Full Content", + updatedBottomNavBarVisibility = true ) DetailedNoticeContent() Spacer(Modifier.height(20.dp)) } + + composable { + viewModel.updateState( + updatedCurrentLocation = Destination.EDIT_BOOKMARK, + updatedBottomNavBarVisibility = false + ) + EditBookmark(Modifier.fillMaxSize().padding(10.dp)) { + // onSavedClicked + navController.navigate(NavDestination(Destination.BOOKMARKS)) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/BookmarkComposable.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/BookmarkComposable.kt index c8764354..1d348a2c 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/BookmarkComposable.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/BookmarkComposable.kt @@ -1,46 +1,87 @@ package com.doyoonkim.knutice.presentation +import android.util.Log +import androidx.activity.compose.BackHandler 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.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import com.doyoonkim.knutice.R +import com.doyoonkim.knutice.model.Notice import com.doyoonkim.knutice.presentation.component.NotificationPreviewCardMarked import com.doyoonkim.knutice.ui.theme.displayBackground +import com.doyoonkim.knutice.ui.theme.subTitle import com.doyoonkim.knutice.viewModel.BookmarkViewModel @Composable fun BookmarkComposable( modifier: Modifier = Modifier, - viewModel: BookmarkViewModel = hiltViewModel() + viewModel: BookmarkViewModel = hiltViewModel(), + onEachItemClicked: (Notice) -> Unit = { }, + onBackPressed: () -> Unit = { } ) { + val uiState by viewModel.uiState.collectAsState() + + BackHandler { + onBackPressed() + } + +// LaunchedEffect(uiState.bookmarks) { +// viewModel.getAllBookmarks() +// } + Box( modifier = modifier.background(MaterialTheme.colorScheme.displayBackground) .systemBarsPadding() ) { - LazyColumn( - modifier = Modifier.wrapContentHeight() - .fillMaxWidth() - .background(Color.Transparent), - verticalArrangement = Arrangement.SpaceBetween, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - items(1) { - NotificationPreviewCardMarked() + if (uiState.bookmarks.isEmpty()) { + Text( + text = stringResource(R.string.text_no_bookmark), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.subTitle, + modifier = Modifier.align(Alignment.Center) + ) + } else { + LazyColumn( + modifier = Modifier.wrapContentHeight() + .fillMaxWidth() + .padding(top = 12.dp) + .background(Color.Transparent), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + items( + items = uiState.bookmarks, + key = { it.second.nttId } + ) { + // Being called 3 times + Log.d("BookmarkComposable", "Element: $it") + NotificationPreviewCardMarked( + noticeTitle = it.second.title, + noticeSubtitle = "[${it.second.departName}] ${it.second.timestamp}", + onItemClicked = { onEachItemClicked(it.second) } + ) + } } } } 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 a8112fcf..255f8167 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/CategorizedNoficiation.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/CategorizedNoficiation.kt @@ -1,19 +1,21 @@ package com.doyoonkim.knutice.presentation import android.content.res.Configuration -import android.util.Log import androidx.activity.compose.BackHandler -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues 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.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -44,7 +46,7 @@ fun CategorizedNotification( viewModel: CategorizedNotificationViewModel = hiltViewModel(), onGoBackAction: () -> Unit, onMoreNoticeRequested: (Destination) -> Unit, - onFullContentRequested: (FullContent) -> Unit + onFullContentRequested: (Notice) -> Unit ) { val uiState by viewModel.uiState.collectAsState() @@ -65,8 +67,8 @@ fun CategorizedNotification( titleColor = MaterialTheme.colorScheme.notificationType1, contents = uiState.notificationGeneral, onMoreClicked = { onMoreNoticeRequested(Destination.MORE_GENERAL) } - ) { title, info, url, imgUrl -> - onFullContentRequested(FullContent(title, info, url, imgUrl)) + ) { + onFullContentRequested(it) } NotificationPreviewList( @@ -74,8 +76,8 @@ fun CategorizedNotification( titleColor = MaterialTheme.colorScheme.notificationType2, contents = uiState.notificationAcademic, onMoreClicked = { onMoreNoticeRequested(Destination.MORE_ACADEMIC) } - ) { title, info, url, imgUrl -> - onFullContentRequested(FullContent(title, info, url, imgUrl)) + ) { + onFullContentRequested(it) } NotificationPreviewList( @@ -83,8 +85,8 @@ fun CategorizedNotification( titleColor = MaterialTheme.colorScheme.notificationType3, contents = uiState.notificationScholarship, onMoreClicked = { onMoreNoticeRequested(Destination.MORE_SCHOLARSHIP) } - ) { title, info, url, imgUrl -> - onFullContentRequested(FullContent(title, info, url, imgUrl)) + ) { + onFullContentRequested(it) } NotificationPreviewList( @@ -92,8 +94,8 @@ fun CategorizedNotification( titleColor = MaterialTheme.colorScheme.notificationType4, contents = uiState.notificationEvent, onMoreClicked = { onMoreNoticeRequested(Destination.MORE_EVENT) } - ) { title, info, url, imgUrl -> - onFullContentRequested(FullContent(title, info, url, imgUrl)) + ) { + onFullContentRequested(it) } } } @@ -105,7 +107,7 @@ fun NotificationPreviewList( titleColor: Color = Color.Unspecified, contents: List = listOf(), onMoreClicked: () -> Unit = { }, - onNoticeClicked: (String, String, String, String) -> Unit + onNoticeClicked: (Notice) -> Unit ) { Column( modifier = Modifier.fillMaxWidth() @@ -114,9 +116,11 @@ fun NotificationPreviewList( horizontalAlignment = Alignment.CenterHorizontally ) { Row( - Modifier.fillMaxWidth() + Modifier.fillMaxWidth( + + ) .wrapContentHeight(), - verticalAlignment = Alignment.Bottom, + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Text( @@ -127,33 +131,36 @@ fun NotificationPreviewList( fontWeight = FontWeight.Bold ) - Text( - modifier = Modifier.fillMaxWidth().weight(1f) - .clickable { onMoreClicked() }, - text = stringResource(R.string.btn_more), - color = MaterialTheme.colorScheme.subTitle, - fontSize = 13.sp, - fontWeight = FontWeight.Medium - ) + TextButton( + modifier = Modifier.fillMaxWidth().weight(1f), + onClick = { onMoreClicked() }, + contentPadding = PaddingValues(0.dp) + ) { + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = stringResource(R.string.btn_more), + color = MaterialTheme.colorScheme.subTitle, + fontWeight = FontWeight.Medium + ) + } } contents.forEach { content -> NotificationPreviewCard( notificationTitle = content.title, notificationInfo = "[${content.departName}] ${content.timestamp}" ) { - onNoticeClicked( - content.title, - "[${content.departName}] ${content.timestamp}", - content.url, - content.imageUrl - ) + onNoticeClicked(content) } } } } @Composable -@Preview(showBackground = true, showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, showSystemUi = false, uiMode = Configuration.UI_MODE_NIGHT_YES) fun CategorizedNotification_Preview() { + NotificationPreviewList( + contents = listOf(Notice(), Notice(), Notice()) + ) { + } } \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/CustomerService.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/CustomerService.kt index 9f55edca..782b8728 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/CustomerService.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/CustomerService.kt @@ -110,7 +110,7 @@ fun CustomerService( Button( modifier = Modifier.fillMaxWidth().wrapContentHeight(), - enabled = !uiState.isSubmissionCompleted && uiState.userReport.isNotBlank(), + enabled = !uiState.isSubmissionCompleted && uiState.exceedMinCharacters, shape = RoundedCornerShape(10.dp), onClick = { viewModel.submitUserReport() } ) { diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/EditBookmark.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/EditBookmark.kt new file mode 100644 index 00000000..2ec2e569 --- /dev/null +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/EditBookmark.kt @@ -0,0 +1,227 @@ +package com.doyoonkim.knutice.presentation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +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 androidx.hilt.navigation.compose.hiltViewModel +import com.doyoonkim.knutice.R +import com.doyoonkim.knutice.presentation.component.NotificationPreviewCard +import com.doyoonkim.knutice.ui.theme.containerBackground +import com.doyoonkim.knutice.ui.theme.notificationType1 +import com.doyoonkim.knutice.ui.theme.subTitle +import com.doyoonkim.knutice.ui.theme.title +import com.doyoonkim.knutice.viewModel.EditBookmarkViewModel + +@Composable +fun EditBookmark( + modifier: Modifier = Modifier, + viewModel: EditBookmarkViewModel = hiltViewModel(), + onSaveClicked: () -> Unit = { } +) { + val uiState by viewModel.uiState.collectAsState() + val localContext = LocalContext.current + + Column( + modifier = modifier + ) { + NotificationPreviewCard( + modifier = Modifier.padding(5.dp), + notificationTitle = uiState.targetNotice.title, + notificationInfo = uiState.targetNotice.departName + ) + + Spacer(Modifier.height(30.dp)) + + Text( + text = stringResource(R.string.subtitle_set_reminder), + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.title + ) + + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight() + .background(Color.Transparent), + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally + ) { + var notSupportedMessageShowed by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight() + .background(Color.Transparent) + .clip(RoundedCornerShape(10.dp)) + .border(2.dp, MaterialTheme.colorScheme.containerBackground) + .padding(start = 10.dp, end = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.subtitle_get_reminder), + textAlign = TextAlign.Start, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.title, + modifier = Modifier.padding(10.dp).weight(5f) + ) + + Switch( + checked = false, + enabled = true, + modifier = Modifier.padding(10.dp).weight(1f), + onCheckedChange = { notSupportedMessageShowed = true } + ) + } + + AnimatedVisibility( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + visible = notSupportedMessageShowed, + enter = slideInVertically() + ) { + Text( + text = stringResource(R.string.text_not_supported), + textAlign = TextAlign.Start, + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.notificationType1, + modifier = Modifier.padding(start = 10.dp, end = 10.dp) + ) + } + } + + Spacer(Modifier.height(15.dp)) + + Text( + text = stringResource(R.string.subtitle_notes), + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.title + ) + + Box( + modifier = Modifier.fillMaxWidth().weight(5f) + .padding(top = 5.dp, bottom = 25.dp) + ) { + TextField( + modifier = Modifier.fillMaxSize(), + value = uiState.bookmarkNote, + placeholder = { Text(text = stringResource(R.string.placeholder_notes)) }, + enabled = true, + onValueChange = { + viewModel.updateBookmarkNotes(it) + }, + colors = TextFieldDefaults.colors( + focusedTextColor = MaterialTheme.colorScheme.title, + unfocusedTextColor = MaterialTheme.colorScheme.subTitle, + focusedContainerColor = MaterialTheme.colorScheme.containerBackground, + unfocusedContainerColor = MaterialTheme.colorScheme.containerBackground, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp) + ) + + Text( + text = "${uiState.bookmarkNote.length}/500", + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.subTitle, + modifier = Modifier.wrapContentSize() + .padding(15.dp) + .align(Alignment.BottomEnd) + ) + } + + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + Button( + modifier = Modifier.wrapContentHeight().weight(1f), + enabled = true, + shape = RoundedCornerShape(10.dp), + onClick = { + if (!uiState.requireCreation) { + viewModel.modifyBookmark() + } else { + viewModel.createNewBookmark() + } + onSaveClicked() + } + ) { + Text( + text = stringResource(R.string.btn_save), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp) + ) + } + + if (!uiState.requireCreation) { + OutlinedButton( + modifier = Modifier.wrapContentHeight().weight(1f), + enabled = true, + shape = RoundedCornerShape(10.dp), + onClick = { + viewModel.removeBookmark() + onSaveClicked() + } + ) { + Text( + text = stringResource(R.string.btn_delete), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp) + ) + } + } + } + + } +} + +@Preview(showBackground = true) +@Composable +fun EditBookmark_Preview() { + EditBookmark(Modifier.fillMaxSize().padding(10.dp)) { } +} + 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 caa52451..99385025 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/MainActivity.kt @@ -8,13 +8,19 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.BottomNavigationItem import androidx.compose.material.icons.Icons @@ -22,11 +28,11 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon +import androidx.compose.material.Icon // For Using BottomNavigationItem import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text +import androidx.compose.material.Text // For Using BottomNavigationItem import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -47,16 +53,17 @@ import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.rememberNavController +import com.doyoonkim.knutice.R import com.doyoonkim.knutice.model.Destination +import com.doyoonkim.knutice.model.NavDestination import com.doyoonkim.knutice.navigation.MainNavigator import com.doyoonkim.knutice.ui.theme.KNUTICETheme import com.doyoonkim.knutice.ui.theme.containerBackground +import com.doyoonkim.knutice.ui.theme.displayBackground import com.doyoonkim.knutice.ui.theme.notificationType1 +import com.doyoonkim.knutice.ui.theme.subTitle import com.doyoonkim.knutice.ui.theme.title import com.doyoonkim.knutice.viewModel.MainActivityViewModel -import com.doyoonkim.knutice.R -import com.doyoonkim.knutice.model.NavDestination -import com.doyoonkim.knutice.ui.theme.displayBackground import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -94,7 +101,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - +// applicationContext.deleteDatabase("Main Local Database") enableEdgeToEdge() setContent { KNUTICETheme { @@ -122,7 +129,9 @@ fun MainServiceScreen( val navController = rememberNavController() Scaffold( - modifier = Modifier.fillMaxSize().background(Color.Transparent), + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.displayBackground), topBar = { TopAppBar( title = { @@ -130,9 +139,9 @@ fun MainServiceScreen( modifier = Modifier.wrapContentSize(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically - ) { - if (mainAppState.currentLocation != Destination.MAIN) { + if (mainAppState.currentLocation != Destination.MAIN + && mainAppState.currentLocation != Destination.BOOKMARKS) { IconButton( onClick = { navController.popBackStack() @@ -163,6 +172,7 @@ fun MainServiceScreen( Destination.SEARCH -> stringResource(R.string.title_search) Destination.NOTIFICATION -> stringResource(R.string.title_notification_pref) Destination.BOOKMARKS -> stringResource(R.string.app_name) + Destination.EDIT_BOOKMARK -> stringResource(R.string.title_edit_bookmark) Destination.DETAILED -> mainAppState.currentScaffoldTitle Destination.Unspecified -> mainAppState.currentLocation.name }, @@ -175,7 +185,8 @@ fun MainServiceScreen( fontSize = 20.sp, fontWeight = FontWeight.Bold, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.title ) } }, @@ -216,68 +227,87 @@ fun MainServiceScreen( floatingActionButton = { if (mainAppState.currentLocation == Destination.DETAILED) { FloatingActionButton( - onClick = { } + onClick = { + if (mainAppState.tempReserveNoticeForBookmark.title.isNotBlank()) { + navController.navigate(mainAppState.tempReserveNoticeForBookmark) + } + } ) { Icon(Icons.Filled.Add, "Floating Action Button") } } }, bottomBar = { - BottomAppBar( - modifier = Modifier - .background(Color.Transparent) + AnimatedVisibility( + visible = mainAppState.isBottomNavBarVisible, + enter = slideInVertically(initialOffsetY = { it + (it * 1/2) }), + exit = slideOutVertically(targetOffsetY = { it + (it * 1/2) }) + ) { + BottomAppBar( + modifier = Modifier + .background(Color.Transparent), // .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) // .offset(y = 20.dp) - , - actions = { - BottomNavigationItem( - selected = mainAppState.currentLocation == Destination.MAIN, - enabled = true, - onClick = { - navController.navigate(NavDestination(Destination.MAIN)) - }, - icon = { - Icon( - painter = painterResource(R.drawable.baseline_home_24), - contentDescription = "Main", - modifier = Modifier.padding(bottom = 5.dp) - ) - }, - label = { - Text("Home") - } - ) - BottomNavigationItem( - selected = mainAppState.currentLocation == Destination.BOOKMARKS, - enabled = true, - onClick = { - navController.navigate(NavDestination(Destination.BOOKMARKS)) - }, - icon = { - Icon( - painter = painterResource(R.drawable.baseline_bookmarks_24), - contentDescription = "Bookmarks", - modifier = Modifier.padding(bottom = 5.dp) - ) - }, - label = { - Text("Bookmarks") - } - ) - }, - containerColor = MaterialTheme.colorScheme.containerBackground, - contentColor = MaterialTheme.colorScheme.title, - ) + actions = { + BottomNavigationItem( + selected = mainAppState.currentLocation == Destination.MAIN, + enabled = true, + onClick = { + navController.navigate(NavDestination(Destination.MAIN)) + }, + icon = { + Icon( + painter = painterResource(R.drawable.baseline_home_24), + contentDescription = "Main", + modifier = Modifier.padding(bottom = 5.dp) + ) + }, + label = { + Text("Home") + }, + selectedContentColor = MaterialTheme.colorScheme.title, + unselectedContentColor = MaterialTheme.colorScheme.subTitle + ) + BottomNavigationItem( + selected = mainAppState.currentLocation == Destination.BOOKMARKS, + enabled = true, + onClick = { + navController.navigate(NavDestination(Destination.BOOKMARKS)) + }, + icon = { + Icon( + painter = painterResource(R.drawable.baseline_bookmarks_24), + contentDescription = "Bookmarks", + modifier = Modifier.padding(bottom = 5.dp) + ) + }, + label = { + Text("Bookmarks") + }, + selectedContentColor = MaterialTheme.colorScheme.title, + unselectedContentColor = MaterialTheme.colorScheme.subTitle + ) + }, + containerColor = MaterialTheme.colorScheme.containerBackground, + contentColor = MaterialTheme.colorScheme.title + ) + } + if (mainAppState.currentLocation != Destination.EDIT_BOOKMARK) { + + } }, containerColor = Color.Transparent ) { innerPadding -> val adjustmentFactor = 10.dp - MainNavigator(navController = navController, modifier = Modifier.padding( + MainNavigator(navController = navController, modifier = Modifier + .consumeWindowInsets(WindowInsets.systemBars) + .padding( PaddingValues( top = innerPadding.calculateTopPadding(), bottom = innerPadding.calculateBottomPadding() ) - ).background(MaterialTheme.colorScheme.displayBackground) + ) + .background(MaterialTheme.colorScheme.displayBackground) ) } } 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 c346f3e0..768de7d2 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/MoreCategorizedNoticiation.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.doyoonkim.knutice.model.FullContent +import com.doyoonkim.knutice.model.Notice import com.doyoonkim.knutice.presentation.component.NotificationPreview import com.doyoonkim.knutice.ui.theme.containerBackground import com.doyoonkim.knutice.ui.theme.subTitle @@ -39,7 +39,7 @@ fun MoreCategorizedNotification( modifier: Modifier = Modifier, viewModel: MoreCategorizedNotificationViewModel = hiltViewModel(), backButtonHandler: () -> Unit = { }, - onNoticeSelected: (FullContent) -> Unit = { } + onNoticeSelected: (Notice) -> Unit = { } ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -94,12 +94,7 @@ fun MoreCategorizedNotification( Row( modifier = Modifier.wrapContentSize() .clickable { - onNoticeSelected(FullContent( - notice.title, - "[${notice.departName}] ${notice.timestamp}", - notice.url, - notice.imageUrl - )) + onNoticeSelected(notice) } ) { NotificationPreview( diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/SearchNoticeComposable.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/SearchNoticeComposable.kt similarity index 92% rename from app/src/main/java/com/doyoonkim/knutice/presentation/component/SearchNoticeComposable.kt rename to app/src/main/java/com/doyoonkim/knutice/presentation/SearchNoticeComposable.kt index 00909bdf..ab1c114b 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/SearchNoticeComposable.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/SearchNoticeComposable.kt @@ -1,4 +1,4 @@ -package com.doyoonkim.knutice.presentation.component +package com.doyoonkim.knutice.presentation import android.content.res.Configuration import androidx.activity.compose.BackHandler @@ -37,7 +37,8 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.doyoonkim.knutice.R -import com.doyoonkim.knutice.model.FullContent +import com.doyoonkim.knutice.model.Notice +import com.doyoonkim.knutice.presentation.component.NotificationPreview import com.doyoonkim.knutice.ui.theme.containerBackground import com.doyoonkim.knutice.ui.theme.subTitle import com.doyoonkim.knutice.ui.theme.textPurple @@ -55,7 +56,7 @@ fun SearchNotice( modifier: Modifier = Modifier, viewModel: SearchNoticeViewModel = hiltViewModel(), onBackClicked: () -> Unit = { }, - onNoticeClicked: (FullContent) -> Unit + onNoticeClicked: (Notice) -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -123,12 +124,7 @@ fun SearchNotice( Row( modifier = Modifier.fillMaxWidth().wrapContentHeight() .clickable { - onNoticeClicked(FullContent( - title = notice.title, - info = "[${notice.departName}] ${notice.timestamp}", - url = notice.url, - imgUrl = notice.imageUrl - )) + onNoticeClicked(notice) } ) { NotificationPreview( diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCard.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCard.kt index 2e51ff4c..4e9dbb66 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCard.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCard.kt @@ -17,12 +17,13 @@ import com.doyoonkim.knutice.ui.theme.containerBackground @Composable fun NotificationPreviewCard( + modifier: Modifier = Modifier, notificationTitle: String = "Title goes here.", notificationInfo: String = "Notification info goes here.", onClick: () -> Unit = { /* Action should be defined. */ } ) { Card( - Modifier.fillMaxWidth() + modifier.fillMaxWidth() .wrapContentHeight() .clickable { onClick() diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCardMakred.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCardMakred.kt index 22ea2a9d..2017e6a0 100644 --- a/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCardMakred.kt +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/component/NotificationPreviewCardMakred.kt @@ -3,8 +3,6 @@ package com.doyoonkim.knutice.presentation.component import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable @@ -22,6 +20,7 @@ fun NotificationPreviewCardMarked( modifier: Modifier = Modifier, noticeTitle: String = "Title goes here", noticeSubtitle: String = "Subtitle goes here", + onItemClicked: () -> Unit = { }, onBackPressed: () -> Unit = { } ) { Box( @@ -32,7 +31,7 @@ fun NotificationPreviewCardMarked( notificationTitle = noticeTitle, notificationInfo = noticeSubtitle, ) { - + onItemClicked() } Image( painter = painterResource(R.drawable.baseline_bookmarks_24), diff --git a/app/src/main/java/com/doyoonkim/knutice/presentation/component/PopUpDialog.kt b/app/src/main/java/com/doyoonkim/knutice/presentation/component/PopUpDialog.kt new file mode 100644 index 00000000..81bc93cd --- /dev/null +++ b/app/src/main/java/com/doyoonkim/knutice/presentation/component/PopUpDialog.kt @@ -0,0 +1,82 @@ +package com.doyoonkim.knutice.presentation.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +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.Row +import androidx.compose.foundation.layout.fillMaxSize +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.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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun PopUpDialog( + modifier: Modifier = Modifier, + isVisible: Boolean = false, + onCloseClicked: () -> Unit = { }, + dialogContent: @Composable () -> Unit +) { + AnimatedVisibility( + modifier = Modifier.wrapContentSize().background(Color.Transparent), + visible = isVisible, + enter = slideInVertically(initialOffsetY = { it + it / 2 }), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + ) { + Box( + modifier = Modifier.fillMaxSize() + .clickable(false) { /* DO NOTHING HERE */ } + .background(Color.Transparent) + .padding(10.dp) + ) { + Column( + modifier = modifier.fillMaxWidth() + .wrapContentHeight() + .align(Alignment.BottomCenter), + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.clickable { onCloseClicked() }, + text = "Close" + ) + } + + Surface( + modifier = Modifier.fillMaxWidth().wrapContentHeight() + .background(Color.Transparent) + .clip(RoundedCornerShape(10.dp)) + ) { + dialogContent() + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun PopUpDialog_Preview() { + +} + diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/BookmarkViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/BookmarkViewModel.kt index 8ad7b424..26ec5919 100644 --- a/app/src/main/java/com/doyoonkim/knutice/viewModel/BookmarkViewModel.kt +++ b/app/src/main/java/com/doyoonkim/knutice/viewModel/BookmarkViewModel.kt @@ -1,18 +1,16 @@ package com.doyoonkim.knutice.viewModel -import android.content.Context import android.util.Log -import androidx.compose.runtime.MutableState import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.doyoonkim.knutice.data.NoticeLocalRepository +import com.doyoonkim.knutice.domain.FetchBookmarkFromDatabase import com.doyoonkim.knutice.model.Bookmark import com.doyoonkim.knutice.model.BookmarkComposableState +import com.doyoonkim.knutice.model.Notice import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest @@ -23,33 +21,43 @@ import javax.inject.Inject @HiltViewModel class BookmarkViewModel @Inject constructor( - private val localRepository: NoticeLocalRepository + private val localRepository: NoticeLocalRepository, + private val fetchBookmarkFromDatabase: FetchBookmarkFromDatabase ): ViewModel() { - // Prevent direct access to the UI STATE from the 'View' private var _uiState = MutableStateFlow(BookmarkComposableState()) val uiState = _uiState.asStateFlow() - fun updateBookmarks(bookmarks: List) { + init { + getAllBookmarks() + } + + fun updateBookmarks(newPair: Pair) { _uiState.update { it.copy( - bookmarks = bookmarks + bookmarks = uiState.value.bookmarks.toMutableList().apply { + this.add(newPair) + }.distinctBy { e -> e.first.bookmarkId }.toList() +// bookmarks = newList ) } + + Log.d("BookmarkViewModel", "Updated Bookmark Status: ${uiState.value.bookmarks}") } fun getAllBookmarks() { viewModelScope.launch(Dispatchers.IO) { - localRepository.getAllBookmarks() + fetchBookmarkFromDatabase.getAllBookmarkWithNotices() .map { Result.success(it) } .catch { emit(Result.failure(it)) } .collectLatest { result -> result.fold( onSuccess = { + Log.d("BookmarkViewModel", "Collected: ${it.toString()}") updateBookmarks(it) }, onFailure = { - Log.d("BookmarkViewModel", "Failed to fetch bookmarks") + Log.d("BookmarkViewModel", "Unable to receive bookmark pair.\nREASON:${it.message}") } ) } diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/CustomerServiceViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/CustomerServiceViewModel.kt index fc214300..663acc97 100644 --- a/app/src/main/java/com/doyoonkim/knutice/viewModel/CustomerServiceViewModel.kt +++ b/app/src/main/java/com/doyoonkim/knutice/viewModel/CustomerServiceViewModel.kt @@ -35,6 +35,7 @@ class CustomerServiceViewModel @Inject constructor( _uiState.update { it.copy( userReport = content, + exceedMinCharacters = content.length >= 5, reachedMaxCharacters = content.length >= 500 ) } diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/EditBookmarkViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/EditBookmarkViewModel.kt new file mode 100644 index 00000000..701ecb6a --- /dev/null +++ b/app/src/main/java/com/doyoonkim/knutice/viewModel/EditBookmarkViewModel.kt @@ -0,0 +1,132 @@ +package com.doyoonkim.knutice.viewModel + +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import com.doyoonkim.knutice.data.NoticeLocalRepository +import com.doyoonkim.knutice.model.Bookmark +import com.doyoonkim.knutice.model.EditBookmarkState +import com.doyoonkim.knutice.model.Notice +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +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 javax.inject.Inject + +@HiltViewModel +class EditBookmarkViewModel @Inject constructor( + private val localRepository: NoticeLocalRepository, + private val savedStateHandle: SavedStateHandle +) : ViewModel() { + + private var _uiState = MutableStateFlow(EditBookmarkState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val requestNotice = savedStateHandle.toRoute() + + init { + _uiState.update { + it.copy( + targetNotice = requestNotice + ) + } + getBookmarkByNotice() + } + + fun updateBookmarkNotes(newString: String) { + viewModelScope.launch(Dispatchers.Default) { + if (uiState.value.bookmarkNote.length < 500) { + _uiState.update { + it.copy( + bookmarkNote = newString + ) + } + } + } + } + + private fun getBookmarkByNotice() { + viewModelScope.launch(Dispatchers.IO) { + localRepository.getBookmarkByNttID(requestNotice.nttId) + .fold( + onSuccess = { bookmark -> + if (bookmark.bookmarkId != -1) { + _uiState.update { + it.copy( + bookmarkId = bookmark.bookmarkId ?: 0, + isReminderRequested = bookmark.isScheduled ?: false, + timeForRemind = bookmark.reminderSchedule ?: 0, + bookmarkNote = bookmark.note ?: "", + requireCreation = false + ) + } + } + }, + onFailure = { + Log.d("EditBookmarkViewModel", "There is no existing bookmark for this notice.") + } + ) + } + } + + fun createNewBookmark() { + viewModelScope.launch(Dispatchers.IO) { + val targetNotice = uiState.value.targetNotice + val newBookmark = Bookmark( + bookmarkId = 0, + isScheduled = uiState.value.isReminderRequested, + reminderSchedule = 0, + note = uiState.value.bookmarkNote, + nttId = targetNotice.nttId + ) + + localRepository.createBookmark(newBookmark, targetNotice).fold( + onSuccess = { + Log.d("EditBookmarkViewModel", "Creation Inquiry Processed Successfully with state: $it") + }, + onFailure = { + Log.d("EditBookmarkViewModel", "Unable to process creation inquiry\nREASON:${it.message}") + } + ) + } + } + + fun modifyBookmark() { + viewModelScope.launch(Dispatchers.IO) { + val targetNotice = uiState.value.targetNotice + val modifiedBookmark = Bookmark( + bookmarkId = uiState.value.bookmarkId, + isScheduled = uiState.value.isReminderRequested, + reminderSchedule = uiState.value.timeForRemind, + note = uiState.value.bookmarkNote, + nttId = uiState.value.targetNotice.nttId + ) + + localRepository.updateBookmark(modifiedBookmark) + } + } + + fun removeBookmark() { + viewModelScope.launch(Dispatchers.IO) { + val targetBookmark = Bookmark( + bookmarkId = uiState.value.bookmarkId, + isScheduled = uiState.value.isReminderRequested, + reminderSchedule = uiState.value.timeForRemind, + note = uiState.value.bookmarkNote, + nttId = uiState.value.targetNotice.nttId + ) + + // The code snippet below should be isolated into separated class under the domain layer. + // Remove related notice entity first. + localRepository.deleteNoticeEntity(uiState.value.targetNotice.toNoticeEntity()) + // Once notice entity is being deleted, target bookmark would be deleted. + localRepository.deleteBookmark(targetBookmark) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/MainActivityViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/MainActivityViewModel.kt index 85f12628..567dce6f 100644 --- a/app/src/main/java/com/doyoonkim/knutice/viewModel/MainActivityViewModel.kt +++ b/app/src/main/java/com/doyoonkim/knutice/viewModel/MainActivityViewModel.kt @@ -2,6 +2,7 @@ package com.doyoonkim.knutice.viewModel import androidx.lifecycle.ViewModel import com.doyoonkim.knutice.model.Destination +import com.doyoonkim.knutice.model.Notice import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -15,12 +16,16 @@ class MainActivityViewModel @Inject constructor() : ViewModel() { fun updateState( updatedCurrentLocation: Destination = _uiState.value.currentLocation, - updatedCurrentScaffoldTitle: String = _uiState.value.currentScaffoldTitle + updatedCurrentScaffoldTitle: String = _uiState.value.currentScaffoldTitle, + updatedBottomNavBarVisibility: Boolean = _uiState.value.isBottomNavBarVisible, + updatedTempReservedNoticeForBookmark: Notice = _uiState.value.tempReserveNoticeForBookmark ) { _uiState.update { it.copy( currentLocation = updatedCurrentLocation, - currentScaffoldTitle = updatedCurrentScaffoldTitle + currentScaffoldTitle = updatedCurrentScaffoldTitle, + isBottomNavBarVisible = updatedBottomNavBarVisibility, + tempReserveNoticeForBookmark = updatedTempReservedNoticeForBookmark ) } } @@ -28,5 +33,7 @@ class MainActivityViewModel @Inject constructor() : ViewModel() { data class MainAppState( val currentLocation: Destination = Destination.MAIN, - val currentScaffoldTitle: String = "" + val currentScaffoldTitle: String = "", + val isBottomNavBarVisible: Boolean = false, + val tempReserveNoticeForBookmark: Notice = Notice() ) \ No newline at end of file diff --git a/app/src/main/java/com/doyoonkim/knutice/viewModel/NotificationPreferenceViewModel.kt b/app/src/main/java/com/doyoonkim/knutice/viewModel/NotificationPreferenceViewModel.kt index 880c725a..db20eee3 100644 --- a/app/src/main/java/com/doyoonkim/knutice/viewModel/NotificationPreferenceViewModel.kt +++ b/app/src/main/java/com/doyoonkim/knutice/viewModel/NotificationPreferenceViewModel.kt @@ -79,6 +79,8 @@ class NotificationPreferenceViewModel @Inject constructor( } } + + // Needed to be refined later. (If user enters this point without initial permission allowance. fun checkMainNotificationPreferenceStatus() { val isNotificationAllowed = ContextCompat.checkSelfPermission( context, Manifest.permission.POST_NOTIFICATIONS diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index fda657e6..74a6fc3f 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -41,4 +41,15 @@ 새로운 학사 공지가 올라오면 알림을 받을래요. 새로운 행사 공지가 올라오면 알림을 받을래요. 알림 설정 + 알림 설정하기 + 북마크 수정 + 알림을 받을래요 + 메모 + 북마크 한 공지에 메모를 남겨주세요. + 저장하기 + 저장된 북마크가 없어요 + 삭제 + + 북마크 + 아직 준비중이에요. 다음 업데이트에서 만나요. \ 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 7d580267..2b4c623c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,7 +11,7 @@ About Version Open Source License - 1.2.0 + 1.3.0 Notification Preference New Notice has been delivered! @@ -50,4 +50,16 @@ Get push notification when new academic notice is being posted. Get push notification when new event notice is being posted. Notification Preference + Set Reminder + Edit Bookmark + Get Reminder + Notes + Enter any notes for your bookmarked notice. + Save + No bookmarks to be shown + delete + Home + Bookmarks + Very Long Text for testing. Very Long Text for testing. Very Long Text for testing. Very Long Text for testing. Very Long Text for testing. Very Long Text for testing. Very Long Text for testing. Very Long Text for testing. Very Long Text for testing. Very Long Text for testing. + Not yet supported. \ No newline at end of file