Compare commits

...

No commits in common. "3693612b209eef084209a222aaacb4ce1970ef71" and "master" have entirely different histories.

41 changed files with 3808 additions and 2434 deletions

26
.idea/appInsightsSettings.xml generated Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-12-10T12:35:04.538299500Z"> <DropdownSelection timestamp="2025-12-13T07:41:36.634314200Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=10DEC90GZE0004R" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=RR8T103A6JZ" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

8
.idea/markdown.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

6
.idea/studiobot.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="StudioBotProjectSettings">
<option name="shareContext" value="OptedIn" />
</component>
</project>

198
Readme.md
View File

@ -1,98 +1,135 @@
---
# **AI Notes Changelog** # **AI Notes Changelog**
## **Tim Pengembang** ## **Tim Pengembang**
* Dendi Yogia Pratama * Dendi Yogia Pratama
--- * Raihan Ariq Muzakki
* Fazri Abdurrahman
# **Version 1.0.0 Initial Release** # **Version 1.0.0 Initial Release**
---
## **Sprint 1: Struktur Dasar Aplikasi** ## **Sprint 1: Struktur Dasar Aplikasi**
* Implementasi struktur navigasi dasar aplikasi * **Implementasi struktur navigasi dasar aplikasi** - Setup navigation system
* Pembuatan menu drawer untuk navigasi screen * **Pembuatan menu drawer untuk navigasi screen** - Drawer dengan list menu (Beranda, Berbintang, Arsip, Sampah)
* Pembuatan screen Arsip dan Sampah * **Pembuatan screen Arsip dan Sampah** - Layout dan UI untuk Archive & Trash
* Implementasi routing antar halaman (Beranda, Arsip, Sampah) * **Implementasi routing antar halaman** - Navigation flow (Beranda, Arsip, Sampah)
* Penambahan Bottom Navigation (Home & AI Helper) * **Penambahan Bottom Navigation** - Home & AI Helper tabs
* Penambahan Top App Bar dengan menu dan search * **Penambahan Top App Bar** - Menu hamburger dan search icon
* Setup Material3 dengan Dark Theme * **Setup Material3 dengan Dark Theme** - Color scheme dark mode
* Implementasi color scheme & gradient header * **Implementasi color scheme & gradient header** - Primary/Secondary colors dengan gradient
* Pembuatan data class: Category, Note, ChatMessage * **Pembuatan data class** - Category, Note, ChatMessage models
* Implementasi sistem kategori pada halaman beranda * **Implementasi sistem kategori pada halaman beranda** - Category management system
* Pembuatan dialog tambah kategori (nama + gradient picker) * **Pembuatan dialog tambah kategori** - Form dengan nama + gradient picker
* Penambahan validasi input form kategori * **Penambahan validasi input form kategori** - Prevent empty category name
* Tampilan kategori Staggered Grid (2 kolom) * **Tampilan kategori Staggered Grid** - 2 kolom responsive layout
* Category Card (ikon folder, nama, jumlah catatan, gradient) * **Category Card design** - Ikon folder, nama, jumlah catatan, gradient background
* Long press untuk menghapus kategori * **Empty state kategori** - Pesan "Buat kategori pertama Anda"
* Empty state kategori * **Implementasi LazyVerticalStaggeredGrid** - Compose grid layout
* Implementasi LazyVerticalStaggeredGrid * **Gradient preset 8 warna** - Pre-defined color combinations
* Gradient preset 8 warna * **Manajemen state kategori** - Remember state untuk categories list
* Manajemen state kategori * **Implementasi pembuatan dan pengeditan catatan** - Note CRUD operations
* Implementasi pembuatan dan pengeditan catatan * **Dialog catatan** - Form dengan judul, isi, simpan, batal, hapus
* Dialog catatan (judul, isi, simpan, batal, hapus) * **Note Card design** - Judul, preview, timestamp, pin icon
* Note Card (judul, preview, timestamp, pin/unpin) * **Fitur pin untuk catatan penting** - Toggle pin/unpin functionality
* Fitur pin untuk catatan penting * **Full-screen editable note view** - Editor dengan auto-save
* Full-screen editable note view dengan auto-save * **Fitur arsip, hapus, dan pin** - Actions di full-screen mode
* Fitur arsip, hapus, dan pin di full-screen mode * **Fitur search catatan** - Filter berdasarkan judul + isi
* Long press untuk mengarsipkan catatan * **Sorting catatan** - Berdasarkan pin & timestamp (descending)
* Fitur search catatan (judul + isi) * **Implementasi custom TextField** - Styled text input fields
* Sorting catatan berdasarkan pin & timestamp * **Date formatter utility** - Format timestamp ke readable format
* Implementasi custom TextField dan date formatter * **Edit in-place full-screen note** - Direct editing tanpa dialog
* Edit in-place full-screen note * **Pembuatan screen AI Helper** - Layout untuk chat dengan AI
* Pembuatan screen AI Helper * **Header AI dengan ikon bintang** - Badge "Powered by Gemini AI"
* Header AI dengan ikon bintang & badge Gemini * **Category selector** - Dropdown untuk filter konteks AI
* Category selector untuk filter konteks AI * **Statistik ringkas** - Total catatan, pinned, jumlah kategori
* Statistik ringkas (total, pinned, kategori) * **Welcome state AI** - Icon + greeting message
* Welcome state dengan icon + greeting * **Suggestion chips** - Quick question templates
* Suggestion chips untuk pertanyaan cepat * **Input area multiline** - TextField dengan tombol kirim gradient
* Input area multiline dengan tombol kirim gradient * **Auto-scroll chat** - Scroll ke bottom dengan LaunchedEffect
* Auto-scroll menggunakan LaunchedEffect * **State management chat messages** - List of ChatMessage
* State management chat messages * **Integrasi Gemini 2.5 Flash API** - Setup API connection
* Integrasi Gemini 2.5 Flash API * **Prompt engineering** - Context dari data catatan user
* Implementasi prompt engineering menggunakan data catatan * **Chat bubble user & AI** - Different styling untuk user/AI
* Chat bubble user & AI * **Copy-to-clipboard** - Copy jawaban AI ke clipboard
* Copy-to-clipboard untuk jawaban AI * **Loading indicator** - Circular progress saat AI processing
* Loading indicator saat AI memproses * **Error message informatif** - Display error dengan jelas
* Error message informatif * **Timestamp pada setiap pesan** - Format HH:mm
* Timestamp pada setiap pesan * **Filter catatan berdasarkan kategori** - Context untuk AI berdasarkan selected category
* Filter catatan berdasarkan kategori untuk konteks * **Pembatasan 10 catatan terbaru** - Optimasi token usage
* Pembatasan 10 catatan terbaru (optimasi token) * **Implementasi Google AI SDK** - Configuration (temperature, topK, topP, maxOutputTokens)
* Implementasi Google AI SDK (temperature, topK, topP, token limit) * **Context builder** - String builder untuk kategori & catatan
* Context builder untuk kategori & catatan * **API calls dengan coroutine** - Async operations menggunakan launch
* API calls menggunakan coroutine async * **Refinement warna & gradient** - Polish color palette
* Refinement warna & gradient aplikasi * **Smooth animations** - Drawer slide, FAB scale, transitions
* Smooth animations (drawer, FAB, transitions) * **Peningkatan shadow dan elevation** - Card depth visual
* Peningkatan shadow dan elevation komponen * **Konsistensi spacing dan padding** - 8dp, 12dp, 16dp, 20dp standards
* Konsistensi spacing dan padding * **Peningkatan desain Card** - Rounded corners (12dp, 16dp, 20dp)
* Peningkatan desain Card dengan rounded corners * **Optimasi readability teks** - Font sizes dan line heights
* Optimasi readability teks * **Visual feedback** - Click ripples, copy confirmation, loading states
* Visual feedback (klik, copy message, loading states) * **Empty state improvements** - Icon + pesan yang lebih jelas
* Empty state baru dengan icon & pesan * **Perbaikan error messages** - Dengan ikon dan warna merah
* Perbaikan error messages dengan ikon dan warna * **State hoisting** - Optimasi recomposition
* State hoisting untuk optimasi recomposition * **Perbaikan smooth scroll** - Keyboard handling di chat
* Perbaikan smooth scroll & keyboard handling * **Implementasi DataStore** - Preferences DataStore untuk persistence
* Implementasi DataStore sebagai penyimpanan lokal * **Auto-save dengan debounce** - 500ms delay sebelum save
* Auto-save kategori & catatan dengan debounce (500ms) * **Persistence data penuh** - Data tetap ada setelah app ditutup
* Persistence data penuh setelah app ditutup * **Error handling DataStore** - Try-catch untuk I/O operations
* Error handling read/write DataStore * **Flow-based data loading** - Collect dari Flow dengan LaunchedEffect
* Flow-based data loading menggunakan LaunchedEffect * **Implementasi DataStoreManager** - Class dengan categoriesFlow & notesFlow
* Implementasi DataStoreManager (categoriesFlow & notesFlow) * **Try-catch semua operasi I/O** - Comprehensive error handling
* Try-catch semua operasi I/O * **Optimasi lifecycle data** - Proper state management
* Optimasi lifecycle data * **Halaman Catatan Berbintang** - StarredNotesScreen dengan filter isPinned
* **Ikon bintang untuk pesan** - Star icon pada note cards
* **Konfirmasi Arsip** - AlertDialog "Arsipkan Catatan?"
* **Konfirmasi Hapus** - AlertDialog "Hapus Catatan?"
## **Sprint 2: Project Restructuring, Fitur Search, Delete Kategori**
* **Fitur search beranda** - Cari kategori berdasarkan nama
* **Search filtering real-time** - Kategori otomatis filter saat mengetik
* **Delete kategori dengan UI** - Tombol X di top-right corner setiap kategori
* **Confirmation dialog untuk delete** - Prevent accidental deletion dengan warning message
* **Search di kategori** - Cari catatan berdasarkan judul & isi (case-insensitive)
* **Search empty state** - Tampilkan pesan "Tidak ada hasil" saat search kosong
* **Gradle optimization** - Cleanup dependencies yang tidak diperlukan
* **Hilangkan Fitur Tahan Untuk Hapus**
* **Project restructuring** - Migrasi dari 3 file monolith ke Clean Architecture
* **Data layer separation** - Pisahkan Category, Note, ChatMessage ke `data/model/`
* **DataStore refactoring** - Pindahkan DataStoreManager ke `data/local/` dengan PreferencesKeys
* **Component extraction** - Pisahkan MainScreen, CategoryCard, NoteCard ke folder terpisah
* **Utilities creation** - Buat Constants.kt, DateFormatter.kt, Extensions.kt
* **SerializableModels dengan extension functions** - Konversi model lebih clean
* **Import optimization** - Update semua import ke package structure baru
* **Menu dropdown kategori** - Icon titik tiga (⋮) untuk edit & delete
* **Edit kategori feature** - Dialog untuk ubah nama dan gradient kategori
* **Pre-filled edit form** - Auto-select gradient yang sedang dipakai
* **Soft delete implementation** - Pindahkan ke trash (bukan hapus permanen)
* **Trash system dengan kategori** - Tampilkan kategori & note yang dihapus
* **TrashCategoryCard component** - Card khusus untuk kategori di trash
* **Restore kategori feature** - Pulihkan kategori beserta semua note
* **Delete permanen kategori** - Hapus kategori dan note secara permanent
* **Counter display di trash** - Jumlah kategori dan note terhapus
* **Category model extension** - Tambah field `isDeleted` untuk soft delete
* **Global category filter** - Filter `!isDeleted` di semua screen
* **Gradient preview di trash** - Kategori tetap tampilkan gradient (opacity)
* **Dialog konfirmasi delete permanent** - Warning untuk tindakan irreversible
* **Runtime error debugging** - Fix NotImplementedError & FATAL EXCEPTION
* **Google Play Services error handling** - Handle GMS error untuk Gemini AI
* **HorizontalDivider migration** - Ganti deprecated Divider component
* **Migration guide documentation** - Panduan lengkap step-by-step migrasi
* **Debugging documentation** - Guide untuk troubleshoot common issues
--- ---
## **Fitur Utama Aplikasi** ## **Fitur Utama Aplikasi**
* Sistem kategori dengan gradient * Sistem kategori dengan gradient
* Buat/edit/hapus kategori dengan confirmation dialog
* Buat/edit/hapus catatan * Buat/edit/hapus catatan
* Pin catatan penting * Pin catatan penting
* Full-screen editor * Full-screen editor
* Search catatan * Search kategori di beranda
* Search catatan dalam kategori
* Arsip & Sampah dengan restore/delete permanen * Arsip & Sampah dengan restore/delete permanen
* AI Chat powered by Gemini * AI Chat powered by Gemini
* AI membaca & menganalisis catatan pengguna * AI membaca & menganalisis catatan pengguna
@ -111,5 +148,10 @@
* Rich text editor * Rich text editor
* Dark theme toggle * Dark theme toggle
* Multi-language support * Multi-language support
* AI Agent Catatan
* Fungsi AI (Summary berdasarkan catatan, Upload File)
* Markdown Parser
* Opsi memilih kategori dan catatan
* Penyesuaian User Interface dan User Experience
---
---

