Compare commits

...

2 Commits

6 changed files with 327 additions and 150 deletions

View File

@ -199,13 +199,9 @@ fun NotesApp() {
"trash" -> "Sampah" "trash" -> "Sampah"
else -> "AI Notes" else -> "AI Notes"
}, },
showBackButton = (selectedCategory != null && currentScreen == "main") || currentScreen == "starred", showBackButton = (selectedCategory != null && currentScreen == "main"),
onBackClick = { onBackClick = {
if (currentScreen == "starred") { selectedCategory = null
currentScreen = "main"
} else {
selectedCategory = null
}
}, },
onMenuClick = { drawerState = !drawerState }, onMenuClick = { drawerState = !drawerState },
onSearchClick = { showSearch = !showSearch }, onSearchClick = { showSearch = !showSearch },
@ -354,6 +350,16 @@ fun NotesApp() {
it it
} }
} }
},
onNoteEdit = { note ->
editingNote = note
showNoteDialog = true
},
onNoteDelete = { note ->
notes = notes.map {
if (it.id == note.id) it.copy(isDeleted = true)
else it
}
} }
) )
@ -392,8 +398,6 @@ fun NotesApp() {
fullScreenNote = note fullScreenNote = note
showFullScreenNote = true showFullScreenNote = true
}, },
onMenuClick = { drawerState = true },
onBack = { currentScreen = "main" },
onUnpin = { note -> onUnpin = { note ->
notes = notes.map { notes = notes.map {
if (it.id == note.id) it.copy(isPinned = false) if (it.id == note.id) it.copy(isPinned = false)

View File

@ -4,6 +4,7 @@ import androidx.compose.animation.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
@ -14,6 +15,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -34,113 +36,141 @@ fun ModernTopBar(
onSearchQueryChange: (String) -> Unit, onSearchQueryChange: (String) -> Unit,
showSearch: Boolean showSearch: Boolean
) { ) {
// Smooth transition for search bar // Floating Top Bar with same style as Bottom Bar
AnimatedContent( Box(
targetState = showSearch, modifier = Modifier
transitionSpec = { .fillMaxWidth()
fadeIn(animationSpec = tween(Constants.ANIMATION_DURATION_MEDIUM)) togetherWith .padding(horizontal = 16.dp, vertical = 8.dp)
fadeOut(animationSpec = tween(Constants.ANIMATION_DURATION_MEDIUM)) ) {
}, Surface(
label = "topbar" modifier = Modifier
) { isSearching -> .fillMaxWidth()
if (isSearching) { .shadow(
// Search Mode - Minimalist elevation = Constants.Elevation.Large.dp,
Surface( shape = RoundedCornerShape(Constants.Radius.ExtraLarge.dp)
modifier = Modifier ),
.fillMaxWidth() color = AppColors.SurfaceElevated,
.shadow(Constants.Elevation.Small.dp), shape = RoundedCornerShape(Constants.Radius.ExtraLarge.dp)
color = AppColors.Surface ) {
) { // Smooth transition for search bar
Row( AnimatedContent(
modifier = Modifier targetState = showSearch,
.fillMaxWidth() transitionSpec = {
.padding(horizontal = Constants.Spacing.Medium.dp, vertical = Constants.Spacing.Small.dp), fadeIn(animationSpec = tween(Constants.ANIMATION_DURATION_MEDIUM)) togetherWith
verticalAlignment = Alignment.CenterVertically fadeOut(animationSpec = tween(Constants.ANIMATION_DURATION_MEDIUM))
) { },
// Search TextField label = "topbar"
TextField( ) { isSearching ->
value = searchQuery, if (isSearching) {
onValueChange = onSearchQueryChange, // Search Mode
placeholder = { Row(
Text(
"Cari catatan atau kategori...",
color = AppColors.OnSurfaceVariant,
fontSize = 15.sp
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = AppColors.SurfaceVariant,
unfocusedContainerColor = AppColors.SurfaceVariant,
focusedTextColor = AppColors.OnBackground,
unfocusedTextColor = AppColors.OnSurface,
cursorColor = AppColors.Primary,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxWidth()
.heightIn(min = 48.dp), .padding(horizontal = 12.dp, vertical = 8.dp),
shape = RoundedCornerShape(Constants.Radius.Medium.dp), verticalAlignment = Alignment.CenterVertically,
singleLine = true, horizontalArrangement = Arrangement.spacedBy(8.dp)
textStyle = LocalTextStyle.current.copy(fontSize = 15.sp)
)
Spacer(modifier = Modifier.width(Constants.Spacing.Small.dp))
// Close Search Button
IconButton(
onClick = {
onSearchQueryChange("")
onSearchClick()
}
) { ) {
Icon( // Search TextField
Icons.Default.Close, OutlinedTextField(
contentDescription = "Close Search", value = searchQuery,
tint = AppColors.OnSurfaceVariant onValueChange = onSearchQueryChange,
placeholder = {
Text(
"Cari...",
color = AppColors.OnSurfaceVariant,
fontSize = 15.sp
)
},
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = AppColors.SurfaceVariant,
unfocusedContainerColor = AppColors.SurfaceVariant,
focusedTextColor = AppColors.OnBackground,
unfocusedTextColor = AppColors.OnSurface,
cursorColor = AppColors.Primary,
focusedBorderColor = AppColors.Primary,
unfocusedBorderColor = Color.Transparent
),
modifier = Modifier
.weight(1f)
.heightIn(min = 48.dp),
shape = RoundedCornerShape(Constants.Radius.Large.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 15.sp)
) )
}
}
}
} else {
// Normal Mode - Minimalist
Surface(
modifier = Modifier
.fillMaxWidth()
.shadow(Constants.Elevation.Small.dp),
color = AppColors.Surface
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = Constants.Spacing.Small.dp, vertical = Constants.Spacing.Small.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Back/Menu Button
IconButton(onClick = if (showBackButton) onBackClick else onMenuClick) {
Icon(
if (showBackButton) Icons.AutoMirrored.Filled.ArrowBack else Icons.Default.Menu,
contentDescription = if (showBackButton) "Back" else "Menu",
tint = AppColors.OnSurface
)
}
// Title // Close Search Button
Text( IconButton(
title, onClick = {
fontWeight = FontWeight.Bold, onSearchQueryChange("")
fontSize = 20.sp, onSearchClick()
color = AppColors.OnBackground, },
modifier = Modifier.weight(1f) modifier = Modifier
) .size(40.dp)
.clip(CircleShape)
.background(AppColors.SurfaceVariant)
) {
Icon(
Icons.Default.Close,
contentDescription = "Close Search",
tint = AppColors.OnSurfaceVariant,
modifier = Modifier.size(20.dp)
)
}
}
} else {
// Normal Mode
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
// Back/Menu Button
IconButton(
onClick = if (showBackButton) onBackClick else onMenuClick,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(
if (showBackButton) AppColors.SurfaceVariant
else AppColors.Primary.copy(alpha = 0.1f)
)
) {
Icon(
if (showBackButton) Icons.AutoMirrored.Filled.ArrowBack else Icons.Default.Menu,
contentDescription = if (showBackButton) "Back" else "Menu",
tint = if (showBackButton) AppColors.OnSurface else AppColors.Primary,
modifier = Modifier.size(20.dp)
)
}
// Search Button // Title
IconButton(onClick = onSearchClick) { Text(
Icon( title,
Icons.Default.Search, fontWeight = FontWeight.Bold,
contentDescription = "Search", fontSize = 18.sp,
tint = AppColors.OnSurfaceVariant color = AppColors.OnBackground,
modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp)
) )
// Search Button
IconButton(
onClick = onSearchClick,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(AppColors.SurfaceVariant)
) {
Icon(
Icons.Default.Search,
contentDescription = "Search",
tint = AppColors.OnSurfaceVariant,
modifier = Modifier.size(20.dp)
)
}
} }
} }
} }

View File

@ -14,6 +14,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -22,6 +23,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.example.notesai.config.APIKey
import com.example.notesai.data.local.DataStoreManager import com.example.notesai.data.local.DataStoreManager
import com.example.notesai.data.model.* import com.example.notesai.data.model.*
import com.example.notesai.util.Constants import com.example.notesai.util.Constants
@ -35,7 +37,6 @@ import com.example.notesai.presentation.screens.ai.components.ChatHistoryDrawer
import com.example.notesai.presentation.screens.ai.components.CompactStatItem import com.example.notesai.presentation.screens.ai.components.CompactStatItem
import com.example.notesai.presentation.screens.ai.components.SuggestionChip import com.example.notesai.presentation.screens.ai.components.SuggestionChip
import com.example.notesai.util.AppColors import com.example.notesai.util.AppColors
import com.example.notesai.config.APIKey
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable

View File

@ -27,7 +27,9 @@ fun MainScreen(
onNoteClick: (Note) -> Unit, onNoteClick: (Note) -> Unit,
onPinToggle: (Note) -> Unit, onPinToggle: (Note) -> Unit,
onCategoryDelete: (Category) -> Unit, onCategoryDelete: (Category) -> Unit,
onCategoryEdit: (Category, String, Long, Long) -> Unit onCategoryEdit: (Category, String, Long, Long) -> Unit,
onNoteEdit: (Note) -> Unit = {},
onNoteDelete: (Note) -> Unit = {}
) { ) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
if (selectedCategory == null) { if (selectedCategory == null) {
@ -120,7 +122,9 @@ fun MainScreen(
NoteCard( NoteCard(
note = note, note = note,
onClick = { onNoteClick(note) }, onClick = { onNoteClick(note) },
onPinClick = { onPinToggle(note) } onPinClick = { onPinToggle(note) },
onEdit = { onNoteEdit(note) },
onDelete = { onNoteDelete(note) }
) )
} }
} }

View File

@ -3,11 +3,12 @@ package com.example.notesai.presentation.screens.main.components
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.StarBorder import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -30,9 +31,13 @@ import java.util.*
fun NoteCard( fun NoteCard(
note: Note, note: Note,
onClick: () -> Unit, onClick: () -> Unit,
onPinClick: () -> Unit onPinClick: () -> Unit,
onEdit: () -> Unit = {},
onDelete: () -> Unit = {}
) { ) {
val dateFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID")) val dateFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID"))
var showMenu by remember { mutableStateOf(false) }
var showDeleteConfirm by remember { mutableStateOf(false) }
// Scale animation on press // Scale animation on press
var isPressed by remember { mutableStateOf(false) } var isPressed by remember { mutableStateOf(false) }
@ -45,31 +50,78 @@ fun NoteCard(
label = "scale" label = "scale"
) )
// Delete Confirmation Dialog
if (showDeleteConfirm) {
AlertDialog(
onDismissRequest = { showDeleteConfirm = false },
icon = {
Icon(
Icons.Default.DeleteForever,
contentDescription = null,
tint = AppColors.Error
)
},
title = {
Text(
"Pindahkan ke Sampah?",
color = AppColors.OnBackground,
fontWeight = FontWeight.Bold
)
},
text = {
Text(
"Catatan '${note.title}' akan dipindahkan ke sampah.",
color = AppColors.OnSurfaceVariant
)
},
confirmButton = {
Button(
onClick = {
onDelete()
showDeleteConfirm = false
},
colors = ButtonDefaults.buttonColors(
containerColor = AppColors.Error
)
) {
Text("Hapus", color = Color.White)
}
},
dismissButton = {
TextButton(onClick = { showDeleteConfirm = false }) {
Text("Batal", color = AppColors.OnSurfaceVariant)
}
},
containerColor = AppColors.Surface,
shape = RoundedCornerShape(Constants.Radius.Large.dp)
)
}
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.scale(scale) .scale(scale)
.combinedClickable(onClick = onClick), .combinedClickable(onClick = onClick),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(Constants.Radius.Large.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = AppColors.SurfaceVariant containerColor = AppColors.SurfaceVariant
), ),
elevation = CardDefaults.cardElevation( elevation = CardDefaults.cardElevation(
defaultElevation = 2.dp defaultElevation = Constants.Elevation.Small.dp
) )
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(Constants.Spacing.Large.dp)
) { ) {
// Header: Title + Pin // Header: Title + Actions (Vertical)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top verticalAlignment = Alignment.Top
) { ) {
// Title // Title - takes most space
Text( Text(
note.title, note.title,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
@ -81,17 +133,94 @@ fun NoteCard(
fontSize = 18.sp fontSize = 18.sp
) )
// Pin Button // Vertical Actions Stack
IconButton( Column(
onClick = onPinClick, horizontalAlignment = Alignment.End,
modifier = Modifier.size(32.dp) verticalArrangement = Arrangement.spacedBy(0.dp)
) { ) {
Icon( // Menu Button
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder, Box {
contentDescription = "Pin", IconButton(
tint = if (note.isPinned) AppColors.Warning else AppColors.OnSurfaceVariant, onClick = { showMenu = true },
modifier = Modifier.size(18.dp) modifier = Modifier.size(28.dp)
) ) {
Icon(
Icons.Default.MoreVert,
contentDescription = "Menu",
tint = AppColors.OnSurfaceVariant,
modifier = Modifier.size(18.dp)
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
modifier = Modifier.background(AppColors.SurfaceElevated)
) {
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
) {
Icon(
Icons.Default.Edit,
contentDescription = null,
tint = AppColors.Primary,
modifier = Modifier.size(18.dp)
)
Text(
"Edit Catatan",
color = AppColors.OnSurface,
fontSize = 14.sp
)
}
},
onClick = {
showMenu = false
onEdit()
}
)
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
) {
Icon(
Icons.Default.Delete,
contentDescription = null,
tint = AppColors.Error,
modifier = Modifier.size(18.dp)
)
Text(
"Pindah ke Sampah",
color = AppColors.OnSurface,
fontSize = 14.sp
)
}
},
onClick = {
showMenu = false
showDeleteConfirm = true
}
)
}
}
// Pin Button
IconButton(
onClick = onPinClick,
modifier = Modifier.size(28.dp)
) {
Icon(
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
contentDescription = "Pin",
tint = if (note.isPinned) AppColors.Warning else AppColors.OnSurfaceVariant,
modifier = Modifier.size(18.dp)
)
}
} }
} }

