Compare commits

..

No commits in common. "05b7a2a71b0fed99a773c202e4db854d75f89c61" and "b264f87b14af0de38dd35e0481d06a95bfb9ba92" have entirely different histories.

6 changed files with 151 additions and 328 deletions

View File

@ -199,9 +199,13 @@ fun NotesApp() {
"trash" -> "Sampah" "trash" -> "Sampah"
else -> "AI Notes" else -> "AI Notes"
}, },
showBackButton = (selectedCategory != null && currentScreen == "main"), showBackButton = (selectedCategory != null && currentScreen == "main") || currentScreen == "starred",
onBackClick = { onBackClick = {
selectedCategory = null if (currentScreen == "starred") {
currentScreen = "main"
} else {
selectedCategory = null
}
}, },
onMenuClick = { drawerState = !drawerState }, onMenuClick = { drawerState = !drawerState },
onSearchClick = { showSearch = !showSearch }, onSearchClick = { showSearch = !showSearch },
@ -350,16 +354,6 @@ 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
}
} }
) )
@ -398,6 +392,8 @@ 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,7 +4,6 @@ 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
@ -15,7 +14,6 @@ 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
@ -36,141 +34,113 @@ fun ModernTopBar(
onSearchQueryChange: (String) -> Unit, onSearchQueryChange: (String) -> Unit,
showSearch: Boolean showSearch: Boolean
) { ) {
// Floating Top Bar with same style as Bottom Bar // Smooth transition for search bar
Box( AnimatedContent(
modifier = Modifier targetState = showSearch,
.fillMaxWidth() transitionSpec = {
.padding(horizontal = 16.dp, vertical = 8.dp) fadeIn(animationSpec = tween(Constants.ANIMATION_DURATION_MEDIUM)) togetherWith
) { fadeOut(animationSpec = tween(Constants.ANIMATION_DURATION_MEDIUM))
Surface( },
modifier = Modifier label = "topbar"
.fillMaxWidth() ) { isSearching ->
.shadow( if (isSearching) {
elevation = Constants.Elevation.Large.dp, // Search Mode - Minimalist
shape = RoundedCornerShape(Constants.Radius.ExtraLarge.dp) Surface(
), modifier = Modifier
color = AppColors.SurfaceElevated, .fillMaxWidth()
shape = RoundedCornerShape(Constants.Radius.ExtraLarge.dp) .shadow(Constants.Elevation.Small.dp),
) { color = AppColors.Surface
// Smooth transition for search bar ) {
AnimatedContent( Row(
targetState = showSearch, modifier = Modifier
transitionSpec = { .fillMaxWidth()
fadeIn(animationSpec = tween(Constants.ANIMATION_DURATION_MEDIUM)) togetherWith .padding(horizontal = Constants.Spacing.Medium.dp, vertical = Constants.Spacing.Small.dp),
fadeOut(animationSpec = tween(Constants.ANIMATION_DURATION_MEDIUM)) verticalAlignment = Alignment.CenterVertically
}, ) {
label = "topbar" // Search TextField
) { isSearching -> TextField(
if (isSearching) { value = searchQuery,
// Search Mode onValueChange = onSearchQueryChange,
Row( placeholder = {
modifier = Modifier Text(
.fillMaxWidth() "Cari catatan atau kategori...",
.padding(horizontal = 12.dp, vertical = 8.dp), color = AppColors.OnSurfaceVariant,
verticalAlignment = Alignment.CenterVertically, fontSize = 15.sp
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Search TextField
OutlinedTextField(
value = searchQuery,
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)
)
// Close Search Button
IconButton(
onClick = {
onSearchQueryChange("")
onSearchClick()
},
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)
) )
},
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
.weight(1f)
.heightIn(min = 48.dp),
shape = RoundedCornerShape(Constants.Radius.Medium.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 15.sp)
)
Spacer(modifier = Modifier.width(Constants.Spacing.Small.dp))
// Close Search Button
IconButton(
onClick = {
onSearchQueryChange("")
onSearchClick()
} }
) {
Icon(
Icons.Default.Close,
contentDescription = "Close Search",
tint = AppColors.OnSurfaceVariant
)
} }
} else { }
// Normal Mode }
Row( } else {
modifier = Modifier // Normal Mode - Minimalist
.fillMaxWidth() Surface(
.padding(horizontal = 12.dp, vertical = 8.dp), modifier = Modifier
verticalAlignment = Alignment.CenterVertically, .fillMaxWidth()
horizontalArrangement = Arrangement.SpaceBetween .shadow(Constants.Elevation.Small.dp),
) { color = AppColors.Surface
// Back/Menu Button ) {
IconButton( Row(
onClick = if (showBackButton) onBackClick else onMenuClick, modifier = Modifier
modifier = Modifier .fillMaxWidth()
.size(40.dp) .padding(horizontal = Constants.Spacing.Small.dp, vertical = Constants.Spacing.Small.dp),
.clip(CircleShape) verticalAlignment = Alignment.CenterVertically
.background( ) {
if (showBackButton) AppColors.SurfaceVariant // Back/Menu Button
else AppColors.Primary.copy(alpha = 0.1f) IconButton(onClick = if (showBackButton) onBackClick else onMenuClick) {
) Icon(
) { if (showBackButton) Icons.AutoMirrored.Filled.ArrowBack else Icons.Default.Menu,
Icon( contentDescription = if (showBackButton) "Back" else "Menu",
if (showBackButton) Icons.AutoMirrored.Filled.ArrowBack else Icons.Default.Menu, tint = AppColors.OnSurface
contentDescription = if (showBackButton) "Back" else "Menu",
tint = if (showBackButton) AppColors.OnSurface else AppColors.Primary,
modifier = Modifier.size(20.dp)
)
}
// Title
Text(
title,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = AppColors.OnBackground,
modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp)
) )
}
// Search Button // Title
IconButton( Text(
onClick = onSearchClick, title,
modifier = Modifier fontWeight = FontWeight.Bold,
.size(40.dp) fontSize = 20.sp,
.clip(CircleShape) color = AppColors.OnBackground,
.background(AppColors.SurfaceVariant) modifier = Modifier.weight(1f)
) { )
Icon(
Icons.Default.Search, // Search Button
contentDescription = "Search", IconButton(onClick = onSearchClick) {
tint = AppColors.OnSurfaceVariant, Icon(
modifier = Modifier.size(20.dp) Icons.Default.Search,
) contentDescription = "Search",
} tint = AppColors.OnSurfaceVariant
)
} }
} }
} }

View File

@ -14,7 +14,6 @@ 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
@ -23,7 +22,6 @@ 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
@ -37,6 +35,7 @@ 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,9 +27,7 @@ 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) {
@ -122,9 +120,7 @@ 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,12 +3,11 @@ 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.* import androidx.compose.material.icons.filled.Star
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.*
@ -31,13 +30,9 @@ 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) }
@ -50,78 +45,31 @@ 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(Constants.Radius.Large.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = AppColors.SurfaceVariant containerColor = AppColors.SurfaceVariant
), ),
elevation = CardDefaults.cardElevation( elevation = CardDefaults.cardElevation(
defaultElevation = Constants.Elevation.Small.dp defaultElevation = 2.dp
) )
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(Constants.Spacing.Large.dp) .padding(16.dp)
) { ) {
// Header: Title + Actions (Vertical) // Header: Title + Pin
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top verticalAlignment = Alignment.Top
) { ) {
// Title - takes most space // Title
Text( Text(
note.title, note.title,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
@ -133,94 +81,17 @@ fun NoteCard(
fontSize = 18.sp fontSize = 18.sp
) )
// Vertical Actions Stack // Pin Button
Column( IconButton(
horizontalAlignment = Alignment.End, onClick = onPinClick,
verticalArrangement = Arrangement.spacedBy(0.dp) modifier = Modifier.size(32.dp)
) { ) {
// Menu Button Icon(
Box { if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
IconButton( contentDescription = "Pin",
onClick = { showMenu = true }, tint = if (note.isPinned) AppColors.Warning else AppColors.OnSurfaceVariant,
modifier = Modifier.size(28.dp) modifier = Modifier.size(18.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,16 +1,13 @@
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
@ -23,37 +20,31 @@ 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 }
Column(modifier = Modifier.fillMaxSize()) { if (starredNotes.isEmpty()) {
if (starredNotes.isEmpty()) { EmptyState(
EmptyState( icon = Icons.Default.Star,
icon = Icons.Default.Star, message = "Belum ada catatan berbintang",
message = "Belum ada catatan berbintang", subtitle = "Catatan yang ditandai berbintang akan muncul di sini"
subtitle = "Catatan yang ditandai berbintang akan muncul di sini" )
) } else {
} else { LazyColumn(
LazyColumn( contentPadding = PaddingValues(16.dp),
contentPadding = PaddingValues( verticalArrangement = Arrangement.spacedBy(12.dp)
start = 16.dp, ) {
end = 16.dp, items(starredNotes) { note ->
top = 16.dp, val category = categories.find { it.id == note.categoryId }
bottom = 100.dp // Extra space untuk bottom bar StarredNoteCard(
), note = note,
verticalArrangement = Arrangement.spacedBy(12.dp) categoryName = category?.name ?: "Unknown",
) { onClick = { onNoteClick(note) },
items(starredNotes) { note -> onUnpin = { onUnpin(note) }
val category = categories.find { it.id == note.categoryId } )
StarredNoteCard(
note = note,
categoryName = category?.name ?: "Unknown",
onClick = { onNoteClick(note) },
onUnpin = { onUnpin(note) }
)
}
} }
} }
} }