0
app/Readme.txt Normal file
View File

View File

@ -1,5 +0,0 @@
package com.example.notesai
object APIKey {
const val GEMINI_API_KEY = "AIzaSyBzC64RXsNtSERlts_FSd8HXKEpkLdT7-8"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
package com.example.notesai.config
object APIKey {
const val GEMINI_API_KEY = "MY_GEMINI_KEY"
}

View File

@ -1,16 +1,20 @@
@file:OptIn(kotlinx.serialization.InternalSerializationApi::class) @file:OptIn(InternalSerializationApi::class)
package com.example.notesai package com.example.notesai.data.local
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.Category
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -53,7 +57,7 @@ class DataStoreManager(private val context: Context) {
val categoriesFlow: Flow<List<Category>> = context.dataStore.data val categoriesFlow: Flow<List<Category>> = context.dataStore.data
.catch { exception -> .catch { exception ->
if (exception is IOException) { if (exception is IOException) {
emit(androidx.datastore.preferences.core.emptyPreferences()) emit(emptyPreferences())
} else { } else {
throw exception throw exception
} }
@ -72,7 +76,7 @@ class DataStoreManager(private val context: Context) {
val notesFlow: Flow<List<Note>> = context.dataStore.data val notesFlow: Flow<List<Note>> = context.dataStore.data
.catch { exception -> .catch { exception ->
if (exception is IOException) { if (exception is IOException) {
emit(androidx.datastore.preferences.core.emptyPreferences()) emit(emptyPreferences())
} else { } else {
throw exception throw exception
} }
@ -81,7 +85,16 @@ class DataStoreManager(private val context: Context) {
val jsonString = preferences[NOTES_KEY] ?: "[]" val jsonString = preferences[NOTES_KEY] ?: "[]"
try { try {
json.decodeFromString<List<SerializableNote>>(jsonString).map { json.decodeFromString<List<SerializableNote>>(jsonString).map {
Note(it.id, it.categoryId, it.title, it.content, it.timestamp, it.isArchived, it.isDeleted, it.isPinned) Note(
it.id,
it.categoryId,
it.title,
it.content,
it.timestamp,
it.isArchived,
it.isDeleted,
it.isPinned
)
} }
} catch (e: Exception) { } catch (e: Exception) {
emptyList() emptyList()

View File

@ -0,0 +1,13 @@
// File: data/model/Category.kt
package com.example.notesai.data.model
import java.util.UUID
data class Category(
val id: String = UUID.randomUUID().toString(),
val name: String,
val gradientStart: Long,
val gradientEnd: Long,
val timestamp: Long = System.currentTimeMillis(),
val isDeleted: Boolean = false // TAMBAHKAN INI
)

View File

@ -0,0 +1,11 @@
// File: data/model/ChatMessage.kt
package com.example.notesai.data.model
import java.util.UUID
data class ChatMessage(
val id: String = UUID.randomUUID().toString(),
val message: String,
val isUser: Boolean,
val timestamp: Long = System.currentTimeMillis()
)

View File

@ -0,0 +1,15 @@
// File: data/model/Note.kt
package com.example.notesai.data.model
import java.util.UUID
data class Note(
val id: String = UUID.randomUUID().toString(),
val categoryId: String,
val title: String,
val content: String,
val timestamp: Long = System.currentTimeMillis(),
val isArchived: Boolean = false,
val isDeleted: Boolean = false,
val isPinned: Boolean = false
)

View File

@ -0,0 +1,69 @@
package com.example.notesai.data.model
import android.annotation.SuppressLint
import kotlinx.serialization.Serializable
@SuppressLint("UnsafeOptInUsageError")
@Serializable
data class SerializableCategory(
val id: String,
val name: String,
val gradientStart: Long,
val gradientEnd: Long,
val timestamp: Long,
val isDeleted: Boolean = false // TAMBAHKAN INI
)
@SuppressLint("UnsafeOptInUsageError")
@Serializable
data class SerializableNote(
val id: String,
val categoryId: String,
val title: String,
val content: String,
val timestamp: Long,
val isArchived: Boolean,
val isDeleted: Boolean,
val isPinned: Boolean
)
// Extension functions untuk konversi
fun Category.toSerializable() = SerializableCategory(
id = id,
name = name,
gradientStart = gradientStart,
gradientEnd = gradientEnd,
timestamp = timestamp,
isDeleted = isDeleted // TAMBAHKAN INI
)
fun SerializableCategory.toCategory() = Category(
id = id,
name = name,
gradientStart = gradientStart,
gradientEnd = gradientEnd,
timestamp = timestamp,
isDeleted = isDeleted // TAMBAHKAN INI
)
fun Note.toSerializable() = SerializableNote(
id = id,
categoryId = categoryId,
title = title,
content = content,
timestamp = timestamp,
isArchived = isArchived,
isDeleted = isDeleted,
isPinned = isPinned
)
fun SerializableNote.toNote() = Note(
id = id,
categoryId = categoryId,
title = title,
content = content,
timestamp = timestamp,
isArchived = isArchived,
isDeleted = isDeleted,
isPinned = isPinned
)

View File

@ -0,0 +1,177 @@
package com.example.notesai.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun DrawerMenu(
currentScreen: String,
onDismiss: () -> Unit,
onItemClick: (String) -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding() // Padding untuk status bar
.background(Color.Black.copy(alpha = 0.5f))
.clickable(
onClick = onDismiss,
indication = null,
interactionSource = remember { MutableInteractionSource() }
)
) {
Card(
modifier = Modifier
.fillMaxHeight()
.width(250.dp)
.align(Alignment.CenterStart)
.clickable(
onClick = {},
indication = null,
interactionSource = remember { MutableInteractionSource() }
),
shape = RoundedCornerShape(topEnd = 0.dp, bottomEnd = 0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
elevation = CardDefaults.cardElevation(defaultElevation = 16.dp)
) {
Column(modifier = Modifier.fillMaxSize()) {
// Header Drawer dengan tombol close
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.verticalGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
)
)
.padding(24.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Icon(
Icons.Default.Create,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(36.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
"AI Notes",
style = MaterialTheme.typography.headlineMedium,
color = Color.White,
fontWeight = FontWeight.Bold
)
Text(
"Smart & Modern",
style = MaterialTheme.typography.bodyMedium,
color = Color.White.copy(0.8f)
)
}
// // Tombol Close
// IconButton(
// onClick = onDismiss,
// modifier = Modifier
// .size(40.dp)
// .background(
// Color.White.copy(alpha = 0.2f),
// shape = CircleShape
// )
// ) {
// Icon(
// Icons.Default.Close,
// contentDescription = "Tutup Menu",
// tint = Color.White,
// modifier = Modifier.size(24.dp)
// )
// }
}
}
Spacer(modifier = Modifier.height(16.dp))
// Menu Items
MenuItem(
icon = Icons.Default.Home,
text = "Beranda",
isSelected = currentScreen == "main"
) { onItemClick("main") }
MenuItem(
icon = Icons.Default.Star,
text = "Berbintang",
isSelected = currentScreen == "starred"
) { onItemClick("starred") }
MenuItem(
icon = Icons.Default.Archive,
text = "Arsip",
isSelected = currentScreen == "archive"
) { onItemClick("archive") }
MenuItem(
icon = Icons.Default.Delete,
text = "Sampah",
isSelected = currentScreen == "trash"
) { onItemClick("trash") }
Spacer(modifier = Modifier.weight(1f))
// Footer
Divider(
color = Color.White.copy(alpha = 0.1f),
modifier = Modifier.padding(horizontal = 16.dp)
)
Text(
text = "Version 1.0.0",
style = MaterialTheme.typography.bodySmall,
color = Color.White.copy(alpha = 0.5f),
modifier = Modifier.padding(16.dp)
)
}
}
}
}

View File

@ -0,0 +1,56 @@
package com.example.notesai.presentation.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
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.unit.dp
@Composable
fun EmptyState(
icon: ImageVector,
message: String,
subtitle: String
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(32.dp)
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = Color(0xFF475569)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
message,
style = MaterialTheme.typography.titleLarge,
color = Color(0xFF94A3B8),
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
subtitle,
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF64748B)
)
}
}
}

View File

@ -0,0 +1,51 @@
package com.example.notesai.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
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.unit.dp
@Composable
fun MenuItem(
icon: ImageVector,
text: String,
isSelected: Boolean,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.background(
if (isSelected) Color(0xFF334155) else Color.Transparent
)
.padding(horizontal = 20.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
icon,
contentDescription = null,
tint = if (isSelected) Color(0xFFA855F7) else Color(0xFF94A3B8)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text,
style = MaterialTheme.typography.bodyLarge,
color = if (isSelected) Color.White else Color(0xFF94A3B8),
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
)
}
}

View File

@ -0,0 +1,102 @@
package com.example.notesai.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun ModernBottomBar(
currentScreen: String,
onHomeClick: () -> Unit,
onAIClick: () -> Unit
) {
BottomAppBar(
containerColor = Color.Transparent,
modifier = Modifier
.shadow(8.dp, RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0xFF1E293B).copy(0.95f),
Color(0xFF334155).copy(0.95f)
)
),
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.clickable(onClick = onHomeClick)
.padding(vertical = 8.dp)
) {
Icon(
Icons.Default.Home,
contentDescription = "Home",
tint = if (currentScreen == "main") Color(0xFF6366F1) else Color(0xFF94A3B8),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"Home",
color = if (currentScreen == "main") Color(0xFF6366F1) else Color(0xFF94A3B8),
style = MaterialTheme.typography.bodySmall,
fontWeight = if (currentScreen == "main") FontWeight.Bold else FontWeight.Normal
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.clickable(onClick = onAIClick)
.padding(vertical = 8.dp)
) {
Icon(
Icons.Default.Star,
contentDescription = "AI Helper",
tint = if (currentScreen == "ai") Color(0xFFFBBF24) else Color(0xFF94A3B8),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"AI Helper",
color = if (currentScreen == "ai") Color(0xFFFBBF24) else Color(0xFF94A3B8),
style = MaterialTheme.typography.bodySmall,
fontWeight = if (currentScreen == "ai") FontWeight.Bold else FontWeight.Normal
)
}
}
}
}