View File

@ -1,13 +1,16 @@
package com.example.notesai.presentation.screens.starred package com.example.notesai.presentation.screens.starred
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.notesai.presentation.components.EmptyState import com.example.notesai.presentation.components.EmptyState
import com.example.notesai.presentation.screens.starred.components.StarredNoteCard import com.example.notesai.presentation.screens.starred.components.StarredNoteCard
@ -20,31 +23,37 @@ fun StarredNotesScreen(
notes: List<Note>, notes: List<Note>,
categories: List<Category>, categories: List<Category>,
onNoteClick: (Note) -> Unit, onNoteClick: (Note) -> Unit,
onMenuClick: () -> Unit,
onBack: () -> Unit,
onUnpin: (Note) -> Unit onUnpin: (Note) -> Unit
) { ) {
val starredNotes = notes.filter { it.isPinned && !it.isArchived && !it.isDeleted } val starredNotes = notes.filter { it.isPinned && !it.isArchived && !it.isDeleted }
.sortedByDescending { it.timestamp }
if (starredNotes.isEmpty()) { Column(modifier = Modifier.fillMaxSize()) {
EmptyState( if (starredNotes.isEmpty()) {
icon = Icons.Default.Star, EmptyState(
message = "Belum ada catatan berbintang", icon = Icons.Default.Star,
subtitle = "Catatan yang ditandai berbintang akan muncul di sini" message = "Belum ada catatan berbintang",
) subtitle = "Catatan yang ditandai berbintang akan muncul di sini"
} else { )
LazyColumn( } else {
contentPadding = PaddingValues(16.dp), LazyColumn(
verticalArrangement = Arrangement.spacedBy(12.dp) contentPadding = PaddingValues(
) { start = 16.dp,
items(starredNotes) { note -> end = 16.dp,
val category = categories.find { it.id == note.categoryId } top = 16.dp,
StarredNoteCard( bottom = 100.dp // Extra space untuk bottom bar
note = note, ),
categoryName = category?.name ?: "Unknown", verticalArrangement = Arrangement.spacedBy(12.dp)
onClick = { onNoteClick(note) }, ) {
onUnpin = { onUnpin(note) } items(starredNotes) { note ->
) val category = categories.find { it.id == note.categoryId }
StarredNoteCard(
note = note,
categoryName = category?.name ?: "Unknown",
onClick = { onNoteClick(note) },
onUnpin = { onUnpin(note) }
)
}
} }
} }
} }