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") {
currentScreen = "main"
} else {
selectedCategory = null 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,6 +36,22 @@ fun ModernTopBar(
onSearchQueryChange: (String) -> Unit, onSearchQueryChange: (String) -> Unit,
showSearch: Boolean showSearch: Boolean
) { ) {
// Floating Top Bar with same style as Bottom Bar
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.shadow(
elevation = Constants.Elevation.Large.dp,
shape = RoundedCornerShape(Constants.Radius.ExtraLarge.dp)
),
color = AppColors.SurfaceElevated,
shape = RoundedCornerShape(Constants.Radius.ExtraLarge.dp)
) {
// Smooth transition for search bar // Smooth transition for search bar
AnimatedContent( AnimatedContent(
targetState = showSearch, targetState = showSearch,
@ -44,84 +62,86 @@ fun ModernTopBar(
label = "topbar" label = "topbar"
) { isSearching -> ) { isSearching ->
if (isSearching) { if (isSearching) {
// Search Mode - Minimalist // Search Mode
Surface(
modifier = Modifier
.fillMaxWidth()
.shadow(Constants.Elevation.Small.dp),
color = AppColors.Surface
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = Constants.Spacing.Medium.dp, vertical = Constants.Spacing.Small.dp), .padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
// Search TextField // Search TextField
TextField( OutlinedTextField(
value = searchQuery, value = searchQuery,
onValueChange = onSearchQueryChange, onValueChange = onSearchQueryChange,
placeholder = { placeholder = {
Text( Text(
"Cari catatan atau kategori...", "Cari...",
color = AppColors.OnSurfaceVariant, color = AppColors.OnSurfaceVariant,
fontSize = 15.sp fontSize = 15.sp
) )
}, },
colors = TextFieldDefaults.colors( colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = AppColors.SurfaceVariant, focusedContainerColor = AppColors.SurfaceVariant,
unfocusedContainerColor = AppColors.SurfaceVariant, unfocusedContainerColor = AppColors.SurfaceVariant,
focusedTextColor = AppColors.OnBackground, focusedTextColor = AppColors.OnBackground,
unfocusedTextColor = AppColors.OnSurface, unfocusedTextColor = AppColors.OnSurface,
cursorColor = AppColors.Primary, cursorColor = AppColors.Primary,
focusedIndicatorColor = Color.Transparent, focusedBorderColor = AppColors.Primary,
unfocusedIndicatorColor = Color.Transparent unfocusedBorderColor = Color.Transparent
), ),
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.heightIn(min = 48.dp), .heightIn(min = 48.dp),
shape = RoundedCornerShape(Constants.Radius.Medium.dp), shape = RoundedCornerShape(Constants.Radius.Large.dp),
singleLine = true, singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 15.sp) textStyle = LocalTextStyle.current.copy(fontSize = 15.sp)
) )
Spacer(modifier = Modifier.width(Constants.Spacing.Small.dp))
// Close Search Button // Close Search Button
IconButton( IconButton(
onClick = { onClick = {
onSearchQueryChange("") onSearchQueryChange("")
onSearchClick() onSearchClick()
} },
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(AppColors.SurfaceVariant)
) { ) {
Icon( Icon(
Icons.Default.Close, Icons.Default.Close,
contentDescription = "Close Search", contentDescription = "Close Search",
tint = AppColors.OnSurfaceVariant tint = AppColors.OnSurfaceVariant,
modifier = Modifier.size(20.dp)
) )
} }
} }
}
} else { } else {
// Normal Mode - Minimalist // Normal Mode
Surface(
modifier = Modifier
.fillMaxWidth()
.shadow(Constants.Elevation.Small.dp),
color = AppColors.Surface
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = Constants.Spacing.Small.dp, vertical = Constants.Spacing.Small.dp), .padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) { ) {
// Back/Menu Button // Back/Menu Button
IconButton(onClick = if (showBackButton) onBackClick else onMenuClick) { 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( Icon(
if (showBackButton) Icons.AutoMirrored.Filled.ArrowBack else Icons.Default.Menu, if (showBackButton) Icons.AutoMirrored.Filled.ArrowBack else Icons.Default.Menu,
contentDescription = if (showBackButton) "Back" else "Menu", contentDescription = if (showBackButton) "Back" else "Menu",
tint = AppColors.OnSurface tint = if (showBackButton) AppColors.OnSurface else AppColors.Primary,
modifier = Modifier.size(20.dp)
) )
} }
@ -129,21 +149,31 @@ fun ModernTopBar(
Text( Text(
title, title,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 20.sp, fontSize = 18.sp,
color = AppColors.OnBackground, color = AppColors.OnBackground,
modifier = Modifier.weight(1f) modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp)
) )
// Search Button // Search Button
IconButton(onClick = onSearchClick) { IconButton(
onClick = onSearchClick,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(AppColors.SurfaceVariant)
) {
Icon( Icon(
Icons.Default.Search, Icons.Default.Search,
contentDescription = "Search", contentDescription = "Search",
tint = AppColors.OnSurfaceVariant 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,10 +133,86 @@ fun NoteCard(
fontSize = 18.sp fontSize = 18.sp
) )
// Vertical Actions Stack
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
// Menu Button
Box {
IconButton(
onClick = { showMenu = true },
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 // Pin Button
IconButton( IconButton(
onClick = onPinClick, onClick = onPinClick,
modifier = Modifier.size(32.dp) modifier = Modifier.size(28.dp)
) { ) {
Icon( Icon(
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder, if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
@ -94,6 +222,7 @@ fun NoteCard(
) )
} }
} }
}
// Deskripsi Preview // Deskripsi Preview
if (note.description.isNotEmpty()) { if (note.description.isNotEmpty()) {

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,12 +23,12 @@ 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,
@ -34,7 +37,12 @@ fun StarredNotesScreen(
) )
} else { } else {
LazyColumn( LazyColumn(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(
start = 16.dp,
end = 16.dp,
top = 16.dp,
bottom = 100.dp // Extra space untuk bottom bar
),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
items(starredNotes) { note -> items(starredNotes) { note ->
@ -48,4 +56,5 @@ fun StarredNotesScreen(
} }
} }
} }
}
} }