View File

@ -0,0 +1,107 @@
package com.example.notesai.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModernTopBar(
title: String,
showBackButton: Boolean,
onBackClick: () -> Unit,
onMenuClick: () -> Unit,
onSearchClick: () -> Unit,
searchQuery: String,
onSearchQueryChange: (String) -> Unit,
showSearch: Boolean
) {
TopAppBar(
title = {
if (showSearch) {
TextField(
value = searchQuery,
onValueChange = onSearchQueryChange,
placeholder = { Text("Cari catatan...", color = Color.White.copy(0.6f)) },
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
cursorColor = Color.White,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
modifier = Modifier.fillMaxWidth()
)
} else {
Text(
title,
fontWeight = FontWeight.Bold,
fontSize = 22.sp
)
}
},
navigationIcon = {
IconButton(onClick = if (showBackButton) onBackClick else onMenuClick) {
Icon(
if (showBackButton) Icons.AutoMirrored.Filled.ArrowBack else Icons.Default.Menu,
contentDescription = null,
tint = Color.White
)
}
},
actions = {
IconButton(onClick = onSearchClick) {
Icon(
if (showSearch) Icons.Default.Close else Icons.Default.Search,
contentDescription = "Search",
tint = Color.White
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
),
modifier = Modifier
.background(
brush = Brush.horizontalGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
)
)
)
}

View File

@ -0,0 +1,163 @@
package com.example.notesai.presentation.dialogs
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun CategoryDialog(
onDismiss: () -> Unit,
onSave: (String, Long, Long) -> Unit
) {
var name by remember { mutableStateOf("") }
var selectedGradient by remember { mutableStateOf(0) }
val gradients = listOf(
Pair(0xFF6366F1L, 0xFFA855F7L),
Pair(0xFFEC4899L, 0xFFF59E0BL),
Pair(0xFF8B5CF6L, 0xFFEC4899L),
Pair(0xFF06B6D4L, 0xFF3B82F6L),
Pair(0xFF10B981L, 0xFF059669L),
Pair(0xFFF59E0BL, 0xFFEF4444L),
Pair(0xFF6366F1L, 0xFF8B5CF6L),
Pair(0xFFEF4444L, 0xFFDC2626L)
)
AlertDialog(
onDismissRequest = onDismiss,
containerColor = Color(0xFF1E293B),
title = {
Text(
"Buat Kategori Baru",
color = Color.White,
fontWeight = FontWeight.Bold
)
},
text = {
Column {
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Nama Kategori", color = Color(0xFF94A3B8)) },
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedContainerColor = Color(0xFF334155),
unfocusedContainerColor = Color(0xFF334155),
cursorColor = Color(0xFFA855F7),
focusedIndicatorColor = Color(0xFFA855F7),
unfocusedIndicatorColor = Color(0xFF64748B)
),
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(20.dp))
Text(
"Pilih Gradient:",
style = MaterialTheme.typography.bodyMedium,
color = Color.White,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(12.dp))
gradients.chunked(4).forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
row.forEachIndexed { index, gradient ->
val globalIndex = gradients.indexOf(gradient)
Box(
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.clip(RoundedCornerShape(12.dp))
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(gradient.first),
Color(gradient.second)
)
)
)
.clickable { selectedGradient = globalIndex },
contentAlignment = Alignment.Center
) {
if (selectedGradient == globalIndex) {
Icon(
Icons.Default.Check,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}
}
},
confirmButton = {
Button(
onClick = {
if (name.isNotBlank()) {
val gradient = gradients[selectedGradient]
onSave(name, gradient.first, gradient.second)
}
},
enabled = name.isNotBlank(),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
modifier = Modifier.background(
brush = Brush.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
),
shape = RoundedCornerShape(8.dp)
)
) {
Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Batal", color = Color(0xFF94A3B8))
}
}
)
}

