Compare commits

...

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

41 changed files with 2431 additions and 3805 deletions

View File

@ -1,26 +0,0 @@
<?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-13T07:41:36.634314200Z"> <DropdownSelection timestamp="2025-12-10T12:35:04.538299500Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=RR8T103A6JZ" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=10DEC90GZE0004R" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

8
.idea/markdown.xml generated
View File

@ -1,8 +0,0 @@
<?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
View File

@ -1,6 +0,0 @@
<?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,135 +1,98 @@
---
# **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** - Setup navigation system * Implementasi struktur navigasi dasar aplikasi
* **Pembuatan menu drawer untuk navigasi screen** - Drawer dengan list menu (Beranda, Berbintang, Arsip, Sampah) * Pembuatan menu drawer untuk navigasi screen
* **Pembuatan screen Arsip dan Sampah** - Layout dan UI untuk Archive & Trash * Pembuatan screen Arsip dan Sampah
* **Implementasi routing antar halaman** - Navigation flow (Beranda, Arsip, Sampah) * Implementasi routing antar halaman (Beranda, Arsip, Sampah)
* **Penambahan Bottom Navigation** - Home & AI Helper tabs * Penambahan Bottom Navigation (Home & AI Helper)
* **Penambahan Top App Bar** - Menu hamburger dan search icon * Penambahan Top App Bar dengan menu dan search
* **Setup Material3 dengan Dark Theme** - Color scheme dark mode * Setup Material3 dengan Dark Theme
* **Implementasi color scheme & gradient header** - Primary/Secondary colors dengan gradient * Implementasi color scheme & gradient header
* **Pembuatan data class** - Category, Note, ChatMessage models * Pembuatan data class: Category, Note, ChatMessage
* **Implementasi sistem kategori pada halaman beranda** - Category management system * Implementasi sistem kategori pada halaman beranda
* **Pembuatan dialog tambah kategori** - Form dengan nama + gradient picker * Pembuatan dialog tambah kategori (nama + gradient picker)
* **Penambahan validasi input form kategori** - Prevent empty category name * Penambahan validasi input form kategori
* **Tampilan kategori Staggered Grid** - 2 kolom responsive layout * Tampilan kategori Staggered Grid (2 kolom)
* **Category Card design** - Ikon folder, nama, jumlah catatan, gradient background * Category Card (ikon folder, nama, jumlah catatan, gradient)
* **Empty state kategori** - Pesan "Buat kategori pertama Anda" * Long press untuk menghapus kategori
* **Implementasi LazyVerticalStaggeredGrid** - Compose grid layout * Empty state kategori
* **Gradient preset 8 warna** - Pre-defined color combinations * Implementasi LazyVerticalStaggeredGrid
* **Manajemen state kategori** - Remember state untuk categories list * Gradient preset 8 warna
* **Implementasi pembuatan dan pengeditan catatan** - Note CRUD operations * Manajemen state kategori
* **Dialog catatan** - Form dengan judul, isi, simpan, batal, hapus * Implementasi pembuatan dan pengeditan catatan
* **Note Card design** - Judul, preview, timestamp, pin icon * Dialog catatan (judul, isi, simpan, batal, hapus)
* **Fitur pin untuk catatan penting** - Toggle pin/unpin functionality * Note Card (judul, preview, timestamp, pin/unpin)
* **Full-screen editable note view** - Editor dengan auto-save * Fitur pin untuk catatan penting
* **Fitur arsip, hapus, dan pin** - Actions di full-screen mode * Full-screen editable note view dengan auto-save
* **Fitur search catatan** - Filter berdasarkan judul + isi * Fitur arsip, hapus, dan pin di full-screen mode
* **Sorting catatan** - Berdasarkan pin & timestamp (descending) * Long press untuk mengarsipkan catatan
* **Implementasi custom TextField** - Styled text input fields * Fitur search catatan (judul + isi)
* **Date formatter utility** - Format timestamp ke readable format * Sorting catatan berdasarkan pin & timestamp
* **Edit in-place full-screen note** - Direct editing tanpa dialog * Implementasi custom TextField dan date formatter
* **Pembuatan screen AI Helper** - Layout untuk chat dengan AI * Edit in-place full-screen note
* **Header AI dengan ikon bintang** - Badge "Powered by Gemini AI" * Pembuatan screen AI Helper
* **Category selector** - Dropdown untuk filter konteks AI * Header AI dengan ikon bintang & badge Gemini
* **Statistik ringkas** - Total catatan, pinned, jumlah kategori * Category selector untuk filter konteks AI
* **Welcome state AI** - Icon + greeting message * Statistik ringkas (total, pinned, kategori)
* **Suggestion chips** - Quick question templates * Welcome state dengan icon + greeting
* **Input area multiline** - TextField dengan tombol kirim gradient * Suggestion chips untuk pertanyaan cepat
* **Auto-scroll chat** - Scroll ke bottom dengan LaunchedEffect * Input area multiline dengan tombol kirim gradient
* **State management chat messages** - List of ChatMessage * Auto-scroll menggunakan LaunchedEffect
* **Integrasi Gemini 2.5 Flash API** - Setup API connection * State management chat messages
* **Prompt engineering** - Context dari data catatan user * Integrasi Gemini 2.5 Flash API
* **Chat bubble user & AI** - Different styling untuk user/AI * Implementasi prompt engineering menggunakan data catatan
* **Copy-to-clipboard** - Copy jawaban AI ke clipboard * Chat bubble user & AI
* **Loading indicator** - Circular progress saat AI processing * Copy-to-clipboard untuk jawaban AI
* **Error message informatif** - Display error dengan jelas * Loading indicator saat AI memproses
* **Timestamp pada setiap pesan** - Format HH:mm * Error message informatif
* **Filter catatan berdasarkan kategori** - Context untuk AI berdasarkan selected category * Timestamp pada setiap pesan
* **Pembatasan 10 catatan terbaru** - Optimasi token usage * Filter catatan berdasarkan kategori untuk konteks
* **Implementasi Google AI SDK** - Configuration (temperature, topK, topP, maxOutputTokens) * Pembatasan 10 catatan terbaru (optimasi token)
* **Context builder** - String builder untuk kategori & catatan * Implementasi Google AI SDK (temperature, topK, topP, token limit)
* **API calls dengan coroutine** - Async operations menggunakan launch * Context builder untuk kategori & catatan
* **Refinement warna & gradient** - Polish color palette * API calls menggunakan coroutine async
* **Smooth animations** - Drawer slide, FAB scale, transitions * Refinement warna & gradient aplikasi
* **Peningkatan shadow dan elevation** - Card depth visual * Smooth animations (drawer, FAB, transitions)
* **Konsistensi spacing dan padding** - 8dp, 12dp, 16dp, 20dp standards * Peningkatan shadow dan elevation komponen
* **Peningkatan desain Card** - Rounded corners (12dp, 16dp, 20dp) * Konsistensi spacing dan padding
* **Optimasi readability teks** - Font sizes dan line heights * Peningkatan desain Card dengan rounded corners
* **Visual feedback** - Click ripples, copy confirmation, loading states * Optimasi readability teks
* **Empty state improvements** - Icon + pesan yang lebih jelas * Visual feedback (klik, copy message, loading states)
* **Perbaikan error messages** - Dengan ikon dan warna merah * Empty state baru dengan icon & pesan
* **State hoisting** - Optimasi recomposition * Perbaikan error messages dengan ikon dan warna
* **Perbaikan smooth scroll** - Keyboard handling di chat * State hoisting untuk optimasi recomposition
* **Implementasi DataStore** - Preferences DataStore untuk persistence * Perbaikan smooth scroll & keyboard handling
* **Auto-save dengan debounce** - 500ms delay sebelum save * Implementasi DataStore sebagai penyimpanan lokal
* **Persistence data penuh** - Data tetap ada setelah app ditutup * Auto-save kategori & catatan dengan debounce (500ms)
* **Error handling DataStore** - Try-catch untuk I/O operations * Persistence data penuh setelah app ditutup
* **Flow-based data loading** - Collect dari Flow dengan LaunchedEffect * Error handling read/write DataStore
* **Implementasi DataStoreManager** - Class dengan categoriesFlow & notesFlow * Flow-based data loading menggunakan LaunchedEffect
* **Try-catch semua operasi I/O** - Comprehensive error handling * Implementasi DataStoreManager (categoriesFlow & notesFlow)
* **Optimasi lifecycle data** - Proper state management * Try-catch semua operasi I/O
* **Halaman Catatan Berbintang** - StarredNotesScreen dengan filter isPinned * Optimasi lifecycle data
* **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 kategori di beranda * Search catatan
* 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
@ -148,10 +111,5 @@
* 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
---
---

