diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index e43de9d..6d0ee1c 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 57723f3..f65d128 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,8 +17,8 @@ android { applicationId = "com.yangdai.opennote" minSdk = 29 targetSdk = 34 - versionCode = 127 - versionName = "1.2.7" + versionCode = 128 + versionName = "1.2.8" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -69,7 +69,6 @@ android { dependencies { // Kotlin implementation(libs.kotlinx.serialization) - implementation(libs.kotlinx.collections.immutable) // CommonMark, for markdown rendering and parsing implementation(libs.commonmark.ext.task.list.items) diff --git a/app/release/app-release.apk b/app/release/app-release.apk index 10a0d50..df99636 100644 Binary files a/app/release/app-release.apk and b/app/release/app-release.apk differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6203741..234404b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,8 +10,6 @@ android:name="android.hardware.camera" android:required="false" /> - - diff --git a/app/src/main/java/com/yangdai/opennote/ManageSpaceActivity.kt b/app/src/main/java/com/yangdai/opennote/ManageSpaceActivity.kt index 971f1ff..95935ca 100644 --- a/app/src/main/java/com/yangdai/opennote/ManageSpaceActivity.kt +++ b/app/src/main/java/com/yangdai/opennote/ManageSpaceActivity.kt @@ -6,12 +6,14 @@ import android.content.Intent import android.os.Bundle import androidx.core.app.TaskStackBuilder import androidx.core.net.toUri +import com.yangdai.opennote.presentation.navigation.Screen import com.yangdai.opennote.presentation.util.Constants.LINK class ManageSpaceActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val intent = Intent(this, MainActivity::class.java).setData("$LINK/settings".toUri()) + val intent = + Intent(this, MainActivity::class.java).setData("$LINK/${Screen.Settings.route}".toUri()) val pendingIntent = TaskStackBuilder.create(this).run { addNextIntentWithParentStack(intent) getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) diff --git a/app/src/main/java/com/yangdai/opennote/data/di/AppModule.kt b/app/src/main/java/com/yangdai/opennote/data/di/AppModule.kt index f30e59b..7b0b7ea 100644 --- a/app/src/main/java/com/yangdai/opennote/data/di/AppModule.kt +++ b/app/src/main/java/com/yangdai/opennote/data/di/AppModule.kt @@ -14,7 +14,7 @@ import com.yangdai.opennote.domain.usecase.DeleteNote import com.yangdai.opennote.domain.usecase.DeleteNotesByFolderId import com.yangdai.opennote.domain.usecase.GetFolders import com.yangdai.opennote.domain.usecase.GetNotes -import com.yangdai.opennote.domain.usecase.Operations +import com.yangdai.opennote.domain.usecase.UseCases import com.yangdai.opennote.domain.usecase.SearchNotes import com.yangdai.opennote.domain.usecase.UpdateFolder import com.yangdai.opennote.domain.usecase.UpdateNote @@ -34,9 +34,8 @@ object AppModule { @Singleton @Provides - fun provideDataStoreRepository( - @ApplicationContext context: Context - ): DataStoreRepository = DataStoreRepositoryImpl(context) + fun provideDataStoreRepository(@ApplicationContext context: Context): DataStoreRepository = + DataStoreRepositoryImpl(context) @Provides @Singleton @@ -62,8 +61,8 @@ object AppModule { fun provideNoteUseCases( noteRepository: NoteRepository, folderRepository: FolderRepository - ): Operations { - return Operations( + ): UseCases { + return UseCases( getNotes = GetNotes(noteRepository), getNoteById = GetNoteById(noteRepository), deleteNote = DeleteNote(noteRepository), diff --git a/app/src/main/java/com/yangdai/opennote/domain/usecase/GetNotes.kt b/app/src/main/java/com/yangdai/opennote/domain/usecase/GetNotes.kt index a54ec30..a324863 100644 --- a/app/src/main/java/com/yangdai/opennote/domain/usecase/GetNotes.kt +++ b/app/src/main/java/com/yangdai/opennote/domain/usecase/GetNotes.kt @@ -32,7 +32,7 @@ class GetNotes( is OrderType.Ascending -> { when (noteOrder) { is NoteOrder.Title -> notes.sortedBy { it.title.lowercase() } - is NoteOrder.Date -> notes.sortedBy { it.timestamp } + is NoteOrder.Date -> notes.reversed() } } diff --git a/app/src/main/java/com/yangdai/opennote/domain/usecase/Operations.kt b/app/src/main/java/com/yangdai/opennote/domain/usecase/UseCases.kt similarity index 94% rename from app/src/main/java/com/yangdai/opennote/domain/usecase/Operations.kt rename to app/src/main/java/com/yangdai/opennote/domain/usecase/UseCases.kt index e916167..597c19e 100644 --- a/app/src/main/java/com/yangdai/opennote/domain/usecase/Operations.kt +++ b/app/src/main/java/com/yangdai/opennote/domain/usecase/UseCases.kt @@ -1,6 +1,6 @@ package com.yangdai.opennote.domain.usecase -data class Operations( +data class UseCases( val getNotes: GetNotes, val getNoteById: GetNoteById, val deleteNote: DeleteNote, diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/AdaptiveNavigationScreen.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/AdaptiveNavigationScreen.kt new file mode 100644 index 0000000..280d47c --- /dev/null +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/AdaptiveNavigationScreen.kt @@ -0,0 +1,70 @@ +package com.yangdai.opennote.presentation.component + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material3.DrawerState +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.PermanentDrawerSheet +import androidx.compose.material3.PermanentNavigationDrawer +import androidx.compose.runtime.Composable + +/** + * A composable function that adapts the navigation drawer based on the screen size. + * + * @param isLargeScreen A boolean indicating whether the screen is large or not. + * @param drawerState The state of the drawer. + * @param gesturesEnabled A boolean indicating whether gestures are enabled or not. + * @param drawerContent The content of the drawer. + * @param content The main content. + */ +@Composable +fun AdaptiveNavigationScreen( + isLargeScreen: Boolean, + drawerState: DrawerState, + gesturesEnabled: Boolean, + drawerContent: @Composable (ColumnScope.() -> Unit), + content: @Composable () -> Unit +) = if (isLargeScreen) { + PermanentNavigationScreen( + drawerContent = drawerContent, + content = content + ) +} else { + ModalNavigationScreen( + drawerContent = drawerContent, + drawerState = drawerState, + gesturesEnabled = gesturesEnabled, + content = content + ) +} + +@Composable +fun ModalNavigationScreen( + drawerContent: @Composable (ColumnScope.() -> Unit), + drawerState: DrawerState, + gesturesEnabled: Boolean, + content: @Composable () -> Unit +) = ModalNavigationDrawer( + drawerContent = { + ModalDrawerSheet( + drawerState = drawerState, + content = drawerContent + ) + }, + drawerState = drawerState, + gesturesEnabled = gesturesEnabled, + content = content +) + +@Composable +fun PermanentNavigationScreen( + drawerContent: @Composable (ColumnScope.() -> Unit), + content: @Composable () -> Unit +) = PermanentNavigationDrawer( + drawerContent = { + PermanentDrawerSheet( + content = drawerContent + ) + }, + content = content +) \ No newline at end of file diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/AdaptiveTopSearchbar.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/AdaptiveTopSearchbar.kt new file mode 100644 index 0000000..0f32697 --- /dev/null +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/AdaptiveTopSearchbar.kt @@ -0,0 +1,309 @@ +package com.yangdai.opennote.presentation.component + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ContextualFlowRow +import androidx.compose.foundation.layout.ContextualFlowRowOverflow +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.material.icons.outlined.GridView +import androidx.compose.material.icons.outlined.History +import androidx.compose.material.icons.outlined.KeyboardArrowDown +import androidx.compose.material.icons.outlined.KeyboardArrowUp +import androidx.compose.material.icons.outlined.Menu +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material.icons.outlined.SortByAlpha +import androidx.compose.material.icons.outlined.ViewAgenda +import androidx.compose.material3.DockedSearchBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.yangdai.opennote.MainActivity +import com.yangdai.opennote.R +import com.yangdai.opennote.presentation.event.ListEvent +import com.yangdai.opennote.presentation.util.Constants +import com.yangdai.opennote.presentation.util.Constants.DEFAULT_MAX_LINES +import com.yangdai.opennote.presentation.viewmodel.SharedViewModel + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun AdaptiveTopSearchbar( + viewModel: SharedViewModel = hiltViewModel(LocalContext.current as MainActivity), + isLargeScreen: Boolean, + enabled: Boolean, + onSearchBarActivationChange: (Boolean) -> Unit, + onDrawerStateChange: () -> Unit +) { + + val historySet by viewModel.historyStateFlow.collectAsStateWithLifecycle() + val settingsState by viewModel.settingsStateFlow.collectAsStateWithLifecycle() + + var inputText by rememberSaveable { + mutableStateOf("") + } + var expanded by rememberSaveable { + mutableStateOf(false) + } + var maxLines by rememberSaveable { + mutableIntStateOf(DEFAULT_MAX_LINES) + } + + LaunchedEffect(expanded) { + onSearchBarActivationChange(expanded) + } + + val configuration = LocalConfiguration.current + val orientation = remember(configuration) { configuration.orientation } + + fun search(text: String) { + if (text.isNotEmpty()) { + val newSet = historySet.toMutableSet() + newSet.add(text) + viewModel.putPreferenceValue(Constants.Preferences.SEARCH_HISTORY, newSet.toSet()) + viewModel.onListEvent(ListEvent.Search(text)) + } else { + viewModel.onListEvent( + ListEvent.Sort( + viewModel.dataStateFlow.value.noteOrder, + false, + null, + false + ) + ) + } + expanded = false + } + + AdaptiveSearchBar( + isDocked = orientation != Configuration.ORIENTATION_PORTRAIT || isLargeScreen, + expanded = expanded, + onExpandedChange = { expanded = it }, + inputField = { + SearchBarDefaults.InputField( + query = inputText, + onQueryChange = { inputText = it }, + onSearch = { search(it) }, + enabled = enabled, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = { Text(text = stringResource(R.string.search)) }, + leadingIcon = { + if (!isLargeScreen) { + AnimatedContent(targetState = expanded, label = "leading") { + if (it) { + IconButton(onClick = { search(inputText) }) { + Icon( + imageVector = Icons.Outlined.Search, + contentDescription = "Search" + ) + } + } else { + IconButton( + enabled = enabled, + onClick = onDrawerStateChange + ) { + Icon( + imageVector = Icons.Outlined.Menu, + contentDescription = "Open Menu" + ) + } + } + } + } else { + Icon( + imageVector = Icons.Outlined.Search, + contentDescription = "Search" + ) + } + }, + trailingIcon = { + AnimatedContent(targetState = expanded, label = "trailing") { + if (it) { + IconButton(onClick = { + if (inputText.isNotEmpty()) { + inputText = "" + } else { + search("") + } + }) { + Icon( + imageVector = Icons.Outlined.Clear, + contentDescription = "Clear" + ) + } + } else { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = { viewModel.onListEvent(ListEvent.ChangeViewMode) }) { + Icon( + imageVector = if (!settingsState.isListView) Icons.Outlined.ViewAgenda else Icons.Outlined.GridView, + contentDescription = "View Mode" + ) + } + IconButton(onClick = { viewModel.onListEvent(ListEvent.ToggleOrderSection) }) { + Icon( + imageVector = Icons.Outlined.SortByAlpha, + contentDescription = "Sort" + ) + } + } + } + } + }, + ) + } + ) { + + if (historySet.isEmpty()) return@AdaptiveSearchBar + + ListItem( + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + leadingContent = { + Icon( + imageVector = Icons.Outlined.History, + contentDescription = "History" + ) + }, + headlineContent = { Text(text = stringResource(R.string.search_history)) }, + trailingContent = { + Icon( + modifier = Modifier.clickable { + viewModel.putPreferenceValue( + Constants.Preferences.SEARCH_HISTORY, + setOf() + ) + }, + imageVector = Icons.Outlined.DeleteForever, + contentDescription = "Clear History" + ) + } + ) + + ContextualFlowRow( + itemCount = historySet.size, + modifier = Modifier.padding(horizontal = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + maxLines = maxLines, + overflow = ContextualFlowRowOverflow.expandOrCollapseIndicator( + minRowsToShowCollapse = DEFAULT_MAX_LINES + 1, + expandIndicator = { + IconButton(onClick = { maxLines = Int.MAX_VALUE }) { + Icon( + imageVector = Icons.Outlined.KeyboardArrowDown, + contentDescription = "Expand" + ) + } + }, + collapseIndicator = { + IconButton(onClick = { maxLines = DEFAULT_MAX_LINES }) { + Icon( + imageVector = Icons.Outlined.KeyboardArrowUp, + contentDescription = "Collapse" + ) + } + } + ) + ) { index -> + SuggestionChip( + modifier = Modifier.defaultMinSize(48.dp), + onClick = { inputText = historySet.elementAt(index) }, + label = { + Text( + text = historySet.elementAt(index), + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }) + } + } +} + +/** + * A composable function that adapts the search bar based on the screen size and orientation. + * + * @param isDocked A boolean indicating whether the search bar is docked or not. + * @param expanded A boolean indicating whether the search bar is expanded or not. + * @param onExpandedChange A callback that is called when the search bar is expanded or collapsed. + * @param inputField The input field of the search bar. + * @param content The content of the search bar. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AdaptiveSearchBar( + isDocked: Boolean, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + inputField: @Composable () -> Unit, + content: @Composable (ColumnScope.() -> Unit) +) = if (!isDocked) { + + // Animate search bar padding when active state changes + val searchBarPadding by animateDpAsState( + targetValue = if (expanded) 0.dp else 16.dp, + label = "searchBarPadding" + ) + + SearchBar( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = searchBarPadding), + inputField = inputField, + expanded = expanded, + onExpandedChange = onExpandedChange, + content = content + ) +} else { + Box( + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding() + .padding(top = 8.dp), + contentAlignment = Alignment.Center + ) { + DockedSearchBar( + inputField = inputField, + expanded = expanded, + onExpandedChange = onExpandedChange, + content = content + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/DrawerContent.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/DrawerContent.kt index c8f59a9..330d1e5 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/DrawerContent.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/DrawerContent.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationDrawerItem import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -29,20 +30,22 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector 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.unit.dp import com.yangdai.opennote.R import com.yangdai.opennote.data.local.entity.FolderEntity -import com.yangdai.opennote.presentation.navigation.Folders -import com.yangdai.opennote.presentation.navigation.Settings -import kotlinx.collections.immutable.ImmutableList +import com.yangdai.opennote.presentation.navigation.Screen +import com.yangdai.opennote.presentation.navigation.Screen.* @Composable fun DrawerContent( - folderList: ImmutableList, + folderList: List, selectedDrawerIndex: Int, - navigateTo: (Any) -> Unit, + navigateTo: (Screen) -> Unit, onClick: (Int, FolderEntity) -> Unit ) { @@ -122,3 +125,41 @@ fun DrawerContent( } } } + +@Composable +fun DrawerItem( + icon: ImageVector, + iconTint: Color = MaterialTheme.colorScheme.onSurface, + label: String, + badge: String = "", + isSelected: Boolean, + onClick: () -> Unit +) = NavigationDrawerItem( + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + icon = { + Icon( + modifier = Modifier.padding(horizontal = 12.dp), + imageVector = icon, + tint = iconTint, + contentDescription = "Leading Icon" + ) + }, + label = { + Text( + text = label, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + badge = { + Text( + text = badge, + style = MaterialTheme.typography.labelMedium + ) + }, + shape = MaterialTheme.shapes.large, + selected = isSelected, + onClick = onClick +) diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/DrawerItem.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/DrawerItem.kt deleted file mode 100644 index 71cf842..0000000 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/DrawerItem.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.yangdai.opennote.presentation.component - -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationDrawerItem -import androidx.compose.material3.NavigationDrawerItemDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp - -@Composable -fun DrawerItem( - icon: ImageVector, - iconTint: Color = MaterialTheme.colorScheme.onSurface, - label: String, - badge: String = "", - isSelected: Boolean, - onClick: () -> Unit -) = NavigationDrawerItem( - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), - icon = { - Icon( - modifier = Modifier.padding(horizontal = 12.dp), - imageVector = icon, - tint = iconTint, - contentDescription = "Leading Icon" - ) - }, - label = { - Text( - text = label, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - badge = { - Text( - text = badge, - style = MaterialTheme.typography.labelMedium - ) - }, - shape = MaterialTheme.shapes.large, - selected = isSelected, - onClick = onClick -) \ No newline at end of file diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/ExportDialog.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/ExportDialog.kt index 063a336..ab83d45 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/ExportDialog.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/ExportDialog.kt @@ -20,31 +20,28 @@ enum class ExportType { fun ExportDialog( onDismissRequest: () -> Unit, onConfirm: (ExportType) -> Unit -) { - - AlertDialog( - title = { - Text(text = stringResource(R.string.export_as)) - }, - text = { - Column(modifier = Modifier.fillMaxWidth()) { - TextOptionButton(text = "TXT") { - onConfirm(ExportType.TXT) - } +) = AlertDialog( + title = { + Text(text = stringResource(R.string.export_as)) + }, + text = { + Column(modifier = Modifier.fillMaxWidth()) { + TextOptionButton(text = "TXT") { + onConfirm(ExportType.TXT) + } - TextOptionButton(text = "MARKDOWN") { - onConfirm(ExportType.MARKDOWN) - } + TextOptionButton(text = "MARKDOWN") { + onConfirm(ExportType.MARKDOWN) + } - TextOptionButton(text = "HTML") { - onConfirm(ExportType.HTML) - } + TextOptionButton(text = "HTML") { + onConfirm(ExportType.HTML) } - }, - onDismissRequest = onDismissRequest, - confirmButton = {} - ) -} + } + }, + onDismissRequest = onDismissRequest, + confirmButton = {} +) @Composable @Preview diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/FilterRadioButton.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/FilterRadioButton.kt deleted file mode 100644 index 2849588..0000000 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/FilterRadioButton.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.yangdai.opennote.presentation.component - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.RadioButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun FilterRadioButton( - text: String, - selected: Boolean, - onSelect: () -> Unit, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selected, - onClick = onSelect, - colors = RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colorScheme.primary, - unselectedColor = MaterialTheme.colorScheme.onBackground - ) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = text, style = MaterialTheme.typography.bodyMedium) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/FolderItem.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/FolderItem.kt index c8a42d6..2d10541 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/FolderItem.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/FolderItem.kt @@ -34,11 +34,11 @@ fun LazyGridItemScope.FolderItem( onDelete: () -> Unit ) { - var modify by remember { + var showModifyDialog by remember { mutableStateOf(false) } - var delete by remember { + var showWarningDialog by remember { mutableStateOf(false) } @@ -68,7 +68,7 @@ fun LazyGridItemScope.FolderItem( Row { IconButton(onClick = { - modify = !modify + showModifyDialog = !showModifyDialog }) { Icon( imageVector = Icons.Outlined.DriveFileRenameOutline, @@ -77,7 +77,7 @@ fun LazyGridItemScope.FolderItem( } IconButton(onClick = { - delete = !delete + showWarningDialog = !showWarningDialog }) { Icon( imageVector = Icons.Outlined.Delete, @@ -87,18 +87,20 @@ fun LazyGridItemScope.FolderItem( } } - WarningDialog( - showDialog = delete, - message = stringResource(R.string.deleting_a_folder_will_also_delete_all_the_notes_it_contains_and_they_cannot_be_restored_do_you_want_to_continue), - onDismissRequest = { delete = false }) { - onDelete() + if (showWarningDialog) { + WarningDialog( + message = stringResource(R.string.deleting_a_folder_will_also_delete_all_the_notes_it_contains_and_they_cannot_be_restored_do_you_want_to_continue), + onDismissRequest = { showWarningDialog = false }, + onConfirm = onDelete + ) } - ModifyFolderDialog( - showDialog = modify, - folder = folder, - onDismissRequest = { modify = false }) { - onModify(it) + if (showModifyDialog) { + ModifyFolderDialog( + folder = folder, + onDismissRequest = { showModifyDialog = false }) { + onModify(it) + } } } } diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/FolderListDialog.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/FolderListDialog.kt index bb264d7..e8248e9 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/FolderListDialog.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/FolderListDialog.kt @@ -30,15 +30,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.yangdai.opennote.R import com.yangdai.opennote.data.local.entity.FolderEntity -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList @Composable fun FolderListDialog( hint: String = "", oFolderId: Long?, - folders: ImmutableList, + folders: List, onDismissRequest: () -> Unit, onSelect: (Long?) -> Unit ) { @@ -143,7 +141,7 @@ fun FolderListDialogPreview() { FolderEntity(1, "Folder 1", null), FolderEntity(2, "Folder 2", null), FolderEntity(3, "Folder 3", null) - ).toImmutableList(), + ), onDismissRequest = {}, onSelect = {} ) diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/LinkDialog.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/LinkDialog.kt index 86daf32..624dfdc 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/LinkDialog.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/LinkDialog.kt @@ -70,9 +70,7 @@ fun LinkDialog( ) } }, - onDismissRequest = { - onDismissRequest() - }, + onDismissRequest = onDismissRequest, confirmButton = { TextButton( onClick = { @@ -93,7 +91,7 @@ fun LinkDialog( } }, dismissButton = { - TextButton(onClick = { onDismissRequest() }) { + TextButton(onClick = onDismissRequest) { Text(text = stringResource(id = android.R.string.cancel)) } } diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/MainContent.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/MainContent.kt deleted file mode 100644 index 52f6424..0000000 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/MainContent.kt +++ /dev/null @@ -1,474 +0,0 @@ -package com.yangdai.opennote.presentation.component - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.displayCutout -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid -import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells -import androidx.compose.foundation.lazy.staggeredgrid.items -import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.DriveFileMove -import androidx.compose.material.icons.outlined.Add -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.GridView -import androidx.compose.material.icons.outlined.Menu -import androidx.compose.material.icons.outlined.MoreVert -import androidx.compose.material.icons.outlined.RestartAlt -import androidx.compose.material.icons.outlined.SortByAlpha -import androidx.compose.material.icons.outlined.Upload -import androidx.compose.material.icons.outlined.ViewAgenda -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.Checkbox -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.VerticalDivider -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.yangdai.opennote.R -import com.yangdai.opennote.data.local.entity.FolderEntity -import com.yangdai.opennote.data.local.entity.NoteEntity -import com.yangdai.opennote.presentation.event.ListEvent -import com.yangdai.opennote.presentation.state.DataActionState -import com.yangdai.opennote.presentation.state.DataState -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MainContent( - isListViewMode: Boolean, - dataActionState: DataActionState, - isFloatingButtonVisible: Boolean, - selectedFolder: FolderEntity, - selectedDrawerIndex: Int, - allNotesSelected: Boolean, - selectedNotes: ImmutableList, - isMultiSelectionModeEnabled: Boolean, - isLargeScreen: Boolean, - dataState: DataState, - folderList: ImmutableList, - navigateToNote: (Long) -> Unit, - initializeNoteSelection: () -> Unit, - onSearchBarActivationChange: (Boolean) -> Unit, - onAllNotesSelectionChange: (Boolean) -> Unit, - onMultiSelectionModeChange: (Boolean) -> Unit, - onNoteClick: (NoteEntity) -> Unit, - onListEvent: (ListEvent) -> Unit, - onDrawerStateChange: () -> Unit, - onExportClick: (ExportType) -> Unit, - onExportCancelled: () -> Unit -) { - - val staggeredGridState = rememberLazyStaggeredGridState() - val lazyListState = rememberLazyListState() - - var folderName by rememberSaveable { - mutableStateOf("") - } - - // Bottom sheet visibility, reset when configuration changes, no need to use rememberSaveable - var isFolderDialogVisible by remember { - mutableStateOf(false) - } - - // Whether to show the export dialog - var isExportDialogVisible by remember { - mutableStateOf(false) - } - - LaunchedEffect(selectedFolder) { - if (selectedFolder.id != null) { - folderName = selectedFolder.name - } - } - - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - AnimatedContent(targetState = selectedDrawerIndex == 0, label = "") { - if (it) { - TopSearchbar( - enabled = !isMultiSelectionModeEnabled, - isLargeScreen = isLargeScreen, - onSearchBarActivationChange = onSearchBarActivationChange, - onDrawerStateChange = onDrawerStateChange - ) - } else { - - var showMenu by remember { - mutableStateOf(false) - } - - TopAppBar( - title = { - Text( - text = if (selectedDrawerIndex == 1) stringResource(id = R.string.trash) - else folderName, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - navigationIcon = { - if (!isLargeScreen) { - IconButton( - enabled = !isMultiSelectionModeEnabled, - onClick = onDrawerStateChange - ) { - Icon( - imageVector = Icons.Outlined.Menu, - contentDescription = "Open Menu" - ) - } - } - }, - actions = { - IconButton(onClick = { onListEvent(ListEvent.ChangeViewMode) }) { - Icon( - imageVector = if (!isListViewMode) Icons.Outlined.ViewAgenda else Icons.Outlined.GridView, - contentDescription = "View Mode" - ) - } - IconButton(onClick = { onListEvent(ListEvent.ToggleOrderSection) }) { - Icon( - imageVector = Icons.Outlined.SortByAlpha, - contentDescription = "Sort" - ) - } - if (selectedDrawerIndex == 1) { - IconButton(onClick = { showMenu = !showMenu }) { - Icon( - imageVector = Icons.Outlined.MoreVert, - contentDescription = "More" - ) - } - - DropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false }) { - DropdownMenuItem( - leadingIcon = { - Icon( - imageVector = Icons.Outlined.RestartAlt, - contentDescription = "Restore" - ) - }, - text = { Text(text = stringResource(id = R.string.restore_all)) }, - onClick = { - onListEvent(ListEvent.RestoreNotes(dataState.notes.toImmutableList())) - }) - - DropdownMenuItem( - leadingIcon = { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = "Delete" - ) - }, - text = { Text(text = stringResource(id = R.string.delete_all)) }, - onClick = { - onListEvent( - ListEvent.DeleteNotes( - dataState.notes.toImmutableList(), - false - ) - ) - }) - } - } - } - ) - } - } - }, - bottomBar = { - AnimatedVisibility( - visible = isMultiSelectionModeEnabled, - enter = slideInVertically { fullHeight -> fullHeight }, - exit = slideOutVertically { fullHeight -> fullHeight } - ) { - BottomAppBar { - Row( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - - Row( - modifier = Modifier.fillMaxHeight(), - verticalAlignment = Alignment.CenterVertically - ) { - - Checkbox( - checked = allNotesSelected, - onCheckedChange = onAllNotesSelectionChange - ) - - Text(text = stringResource(R.string.checked)) - - Text(text = selectedNotes.size.toString()) - } - - Row( - modifier = Modifier.fillMaxHeight(), - verticalAlignment = Alignment.CenterVertically - ) { - - if (selectedDrawerIndex == 1) { - TextButton(onClick = { - onListEvent(ListEvent.RestoreNotes(selectedNotes)) - initializeNoteSelection() - }) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - imageVector = Icons.Outlined.RestartAlt, - contentDescription = "Restore" - ) - Text(text = stringResource(id = R.string.restore)) - } - } - } else { - TextButton(onClick = { - isExportDialogVisible = true - }) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - imageVector = Icons.Outlined.Upload, - contentDescription = "Export" - ) - Text(text = stringResource(id = R.string.export)) - } - } - - TextButton(onClick = { isFolderDialogVisible = true }) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - imageVector = Icons.AutoMirrored.Outlined.DriveFileMove, - contentDescription = "Move" - ) - Text(text = stringResource(id = R.string.move)) - } - } - } - - TextButton(onClick = { - onListEvent( - ListEvent.DeleteNotes( - selectedNotes, - selectedDrawerIndex != 1 - ) - ) - initializeNoteSelection() - }) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = "Delete" - ) - Text(text = stringResource(id = R.string.delete)) - } - } - } - } - } - } - }, - floatingActionButton = { - AnimatedVisibility( - visible = isFloatingButtonVisible && !staggeredGridState.isScrollInProgress && !lazyListState.isScrollInProgress, - enter = slideInHorizontally { fullWidth -> fullWidth * 3 / 2 }, - exit = slideOutHorizontally { fullWidth -> fullWidth * 3 / 2 }) { - FloatingActionButton( - onClick = { - onListEvent(ListEvent.AddNote) - navigateToNote(-1) - } - ) { - Icon(imageVector = Icons.Outlined.Add, contentDescription = "Add") - } - } - - }) { innerPadding -> - - // Add layoutDirection, displayCutout, startPadding, and endPadding. - val layoutDirection = LocalLayoutDirection.current - val displayCutout = WindowInsets.displayCutout.asPaddingValues() - val startPadding = displayCutout.calculateStartPadding(layoutDirection) - val endPadding = displayCutout.calculateEndPadding(layoutDirection) - - Box( - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .padding(top = 72.dp, start = startPadding, end = endPadding) - ) { - if (!isListViewMode) { - LazyVerticalStaggeredGrid( - modifier = Modifier - .fillMaxSize(), - state = staggeredGridState, - // The staggered grid layout is adaptive, with a minimum column width of 160dp(mdpi) - columns = StaggeredGridCells.Adaptive(160.dp), - verticalItemSpacing = 8.dp, - horizontalArrangement = Arrangement.spacedBy(8.dp), - // for better edgeToEdge experience - contentPadding = PaddingValues( - start = 16.dp, - end = 16.dp, - bottom = innerPadding.calculateBottomPadding() - ), - content = { - items( - dataState.notes, - key = { item: NoteEntity -> item.id!! }) { note -> - GridNoteCard( - modifier = Modifier - .fillMaxWidth() - .animateItem(), // Add animation to the item - note = note, - isEnabled = isMultiSelectionModeEnabled, - isSelected = selectedNotes.contains(note), - onEnableChange = onMultiSelectionModeChange, - onNoteClick = onNoteClick - ) - } - } - ) - } else { - - if (dataState.notes.isEmpty()) { - return@Box - } - - VerticalDivider( - Modifier - .align(Alignment.TopStart) - .fillMaxHeight() - .padding(start = 15.dp), - thickness = 2.dp - ) - LazyColumn( - modifier = Modifier - .align(Alignment.Center) - .fillMaxSize(), - state = lazyListState, - contentPadding = PaddingValues( - start = 12.dp, - end = 16.dp, - bottom = innerPadding.calculateBottomPadding() - ) - ) { - items( - dataState.notes, - key = { item: NoteEntity -> item.id!! }) { note -> - ColumnNoteCard( - modifier = Modifier - .fillMaxWidth() - .animateItem(), // Add animation to the item - note = note, - isEnabled = isMultiSelectionModeEnabled, - isSelected = selectedNotes.contains(note), - onEnableChange = onMultiSelectionModeChange, - onNoteClick = onNoteClick - ) - } - } - } - } - - if (dataState.isOrderSectionVisible) { - AlertDialog( - title = { Text(text = stringResource(R.string.sort_by)) }, - text = { - OrderSection( - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp), - noteOrder = dataState.noteOrder, - onOrderChange = { - onListEvent( - ListEvent.Sort( - noteOrder = it, - trash = selectedDrawerIndex == 1, - filterFolder = selectedDrawerIndex != 0 && selectedDrawerIndex != 1, - folderId = selectedFolder.id - ) - ) - } - ) - }, - onDismissRequest = { onListEvent(ListEvent.ToggleOrderSection) }, - confirmButton = { - TextButton(onClick = { onListEvent(ListEvent.ToggleOrderSection) }) { - Text(stringResource(id = android.R.string.ok)) - } - }) - } - - if (isExportDialogVisible) { - ExportDialog(onDismissRequest = { isExportDialogVisible = false }) { - onExportClick(it) - isExportDialogVisible = false - } - } - - if (isFolderDialogVisible) { - FolderListDialog( - hint = stringResource(R.string.destination_folder), - oFolderId = selectedFolder.id, - folders = folderList, - onDismissRequest = { isFolderDialogVisible = false } - ) { - onListEvent(ListEvent.MoveNotes(selectedNotes, it)) - initializeNoteSelection() - } - } - - ProgressDialog(isLoading = dataActionState.loading, progress = dataActionState.progress) { - onExportCancelled() - } - } -} diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/MarkdownText.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/MarkdownText.kt index 1a10375..7f2fa69 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/MarkdownText.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/MarkdownText.kt @@ -7,88 +7,58 @@ import android.view.ViewGroup import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.yangdai.opennote.presentation.theme.linkColor +import com.yangdai.opennote.presentation.util.rememberCustomTabsIntent @SuppressLint("SetJavaScriptEnabled") @Composable -fun MarkdownText(html: String) { +fun MarkdownText( + html: String, + colorScheme: ColorScheme = MaterialTheme.colorScheme +) { - val textColor = MaterialTheme.colorScheme.onSurface.toArgb() - val codeBackgroundColor = MaterialTheme.colorScheme.surfaceVariant.toArgb() - val preBackgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp).toArgb() - val quoteBackgroundColor = MaterialTheme.colorScheme.secondaryContainer.toArgb() - val borderColor = MaterialTheme.colorScheme.outline.toArgb() - - val hexTextColor = remember { - String.format("#%06X", 0xFFFFFF and textColor) + val hexTextColor = remember(colorScheme) { + String.format("#%06X", 0xFFFFFF and colorScheme.onSurface.toArgb()) } - val hexCodeBackgroundColor = remember { - String.format("#%06X", 0xFFFFFF and codeBackgroundColor) + val hexCodeBackgroundColor = remember(colorScheme) { + String.format("#%06X", 0xFFFFFF and colorScheme.surfaceVariant.toArgb()) } - val hexPreBackgroundColor = remember { - String.format("#%06X", 0xFFFFFF and preBackgroundColor) + val hexPreBackgroundColor = remember(colorScheme) { + String.format("#%06X", 0xFFFFFF and colorScheme.surfaceColorAtElevation(1.dp).toArgb()) } - val hexQuoteBackgroundColor = remember { - String.format("#%06X", 0xFFFFFF and quoteBackgroundColor) + val hexQuoteBackgroundColor = remember(colorScheme) { + String.format("#%06X", 0xFFFFFF and colorScheme.secondaryContainer.toArgb()) } - val hexLinkColor = remember { + val hexLinkColor = remember(colorScheme) { String.format("#%06X", 0xFFFFFF and linkColor.toArgb()) } - val hexBorderColor = remember { - String.format("#%06X", 0xFFFFFF and borderColor) - } - - val customTabsIntent = remember { - CustomTabsIntent.Builder() - .setShowTitle(true) - .build() + val hexBorderColor = remember(colorScheme) { + String.format("#%06X", 0xFFFFFF and colorScheme.outline.toArgb()) } - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { - WebView(it).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - webViewClient = object : WebViewClient() { - override fun shouldOverrideUrlLoading( - view: WebView?, - request: WebResourceRequest - ): Boolean { - val url = request.url.toString() - if (url.startsWith("http://") || url.startsWith("https://")) { - customTabsIntent.launchUrl(it, Uri.parse(url)) - } - return true - } - } - settings.javaScriptEnabled = true - settings.loadsImagesAutomatically = true - settings.defaultTextEncodingName = "utf-8" - isVerticalScrollBarEnabled = false - isHorizontalScrollBarEnabled = false - settings.setSupportZoom(true) - settings.builtInZoomControls = true - settings.displayZoomControls = false - settings.useWideViewPort = false - settings.loadWithOverviewMode = false - setBackgroundColor(Color.TRANSPARENT) - } - }, - update = { - val data = """ + val data by remember( + html, + hexTextColor, + hexCodeBackgroundColor, + hexPreBackgroundColor, + hexQuoteBackgroundColor, + hexLinkColor, + hexBorderColor + ) { + mutableStateOf( + """ @@ -126,6 +96,45 @@ fun MarkdownText(html: String) { """.trimIndent() + ) + } + + val customTabsIntent = rememberCustomTabsIntent() + + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { + WebView(it).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest + ): Boolean { + val url = request.url.toString() + if (url.startsWith("http://") || url.startsWith("https://")) { + customTabsIntent.launchUrl(it, Uri.parse(url)) + } + return true + } + } + settings.javaScriptEnabled = true + settings.loadsImagesAutomatically = true + settings.defaultTextEncodingName = "UTF-8" + isVerticalScrollBarEnabled = false + isHorizontalScrollBarEnabled = false + settings.setSupportZoom(true) + settings.builtInZoomControls = true + settings.displayZoomControls = false + settings.useWideViewPort = false + settings.loadWithOverviewMode = false + setBackgroundColor(Color.TRANSPARENT) + } + }, + update = { it.loadDataWithBaseURL( null, data, diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/MaskBox.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/MaskBox.kt index a0ac359..1a542cf 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/MaskBox.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/MaskBox.kt @@ -42,7 +42,7 @@ typealias MaskAnimActive = (MaskAnimModel, Float, Float) -> Unit @SuppressLint("Recycle") @Composable fun MaskBox( - animTime: Long = 650, + animTime: Long = 650L, maskComplete: (MaskAnimModel) -> Unit, animFinish: () -> Unit, content: @Composable (MaskAnimActive) -> Unit, diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/ModalNavigationScreen.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/ModalNavigationScreen.kt deleted file mode 100644 index 86b6686..0000000 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/ModalNavigationScreen.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.yangdai.opennote.presentation.component - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.DrawerState -import androidx.compose.material3.ModalDrawerSheet -import androidx.compose.material3.ModalNavigationDrawer -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.yangdai.opennote.data.local.entity.FolderEntity -import kotlinx.collections.immutable.ImmutableList - -@Composable -fun ModalNavigationScreen( - drawerState: DrawerState, - gesturesEnabled: Boolean, - folderList: ImmutableList, - selectedDrawerIndex: Int, - content: @Composable () -> Unit, - navigateTo: (Any) -> Unit, - selectDrawer: (Int, FolderEntity)-> Unit -) = ModalNavigationDrawer( - modifier = Modifier.fillMaxSize(), - drawerState = drawerState, - gesturesEnabled = gesturesEnabled, - drawerContent = { - ModalDrawerSheet(drawerState = drawerState) { - DrawerContent( - folderList = folderList, - selectedDrawerIndex = selectedDrawerIndex, - navigateTo = { navigateTo(it) } - ) { position, folder -> - selectDrawer(position, folder) - } - } - } -) { - content() -} \ No newline at end of file diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/ModifyFolderDialog.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/ModifyFolderDialog.kt index 2036ab0..c477786 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/ModifyFolderDialog.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/ModifyFolderDialog.kt @@ -38,7 +38,6 @@ import com.yangdai.opennote.data.local.entity.FolderEntity @Preview fun ModifyFolderDialogPreview() { ModifyFolderDialog( - showDialog = true, folder = FolderEntity(), onDismissRequest = {}, onModify = {} @@ -47,14 +46,11 @@ fun ModifyFolderDialogPreview() { @Composable fun ModifyFolderDialog( - showDialog: Boolean, folder: FolderEntity, onDismissRequest: () -> Unit, onModify: (FolderEntity) -> Unit ) { - if (!showDialog) return - var text by remember { mutableStateOf(folder.name) } var color by remember { mutableStateOf(folder.color) } @@ -95,9 +91,7 @@ fun ModifyFolderDialog( } } }, - onDismissRequest = { - onDismissRequest() - }, + onDismissRequest = onDismissRequest, confirmButton = { TextButton( onClick = { @@ -120,7 +114,7 @@ fun ModifyFolderDialog( } }, dismissButton = { - TextButton(onClick = { onDismissRequest() }) { + TextButton(onClick = onDismissRequest) { Text(text = stringResource(id = android.R.string.cancel)) } } diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/NoteEditorRow.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/NoteEditorRow.kt index d4138fa..0f23c70 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/NoteEditorRow.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/NoteEditorRow.kt @@ -61,32 +61,30 @@ fun IconButtonWithTooltip( contentDescription: String, shortCutDescription: String? = null, onClick: () -> Unit +) = TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + if (shortCutDescription != null) { + PlainTooltip( + content = { Text(shortCutDescription) } + ) + } + }, + state = rememberTooltipState(), + focusable = false, + enableUserInput = true ) { - TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { - if (shortCutDescription != null) { - PlainTooltip( - content = { Text(shortCutDescription) } - ) - } - }, - state = rememberTooltipState(), - focusable = false, - enableUserInput = true - ) { - IconButton(onClick = onClick, enabled = enabled) { - if (imageVector != null) { - Icon( - imageVector = imageVector, - contentDescription = contentDescription - ) - } else { - Icon( - painter = painterResource(id = painter!!), - contentDescription = contentDescription - ) - } + IconButton(onClick = onClick, enabled = enabled) { + if (imageVector != null) { + Icon( + imageVector = imageVector, + contentDescription = contentDescription + ) + } else { + Icon( + painter = painterResource(id = painter!!), + contentDescription = contentDescription + ) } } } @@ -298,10 +296,9 @@ fun NoteEditorRow( IconButtonWithTooltip( imageVector = Icons.Outlined.TableChart, contentDescription = "Table", - shortCutDescription = "Ctrl + T" - ) { - onTableButtonClick() - } + shortCutDescription = "Ctrl + T", + onClick = onTableButtonClick + ) IconButtonWithTooltip( imageVector = Icons.Outlined.AddChart, @@ -315,26 +312,23 @@ fun NoteEditorRow( IconButtonWithTooltip( imageVector = Icons.Outlined.CheckBox, contentDescription = "Task", - shortCutDescription = "Ctrl + Shift + T" - ) { - onTaskButtonClick() - } + shortCutDescription = "Ctrl + Shift + T", + onClick = onTaskButtonClick + ) IconButtonWithTooltip( imageVector = Icons.Outlined.Link, contentDescription = "Link", - shortCutDescription = "Ctrl + K" - ) { - onLinkButtonClick() - } + shortCutDescription = "Ctrl + K", + onClick = onLinkButtonClick + ) IconButtonWithTooltip( imageVector = Icons.Outlined.DocumentScanner, contentDescription = "OCR", - shortCutDescription = "Ctrl + S" - ) { - onScanButtonClick() - } + shortCutDescription = "Ctrl + S", + onClick = onScanButtonClick + ) } } } diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/OrderSection.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/OrderSection.kt deleted file mode 100644 index c0ee3dc..0000000 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/OrderSection.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.yangdai.opennote.presentation.component - -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.yangdai.opennote.R -import com.yangdai.opennote.domain.usecase.NoteOrder -import com.yangdai.opennote.domain.usecase.OrderType - -@Composable -fun OrderSection( - modifier: Modifier = Modifier, - noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending), - onOrderChange: (NoteOrder) -> Unit -) { - Column( - modifier = modifier - ) { - Row( - modifier = Modifier.fillMaxWidth() - ) { - FilterRadioButton( - text = stringResource(R.string.title), - selected = noteOrder is NoteOrder.Title, - onSelect = { onOrderChange(NoteOrder.Title(noteOrder.orderType)) } - ) - Spacer(modifier = Modifier.width(8.dp)) - FilterRadioButton( - text = stringResource(R.string.date), - selected = noteOrder is NoteOrder.Date, - onSelect = { onOrderChange(NoteOrder.Date(noteOrder.orderType)) } - ) - } - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth() - ) { - FilterRadioButton( - text = stringResource(R.string.ascending), - selected = noteOrder.orderType is OrderType.Ascending, - onSelect = { - onOrderChange(noteOrder.copy(OrderType.Ascending)) - } - ) - Spacer(modifier = Modifier.width(8.dp)) - FilterRadioButton( - text = stringResource(R.string.descending), - selected = noteOrder.orderType is OrderType.Descending, - onSelect = { - onOrderChange(noteOrder.copy(OrderType.Descending)) - } - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/OrderSectionDialog.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/OrderSectionDialog.kt new file mode 100644 index 0000000..6b1c603 --- /dev/null +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/OrderSectionDialog.kt @@ -0,0 +1,120 @@ +package com.yangdai.opennote.presentation.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.yangdai.opennote.R +import com.yangdai.opennote.domain.usecase.NoteOrder +import com.yangdai.opennote.domain.usecase.OrderType + +@Composable +fun OrderSectionDialog( + noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending), + onOrderChange: (NoteOrder) -> Unit, + onDismiss: () -> Unit +) { + + var newOrder by remember { mutableStateOf(noteOrder) } + + val typeOptions = listOf( + stringResource(R.string.title), + stringResource(R.string.date) + ) + + val orderOptions = listOf( + stringResource(R.string.ascending), + stringResource(R.string.descending) + ) + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(text = stringResource(R.string.sort_by)) }, + text = { + Column { + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = 0, + count = typeOptions.size + ), + onClick = { newOrder = NoteOrder.Title(noteOrder.orderType) }, + selected = newOrder is NoteOrder.Title + ) { + Text(typeOptions[0]) + } + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = 1, + count = typeOptions.size + ), + onClick = { newOrder = NoteOrder.Date(noteOrder.orderType) }, + selected = newOrder is NoteOrder.Date + ) { + Text(typeOptions[1]) + } + } + Spacer(modifier = Modifier.height(8.dp)) + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = 0, + count = typeOptions.size + ), + onClick = { newOrder = newOrder.copy(OrderType.Ascending) }, + selected = newOrder.orderType is OrderType.Ascending + ) { + Text(orderOptions[0]) + } + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = 1, + count = typeOptions.size + ), + onClick = { newOrder = newOrder.copy(OrderType.Descending) }, + selected = newOrder.orderType is OrderType.Descending + ) { + Text(orderOptions[1]) + } + } + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(id = android.R.string.cancel)) + } + }, + confirmButton = { + TextButton(onClick = { + onDismiss() + onOrderChange(newOrder) + }) { + Text(stringResource(id = android.R.string.ok)) + } + } + ) +} + +@Preview +@Composable +fun OrderSectionDialogPreview() { + OrderSectionDialog( + noteOrder = NoteOrder.Date(OrderType.Descending), + onOrderChange = {}, + onDismiss = {}) +} \ No newline at end of file diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/PermanentNavigationScreen.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/PermanentNavigationScreen.kt deleted file mode 100644 index b051c92..0000000 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/PermanentNavigationScreen.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.yangdai.opennote.presentation.component - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.PermanentDrawerSheet -import androidx.compose.material3.PermanentNavigationDrawer -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.yangdai.opennote.data.local.entity.FolderEntity -import kotlinx.collections.immutable.ImmutableList - -@Composable -fun PermanentNavigationScreen( - folderList: ImmutableList, - selectedDrawerIndex: Int, - content: @Composable () -> Unit, - navigateTo: (Any) -> Unit, - selectDrawer: (Int, FolderEntity) -> Unit -) = PermanentNavigationDrawer( - modifier = Modifier.fillMaxSize(), - drawerContent = { - PermanentDrawerSheet { - DrawerContent( - folderList = folderList, - selectedDrawerIndex = selectedDrawerIndex, - navigateTo = { navigateTo(it) } - ) { position, folder -> - selectDrawer(position, folder) - } - } - } -) { - content() -} \ No newline at end of file diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/RatingDialog.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/RatingDialog.kt index f9d88c1..39b4c95 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/RatingDialog.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/RatingDialog.kt @@ -22,13 +22,10 @@ import com.yangdai.opennote.R @Composable fun RatingDialog( - showDialog: Boolean, onDismissRequest: () -> Unit, onRatingChanged: (Int) -> Unit ) { - if (!showDialog) return - var rating by remember { mutableIntStateOf(0) } AlertDialog( @@ -63,5 +60,5 @@ fun RatingDialog( @Preview @Composable fun RatingDialogPreview() { - RatingDialog(showDialog = true, onDismissRequest = {}, onRatingChanged = {}) + RatingDialog(onDismissRequest = {}, onRatingChanged = {}) } \ No newline at end of file diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/RichText.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/RichText.kt index e1ec427..3b70bc1 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/RichText.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/RichText.kt @@ -2,7 +2,6 @@ package com.yangdai.opennote.presentation.component import android.net.Uri import android.widget.Toast -import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer @@ -11,6 +10,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -21,62 +22,27 @@ import androidx.compose.ui.text.style.LineBreak import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withLink import com.yangdai.opennote.presentation.theme.linkColor +import com.yangdai.opennote.presentation.util.rememberCustomTabsIntent @Composable fun RichText(str: String) { val context = LocalContext.current - val customTabsIntent = remember { - CustomTabsIntent.Builder() - .setShowTitle(true) - .build() - } + val customTabsIntent = rememberCustomTabsIntent() val pattern = remember { "\\[(.*?)]\\((.*?)\\)".toRegex() } - val text = str.replace("- [ ]", "◎").replace("- [x]", "◉") - - val matches = pattern.findAll(text) - - val annotatedString = buildAnnotatedString { - - var lastIndex = 0 - - matches.forEach { matchResult -> - val range = matchResult.range - val title = matchResult.groupValues[1] - val link = matchResult.groupValues[2] - - // Append plain text - append(text.substring(lastIndex, range.first)) - - val url = LinkAnnotation.Url(link) { - // Handle click event - val url = (it as LinkAnnotation.Url).url - - try { - if (url.startsWith("http://") || url.startsWith("https://")) { - customTabsIntent.launchUrl(context, Uri.parse(url)) - } - } catch (e: Exception) { - e.printStackTrace() - // Show error message to the user - Toast.makeText(context, "Failed to open link: $url", Toast.LENGTH_SHORT).show() - } - } - - withLink(url) { - append(title) - } - - lastIndex = range.last + 1 - } + val text by remember(str) { + mutableStateOf(str.replace("- [ ]", "◎").replace("- [x]", "◉")) + } - // Append the rest of the text - append(text.substring(lastIndex, text.length)) + val matches by remember(pattern, text) { + mutableStateOf( + pattern.findAll(text) + ) } SelectionContainer { @@ -84,7 +50,44 @@ fun RichText(str: String) { modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()), - text = annotatedString, + text = buildAnnotatedString { + + var lastIndex = 0 + + matches.forEach { matchResult -> + val range = matchResult.range + val title = matchResult.groupValues[1] + val link = matchResult.groupValues[2] + + // Append plain text + append(text.substring(lastIndex, range.first)) + + val url = LinkAnnotation.Url(link) { + // Handle click event + val url = (it as LinkAnnotation.Url).url + + try { + if (url.startsWith("http://") || url.startsWith("https://")) { + customTabsIntent.launchUrl(context, Uri.parse(url)) + } + } catch (e: Exception) { + e.printStackTrace() + // Show error message to the user + Toast.makeText(context, "Failed to open link: $url", Toast.LENGTH_SHORT) + .show() + } + } + + withLink(url) { + append(title) + } + + lastIndex = range.last + 1 + } + + // Append the rest of the text + append(text.substring(lastIndex, text.length)) + }, style = MaterialTheme.typography.bodyLarge.copy( color = MaterialTheme.colorScheme.onSurface, lineBreak = LineBreak.Paragraph @@ -94,4 +97,4 @@ fun RichText(str: String) { ) ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/SelectableColorPlatte.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/SelectableColorPlatte.kt index ebbfa63..2a54556 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/SelectableColorPlatte.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/SelectableColorPlatte.kt @@ -30,51 +30,49 @@ fun SelectableColorPlatte( selected: Boolean, colorScheme: ColorScheme, onClick: () -> Unit +) = Surface( + modifier = modifier, + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer ) { Surface( - modifier = modifier, - shape = MaterialTheme.shapes.large, - color = MaterialTheme.colorScheme.surfaceContainer + modifier = Modifier + .clickable { onClick() } + .padding(12.dp) + .size(48.dp), + shape = CircleShape, + color = colorScheme.primary, ) { - Surface( - modifier = Modifier - .clickable { onClick() } - .padding(12.dp) - .size(48.dp), - shape = CircleShape, - color = colorScheme.primary, - ) { - Box { - Surface( + Box { + Surface( + modifier = Modifier + .size(48.dp) + .offset((-24).dp, 24.dp), + color = colorScheme.tertiary, + ) {} + Surface( + modifier = Modifier + .size(48.dp) + .offset(24.dp, 24.dp), + color = colorScheme.secondaryContainer, + ) {} + AnimatedVisibility( + visible = selected, + modifier = Modifier + .align(Alignment.Center) + .clip(CircleShape) + .background(colorScheme.tertiaryContainer), + enter = fadeIn() + expandIn(expandFrom = Alignment.Center), + exit = shrinkOut(shrinkTowards = Alignment.Center) + fadeOut() + ) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = "Checked", modifier = Modifier - .size(48.dp) - .offset((-24).dp, 24.dp), - color = colorScheme.tertiary, - ) {} - Surface( - modifier = Modifier - .size(48.dp) - .offset(24.dp, 24.dp), - color = colorScheme.secondaryContainer, - ) {} - AnimatedVisibility( - visible = selected, - modifier = Modifier - .align(Alignment.Center) - .clip(CircleShape) - .background(colorScheme.tertiaryContainer), - enter = fadeIn() + expandIn(expandFrom = Alignment.Center), - exit = shrinkOut(shrinkTowards = Alignment.Center) + fadeOut() - ) { - Icon( - imageVector = Icons.Outlined.Check, - contentDescription = "Checked", - modifier = Modifier - .padding(8.dp) - .size(16.dp), - tint = MaterialTheme.colorScheme.surface - ) - } + .padding(8.dp) + .size(16.dp), + tint = MaterialTheme.colorScheme.surface + ) } } } diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/SettingsDetailPane.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/SettingsDetailPane.kt index eb60aef..c46c943 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/SettingsDetailPane.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/SettingsDetailPane.kt @@ -8,7 +8,6 @@ import android.os.Build import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.Image @@ -103,8 +102,8 @@ import com.yangdai.opennote.presentation.theme.DarkOrangeColors import com.yangdai.opennote.presentation.theme.DarkRedColors import com.yangdai.opennote.presentation.theme.DarkPurpleColors import com.yangdai.opennote.presentation.util.Constants +import com.yangdai.opennote.presentation.util.rememberCustomTabsIntent import com.yangdai.opennote.presentation.viewmodel.SharedViewModel -import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -120,11 +119,7 @@ fun SettingsDetailPane( val folderEntities by sharedViewModel.foldersStateFlow.collectAsStateWithLifecycle() val actionState by sharedViewModel.dataActionStateFlow.collectAsStateWithLifecycle() - val customTabsIntent = remember { - CustomTabsIntent.Builder() - .setShowTitle(true) - .build() - } + val customTabsIntent = rememberCustomTabsIntent() val modeOptions = listOf( stringResource(R.string.system_default), @@ -146,7 +141,7 @@ fun SettingsDetailPane( var showFolderDialog by rememberSaveable { mutableStateOf(false) } var showRatingDialog by rememberSaveable { mutableStateOf(false) } - var folderId: Long? = null + var folderId: Long? by rememberSaveable { mutableStateOf(null) } val importLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.OpenMultipleDocuments() ) { uriList -> @@ -672,59 +667,65 @@ fun SettingsDetailPane( } } - RatingDialog( - showDialog = showRatingDialog, - onDismissRequest = { showRatingDialog = false }) { stars -> - if (stars > 3) { - customTabsIntent.launchUrl( - context, - Uri.parse("https://play.google.com/store/apps/details?id=com.yangdai.opennote") - ) - } else { - if (stars == 0) return@RatingDialog - // 获取当前应用的版本号 - val packageInfo = - context.packageManager.getPackageInfo(context.packageName, 0) - val appVersion = packageInfo.versionName - val deviceModel = Build.MODEL - val systemVersion = Build.VERSION.SDK_INT - - val emailIntent = Intent(Intent.ACTION_SENDTO).apply { - data = Uri.parse("mailto:") - putExtra(Intent.EXTRA_EMAIL, arrayOf("dy15800837435@gmail.com")) - putExtra(Intent.EXTRA_SUBJECT, "Feedback - Open Note") - putExtra( - Intent.EXTRA_TEXT, - "Version: $appVersion\nDevice: $deviceModel\nSystem: $systemVersion\n" + if (showRatingDialog) { + RatingDialog( + onDismissRequest = { showRatingDialog = false } + ) { stars -> + if (stars > 3) { + customTabsIntent.launchUrl( + context, + Uri.parse("https://play.google.com/store/apps/details?id=com.yangdai.opennote") ) - } - context.startActivity( - Intent.createChooser( - emailIntent, - "Feedback (E-mail)" + } else { + if (stars == 0) return@RatingDialog + // 获取当前应用的版本号 + val packageInfo = + context.packageManager.getPackageInfo(context.packageName, 0) + val appVersion = packageInfo.versionName + val deviceModel = Build.MODEL + val systemVersion = Build.VERSION.SDK_INT + + val emailIntent = Intent(Intent.ACTION_SENDTO).apply { + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf("dy15800837435@gmail.com")) + putExtra(Intent.EXTRA_SUBJECT, "Feedback - Open Note") + putExtra( + Intent.EXTRA_TEXT, + "Version: $appVersion\nDevice: $deviceModel\nSystem: $systemVersion\n" + ) + } + context.startActivity( + Intent.createChooser( + emailIntent, + "Feedback (E-mail)" + ) ) - ) + } } } - WarningDialog( - showDialog = showWarningDialog, - message = stringResource(R.string.reset_database_warning), - onDismissRequest = { showWarningDialog = false }) { - sharedViewModel.onDatabaseEvent(DatabaseEvent.Reset) + + if (showWarningDialog) { + WarningDialog( + message = stringResource(R.string.reset_database_warning), + onDismissRequest = { showWarningDialog = false } + ) { + sharedViewModel.onDatabaseEvent(DatabaseEvent.Reset) + } } + ProgressDialog( isLoading = actionState.loading, progress = actionState.progress, infinite = actionState.infinite, - errorMessage = actionState.error - ) { - sharedViewModel.cancelDataAction() - } + errorMessage = actionState.error, + onDismissRequest = sharedViewModel::cancelDataAction + ) + if (showFolderDialog) { FolderListDialog( hint = stringResource(R.string.destination_folder), oFolderId = folderId, - folders = folderEntities.toImmutableList(), + folders = folderEntities, onDismissRequest = { showFolderDialog = false } ) { id -> folderId = id diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/ShareDialog.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/ShareDialog.kt index 68b7d7d..0857864 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/ShareDialog.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/ShareDialog.kt @@ -19,26 +19,24 @@ enum class ShareType { fun ShareDialog( onDismissRequest: () -> Unit, onConfirm: (ShareType) -> Unit -) { - AlertDialog( - onDismissRequest = onDismissRequest, - title = { - Text(text = stringResource(R.string.share_note_as)) - }, - text = { - Column(modifier = Modifier.fillMaxWidth()) { - TextOptionButton(text = stringResource(R.string.file)) { - onConfirm(ShareType.FILE) - } +) = AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(R.string.share_note_as)) + }, + text = { + Column(modifier = Modifier.fillMaxWidth()) { + TextOptionButton(text = stringResource(R.string.file)) { + onConfirm(ShareType.FILE) + } - TextOptionButton(text = stringResource(R.string.text)) { - onConfirm(ShareType.TEXT) - } + TextOptionButton(text = stringResource(R.string.text)) { + onConfirm(ShareType.TEXT) } - }, - confirmButton = {} - ) -} + } + }, + confirmButton = {} +) @Composable @Preview diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/TableDialog.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/TableDialog.kt index 6ba1b40..0dbaccb 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/TableDialog.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/TableDialog.kt @@ -71,9 +71,7 @@ fun TableDialog( ) } }, - onDismissRequest = { - onDismissRequest() - }, + onDismissRequest = onDismissRequest, confirmButton = { TextButton( onClick = { @@ -95,7 +93,7 @@ fun TableDialog( } }, dismissButton = { - TextButton(onClick = { onDismissRequest() }) { + TextButton(onClick = onDismissRequest) { Text(text = stringResource(id = android.R.string.cancel)) } } diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/TaskDialog.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/TaskDialog.kt index 9e8a02a..a2f6e4d 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/TaskDialog.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/TaskDialog.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.yangdai.opennote.R @@ -103,9 +104,7 @@ fun TaskDialog( } } }, - onDismissRequest = { - onDismissRequest() - }, + onDismissRequest = onDismissRequest, confirmButton = { TextButton( onClick = { @@ -117,9 +116,15 @@ fun TaskDialog( } }, dismissButton = { - TextButton(onClick = { onDismissRequest() }) { + TextButton(onClick = onDismissRequest) { Text(text = stringResource(id = android.R.string.cancel)) } } ) } + +@Preview +@Composable +fun TaskDialogPreview() { + TaskDialog(onDismissRequest = {}, onConfirm = {}) +} diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/TopSearchbar.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/TopSearchbar.kt deleted file mode 100644 index 5e4b7a2..0000000 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/TopSearchbar.kt +++ /dev/null @@ -1,277 +0,0 @@ -package com.yangdai.opennote.presentation.component - -import android.content.res.Configuration -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Clear -import androidx.compose.material.icons.outlined.DeleteForever -import androidx.compose.material.icons.outlined.GridView -import androidx.compose.material.icons.outlined.History -import androidx.compose.material.icons.outlined.Menu -import androidx.compose.material.icons.outlined.Search -import androidx.compose.material.icons.outlined.SortByAlpha -import androidx.compose.material.icons.outlined.ViewAgenda -import androidx.compose.material3.DockedSearchBar -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.SearchBar -import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.SuggestionChip -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.yangdai.opennote.MainActivity -import com.yangdai.opennote.R -import com.yangdai.opennote.presentation.event.ListEvent -import com.yangdai.opennote.presentation.util.Constants -import com.yangdai.opennote.presentation.viewmodel.SharedViewModel - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) -@Composable -fun TopSearchbar( - viewModel: SharedViewModel = hiltViewModel(LocalContext.current as MainActivity), - isLargeScreen: Boolean, - enabled: Boolean, - onSearchBarActivationChange: (Boolean) -> Unit, - onDrawerStateChange: () -> Unit -) { - - val historySet by viewModel.historyStateFlow.collectAsStateWithLifecycle() - val settingsState by viewModel.settingsStateFlow.collectAsStateWithLifecycle() - - var inputText by rememberSaveable { - mutableStateOf("") - } - var expanded by rememberSaveable { - mutableStateOf(false) - } - - LaunchedEffect(expanded) { - onSearchBarActivationChange(expanded) - } - - val configuration = LocalConfiguration.current - val orientation = remember(configuration) { configuration.orientation } - - fun search(text: String) { - if (text.isNotEmpty()) { - val newSet = historySet.toMutableSet() - newSet.add(text) - viewModel.putPreferenceValue(Constants.Preferences.SEARCH_HISTORY, newSet.toSet()) - viewModel.onListEvent(ListEvent.Search(text)) - } else { - viewModel.onListEvent( - ListEvent.Sort( - viewModel.dataStateFlow.value.noteOrder, - false, - null, - false - ) - ) - } - expanded = false - } - - @Composable - fun LeadingIcon() { - if (!isLargeScreen) { - AnimatedContent(targetState = expanded, label = "leading") { - if (it) { - IconButton(onClick = { search(inputText) }) { - Icon( - imageVector = Icons.Outlined.Search, - contentDescription = "Search" - ) - } - } else { - IconButton( - enabled = enabled, - onClick = onDrawerStateChange - ) { - Icon( - imageVector = Icons.Outlined.Menu, - contentDescription = "Open Menu" - ) - } - } - } - } else { - Icon( - imageVector = Icons.Outlined.Search, - contentDescription = "Search" - ) - } - } - - @Composable - fun TrailingIcon() { - AnimatedContent(targetState = expanded, label = "trailing") { - if (it) { - IconButton(onClick = { - if (inputText.isNotEmpty()) { - inputText = "" - } else { - search("") - } - }) { - Icon( - imageVector = Icons.Outlined.Clear, - contentDescription = "Clear" - ) - } - } else { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - IconButton(onClick = { viewModel.onListEvent(ListEvent.ChangeViewMode) }) { - Icon( - imageVector = if (!settingsState.isListView) Icons.Outlined.ViewAgenda else Icons.Outlined.GridView, - contentDescription = "View Mode" - ) - } - IconButton(onClick = { viewModel.onListEvent(ListEvent.ToggleOrderSection) }) { - Icon( - imageVector = Icons.Outlined.SortByAlpha, - contentDescription = "Sort" - ) - } - } - } - } - } - - @Composable - fun History() { - - ListItem( - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - leadingContent = { - Icon( - imageVector = Icons.Outlined.History, - contentDescription = "History" - ) - }, - headlineContent = { Text(text = stringResource(R.string.search_history)) }, - trailingContent = { - // To align the icon with search bar, use icon.clickable{} instead of IconButton onClick() - Icon( - modifier = Modifier.clickable { - viewModel.putPreferenceValue(Constants.Preferences.SEARCH_HISTORY, setOf()) - }, - imageVector = Icons.Outlined.DeleteForever, - contentDescription = "Clear History" - ) - } - ) - - FlowRow( - Modifier.padding(horizontal = 12.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - historySet.forEach { - SuggestionChip( - modifier = Modifier.defaultMinSize(48.dp), - onClick = { inputText = it }, - label = { - Text( - text = it, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }) - } - } - } - - // Search bar layout, switch between docked and expanded search bar based on window size and orientation - if (orientation == Configuration.ORIENTATION_PORTRAIT && !isLargeScreen) { - - // Animate search bar padding when active state changes - val searchBarPadding by animateDpAsState( - targetValue = if (expanded) 0.dp else 16.dp, - label = "searchBarPadding" - ) - - SearchBar( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = searchBarPadding), - inputField = { - SearchBarDefaults.InputField( - query = inputText, - onQueryChange = { inputText = it }, - onSearch = { search(it) }, - enabled = enabled, - expanded = expanded, - onExpandedChange = { expanded = it }, - placeholder = { Text(text = stringResource(R.string.search)) }, - leadingIcon = { LeadingIcon() }, - trailingIcon = { TrailingIcon() }, - ) - }, - expanded = expanded, - onExpandedChange = { expanded = it } - ) { - History() - } - } else { - Box( - modifier = Modifier - .fillMaxWidth() - .statusBarsPadding() - .padding(top = 8.dp), - contentAlignment = Alignment.Center - ) { - DockedSearchBar( - inputField = { - SearchBarDefaults.InputField( - query = inputText, - onQueryChange = { inputText = it }, - onSearch = { search(it) }, - enabled = enabled, - expanded = expanded, - onExpandedChange = { expanded = it }, - placeholder = { Text(text = stringResource(R.string.search)) }, - leadingIcon = { LeadingIcon() }, - trailingIcon = { TrailingIcon() }, - ) - }, - expanded = expanded, - onExpandedChange = { expanded = it } - ) { - History() - } - } - } -} diff --git a/app/src/main/java/com/yangdai/opennote/presentation/component/WarningDialog.kt b/app/src/main/java/com/yangdai/opennote/presentation/component/WarningDialog.kt index 3ef83e2..7613346 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/component/WarningDialog.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/component/WarningDialog.kt @@ -11,38 +11,30 @@ import com.yangdai.opennote.R @Composable fun WarningDialog( - showDialog: Boolean, message: String, onDismissRequest: () -> Unit, onConfirm: () -> Unit -) { - - if (!showDialog) return - - AlertDialog( - title = { Text(text = stringResource(id = R.string.warning)) }, - text = { - Text(text = message) - }, - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton( - onClick = { - onConfirm() - onDismissRequest() - }, - colors = ButtonDefaults.textButtonColors().copy( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.error - ) - ) { - Text(text = stringResource(id = android.R.string.ok)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(id = android.R.string.cancel)) - } +) = AlertDialog( + title = { Text(text = stringResource(id = R.string.warning)) }, + text = { Text(text = message) }, + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton( + onClick = { + onConfirm() + onDismissRequest() + }, + colors = ButtonDefaults.textButtonColors().copy( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Text(text = stringResource(id = android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(id = android.R.string.cancel)) } - ) -} + } +) diff --git a/app/src/main/java/com/yangdai/opennote/presentation/event/ListEvent.kt b/app/src/main/java/com/yangdai/opennote/presentation/event/ListEvent.kt index 26af0c7..327f0cd 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/event/ListEvent.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/event/ListEvent.kt @@ -3,7 +3,6 @@ package com.yangdai.opennote.presentation.event import com.yangdai.opennote.data.local.entity.NoteEntity import com.yangdai.opennote.domain.usecase.NoteOrder import com.yangdai.opennote.domain.usecase.OrderType -import kotlinx.collections.immutable.ImmutableList sealed interface ListEvent { @@ -17,13 +16,9 @@ sealed interface ListEvent { val trash: Boolean = false ) : ListEvent - data class DeleteNotes(val noteEntities: ImmutableList, val recycle: Boolean) : - ListEvent - - data class MoveNotes(val noteEntities: ImmutableList, val folderId: Long?) : - ListEvent - - data class RestoreNotes(val noteEntities: ImmutableList) : ListEvent + data class DeleteNotes(val noteEntities: Collection, val recycle: Boolean) : ListEvent + data class MoveNotes(val noteEntities: Collection, val folderId: Long?) : ListEvent + data class RestoreNotes(val noteEntities: Collection) : ListEvent data object ToggleOrderSection : ListEvent data object ChangeViewMode : ListEvent diff --git a/app/src/main/java/com/yangdai/opennote/presentation/navigation/AnimatedNavHost.kt b/app/src/main/java/com/yangdai/opennote/presentation/navigation/AnimatedNavHost.kt index 3af3285..8127ff8 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/navigation/AnimatedNavHost.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/navigation/AnimatedNavHost.kt @@ -33,6 +33,7 @@ import com.yangdai.opennote.presentation.screen.SettingsScreen import com.yangdai.opennote.presentation.util.Constants.NAV_ANIMATION_TIME import com.yangdai.opennote.presentation.util.Constants.LINK import com.yangdai.opennote.presentation.util.parseSharedContent +import com.yangdai.opennote.presentation.navigation.Screen.* private const val ProgressThreshold = 0.35f private const val INITIAL_OFFSET_FACTOR = 0.10f @@ -108,14 +109,13 @@ fun AnimatedNavHost( composable { MainScreen( isLargeScreen = isLargeScreen, - navigateToNote = { navController.navigate("$Note/$it") } - ) { route -> - navController.navigate(route) - } + navigateToNote = { navController.navigate(Note.passId(it)) }, + navigateToScreen = { navController.navigate(it) } + ) } composable( - route = "$Note/{id}", + route = Note.route, deepLinks = listOf( navDeepLink { action = Intent.ACTION_SEND @@ -126,7 +126,7 @@ fun AnimatedNavHost( mimeType = "text/*" }, navDeepLink { - uriPattern = "$LINK/note/{id}" + uriPattern = "$LINK/${Note.route}" } ), arguments = listOf( @@ -147,8 +147,9 @@ fun AnimatedNavHost( isLargeScreen = isLargeScreen, sharedText = sharedText, scannedText = scannedText, - navigateUp = { navController.navigateBackWithHapticFeedback(hapticFeedback) } - ) { navController.navigate(CameraX) } + navigateUp = { navController.navigateBackWithHapticFeedback(hapticFeedback) }, + onScanTextClick = { navController.navigate(CameraX) } + ) } composable { @@ -169,7 +170,7 @@ fun AnimatedNavHost( composable( deepLinks = listOf( navDeepLink { - uriPattern = "$LINK/settings" + uriPattern = "$LINK/${Settings.route}" } ) ) { diff --git a/app/src/main/java/com/yangdai/opennote/presentation/navigation/Route.kt b/app/src/main/java/com/yangdai/opennote/presentation/navigation/Route.kt index 85a3bf2..c3ef367 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/navigation/Route.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/navigation/Route.kt @@ -3,15 +3,22 @@ package com.yangdai.opennote.presentation.navigation import kotlinx.serialization.Serializable @Serializable -object Home +sealed class Screen(val route: String) { + @Serializable + data object Home : Screen("home") -const val Note = "note" + data object Note : Screen("note/{id}") { + fun passId(id: Long): String { + return this.route.replace("{id}", id.toString()) + } + } -@Serializable -object Settings + @Serializable + data object Settings : Screen("settings") -@Serializable -object Folders + @Serializable + data object Folders : Screen("folders") -@Serializable -object CameraX \ No newline at end of file + @Serializable + data object CameraX : Screen("cameraX") +} diff --git a/app/src/main/java/com/yangdai/opennote/presentation/screen/BaseScreen.kt b/app/src/main/java/com/yangdai/opennote/presentation/screen/BaseScreen.kt index 06fc924..7dd0c60 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/screen/BaseScreen.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/screen/BaseScreen.kt @@ -129,10 +129,11 @@ fun BaseScreen( LoginOverlayScreen( onAuthenticated = { authenticated = true + }, + onAuthenticationNotEnrolled = { + sharedViewModel.putPreferenceValue(Constants.Preferences.NEED_PASSWORD, false) } - ) { - sharedViewModel.putPreferenceValue(Constants.Preferences.NEED_PASSWORD, false) - } + ) } } } diff --git a/app/src/main/java/com/yangdai/opennote/presentation/screen/FolderScreen.kt b/app/src/main/java/com/yangdai/opennote/presentation/screen/FolderScreen.kt index 5190869..3e8ac85 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/screen/FolderScreen.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/screen/FolderScreen.kt @@ -92,20 +92,22 @@ fun FolderScreen( sharedViewModel.onFolderEvent( FolderEvent.UpdateFolder(folderEntity) ) - }) { + } + ) { sharedViewModel.onFolderEvent(FolderEvent.DeleteFolder(it)) } } } - ModifyFolderDialog( - showDialog = showAddFolderDialog, - folder = FolderEntity(), - onDismissRequest = { showAddFolderDialog = false } - ) { - sharedViewModel.onFolderEvent( - FolderEvent.AddFolder(it) - ) + if (showAddFolderDialog) { + ModifyFolderDialog( + folder = FolderEntity(), + onDismissRequest = { showAddFolderDialog = false } + ) { + sharedViewModel.onFolderEvent( + FolderEvent.AddFolder(it) + ) + } } } } diff --git a/app/src/main/java/com/yangdai/opennote/presentation/screen/MainScreen.kt b/app/src/main/java/com/yangdai/opennote/presentation/screen/MainScreen.kt index 084ea59..603d40e 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/screen/MainScreen.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/screen/MainScreen.kt @@ -1,13 +1,64 @@ package com.yangdai.opennote.presentation.screen import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.items +import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.DriveFileMove +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.GridView +import androidx.compose.material.icons.outlined.Menu +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material.icons.outlined.RestartAlt +import androidx.compose.material.icons.outlined.SortByAlpha +import androidx.compose.material.icons.outlined.Upload +import androidx.compose.material.icons.outlined.ViewAgenda +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Checkbox import androidx.compose.material3.DrawerValue +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.VerticalDivider import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -16,27 +67,41 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.yangdai.opennote.MainActivity +import com.yangdai.opennote.R import com.yangdai.opennote.data.local.entity.FolderEntity import com.yangdai.opennote.data.local.entity.NoteEntity +import com.yangdai.opennote.presentation.component.AdaptiveNavigationScreen +import com.yangdai.opennote.presentation.component.ColumnNoteCard +import com.yangdai.opennote.presentation.component.DrawerContent +import com.yangdai.opennote.presentation.component.ExportDialog +import com.yangdai.opennote.presentation.component.FolderListDialog +import com.yangdai.opennote.presentation.component.GridNoteCard import com.yangdai.opennote.presentation.event.ListEvent -import com.yangdai.opennote.presentation.component.MainContent -import com.yangdai.opennote.presentation.component.ModalNavigationScreen -import com.yangdai.opennote.presentation.component.PermanentNavigationScreen +import com.yangdai.opennote.presentation.component.ProgressDialog +import com.yangdai.opennote.presentation.component.AdaptiveTopSearchbar +import com.yangdai.opennote.presentation.component.OrderSectionDialog import com.yangdai.opennote.presentation.event.DatabaseEvent +import com.yangdai.opennote.presentation.navigation.Screen import com.yangdai.opennote.presentation.viewmodel.SharedViewModel -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen( sharedViewModel: SharedViewModel = hiltViewModel(LocalContext.current as MainActivity), isLargeScreen: Boolean, navigateToNote: (Long) -> Unit, - navigateTo: (Any) -> Unit + navigateToScreen: (Screen) -> Unit ) { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() @@ -46,6 +111,10 @@ fun MainScreen( val folderList by sharedViewModel.foldersStateFlow.collectAsStateWithLifecycle() val dataActionState by sharedViewModel.dataActionStateFlow.collectAsStateWithLifecycle() + val staggeredGridState = rememberLazyStaggeredGridState() + val lazyListState = rememberLazyListState() + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + // Search bar state, reset when configuration changes, no need to use rememberSaveable var isSearchBarActivated by remember { mutableStateOf(false) } @@ -71,6 +140,7 @@ fun MainScreen( val isFloatingButtonVisible by remember { derivedStateOf { selectedDrawerIndex == 0 && !isSearchBarActivated && !isMultiSelectionModeEnabled + && !staggeredGridState.isScrollInProgress && !lazyListState.isScrollInProgress } } @@ -81,27 +151,6 @@ fun MainScreen( allNotesSelected = false } - // Operation caused by selecting the drawer item - fun selectDrawer(position: Int, folderEntity: FolderEntity) { - if (selectedDrawerIndex != position) { - initializeNoteSelection() - selectedDrawerIndex = position - selectedFolder = folderEntity - when (position) { - 0 -> sharedViewModel.onListEvent(ListEvent.Sort(trash = false)) - - 1 -> sharedViewModel.onListEvent(ListEvent.Sort(trash = true)) - - else -> sharedViewModel.onListEvent( - ListEvent.Sort( - filterFolder = true, - folderId = folderEntity.id - ) - ) - } - } - } - // select all and deselect all, triggered by the checkbox in the bottom bar LaunchedEffect(allNotesSelected) { selectedNotes = if (allNotesSelected) selectedNotes.plus(dataState.notes) @@ -115,63 +164,411 @@ fun MainScreen( } } - // Navigation drawer state, confirmStateChange is used to prevent drawer from closing when search bar is active - val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) - - fun closeDrawer() { - coroutineScope.launch { - drawerState.apply { - close() - } - } + // Bottom sheet visibility, reset when configuration changes, no need to use rememberSaveable + var isFolderDialogVisible by remember { + mutableStateOf(false) } - BackHandler(!isLargeScreen && drawerState.isOpen) { - closeDrawer() + // Whether to show the export dialog + var isExportDialogVisible by remember { + mutableStateOf(false) } - val movableContent = remember { - movableContentOf { - MainContent( - isListViewMode = settingsState.isListView, - dataActionState = dataActionState, + AdaptiveNavigationScreen( + isLargeScreen = isLargeScreen, + drawerState = drawerState, + gesturesEnabled = !isMultiSelectionModeEnabled && !isSearchBarActivated, + drawerContent = { + DrawerContent( + folderList = folderList, selectedDrawerIndex = selectedDrawerIndex, - selectedFolder = selectedFolder, - selectedNotes = selectedNotes.toImmutableList(), - allNotesSelected = allNotesSelected, - isMultiSelectionModeEnabled = isMultiSelectionModeEnabled, - isLargeScreen = isLargeScreen, - dataState = dataState, - folderList = folderList.toImmutableList(), - isFloatingButtonVisible = isFloatingButtonVisible, - navigateToNote = navigateToNote, - initializeNoteSelection = { initializeNoteSelection() }, - onSearchBarActivationChange = { isSearchBarActivated = it }, - onAllNotesSelectionChange = { allNotesSelected = it }, - onMultiSelectionModeChange = { isMultiSelectionModeEnabled = it }, - onNoteClick = { - if (isMultiSelectionModeEnabled) { - selectedNotes = - if (selectedNotes.contains(it)) selectedNotes.minus(it) - else selectedNotes.plus(it) - } else { - if (selectedDrawerIndex != 1) { - sharedViewModel.onListEvent(ListEvent.OpenNote(it)) - navigateToNote(it.id!!) - } else { - Unit - } + navigateTo = { navigateToScreen(it) } + ) { position, folderEntity -> + if (selectedDrawerIndex != position) { + initializeNoteSelection() + selectedDrawerIndex = position + selectedFolder = folderEntity + when (position) { + 0 -> sharedViewModel.onListEvent(ListEvent.Sort(trash = false)) + + 1 -> sharedViewModel.onListEvent(ListEvent.Sort(trash = true)) + + else -> sharedViewModel.onListEvent( + ListEvent.Sort( + filterFolder = true, + folderId = folderEntity.id + ) + ) } - }, - onListEvent = { sharedViewModel.onListEvent(it) }, - onDrawerStateChange = { + } + if (!isLargeScreen) coroutineScope.launch { drawerState.apply { - if (isClosed) open() else close() + close() + } + } + } + }, + ) { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + AnimatedContent(targetState = selectedDrawerIndex == 0, label = "") { + if (it) { + AdaptiveTopSearchbar( + enabled = !isMultiSelectionModeEnabled, + isLargeScreen = isLargeScreen, + onSearchBarActivationChange = { activated -> + isSearchBarActivated = activated + }, + onDrawerStateChange = { + coroutineScope.launch { + drawerState.apply { + if (isClosed) open() else close() + } + } + } + ) + } else { + + var showMenu by remember { + mutableStateOf(false) + } + + TopAppBar( + title = { + Text( + text = if (selectedDrawerIndex == 1) stringResource(id = R.string.trash) + else selectedFolder.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = { + if (!isLargeScreen) { + IconButton( + enabled = !isMultiSelectionModeEnabled, + onClick = { + coroutineScope.launch { + drawerState.apply { + if (isClosed) open() else close() + } + } + } + ) { + Icon( + imageVector = Icons.Outlined.Menu, + contentDescription = "Open Menu" + ) + } + } + }, + actions = { + IconButton(onClick = { sharedViewModel.onListEvent(ListEvent.ChangeViewMode) }) { + Icon( + imageVector = if (!settingsState.isListView) Icons.Outlined.ViewAgenda else Icons.Outlined.GridView, + contentDescription = "View Mode" + ) + } + IconButton(onClick = { sharedViewModel.onListEvent(ListEvent.ToggleOrderSection) }) { + Icon( + imageVector = Icons.Outlined.SortByAlpha, + contentDescription = "Sort" + ) + } + if (selectedDrawerIndex == 1) { + IconButton(onClick = { showMenu = !showMenu }) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = "More" + ) + } + + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }) { + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = Icons.Outlined.RestartAlt, + contentDescription = "Restore" + ) + }, + text = { Text(text = stringResource(id = R.string.restore_all)) }, + onClick = { + sharedViewModel.onListEvent( + ListEvent.RestoreNotes(dataState.notes) + ) + }) + + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Delete" + ) + }, + text = { Text(text = stringResource(id = R.string.delete_all)) }, + onClick = { + sharedViewModel.onListEvent( + ListEvent.DeleteNotes(dataState.notes, false) + ) + }) + } + } + } + ) + } + } + }, + bottomBar = { + AnimatedVisibility( + visible = isMultiSelectionModeEnabled, + enter = slideInVertically { fullHeight -> fullHeight }, + exit = slideOutVertically { fullHeight -> fullHeight } + ) { + BottomAppBar { + Row( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + + Row( + modifier = Modifier.fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically + ) { + + Checkbox( + checked = allNotesSelected, + onCheckedChange = { allNotesSelected = it } + ) + + Text(text = stringResource(R.string.checked)) + + Text(text = selectedNotes.size.toString()) + } + + Row( + modifier = Modifier.fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically + ) { + + if (selectedDrawerIndex == 1) { + TextButton(onClick = { + sharedViewModel.onListEvent( + ListEvent.RestoreNotes(selectedNotes) + ) + initializeNoteSelection() + }) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.Outlined.RestartAlt, + contentDescription = "Restore" + ) + Text(text = stringResource(id = R.string.restore)) + } + } + } else { + TextButton(onClick = { + isExportDialogVisible = true + }) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.Outlined.Upload, + contentDescription = "Export" + ) + Text(text = stringResource(id = R.string.export)) + } + } + + TextButton(onClick = { isFolderDialogVisible = true }) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.DriveFileMove, + contentDescription = "Move" + ) + Text(text = stringResource(id = R.string.move)) + } + } + } + + TextButton(onClick = { + sharedViewModel.onListEvent( + ListEvent.DeleteNotes( + selectedNotes, + selectedDrawerIndex != 1 + ) + ) + initializeNoteSelection() + }) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Delete" + ) + Text(text = stringResource(id = R.string.delete)) + } + } + } } } - }, - onExportClick = { + } + }, + floatingActionButton = { + AnimatedVisibility( + visible = isFloatingButtonVisible, + enter = slideInHorizontally { fullWidth -> fullWidth * 3 / 2 }, + exit = slideOutHorizontally { fullWidth -> fullWidth * 3 / 2 }) { + FloatingActionButton( + onClick = { + sharedViewModel.onListEvent(ListEvent.AddNote) + navigateToNote(-1) + } + ) { + Icon(imageVector = Icons.Outlined.Add, contentDescription = "Add") + } + } + + }) { innerPadding -> + + // Add layoutDirection, displayCutout, startPadding, and endPadding. + val layoutDirection = LocalLayoutDirection.current + val displayCutout = WindowInsets.displayCutout.asPaddingValues() + val startPadding = displayCutout.calculateStartPadding(layoutDirection) + val endPadding = displayCutout.calculateEndPadding(layoutDirection) + + Box( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .padding(top = 72.dp, start = startPadding, end = endPadding) + ) { + if (!settingsState.isListView) { + LazyVerticalStaggeredGrid( + modifier = Modifier + .fillMaxSize(), + state = staggeredGridState, + // The staggered grid layout is adaptive, with a minimum column width of 160dp(mdpi) + columns = StaggeredGridCells.Adaptive(160.dp), + verticalItemSpacing = 8.dp, + horizontalArrangement = Arrangement.spacedBy(8.dp), + // for better edgeToEdge experience + contentPadding = PaddingValues( + start = 16.dp, + end = 16.dp, + bottom = innerPadding.calculateBottomPadding() + ), + content = { + items( + dataState.notes, + key = { item: NoteEntity -> item.id!! }) { note -> + GridNoteCard( + modifier = Modifier + .fillMaxWidth() + .animateItem(), // Add animation to the item + note = note, + isEnabled = isMultiSelectionModeEnabled, + isSelected = selectedNotes.contains(note), + onEnableChange = { isMultiSelectionModeEnabled = it }, + onNoteClick = { + if (isMultiSelectionModeEnabled) { + selectedNotes = + if (selectedNotes.contains(it)) selectedNotes.minus( + it + ) + else selectedNotes.plus(it) + } else { + if (selectedDrawerIndex != 1) { + sharedViewModel.onListEvent(ListEvent.OpenNote(it)) + navigateToNote(it.id!!) + } else { + Unit + } + } + } + ) + } + } + ) + } else { + + if (dataState.notes.isEmpty()) { + return@Box + } + + VerticalDivider( + Modifier + .align(Alignment.TopStart) + .fillMaxHeight() + .padding(start = 15.dp), + thickness = 2.dp + ) + LazyColumn( + modifier = Modifier + .align(Alignment.Center) + .fillMaxSize(), + state = lazyListState, + contentPadding = PaddingValues( + start = 12.dp, + end = 16.dp, + bottom = innerPadding.calculateBottomPadding() + ) + ) { + items( + dataState.notes, + key = { item: NoteEntity -> item.id!! }) { note -> + ColumnNoteCard( + modifier = Modifier + .fillMaxWidth() + .animateItem(), // Add animation to the item + note = note, + isEnabled = isMultiSelectionModeEnabled, + isSelected = selectedNotes.contains(note), + onEnableChange = { isMultiSelectionModeEnabled = it }, + onNoteClick = { + if (isMultiSelectionModeEnabled) { + selectedNotes = + if (selectedNotes.contains(it)) selectedNotes.minus( + it + ) + else selectedNotes.plus(it) + } else { + if (selectedDrawerIndex != 1) { + sharedViewModel.onListEvent(ListEvent.OpenNote(it)) + navigateToNote(it.id!!) + } else { + Unit + } + } + } + ) + } + } + } + } + + if (dataState.isOrderSectionVisible) { + OrderSectionDialog( + noteOrder = dataState.noteOrder, + onOrderChange = { + sharedViewModel.onListEvent( + ListEvent.Sort( + noteOrder = it, + trash = selectedDrawerIndex == 1, + filterFolder = selectedDrawerIndex != 0 && selectedDrawerIndex != 1, + folderId = selectedFolder.id + ) + ) + }, + onDismiss = { sharedViewModel.onListEvent(ListEvent.ToggleOrderSection) } + ) + } + + if (isExportDialogVisible) { + ExportDialog(onDismissRequest = { isExportDialogVisible = false }) { sharedViewModel.onDatabaseEvent( DatabaseEvent.Export( context.contentResolver, @@ -179,34 +576,27 @@ fun MainScreen( it ) ) - }, - onExportCancelled = { - sharedViewModel.cancelDataAction() + isExportDialogVisible = false } - ) - } - } + } - if (!isLargeScreen) { - ModalNavigationScreen( - drawerState = drawerState, - gesturesEnabled = !isMultiSelectionModeEnabled && !isSearchBarActivated, - folderList = folderList.toImmutableList(), - selectedDrawerIndex = selectedDrawerIndex, - content = movableContent, - navigateTo = navigateTo - ) { position, folder -> - selectDrawer(position, folder) - closeDrawer() - } - } else { - PermanentNavigationScreen( - folderList = folderList.toImmutableList(), - selectedDrawerIndex = selectedDrawerIndex, - content = movableContent, - navigateTo = navigateTo - ) { position, folder -> - selectDrawer(position, folder) + if (isFolderDialogVisible) { + FolderListDialog( + hint = stringResource(R.string.destination_folder), + oFolderId = selectedFolder.id, + folders = folderList, + onDismissRequest = { isFolderDialogVisible = false } + ) { + sharedViewModel.onListEvent(ListEvent.MoveNotes(selectedNotes, it)) + initializeNoteSelection() + } + } + + ProgressDialog( + isLoading = dataActionState.loading, + progress = dataActionState.progress, + onDismissRequest = sharedViewModel::cancelDataAction + ) } } } diff --git a/app/src/main/java/com/yangdai/opennote/presentation/screen/NoteScreen.kt b/app/src/main/java/com/yangdai/opennote/presentation/screen/NoteScreen.kt index d965e0a..ab4293e 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/screen/NoteScreen.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/screen/NoteScreen.kt @@ -99,7 +99,6 @@ import com.yangdai.opennote.presentation.event.UiEvent import com.yangdai.opennote.presentation.util.Constants import com.yangdai.opennote.presentation.util.timestampToFormatLocalDateTime import com.yangdai.opennote.presentation.viewmodel.SharedViewModel -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import java.io.File @@ -650,7 +649,7 @@ fun NoteScreen( FolderListDialog( hint = stringResource(R.string.destination_folder), oFolderId = noteState.folderId, - folders = folderList.toImmutableList(), + folders = folderList, onDismissRequest = { showFolderDialog = false } ) { sharedViewModel.onNoteEvent(NoteEvent.FolderChanged(it)) @@ -659,8 +658,7 @@ fun NoteScreen( ProgressDialog( isLoading = actionState.loading, - progress = actionState.progress - ) { - sharedViewModel.cancelDataAction() - } + progress = actionState.progress, + onDismissRequest = sharedViewModel::cancelDataAction + ) } diff --git a/app/src/main/java/com/yangdai/opennote/presentation/state/DataActionState.kt b/app/src/main/java/com/yangdai/opennote/presentation/state/DataActionState.kt index bd2ffcd..9b0f356 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/state/DataActionState.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/state/DataActionState.kt @@ -1,5 +1,8 @@ package com.yangdai.opennote.presentation.state +import androidx.compose.runtime.Stable + +@Stable data class DataActionState( val loading: Boolean = false, val progress: Float = 0f, diff --git a/app/src/main/java/com/yangdai/opennote/presentation/state/SettingsState.kt b/app/src/main/java/com/yangdai/opennote/presentation/state/SettingsState.kt index e58102b..030a98f 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/state/SettingsState.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/state/SettingsState.kt @@ -1,5 +1,8 @@ package com.yangdai.opennote.presentation.state +import androidx.compose.runtime.Stable + +@Stable data class SettingsState( val theme: AppTheme = AppTheme.UNDEFINED, val color: AppColor = AppColor.DYNAMIC, @@ -8,11 +11,7 @@ data class SettingsState( val shouldFollowSystem: Boolean = false, val isSwitchActive: Boolean = false, val isListView: Boolean = false -) { - companion object { - val DEFAULT = SettingsState() - } -} +) enum class AppTheme(private val value: Int) { UNDEFINED(-1), diff --git a/app/src/main/java/com/yangdai/opennote/presentation/util/Constants.kt b/app/src/main/java/com/yangdai/opennote/presentation/util/Constants.kt index 72e8854..1fc2b27 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/util/Constants.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/util/Constants.kt @@ -1,7 +1,7 @@ package com.yangdai.opennote.presentation.util object Constants { - + const val DEFAULT_MAX_LINES = 2 const val NAV_ANIMATION_TIME = 300 const val MIME_TYPE_TEXT = "text/" const val LINK = "https://www.yangdai-opennote.com" diff --git a/app/src/main/java/com/yangdai/opennote/presentation/util/Utils.kt b/app/src/main/java/com/yangdai/opennote/presentation/util/Utils.kt index dd6227b..a0ebc43 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/util/Utils.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/util/Utils.kt @@ -3,6 +3,9 @@ package com.yangdai.opennote.presentation.util import android.content.Context import android.content.Intent import android.icu.text.DateFormat +import androidx.browser.customtabs.CustomTabsIntent +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import java.util.Date fun Intent.isTextMimeType() = type?.startsWith(Constants.MIME_TYPE_TEXT) == true @@ -43,3 +46,12 @@ fun Long.timestampToFormatLocalDateTime(): String { return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) .format(Date(this)) } + +@Composable +fun rememberCustomTabsIntent(): CustomTabsIntent { + return remember { + CustomTabsIntent.Builder() + .setShowTitle(true) + .build() + } +} diff --git a/app/src/main/java/com/yangdai/opennote/presentation/viewmodel/SharedViewModel.kt b/app/src/main/java/com/yangdai/opennote/presentation/viewmodel/SharedViewModel.kt index 44447f7..d9c4940 100644 --- a/app/src/main/java/com/yangdai/opennote/presentation/viewmodel/SharedViewModel.kt +++ b/app/src/main/java/com/yangdai/opennote/presentation/viewmodel/SharedViewModel.kt @@ -22,7 +22,7 @@ import com.yangdai.opennote.data.local.entity.NoteEntity import com.yangdai.opennote.presentation.state.SettingsState import com.yangdai.opennote.domain.repository.DataStoreRepository import com.yangdai.opennote.domain.usecase.NoteOrder -import com.yangdai.opennote.domain.usecase.Operations +import com.yangdai.opennote.domain.usecase.UseCases import com.yangdai.opennote.domain.usecase.OrderType import com.yangdai.opennote.presentation.component.ExportType import com.yangdai.opennote.presentation.component.TaskItem @@ -92,7 +92,7 @@ import javax.inject.Inject class SharedViewModel @Inject constructor( private val database: Database, private val dataStoreRepository: DataStoreRepository, - private val operations: Operations + private val useCases: UseCases ) : ViewModel() { // 起始页加载状态,初始值为 true @@ -107,7 +107,7 @@ class SharedViewModel @Inject constructor( private val _noteState = MutableStateFlow(NoteState()) val noteStateFlow = _noteState.asStateFlow() - val foldersStateFlow = operations.getFolders() + val foldersStateFlow = useCases.getFolders() .flowOn(Dispatchers.IO) .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) @@ -186,7 +186,7 @@ class SharedViewModel @Inject constructor( }.flowOn(Dispatchers.IO).stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, - initialValue = SettingsState.DEFAULT + initialValue = SettingsState() ) fun putPreferenceValue(key: String, value: T) { @@ -230,7 +230,7 @@ class SharedViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { if (event.recycle) { event.noteEntities.forEach { - operations.updateNote( + useCases.updateNote( NoteEntity( id = it.id, title = it.title, @@ -244,7 +244,7 @@ class SharedViewModel @Inject constructor( } } else { event.noteEntities.forEach { - operations.deleteNote(it) + useCases.deleteNote(it) } } } @@ -253,7 +253,7 @@ class SharedViewModel @Inject constructor( is ListEvent.RestoreNotes -> { viewModelScope.launch(Dispatchers.IO) { event.noteEntities.forEach { - operations.updateNote( + useCases.updateNote( NoteEntity( id = it.id, title = it.title, @@ -282,7 +282,7 @@ class SharedViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { val folderId = event.folderId event.noteEntities.forEach { - operations.updateNote( + useCases.updateNote( NoteEntity( id = it.id, title = it.title, @@ -319,20 +319,20 @@ class SharedViewModel @Inject constructor( when (event) { is FolderEvent.AddFolder -> { viewModelScope.launch(Dispatchers.IO) { - operations.addFolder(event.folder) + useCases.addFolder(event.folder) } } is FolderEvent.DeleteFolder -> { viewModelScope.launch(Dispatchers.IO) { - operations.deleteNotesByFolderId(event.folder.id) - operations.deleteFolder(event.folder) + useCases.deleteNotesByFolderId(event.folder.id) + useCases.deleteFolder(event.folder) } } is FolderEvent.UpdateFolder -> { viewModelScope.launch(Dispatchers.IO) { - operations.updateFolder(event.folder) + useCases.updateFolder(event.folder) } } } @@ -345,7 +345,7 @@ class SharedViewModel @Inject constructor( folderId: Long? = null, ) { queryNotesJob?.cancel() - queryNotesJob = operations.getNotes(noteOrder, trash, filterFolder, folderId) + queryNotesJob = useCases.getNotes(noteOrder, trash, filterFolder, folderId) .flowOn(Dispatchers.IO) .onEach { notes -> _dataState.update { @@ -364,7 +364,7 @@ class SharedViewModel @Inject constructor( private fun searchNotes(keyWord: String) { queryNotesJob?.cancel() - queryNotesJob = operations.searchNotes(keyWord) + queryNotesJob = useCases.searchNotes(keyWord) .flowOn(Dispatchers.IO) .onEach { notes -> _dataState.update { @@ -409,7 +409,7 @@ class SharedViewModel @Inject constructor( isMarkdown = noteState.isMarkdown, timestamp = System.currentTimeMillis() ) - operations.addNote(note) + useCases.addNote(note) _event.emit(UiEvent.NavigateBack) } } @@ -418,7 +418,7 @@ class SharedViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { val note = noteStateFlow.value note.id?.let { - operations.updateNote( + useCases.updateNote( NoteEntity( id = it, title = titleState.text.toString(), @@ -472,7 +472,7 @@ class SharedViewModel @Inject constructor( // 判断id是否与oNote的id相同,不同则从数据库获取笔记,并更新oNote。 viewModelScope.launch(Dispatchers.IO) { if (event.id != (_oNote.id ?: -1L)) - _oNote = operations.getNoteById(event.id) + _oNote = useCases.getNoteById(event.id) ?: NoteEntity(timestamp = System.currentTimeMillis()) _noteState.update { noteState -> noteState.copy( @@ -500,7 +500,7 @@ class SharedViewModel @Inject constructor( ) if (note.id != null) if (note.title != _oNote.title || note.content != _oNote.content || note.isMarkdown != _oNote.isMarkdown || note.folderId != _oNote.folderId) - operations.updateNote(note) + useCases.updateNote(note) } } } @@ -568,7 +568,7 @@ class SharedViewModel @Inject constructor( || (fileName?.endsWith(".html") == true)), timestamp = System.currentTimeMillis() ) - operations.addNote(note) + useCases.addNote(note) } } } @@ -651,7 +651,7 @@ class SharedViewModel @Inject constructor( it.copy(progress = 0.2f) } dataActionJob = viewModelScope.launch(Dispatchers.IO) { - val notes = operations.getNotes().first() + val notes = useCases.getNotes().first() val json = Json.encodeToString(notes) _dataActionState.update { it.copy(progress = 0.5f) @@ -711,7 +711,7 @@ class SharedViewModel @Inject constructor( it.copy(progress = 0.6f) } notes.forEachIndexed { _, noteEntity -> - operations.addNote(noteEntity) + useCases.addNote(noteEntity) } }.onFailure { throwable -> _dataActionState.update { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f528756..402d607 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,9 @@ [versions] -agp = "8.4.0" +agp = "8.4.1" glance = "1.1.0-rc01" -kotlin = "2.0.0-RC3" -ksp = "2.0.0-RC3-1.0.20" +kotlin = "2.0.0" +ksp = "2.0.0-1.0.21" kotlinxSerialization = "1.6.3" -kotlinxCollectionsImmutable = "0.3.7" composeBom = "2024.05.00" composeFoundation = "1.7.0-beta01" @@ -82,7 +81,6 @@ androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" } kotlinx-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } -kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } text-recognition = { group = "com.google.mlkit", name = "text-recognition", version.ref = "textRecognition" } text-recognition-chinese = { group = "com.google.mlkit", name = "text-recognition-chinese", version.ref = "textRecognition" }