View File

@ -0,0 +1,123 @@
package com.example.notesai.presentation.dialogs
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults
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.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.notesai.data.model.Note
@Composable
fun NoteDialog(
note: Note?,
onDismiss: () -> Unit,
onSave: (String, String) -> Unit,
onDelete: (() -> Unit)?
) {
var title by remember { mutableStateOf(note?.title ?: "") }
var content by remember { mutableStateOf(note?.content ?: "") }
AlertDialog(
onDismissRequest = onDismiss,
containerColor = Color(0xFF1E293B),
title = {
Text(
if (note == null) "Catatan Baru" else "Edit Catatan",
color = Color.White,
fontWeight = FontWeight.Bold
)
},
text = {
Column {
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("Judul", color = Color(0xFF94A3B8)) },
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedContainerColor = Color(0xFF334155),
unfocusedContainerColor = Color(0xFF334155),
cursorColor = Color(0xFFA855F7),
focusedIndicatorColor = Color(0xFFA855F7),
unfocusedIndicatorColor = Color(0xFF64748B)
),
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = content,
onValueChange = { content = it },
label = { Text("Isi Catatan", color = Color(0xFF94A3B8)) },
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
maxLines = 10,
colors = TextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedContainerColor = Color(0xFF334155),
unfocusedContainerColor = Color(0xFF334155),
cursorColor = Color(0xFFA855F7),
focusedIndicatorColor = Color(0xFFA855F7),
unfocusedIndicatorColor = Color(0xFF64748B)
),
shape = RoundedCornerShape(12.dp)
)
}
},
confirmButton = {
Row {
if (onDelete != null) {
TextButton(onClick = onDelete) {
Text("Hapus", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.width(8.dp))
}
Button(
onClick = { if (title.isNotBlank()) onSave(title, content) },
enabled = title.isNotBlank(),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
modifier = Modifier.background(
brush = Brush.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
),
shape = RoundedCornerShape(8.dp)
)
) {
Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
}
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Batal", color = Color(0xFF94A3B8))
}
}
)
}

View File

@ -0,0 +1,528 @@
package com.example.notesai.presentation.screens.ai
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
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.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.ChatMessage
import com.example.notesai.data.model.Category
import com.example.notesai.config.APIKey
import com.example.notesai.presentation.screens.ai.components.ChatBubble
import com.example.notesai.presentation.screens.ai.components.CompactStatItem
import com.example.notesai.presentation.screens.ai.components.SuggestionChip
import com.example.notesai.util.Constants.AppColors.Divider
import com.google.ai.client.generativeai.GenerativeModel
import com.google.ai.client.generativeai.type.generationConfig
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.collections.plus
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AIHelperScreen(
categories: List<Category>,
notes: List<Note>
) {
var prompt by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var selectedCategory by remember { mutableStateOf<Category?>(null) }
var showCategoryDropdown by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf("") }
var chatMessages by remember { mutableStateOf(listOf<ChatMessage>()) }
var showCopiedMessage by remember { mutableStateOf(false) }
var copiedMessageId by remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
val scrollState = rememberScrollState()
// Inisialisasi Gemini Model
val generativeModel = remember {
GenerativeModel(
modelName = "gemini-2.5-flash",
apiKey = APIKey.GEMINI_API_KEY,
generationConfig = generationConfig {
temperature = 0.8f
topK = 40
topP = 0.95f
maxOutputTokens = 4096
candidateCount = 1
}
)
}
// Auto scroll ke bawah saat ada pesan baru
LaunchedEffect(chatMessages.size) {
if (chatMessages.isNotEmpty()) {
delay(100)
scrollState.animateScrollTo(scrollState.maxValue)
}
}
Column(
modifier = Modifier.Companion
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
// Header
Card(
modifier = Modifier.Companion.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.Companion.Transparent),
shape = RoundedCornerShape(0.dp)
) {
Box(
modifier = Modifier.Companion
.fillMaxWidth()
.background(
brush = Brush.Companion.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
)
)
.padding(20.dp)
) {
Column {
Row(verticalAlignment = Alignment.Companion.CenterVertically) {
Icon(
Icons.Default.Star,
contentDescription = null,
tint = Color(0xFFFBBF24),
modifier = Modifier.Companion.size(28.dp)
)
Spacer(modifier = Modifier.Companion.width(12.dp))
Column {
Text(
"AI Helper",
style = MaterialTheme.typography.titleLarge,
color = Color.Companion.White,
fontWeight = FontWeight.Companion.Bold
)
Text(
"Powered by Gemini AI",
style = MaterialTheme.typography.bodySmall,
color = Color.Companion.White.copy(0.8f)
)
}
}
}
}
}
// Category Selector & Stats - Compact Version
Column(
modifier = Modifier.Companion
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(16.dp)
) {
// Category Selector
Box {
Card(
modifier = Modifier.Companion
.fillMaxWidth()
.clickable { showCategoryDropdown = !showCategoryDropdown },
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E293B)),
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.Companion
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Companion.CenterVertically
) {
Row(verticalAlignment = Alignment.Companion.CenterVertically) {
Icon(
Icons.Default.Folder,
contentDescription = null,
tint = Color(0xFF6366F1),
modifier = Modifier.Companion.size(20.dp)
)
Spacer(modifier = Modifier.Companion.width(8.dp))
Text(
selectedCategory?.name ?: "Semua Kategori",
color = Color.Companion.White,
style = MaterialTheme.typography.bodyMedium
)
}
Icon(
Icons.Default.ArrowDropDown,
contentDescription = null,
tint = Color(0xFF94A3B8)
)
}
}
DropdownMenu(
expanded = showCategoryDropdown,
onDismissRequest = { showCategoryDropdown = false },
modifier = Modifier.Companion
.fillMaxWidth()
.background(Color(0xFF1E293B))
) {
DropdownMenuItem(
text = { Text("Semua Kategori", color = Color.Companion.White) },
onClick = {
selectedCategory = null
showCategoryDropdown = false
}
)
categories.forEach { category ->
DropdownMenuItem(
text = { Text(category.name, color = Color.Companion.White) },
onClick = {
selectedCategory = category
showCategoryDropdown = false
}
)
}
}
}
// Stats - Compact
Spacer(modifier = Modifier.Companion.height(12.dp))
val filteredNotes = if (selectedCategory != null) {
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
} else {
notes.filter { !it.isArchived }
}
Row(
modifier = Modifier.Companion.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
CompactStatItem(
label = "Total",
value = filteredNotes.size.toString(),
color = Color(0xFF6366F1)
)
CompactStatItem(
label = "Dipasang",
value = filteredNotes.count { it.isPinned }.toString(),
color = Color(0xFFFBBF24)
)
CompactStatItem(
label = "Kategori",
value = categories.size.toString(),
color = Color(0xFFA855F7)
)
}
}
Divider(color = Color(0xFF334155), thickness = 1.dp)
// Chat Area
Column(
modifier = Modifier.Companion
.weight(1f)
.fillMaxWidth()
) {
if (chatMessages.isEmpty()) {
// Welcome State
Column(
modifier = Modifier.Companion
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.Companion.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
Icons.Default.Star,
contentDescription = null,
modifier = Modifier.Companion.size(64.dp),
tint = Color(0xFF6366F1).copy(0.5f)
)
Spacer(modifier = Modifier.Companion.height(16.dp))
Text(
"Mulai Percakapan",
style = MaterialTheme.typography.titleLarge,
color = Color.Companion.White,
fontWeight = FontWeight.Companion.Bold
)
Spacer(modifier = Modifier.Companion.height(8.dp))
Text(
"Tanyakan apa saja tentang catatan Anda",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF94A3B8),
textAlign = TextAlign.Companion.Center
)
Spacer(modifier = Modifier.Companion.height(24.dp))
// Suggestion Chips
Column(
horizontalAlignment = Alignment.Companion.Start,
modifier = Modifier.Companion.fillMaxWidth(0.8f)
) {
Text(
"Contoh pertanyaan:",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF64748B),
modifier = Modifier.Companion.padding(bottom = 8.dp)
)
SuggestionChip("Analisis catatan saya", onSelect = { prompt = it })
SuggestionChip("Buat ringkasan", onSelect = { prompt = it })
SuggestionChip("Berikan saran organisasi", onSelect = { prompt = it })
}
}
} else {
// Chat Messages
Column(
modifier = Modifier.Companion
.fillMaxSize()
.verticalScroll(scrollState)
.padding(16.dp)
) {
chatMessages.forEach { message ->
ChatBubble(
message = message,
onCopy = {
clipboardManager.setText(AnnotatedString(message.message))
copiedMessageId = message.id
showCopiedMessage = true
scope.launch {
delay(2000)
showCopiedMessage = false
}
},
showCopied = showCopiedMessage && copiedMessageId == message.id
)
Spacer(modifier = Modifier.Companion.height(12.dp))
}
// Loading Indicator
if (isLoading) {
Row(
modifier = Modifier.Companion
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.Start
) {
Card(
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp)
) {
Row(
modifier = Modifier.Companion.padding(16.dp),
verticalAlignment = Alignment.Companion.CenterVertically
) {
CircularProgressIndicator(
modifier = Modifier.Companion.size(20.dp),
color = Color(0xFF6366F1),
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.Companion.width(12.dp))
Text(
"AI sedang berpikir...",
color = Color(0xFF94A3B8),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
// Error Message
if (errorMessage.isNotEmpty()) {
Card(
modifier = Modifier.Companion.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEF4444).copy(0.2f)
),
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.Companion.padding(12.dp),
verticalAlignment = Alignment.Companion.CenterVertically
) {
Icon(
Icons.Default.Warning,
contentDescription = null,
tint = Color(0xFFEF4444),
modifier = Modifier.Companion.size(20.dp)
)
Spacer(modifier = Modifier.Companion.width(8.dp))
Text(
errorMessage,
color = Color(0xFFEF4444),
style = MaterialTheme.typography.bodySmall
)
}
}
}
Spacer(modifier = Modifier.Companion.height(80.dp))
}
}
}
// Input Area
Card(
modifier = Modifier.Companion.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Row(
modifier = Modifier.Companion
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.Companion.Bottom
) {
OutlinedTextField(
value = prompt,
onValueChange = { prompt = it },
placeholder = {
Text(
"Ketik pesan...",
color = Color(0xFF64748B)
)
},
modifier = Modifier.Companion
.weight(1f)
.heightIn(min = 48.dp, max = 120.dp),
colors = TextFieldDefaults.colors(
focusedTextColor = Color.Companion.White,
unfocusedTextColor = Color.Companion.White,
focusedContainerColor = Color(0xFF334155),
unfocusedContainerColor = Color(0xFF334155),
cursorColor = Color(0xFFA855F7),
focusedIndicatorColor = Color(0xFF6366F1),
unfocusedIndicatorColor = Color(0xFF475569)
),
shape = androidx.compose.foundation.shape.RoundedCornerShape(24.dp),
maxLines = 4
)
Spacer(modifier = Modifier.Companion.width(12.dp))
// Send Button
FloatingActionButton(
onClick = {
if (prompt.isNotBlank() && !isLoading) {
scope.launch {
// Add user message
chatMessages = chatMessages + ChatMessage(
message = prompt,
isUser = true
)
val userPrompt = prompt
prompt = ""
isLoading = true
errorMessage = ""
try {
val filteredNotes = if (selectedCategory != null) {
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
} else {
notes.filter { !it.isArchived }
}
val notesContext = buildString {
appendLine("Data catatan pengguna:")
appendLine("Total catatan: ${filteredNotes.size}")
appendLine("Kategori: ${selectedCategory?.name ?: "Semua"}")
appendLine()
appendLine("Daftar catatan:")
filteredNotes.take(10).forEach { note ->
appendLine("- Judul: ${note.title}")
appendLine(" Isi: ${note.content.take(100)}")
appendLine()
}
}
val fullPrompt =
"$notesContext\n\nPertanyaan: $userPrompt\n\nBerikan jawaban dalam bahasa Indonesia yang natural dan membantu."
val result = generativeModel.generateContent(fullPrompt)
val response = result.text ?: "Tidak ada respons dari AI"
// Add AI response
chatMessages = chatMessages + ChatMessage(
message = response,
isUser = false
)
} catch (e: Exception) {
errorMessage = "Error: ${e.message}"
} finally {
isLoading = false
}
}
}
},
containerColor = Color.Companion.Transparent,
modifier = Modifier.Companion
.size(48.dp)
.background(
brush = Brush.Companion.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
),
shape = CircleShape
)
) {
Icon(
Icons.Default.Send,
contentDescription = "Send",
tint = Color.Companion.White,
modifier = Modifier.Companion.size(24.dp)
)
}
}
}
}
}