View File

View File

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

View File

@ -1,20 +1,16 @@
@file:OptIn(InternalSerializationApi::class) @file:OptIn(kotlinx.serialization.InternalSerializationApi::class)
package com.example.notesai.data.local package com.example.notesai
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
@ -57,7 +53,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(emptyPreferences()) emit(androidx.datastore.preferences.core.emptyPreferences())
} else { } else {
throw exception throw exception
} }
@ -76,7 +72,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(emptyPreferences()) emit(androidx.datastore.preferences.core.emptyPreferences())
} else { } else {
throw exception throw exception
} }
@ -85,16 +81,7 @@ 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( Note(it.id, it.categoryId, it.title, it.content, it.timestamp, it.isArchived, it.isDeleted, it.isPinned)
it.id,
it.categoryId,
it.title,
it.content,
it.timestamp,
it.isArchived,
it.isDeleted,
it.isPinned
)
} }
} catch (e: Exception) { } catch (e: Exception) {
emptyList() emptyList()

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,13 +0,0 @@
// 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

@ -1,11 +0,0 @@
// 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

@ -1,15 +0,0 @@
// 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

@ -1,69 +0,0 @@
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

@ -1,177 +0,0 @@
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

@ -1,56 +0,0 @@
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

@ -1,51 +0,0 @@
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

@ -1,102 +0,0 @@
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

@ -1,107 +0,0 @@
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

@ -1,163 +0,0 @@
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

@ -1,123 +0,0 @@
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

@ -1,528 +0,0 @@
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

@ -1,120 +0,0 @@
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

@ -1,42 +0,0 @@
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

@ -1,35 +0,0 @@
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

@ -1,52 +0,0 @@
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

@ -1,45 +0,0 @@
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

@ -1,105 +0,0 @@
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

@ -1,121 +0,0 @@
// 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

@ -1,320 +0,0 @@
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

@ -1,128 +0,0 @@
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

@ -1,250 +0,0 @@
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

@ -1,51 +0,0 @@
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

@ -1,132 +0,0 @@
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

@ -1,93 +0,0 @@
// 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

@ -1,160 +0,0 @@
// 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

@ -1,105 +0,0 @@
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

@ -1,53 +0,0 @@
// 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

@ -1,42 +0,0 @@
// 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

@ -1,31 +0,0 @@
// 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.2" agp = "8.13.1"
kotlin = "2.0.21" kotlin = "2.0.21"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"