View File

@ -0,0 +1,120 @@
package com.example.notesai.presentation.screens.ai.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.notesai.data.model.ChatMessage
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Composable
fun ChatBubble(
message: ChatMessage,
onCopy: () -> Unit,
showCopied: Boolean
) {
val dateFormat = remember { SimpleDateFormat("HH:mm", Locale("id", "ID")) }
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (message.isUser) Arrangement.End else Arrangement.Start
) {
if (!message.isUser) {
// Ganti ikon bintang dengan ikon robot/sparkles
Icon(
Icons.Default.AutoAwesome, // Atau bisa diganti dengan ikon lain seperti AutoAwesome
contentDescription = null,
tint = Color(0xFF6366F1), // Warna ungu/biru untuk AI
modifier = Modifier
.size(32.dp)
.padding(end = 8.dp)
)
}
Column(
modifier = Modifier.fillMaxWidth(0.85f),
horizontalAlignment = if (message.isUser) Alignment.End else Alignment.Start
) {
Card(
colors = CardDefaults.cardColors(
containerColor = if (message.isUser)
Color(0xFF6366F1)
else
Color(0xFF1E293B)
),
shape = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
bottomStart = if (message.isUser) 16.dp else 4.dp,
bottomEnd = if (message.isUser) 4.dp else 16.dp
)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
message.message,
color = Color.White,
style = MaterialTheme.typography.bodyMedium,
lineHeight = 20.sp
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
dateFormat.format(Date(message.timestamp)),
color = Color.White.copy(0.6f),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp)
)
if (!message.isUser) {
IconButton(
onClick = onCopy,
modifier = Modifier.size(32.dp)
) {
Icon(
Icons.Default.ContentCopy,
contentDescription = "Copy",
tint = Color.White.copy(0.7f),
modifier = Modifier.size(16.dp)
)
}
}
}
}
}
if (showCopied && !message.isUser) {
Text(
"✓ Disalin",
color = Color(0xFF10B981),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp, start = 8.dp)
)
}
}
}
}

View File

@ -0,0 +1,42 @@
package com.example.notesai.presentation.screens.ai.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun CompactStatItem(label: String, value: String, color: Color) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.background(
color = Color(0xFF1E293B),
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Text(
value,
style = MaterialTheme.typography.titleMedium,
color = color,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(4.dp))
Text(
label,
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF94A3B8)
)
}
}

View File

@ -0,0 +1,35 @@
package com.example.notesai.presentation.screens.ai.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun StatItem(label: String, value: String, color: Color) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(8.dp)
) {
Text(
value,
style = MaterialTheme.typography.headlineMedium,
color = color,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
label,
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF94A3B8)
)
}
}

View File

@ -0,0 +1,52 @@
package com.example.notesai.presentation.screens.ai.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun SuggestionChip(text: String, onSelect: (String) -> Unit) {
Card(
modifier = Modifier
.padding(vertical = 4.dp)
.clickable { onSelect(text) },
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Star,
contentDescription = null,
tint = Color(0xFF6366F1),
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text,
color = Color.White,
style = MaterialTheme.typography.bodyMedium
)
}
}
}

View File

@ -0,0 +1,45 @@
package com.example.notesai.presentation.screens.archive
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Archive
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.Category
import com.example.notesai.presentation.components.EmptyState
import com.example.notesai.presentation.screens.archive.components.ArchiveNoteCard
@Composable
fun ArchiveScreen(
notes: List<Note>,
categories: List<Category>,
onRestore: (Note) -> Unit,
onDelete: (Note) -> Unit
) {
if (notes.isEmpty()) {
EmptyState(
icon = Icons.Default.Archive,
message = "Arsip kosong",
subtitle = "Catatan yang diarsipkan akan muncul di sini"
)
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(notes) { note ->
val category = categories.find { it.id == note.categoryId }
ArchiveNoteCard(
note = note,
categoryName = category?.name ?: "Unknown",
onRestore = { onRestore(note) },
onDelete = { onDelete(note) }
)
}
}
}
}

View File

@ -0,0 +1,105 @@
package com.example.notesai.presentation.screens.archive.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.notesai.data.model.Note
@Composable
fun ArchiveNoteCard(
note: Note,
categoryName: String,
onRestore: () -> Unit,
onDelete: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
note.title,
fontWeight = FontWeight.Bold,
color = Color.White,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
categoryName,
color = Color(0xFF64748B),
style = MaterialTheme.typography.bodySmall
)
}
}
if (note.content.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
note.content,
maxLines = 2,
color = Color(0xFF94A3B8),
style = MaterialTheme.typography.bodyMedium
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onRestore) {
Icon(
Icons.Default.AccountBox,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFF10B981)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.width(8.dp))
TextButton(onClick = onDelete) {
Icon(
Icons.Default.Delete,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFFEF4444)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Hapus", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
}
}
}
}
}

View File

@ -0,0 +1,121 @@
// File: presentation/screens/main/MainScreen.kt
package com.example.notesai.presentation.screens.main
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.notesai.data.model.Category
import com.example.notesai.data.model.Note
import com.example.notesai.presentation.components.EmptyState
import com.example.notesai.presentation.screens.main.components.CategoryCard
import com.example.notesai.presentation.screens.main.components.NoteCard
@Composable
fun MainScreen(
categories: List<Category>,
notes: List<Note>,
selectedCategory: Category?,
searchQuery: String,
onCategoryClick: (Category) -> Unit,
onNoteClick: (Note) -> Unit,
onPinToggle: (Note) -> Unit,
onCategoryDelete: (Category) -> Unit,
onCategoryEdit: (Category, String, Long, Long) -> Unit // Parameter baru
) {
Column(modifier = Modifier.fillMaxSize()) {
if (selectedCategory == null) {
// Beranda: Tampilkan kategori dengan search filtering
if (categories.isEmpty()) {
EmptyState(
icon = Icons.Default.Create,
message = "Buat kategori pertama Anda",
subtitle = "Tekan tombol + untuk memulai"
)
} else {
// Filter kategori berdasarkan searchQuery
val filteredCategories = if (searchQuery.isEmpty()) {
categories
} else {
categories.filter {
it.name.contains(searchQuery, ignoreCase = true)
}
}
if (filteredCategories.isEmpty()) {
EmptyState(
icon = Icons.Default.Search,
message = "Kategori tidak ditemukan",
subtitle = "Coba kata kunci lain"
)
} else {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalItemSpacing = 12.dp,
modifier = Modifier.fillMaxSize()
) {
items(filteredCategories) { category ->
CategoryCard(
category = category,
noteCount = notes.count {
it.categoryId == category.id &&
!it.isDeleted &&
!it.isArchived
},
onClick = { onCategoryClick(category) },
onDelete = { onCategoryDelete(category) },
onEdit = { name, gradientStart, gradientEnd ->
onCategoryEdit(category, name, gradientStart, gradientEnd)
}
)
}
}
}
}
} else {
val categoryNotes = notes
.filter {
it.categoryId == selectedCategory.id &&
!it.isDeleted &&
!it.isArchived &&
(searchQuery.isEmpty() ||
it.title.contains(searchQuery, ignoreCase = true) ||
it.content.contains(searchQuery, ignoreCase = true))
}
.sortedWith(compareByDescending<Note> { it.isPinned }.thenByDescending { it.timestamp })
if (categoryNotes.isEmpty()) {
EmptyState(
icon = Icons.Default.Add,
message = if (searchQuery.isEmpty()) "Belum ada catatan" else "Tidak ada hasil",
subtitle = if (searchQuery.isEmpty()) "Tekan tombol + untuk membuat catatan" else "Coba kata kunci lain"
)
} else {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalItemSpacing = 12.dp,
modifier = Modifier.fillMaxSize()
) {
items(categoryNotes) { note ->
NoteCard(
note = note,
onClick = { onNoteClick(note) },
onPinClick = { onPinToggle(note) }
)
}
}
}
}
}
}

View File

@ -0,0 +1,320 @@
package com.example.notesai.presentation.screens.main.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.notesai.data.model.Category
@Composable
fun CategoryCard(
category: Category,
noteCount: Int,
onClick: () -> Unit,
onDelete: () -> Unit = {},
onEdit: (String, Long, Long) -> Unit = { _, _, _ -> }
) {
var showDeleteConfirm by remember { mutableStateOf(false) }
var showEditDialog by remember { mutableStateOf(false) }
var showMenu by remember { mutableStateOf(false) }
// Delete Confirmation Dialog
if (showDeleteConfirm) {
AlertDialog(
onDismissRequest = { showDeleteConfirm = false },
title = { Text("Pindahkan ke Sampah?", color = Color.White) },
text = {
Text(
"Kategori '${category.name}' dan semua catatan di dalamnya akan dipindahkan ke sampah.",
color = Color.White
)
},
confirmButton = {
Button(
onClick = {
onDelete()
showDeleteConfirm = false
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFEF4444)
)
) {
Text("Hapus", color = Color.White)
}
},
dismissButton = {
Button(
onClick = { showDeleteConfirm = false },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF64748B)
)
) {
Text("Batal", color = Color.White)
}
},
containerColor = Color(0xFF1E293B)
)
}
// Edit Dialog
if (showEditDialog) {
EditCategoryDialog(
category = category,
onDismiss = { showEditDialog = false },
onSave = { name, gradientStart, gradientEnd ->
onEdit(name, gradientStart, gradientEnd)
showEditDialog = false
}
)
}
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(category.gradientStart),
Color(category.gradientEnd)
)
)
)
.padding(20.dp)
) {
Column {
Icon(
Icons.Default.Folder,
contentDescription = null,
tint = Color.White.copy(0.9f),
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
category.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"$noteCount catatan",
style = MaterialTheme.typography.bodySmall,
color = Color.White.copy(0.8f)
)
}
// Menu Button (Titik Tiga)
Box(
modifier = Modifier.align(Alignment.TopEnd)
) {
IconButton(
onClick = { showMenu = true }
) {
Icon(
Icons.Default.MoreVert,
contentDescription = "Menu",
tint = Color.White.copy(0.9f)
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
modifier = Modifier.background(Color(0xFF1E293B))
) {
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.Edit,
contentDescription = null,
tint = Color(0xFF6366F1),
modifier = Modifier.size(20.dp)
)
Text("Edit Kategori", color = Color.White)
}
},
onClick = {
showMenu = false
showEditDialog = true
}
)
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.Delete,
contentDescription = null,
tint = Color(0xFFEF4444),
modifier = Modifier.size(20.dp)
)
Text("Pindah ke Sampah", color = Color.White)
}
},
onClick = {
showMenu = false
showDeleteConfirm = true
}
)
}
}
}
}
}
@Composable
fun EditCategoryDialog(
category: Category,
onDismiss: () -> Unit,
onSave: (String, Long, Long) -> Unit
) {
val gradients = listOf(
Pair(0xFF6366F1L, 0xFFA855F7L),
Pair(0xFFEC4899L, 0xFFF59E0BL),
Pair(0xFF8B5CF6L, 0xFFEC4899L),
Pair(0xFF06B6D4L, 0xFF3B82F6L),
Pair(0xFF10B981L, 0xFF059669L),
Pair(0xFFF59E0BL, 0xFFEF4444L),
Pair(0xFF6366F1L, 0xFF8B5CF6L),
Pair(0xFFEF4444L, 0xFFDC2626L)
)
var name by remember { mutableStateOf(category.name) }
var selectedGradient by remember {
mutableStateOf(
gradients.indexOfFirst {
it.first == category.gradientStart && it.second == category.gradientEnd
}.takeIf { it >= 0 } ?: 0
)
}
AlertDialog(
onDismissRequest = onDismiss,
containerColor = Color(0xFF1E293B),
title = {
Text(
"Edit Kategori",
color = Color.White,
fontWeight = FontWeight.Bold
)
},
text = {
Column {
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Nama Kategori", color = Color(0xFF94A3B8)) },
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedContainerColor = Color(0xFF334155),
unfocusedContainerColor = Color(0xFF334155),
cursorColor = Color(0xFFA855F7),
focusedIndicatorColor = Color(0xFFA855F7),
unfocusedIndicatorColor = Color(0xFF64748B)
),
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(20.dp))
Text(
"Pilih Gradient:",
style = MaterialTheme.typography.bodyMedium,
color = Color.White,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(12.dp))
gradients.chunked(4).forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
row.forEachIndexed { _, gradient ->
val globalIndex = gradients.indexOf(gradient)
Box(
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.clip(RoundedCornerShape(12.dp))
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(gradient.first),
Color(gradient.second)
)
)
)
.clickable { selectedGradient = globalIndex },
contentAlignment = Alignment.Center
) {
if (selectedGradient == globalIndex) {
Icon(
Icons.Default.Check,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}
}
},
confirmButton = {
Button(
onClick = {
if (name.isNotBlank()) {
val gradient = gradients[selectedGradient]
onSave(name, gradient.first, gradient.second)
}
},
enabled = name.isNotBlank(),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
modifier = Modifier.background(
brush = Brush.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
),
shape = RoundedCornerShape(8.dp)
)
) {
Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Batal", color = Color(0xFF94A3B8))
}
}
)
}

View File

@ -0,0 +1,128 @@
package com.example.notesai.presentation.screens.main.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.notesai.data.model.Note
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NoteCard(
note: Note,
onClick: () -> Unit,
onPinClick: () -> Unit
) {
val dateFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID"))
Card(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = onClick,
),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
// Judul
Text(
note.title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.weight(1f),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
IconButton(
onClick = onPinClick,
modifier = Modifier.size(24.dp)
) {
Icon(
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
contentDescription = "Pin",
tint = if (note.isPinned) Color(0xFFFBBF24) else Color.Gray,
modifier = Modifier.size(18.dp)
)
}
}
// Deskripsi
if (note.content.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Deskripsi",
style = MaterialTheme.typography.labelSmall,
color = Color(0xFF94A3B8),
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
note.content,
style = MaterialTheme.typography.bodyMedium,
maxLines = 4,
overflow = TextOverflow.Ellipsis,
color = Color(0xFFCBD5E1),
lineHeight = 20.sp
)
}
Spacer(modifier = Modifier.height(12.dp))
HorizontalDivider(
color = Color(0xFF334155),
thickness = 1.dp
)
Spacer(modifier = Modifier.height(8.dp))
// Timestamp
Text(
dateFormat.format(Date(note.timestamp)),
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF64748B)
)
}
}
}

View File

@ -0,0 +1,250 @@
package com.example.notesai.presentation.screens.note
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
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.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.notesai.data.model.Note
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditableFullScreenNoteView(
note: Note,
onBack: () -> Unit,
onSave: (String, String) -> Unit,
onArchive: () -> Unit,
onDelete: () -> Unit,
onPinToggle: () -> Unit
) {
var title by remember { mutableStateOf(note.title) }
var content by remember { mutableStateOf(note.content) }
var showArchiveDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
val dateFormat = remember { SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID")) }
// Dialog Konfirmasi Arsip
if (showArchiveDialog) {
AlertDialog(
onDismissRequest = { showArchiveDialog = false },
title = {
Text(
text = "Arsipkan Catatan?",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
Text(
text = "Catatan ini akan dipindahkan ke arsip. Anda masih bisa mengaksesnya nanti.",
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(
onClick = {
if (title.isNotBlank()) {
onSave(title, content)
}
onArchive()
showArchiveDialog = false
}
) {
Text("Arsipkan", color = MaterialTheme.colorScheme.primary)
}
},
dismissButton = {
TextButton(onClick = { showArchiveDialog = false }) {
Text("Batal", color = MaterialTheme.colorScheme.onSurface)
}
}
)
}
// Dialog Konfirmasi Hapus
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = {
Text(
text = "Hapus Catatan?",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
Text(
text = "Catatan ini akan dihapus permanen dan tidak bisa dikembalikan.",
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(
onClick = {
onDelete()
showDeleteDialog = false
}
) {
Text("Hapus", color = Color(0xFFEF4444))
}
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = false }) {
Text("Batal", color = MaterialTheme.colorScheme.onSurface)
}
}
)
}
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
topBar = {
TopAppBar(
title = { },
navigationIcon = {
IconButton(onClick = {
if (title.isNotBlank()) {
onSave(title, content)
}
onBack()
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali", tint = MaterialTheme.colorScheme.onBackground)
}
},
actions = {
IconButton(onClick = {
if (title.isNotBlank()) {
onSave(title, content)
}
onPinToggle()
}) {
Icon(
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
contentDescription = "Pin Catatan",
tint = if (note.isPinned) Color(0xFFFBBF24) else MaterialTheme.colorScheme.onSurface
)
}
IconButton(onClick = { showArchiveDialog = true }) {
Icon(Icons.Default.Archive, contentDescription = "Arsipkan", tint = MaterialTheme.colorScheme.onSurface)
}
IconButton(onClick = { showDeleteDialog = true }) {
Icon(Icons.Default.Delete, contentDescription = "Hapus", tint = MaterialTheme.colorScheme.onSurface)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(horizontal = 20.dp)
) {
TextField(
value = title,
onValueChange = { title = it },
textStyle = MaterialTheme.typography.headlineLarge.copy(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
),
placeholder = {
Text(
"Judul",
style = MaterialTheme.typography.headlineLarge,
color = Color(0xFF64748B)
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
cursorColor = Color(0xFFA855F7)
),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Terakhir diubah: ${dateFormat.format(Date(note.timestamp))}",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF64748B)
)
Divider(
modifier = Modifier.padding(vertical = 20.dp),
color = MaterialTheme.colorScheme.surface
)
TextField(
value = content,
onValueChange = { content = it },
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurface,
lineHeight = 28.sp
),
placeholder = {
Text(
"Mulai menulis...",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF64748B)
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
cursorColor = Color(0xFFA855F7)
),
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 400.dp)
)
Spacer(modifier = Modifier.height(100.dp))
}
}
}

View File

@ -0,0 +1,51 @@
package com.example.notesai.presentation.screens.starred
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.example.notesai.presentation.components.EmptyState
import com.example.notesai.presentation.screens.starred.components.StarredNoteCard
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.Category
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StarredNotesScreen(
notes: List<Note>,
categories: List<Category>,
onNoteClick: (Note) -> Unit,
onMenuClick: () -> Unit,
onBack: () -> Unit,
onUnpin: (Note) -> Unit
) {
val starredNotes = notes.filter { it.isPinned && !it.isArchived && !it.isDeleted }
if (starredNotes.isEmpty()) {
EmptyState(
icon = Icons.Default.Star,
message = "Belum ada catatan berbintang",
subtitle = "Catatan yang ditandai berbintang akan muncul di sini"
)
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(starredNotes) { note ->
val category = categories.find { it.id == note.categoryId }
StarredNoteCard(
note = note,
categoryName = category?.name ?: "Unknown",
onClick = { onNoteClick(note) },
onUnpin = { onUnpin(note) }
)
}
}
}
}

View File

@ -0,0 +1,132 @@
package com.example.notesai.presentation.screens.starred.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.example.notesai.data.model.Note
@Composable
fun StarredNoteCard(
note: Note,
categoryName: String,
onClick: () -> Unit,
onUnpin: () -> Unit
) {
Card(
modifier = Modifier.Companion
.fillMaxWidth()
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.Companion.padding(16.dp)) {
Row(
modifier = Modifier.Companion.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Companion.Top
) {
Column(modifier = Modifier.Companion.weight(1f)) {
Row(
verticalAlignment = Alignment.Companion.CenterVertically
) {
Icon(
Icons.Default.Star,
contentDescription = null,
tint = Color(0xFFFBBF24),
modifier = Modifier.Companion.size(16.dp)
)
Spacer(modifier = Modifier.Companion.width(8.dp))
Text(
note.title,
fontWeight = FontWeight.Companion.Bold,
color = Color.Companion.White,
style = MaterialTheme.typography.titleMedium
)
}
Spacer(modifier = Modifier.Companion.height(4.dp))
Text(
categoryName,
color = Color(0xFF64748B),
style = MaterialTheme.typography.bodySmall
)
}
}
if (note.content.isNotEmpty()) {
Spacer(modifier = Modifier.Companion.height(8.dp))
Text(
note.content,
maxLines = 2,
overflow = TextOverflow.Companion.Ellipsis,
color = Color(0xFF94A3B8),
style = MaterialTheme.typography.bodyMedium
)
}
Row(
modifier = Modifier.Companion
.fillMaxWidth()
.padding(top = 12.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onClick) {
Icon(
Icons.Default.Info,
contentDescription = null,
modifier = Modifier.Companion.size(18.dp),
tint = Color(0xFF6366F1)
)
Spacer(modifier = Modifier.Companion.width(4.dp))
Text(
"Lihat Detail",
color = Color(0xFF6366F1),
fontWeight = FontWeight.Companion.Bold
)
}
Spacer(modifier = Modifier.Companion.width(8.dp))
TextButton(onClick = onUnpin) {
Icon(
Icons.Outlined.StarBorder,
contentDescription = null,
modifier = Modifier.Companion.size(18.dp),
tint = Color(0xFFFBBF24)
)
Spacer(modifier = Modifier.Companion.width(4.dp))
Text(
"Hapus Bintang",
color = Color(0xFFFBBF24),
fontWeight = FontWeight.Companion.Bold
)
}
}
}
}
}

View File

@ -0,0 +1,93 @@
// File: presentation/screens/trash/TrashScreen.kt
package com.example.notesai.presentation.screens.trash
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.notesai.presentation.components.EmptyState
import com.example.notesai.presentation.screens.trash.components.TrashNoteCard
import com.example.notesai.presentation.screens.trash.components.TrashCategoryCard
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.Category
@Composable
fun TrashScreen(
notes: List<Note>,
categories: List<Category>,
onRestoreNote: (Note) -> Unit,
onDeleteNotePermanent: (Note) -> Unit,
onRestoreCategory: (Category) -> Unit,
onDeleteCategoryPermanent: (Category) -> Unit
) {
// Filter kategori dan note yang dihapus
val deletedCategories = categories.filter { it.isDeleted }
val deletedNotes = notes.filter { it.isDeleted }
if (deletedCategories.isEmpty() && deletedNotes.isEmpty()) {
EmptyState(
icon = Icons.Default.Delete,
message = "Sampah kosong",
subtitle = "Kategori dan catatan yang dihapus akan muncul di sini"
)
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Section: Kategori Terhapus
if (deletedCategories.isNotEmpty()) {
item {
Text(
"Kategori Terhapus (${deletedCategories.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = Color(0xFF94A3B8)
)
}
items(deletedCategories) { category ->
val notesInCategory = notes.count {
it.categoryId == category.id && it.isDeleted
}
TrashCategoryCard(
category = category,
noteCount = notesInCategory,
onRestore = { onRestoreCategory(category) },
onDeletePermanent = { onDeleteCategoryPermanent(category) }
)
}
}
// Section: Catatan Terhapus
if (deletedNotes.isNotEmpty()) {
item {
Text(
"Catatan Terhapus (${deletedNotes.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = Color(0xFF94A3B8)
)
}
items(deletedNotes) { note ->
val category = categories.find { it.id == note.categoryId }
TrashNoteCard(
note = note,
categoryName = category?.name ?: "Unknown",
onRestore = { onRestoreNote(note) },
onDeletePermanent = { onDeleteNotePermanent(note) }
)
}
}
}
}
}

View File

@ -0,0 +1,160 @@
// File: presentation/screens/trash/components/TrashCategoryCard.kt
package com.example.notesai.presentation.screens.trash.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Restore
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.notesai.data.model.Category
@Composable
fun TrashCategoryCard(
category: Category,
noteCount: Int,
onRestore: () -> Unit,
onDeletePermanent: () -> Unit
) {
var showDeleteDialog by remember { mutableStateOf(false) }
// Dialog konfirmasi hapus permanen
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = { Text("Hapus Kategori Permanen?", color = Color.White) },
text = {
Text(
"Kategori '${category.name}' dan $noteCount catatan di dalamnya akan dihapus permanen. Tindakan ini tidak dapat dibatalkan!",
color = Color.White
)
},
confirmButton = {
Button(
onClick = {
onDeletePermanent()
showDeleteDialog = false
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFEF4444)
)
) {
Text("Hapus Permanen", color = Color.White)
}
},
dismissButton = {
Button(
onClick = { showDeleteDialog = false },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF64748B)
)
) {
Text("Batal", color = Color.White)
}
},
containerColor = Color(0xFF1E293B)
)
}
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
// Header dengan gradient
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(category.gradientStart).copy(alpha = 0.3f),
Color(category.gradientEnd).copy(alpha = 0.3f)
)
),
shape = RoundedCornerShape(12.dp)
)
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Folder,
contentDescription = null,
tint = Color.White.copy(0.9f),
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
category.name,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"$noteCount catatan",
style = MaterialTheme.typography.bodySmall,
color = Color.White.copy(0.8f)
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
// Info
Text(
"Kategori yang dihapus",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF64748B)
)
// Action buttons
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onRestore) {
Icon(
Icons.Default.Restore,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFF10B981)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.width(8.dp))
TextButton(onClick = { showDeleteDialog = true }) {
Icon(
Icons.Default.Delete,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFFEF4444)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Hapus Permanen", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
}
}
}
}
}

View File

@ -0,0 +1,105 @@
package com.example.notesai.presentation.screens.trash.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.notesai.data.model.Note
@Composable
fun TrashNoteCard(
note: Note,
categoryName: String,
onRestore: () -> Unit,
onDeletePermanent: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF7F1D1D).copy(0.2f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
note.title,
fontWeight = FontWeight.Bold,
color = Color.White,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
categoryName,
color = Color(0xFF64748B),
style = MaterialTheme.typography.bodySmall
)
}
}
if (note.content.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
note.content,
maxLines = 2,
color = Color(0xFF94A3B8),
style = MaterialTheme.typography.bodyMedium
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onRestore) {
Icon(
Icons.Default.AccountBox,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFF10B981)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.width(8.dp))
TextButton(onClick = onDeletePermanent) {
Icon(
Icons.Default.Delete,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFFEF4444)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Hapus Permanen", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
}
}
}
}
}

View File

@ -0,0 +1,53 @@
// File: util/Constants.kt
package com.example.notesai.util
import androidx.compose.ui.graphics.Color
object Constants {
// App Info
const val APP_NAME = "AI Notes"
const val APP_VERSION = "1.0.0"
// DataStore
const val DATASTORE_NAME = "notes_prefs"
const val DEBOUNCE_DELAY = 500L
// UI Constants
const val MAX_NOTE_PREVIEW_LINES = 4
const val MAX_CHAT_PREVIEW_LINES = 2
const val GRID_COLUMNS = 2
// Gradients
val GRADIENT_PRESETS = listOf(
Pair(0xFF6366F1L, 0xFFA855F7L),
Pair(0xFFEC4899L, 0xFFF59E0BL),
Pair(0xFF8B5CF6L, 0xFFEC4899L),
Pair(0xFF06B6D4L, 0xFF3B82F6L),
Pair(0xFF10B981L, 0xFF059669L),
Pair(0xFFF59E0BL, 0xFFEF4444L),
Pair(0xFF6366F1L, 0xFF8B5CF6L),
Pair(0xFFEF4444L, 0xFFDC2626L)
)
// Colors
object AppColors {
val Primary = Color(0xFF6366F1)
val Secondary = Color(0xFFA855F7)
val Background = Color(0xFF0F172A)
val Surface = Color(0xFF1E293B)
val SurfaceVariant = Color(0xFF334155)
val OnBackground = Color(0xFFE2E8F0)
val OnSurface = Color(0xFFE2E8F0)
val Success = Color(0xFF10B981)
val Error = Color(0xFFEF4444)
val Warning = Color(0xFFFBBF24)
val TextSecondary = Color(0xFF94A3B8)
val TextTertiary = Color(0xFF64748B)
val Divider = Color(0xFF334155)
}
// Animation
const val ANIMATION_DURATION = 300
const val FADE_IN_DURATION = 200
const val FADE_OUT_DURATION = 200
}

View File

@ -0,0 +1,42 @@
// File: util/DateFormatter.kt
package com.example.notesai.util
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object DateFormatter {
private val shortFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID"))
private val longFormat = SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID"))
private val timeOnlyFormat = SimpleDateFormat("HH:mm", Locale("id", "ID"))
private val dateOnlyFormat = SimpleDateFormat("dd MMM yyyy", Locale("id", "ID"))
fun formatShort(timestamp: Long): String {
return shortFormat.format(Date(timestamp))
}
fun formatLong(timestamp: Long): String {
return longFormat.format(Date(timestamp))
}
fun formatTimeOnly(timestamp: Long): String {
return timeOnlyFormat.format(Date(timestamp))
}
fun formatDateOnly(timestamp: Long): String {
return dateOnlyFormat.format(Date(timestamp))
}
fun formatRelative(timestamp: Long): String {
val now = System.currentTimeMillis()
val diff = now - timestamp
return when {
diff < 60000 -> "Baru saja"
diff < 3600000 -> "${diff / 60000} menit yang lalu"
diff < 86400000 -> "${diff / 3600000} jam yang lalu"
diff < 604800000 -> "${diff / 86400000} hari yang lalu"
else -> formatDateOnly(timestamp)
}
}
}

View File

@ -0,0 +1,31 @@
// File: util/Extensions.kt
package com.example.notesai.util
import androidx.compose.ui.graphics.Color
// String Extensions
fun String.truncate(maxLength: Int, suffix: String = "..."): String {
return if (this.length > maxLength) {
this.substring(0, maxLength) + suffix
} else {
this
}
}
// Color Extensions
fun Long.toColor(): Color = Color(this)
fun Color.withAlpha(alpha: Float): Color = this.copy(alpha = alpha)
// List Extensions
fun <T> List<T>.replaceWhere(predicate: (T) -> Boolean, transform: (T) -> T): List<T> {
return this.map { if (predicate(it)) transform(it) else it }
}
fun <T> List<T>.removeWhere(predicate: (T) -> Boolean): List<T> {
return this.filter { !predicate(it) }
}
fun <T> List<T>.updateWhere(predicate: (T) -> Boolean, transform: (T) -> T): List<T> {
return this.map { if (predicate(it)) transform(it) else it }
}

View File

@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.13.1" agp = "8.13.2"
kotlin = "2.0.21" kotlin = "2.0.21"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"