Compare commits
No commits in common. "3693612b209eef084209a222aaacb4ce1970ef71" and "master" have entirely different histories.
3693612b20
...
master
26
.idea/appInsightsSettings.xml
generated
Normal file
26
.idea/appInsightsSettings.xml
generated
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AppInsightsSettings">
|
||||
<option name="tabSettings">
|
||||
<map>
|
||||
<entry key="Firebase Crashlytics">
|
||||
<value>
|
||||
<InsightsFilterSettings>
|
||||
<option name="connection">
|
||||
<ConnectionSetting>
|
||||
<option name="appId" value="PLACEHOLDER" />
|
||||
<option name="mobileSdkAppId" value="" />
|
||||
<option name="projectId" value="" />
|
||||
<option name="projectNumber" value="" />
|
||||
</ConnectionSetting>
|
||||
</option>
|
||||
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||
<option name="visibilityType" value="ALL" />
|
||||
</InsightsFilterSettings>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@ -4,10 +4,10 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-12-10T12:35:04.538299500Z">
|
||||
<DropdownSelection timestamp="2025-12-13T07:41:36.634314200Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=10DEC90GZE0004R" />
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=RR8T103A6JZ" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
|
||||
8
.idea/markdown.xml
generated
Normal file
8
.idea/markdown.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/studiobot.xml
generated
Normal file
6
.idea/studiobot.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="StudioBotProjectSettings">
|
||||
<option name="shareContext" value="OptedIn" />
|
||||
</component>
|
||||
</project>
|
||||
198
Readme.md
198
Readme.md
@ -1,98 +1,135 @@
|
||||
---
|
||||
|
||||
# **AI Notes – Changelog**
|
||||
|
||||
## **Tim Pengembang**
|
||||
|
||||
* Dendi Yogia Pratama
|
||||
---
|
||||
* Raihan Ariq Muzakki
|
||||
* Fazri Abdurrahman
|
||||
|
||||
# **Version 1.0.0 – Initial Release**
|
||||
|
||||
---
|
||||
|
||||
## **Sprint 1: Struktur Dasar Aplikasi**
|
||||
|
||||
* Implementasi struktur navigasi dasar aplikasi
|
||||
* Pembuatan menu drawer untuk navigasi screen
|
||||
* Pembuatan screen Arsip dan Sampah
|
||||
* Implementasi routing antar halaman (Beranda, Arsip, Sampah)
|
||||
* Penambahan Bottom Navigation (Home & AI Helper)
|
||||
* Penambahan Top App Bar dengan menu dan search
|
||||
* Setup Material3 dengan Dark Theme
|
||||
* Implementasi color scheme & gradient header
|
||||
* Pembuatan data class: Category, Note, ChatMessage
|
||||
* Implementasi sistem kategori pada halaman beranda
|
||||
* Pembuatan dialog tambah kategori (nama + gradient picker)
|
||||
* Penambahan validasi input form kategori
|
||||
* Tampilan kategori Staggered Grid (2 kolom)
|
||||
* Category Card (ikon folder, nama, jumlah catatan, gradient)
|
||||
* Long press untuk menghapus kategori
|
||||
* Empty state kategori
|
||||
* Implementasi LazyVerticalStaggeredGrid
|
||||
* Gradient preset 8 warna
|
||||
* Manajemen state kategori
|
||||
* Implementasi pembuatan dan pengeditan catatan
|
||||
* Dialog catatan (judul, isi, simpan, batal, hapus)
|
||||
* Note Card (judul, preview, timestamp, pin/unpin)
|
||||
* Fitur pin untuk catatan penting
|
||||
* Full-screen editable note view dengan auto-save
|
||||
* Fitur arsip, hapus, dan pin di full-screen mode
|
||||
* Long press untuk mengarsipkan catatan
|
||||
* Fitur search catatan (judul + isi)
|
||||
* Sorting catatan berdasarkan pin & timestamp
|
||||
* Implementasi custom TextField dan date formatter
|
||||
* Edit in-place full-screen note
|
||||
* Pembuatan screen AI Helper
|
||||
* Header AI dengan ikon bintang & badge Gemini
|
||||
* Category selector untuk filter konteks AI
|
||||
* Statistik ringkas (total, pinned, kategori)
|
||||
* Welcome state dengan icon + greeting
|
||||
* Suggestion chips untuk pertanyaan cepat
|
||||
* Input area multiline dengan tombol kirim gradient
|
||||
* Auto-scroll menggunakan LaunchedEffect
|
||||
* State management chat messages
|
||||
* Integrasi Gemini 2.5 Flash API
|
||||
* Implementasi prompt engineering menggunakan data catatan
|
||||
* Chat bubble user & AI
|
||||
* Copy-to-clipboard untuk jawaban AI
|
||||
* Loading indicator saat AI memproses
|
||||
* Error message informatif
|
||||
* Timestamp pada setiap pesan
|
||||
* Filter catatan berdasarkan kategori untuk konteks
|
||||
* Pembatasan 10 catatan terbaru (optimasi token)
|
||||
* Implementasi Google AI SDK (temperature, topK, topP, token limit)
|
||||
* Context builder untuk kategori & catatan
|
||||
* API calls menggunakan coroutine async
|
||||
* Refinement warna & gradient aplikasi
|
||||
* Smooth animations (drawer, FAB, transitions)
|
||||
* Peningkatan shadow dan elevation komponen
|
||||
* Konsistensi spacing dan padding
|
||||
* Peningkatan desain Card dengan rounded corners
|
||||
* Optimasi readability teks
|
||||
* Visual feedback (klik, copy message, loading states)
|
||||
* Empty state baru dengan icon & pesan
|
||||
* Perbaikan error messages dengan ikon dan warna
|
||||
* State hoisting untuk optimasi recomposition
|
||||
* Perbaikan smooth scroll & keyboard handling
|
||||
* Implementasi DataStore sebagai penyimpanan lokal
|
||||
* Auto-save kategori & catatan dengan debounce (500ms)
|
||||
* Persistence data penuh setelah app ditutup
|
||||
* Error handling read/write DataStore
|
||||
* Flow-based data loading menggunakan LaunchedEffect
|
||||
* Implementasi DataStoreManager (categoriesFlow & notesFlow)
|
||||
* Try-catch semua operasi I/O
|
||||
* Optimasi lifecycle data
|
||||
* **Implementasi struktur navigasi dasar aplikasi** - Setup navigation system
|
||||
* **Pembuatan menu drawer untuk navigasi screen** - Drawer dengan list menu (Beranda, Berbintang, Arsip, Sampah)
|
||||
* **Pembuatan screen Arsip dan Sampah** - Layout dan UI untuk Archive & Trash
|
||||
* **Implementasi routing antar halaman** - Navigation flow (Beranda, Arsip, Sampah)
|
||||
* **Penambahan Bottom Navigation** - Home & AI Helper tabs
|
||||
* **Penambahan Top App Bar** - Menu hamburger dan search icon
|
||||
* **Setup Material3 dengan Dark Theme** - Color scheme dark mode
|
||||
* **Implementasi color scheme & gradient header** - Primary/Secondary colors dengan gradient
|
||||
* **Pembuatan data class** - Category, Note, ChatMessage models
|
||||
* **Implementasi sistem kategori pada halaman beranda** - Category management system
|
||||
* **Pembuatan dialog tambah kategori** - Form dengan nama + gradient picker
|
||||
* **Penambahan validasi input form kategori** - Prevent empty category name
|
||||
* **Tampilan kategori Staggered Grid** - 2 kolom responsive layout
|
||||
* **Category Card design** - Ikon folder, nama, jumlah catatan, gradient background
|
||||
* **Empty state kategori** - Pesan "Buat kategori pertama Anda"
|
||||
* **Implementasi LazyVerticalStaggeredGrid** - Compose grid layout
|
||||
* **Gradient preset 8 warna** - Pre-defined color combinations
|
||||
* **Manajemen state kategori** - Remember state untuk categories list
|
||||
* **Implementasi pembuatan dan pengeditan catatan** - Note CRUD operations
|
||||
* **Dialog catatan** - Form dengan judul, isi, simpan, batal, hapus
|
||||
* **Note Card design** - Judul, preview, timestamp, pin icon
|
||||
* **Fitur pin untuk catatan penting** - Toggle pin/unpin functionality
|
||||
* **Full-screen editable note view** - Editor dengan auto-save
|
||||
* **Fitur arsip, hapus, dan pin** - Actions di full-screen mode
|
||||
* **Fitur search catatan** - Filter berdasarkan judul + isi
|
||||
* **Sorting catatan** - Berdasarkan pin & timestamp (descending)
|
||||
* **Implementasi custom TextField** - Styled text input fields
|
||||
* **Date formatter utility** - Format timestamp ke readable format
|
||||
* **Edit in-place full-screen note** - Direct editing tanpa dialog
|
||||
* **Pembuatan screen AI Helper** - Layout untuk chat dengan AI
|
||||
* **Header AI dengan ikon bintang** - Badge "Powered by Gemini AI"
|
||||
* **Category selector** - Dropdown untuk filter konteks AI
|
||||
* **Statistik ringkas** - Total catatan, pinned, jumlah kategori
|
||||
* **Welcome state AI** - Icon + greeting message
|
||||
* **Suggestion chips** - Quick question templates
|
||||
* **Input area multiline** - TextField dengan tombol kirim gradient
|
||||
* **Auto-scroll chat** - Scroll ke bottom dengan LaunchedEffect
|
||||
* **State management chat messages** - List of ChatMessage
|
||||
* **Integrasi Gemini 2.5 Flash API** - Setup API connection
|
||||
* **Prompt engineering** - Context dari data catatan user
|
||||
* **Chat bubble user & AI** - Different styling untuk user/AI
|
||||
* **Copy-to-clipboard** - Copy jawaban AI ke clipboard
|
||||
* **Loading indicator** - Circular progress saat AI processing
|
||||
* **Error message informatif** - Display error dengan jelas
|
||||
* **Timestamp pada setiap pesan** - Format HH:mm
|
||||
* **Filter catatan berdasarkan kategori** - Context untuk AI berdasarkan selected category
|
||||
* **Pembatasan 10 catatan terbaru** - Optimasi token usage
|
||||
* **Implementasi Google AI SDK** - Configuration (temperature, topK, topP, maxOutputTokens)
|
||||
* **Context builder** - String builder untuk kategori & catatan
|
||||
* **API calls dengan coroutine** - Async operations menggunakan launch
|
||||
* **Refinement warna & gradient** - Polish color palette
|
||||
* **Smooth animations** - Drawer slide, FAB scale, transitions
|
||||
* **Peningkatan shadow dan elevation** - Card depth visual
|
||||
* **Konsistensi spacing dan padding** - 8dp, 12dp, 16dp, 20dp standards
|
||||
* **Peningkatan desain Card** - Rounded corners (12dp, 16dp, 20dp)
|
||||
* **Optimasi readability teks** - Font sizes dan line heights
|
||||
* **Visual feedback** - Click ripples, copy confirmation, loading states
|
||||
* **Empty state improvements** - Icon + pesan yang lebih jelas
|
||||
* **Perbaikan error messages** - Dengan ikon dan warna merah
|
||||
* **State hoisting** - Optimasi recomposition
|
||||
* **Perbaikan smooth scroll** - Keyboard handling di chat
|
||||
* **Implementasi DataStore** - Preferences DataStore untuk persistence
|
||||
* **Auto-save dengan debounce** - 500ms delay sebelum save
|
||||
* **Persistence data penuh** - Data tetap ada setelah app ditutup
|
||||
* **Error handling DataStore** - Try-catch untuk I/O operations
|
||||
* **Flow-based data loading** - Collect dari Flow dengan LaunchedEffect
|
||||
* **Implementasi DataStoreManager** - Class dengan categoriesFlow & notesFlow
|
||||
* **Try-catch semua operasi I/O** - Comprehensive error handling
|
||||
* **Optimasi lifecycle data** - Proper state management
|
||||
* **Halaman Catatan Berbintang** - StarredNotesScreen dengan filter isPinned
|
||||
* **Ikon bintang untuk pesan** - Star icon pada note cards
|
||||
* **Konfirmasi Arsip** - AlertDialog "Arsipkan Catatan?"
|
||||
* **Konfirmasi Hapus** - AlertDialog "Hapus Catatan?"
|
||||
|
||||
## **Sprint 2: Project Restructuring, Fitur Search, Delete Kategori**
|
||||
|
||||
* **Fitur search beranda** - Cari kategori berdasarkan nama
|
||||
* **Search filtering real-time** - Kategori otomatis filter saat mengetik
|
||||
* **Delete kategori dengan UI** - Tombol X di top-right corner setiap kategori
|
||||
* **Confirmation dialog untuk delete** - Prevent accidental deletion dengan warning message
|
||||
* **Search di kategori** - Cari catatan berdasarkan judul & isi (case-insensitive)
|
||||
* **Search empty state** - Tampilkan pesan "Tidak ada hasil" saat search kosong
|
||||
* **Gradle optimization** - Cleanup dependencies yang tidak diperlukan
|
||||
* **Hilangkan Fitur Tahan Untuk Hapus**
|
||||
* **Project restructuring** - Migrasi dari 3 file monolith ke Clean Architecture
|
||||
* **Data layer separation** - Pisahkan Category, Note, ChatMessage ke `data/model/`
|
||||
* **DataStore refactoring** - Pindahkan DataStoreManager ke `data/local/` dengan PreferencesKeys
|
||||
* **Component extraction** - Pisahkan MainScreen, CategoryCard, NoteCard ke folder terpisah
|
||||
* **Utilities creation** - Buat Constants.kt, DateFormatter.kt, Extensions.kt
|
||||
* **SerializableModels dengan extension functions** - Konversi model lebih clean
|
||||
* **Import optimization** - Update semua import ke package structure baru
|
||||
* **Menu dropdown kategori** - Icon titik tiga (⋮) untuk edit & delete
|
||||
* **Edit kategori feature** - Dialog untuk ubah nama dan gradient kategori
|
||||
* **Pre-filled edit form** - Auto-select gradient yang sedang dipakai
|
||||
* **Soft delete implementation** - Pindahkan ke trash (bukan hapus permanen)
|
||||
* **Trash system dengan kategori** - Tampilkan kategori & note yang dihapus
|
||||
* **TrashCategoryCard component** - Card khusus untuk kategori di trash
|
||||
* **Restore kategori feature** - Pulihkan kategori beserta semua note
|
||||
* **Delete permanen kategori** - Hapus kategori dan note secara permanent
|
||||
* **Counter display di trash** - Jumlah kategori dan note terhapus
|
||||
* **Category model extension** - Tambah field `isDeleted` untuk soft delete
|
||||
* **Global category filter** - Filter `!isDeleted` di semua screen
|
||||
* **Gradient preview di trash** - Kategori tetap tampilkan gradient (opacity)
|
||||
* **Dialog konfirmasi delete permanent** - Warning untuk tindakan irreversible
|
||||
* **Runtime error debugging** - Fix NotImplementedError & FATAL EXCEPTION
|
||||
* **Google Play Services error handling** - Handle GMS error untuk Gemini AI
|
||||
* **HorizontalDivider migration** - Ganti deprecated Divider component
|
||||
* **Migration guide documentation** - Panduan lengkap step-by-step migrasi
|
||||
* **Debugging documentation** - Guide untuk troubleshoot common issues
|
||||
|
||||
---
|
||||
|
||||
## **Fitur Utama Aplikasi**
|
||||
|
||||
* Sistem kategori dengan gradient
|
||||
* Buat/edit/hapus kategori dengan confirmation dialog
|
||||
* Buat/edit/hapus catatan
|
||||
* Pin catatan penting
|
||||
* Full-screen editor
|
||||
* Search catatan
|
||||
* Search kategori di beranda
|
||||
* Search catatan dalam kategori
|
||||
* Arsip & Sampah dengan restore/delete permanen
|
||||
* AI Chat powered by Gemini
|
||||
* AI membaca & menganalisis catatan pengguna
|
||||
@ -111,5 +148,10 @@
|
||||
* Rich text editor
|
||||
* Dark theme toggle
|
||||
* Multi-language support
|
||||
* AI Agent Catatan
|
||||
* Fungsi AI (Summary berdasarkan catatan, Upload File)
|
||||
* Markdown Parser
|
||||
* Opsi memilih kategori dan catatan
|
||||
* Penyesuaian User Interface dan User Experience
|
||||
---
|
||||
|
||||
---
|
||||
0
app/Readme.txt
Normal file
0
app/Readme.txt
Normal file
@ -1,5 +0,0 @@
|
||||
package com.example.notesai
|
||||
|
||||
object APIKey {
|
||||
const val GEMINI_API_KEY = "AIzaSyBzC64RXsNtSERlts_FSd8HXKEpkLdT7-8"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
5
app/src/main/java/com/example/notesai/config/APIKEY.kt
Normal file
5
app/src/main/java/com/example/notesai/config/APIKEY.kt
Normal file
@ -0,0 +1,5 @@
|
||||
package com.example.notesai.config
|
||||
|
||||
object APIKey {
|
||||
const val GEMINI_API_KEY = "MY_GEMINI_KEY"
|
||||
}
|
||||
@ -1,16 +1,20 @@
|
||||
@file:OptIn(kotlinx.serialization.InternalSerializationApi::class)
|
||||
@file:OptIn(InternalSerializationApi::class)
|
||||
|
||||
package com.example.notesai
|
||||
package com.example.notesai.data.local
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
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.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.Serializable
|
||||
@ -53,7 +57,7 @@ class DataStoreManager(private val context: Context) {
|
||||
val categoriesFlow: Flow<List<Category>> = context.dataStore.data
|
||||
.catch { exception ->
|
||||
if (exception is IOException) {
|
||||
emit(androidx.datastore.preferences.core.emptyPreferences())
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
@ -72,7 +76,7 @@ class DataStoreManager(private val context: Context) {
|
||||
val notesFlow: Flow<List<Note>> = context.dataStore.data
|
||||
.catch { exception ->
|
||||
if (exception is IOException) {
|
||||
emit(androidx.datastore.preferences.core.emptyPreferences())
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
@ -81,7 +85,16 @@ class DataStoreManager(private val context: Context) {
|
||||
val jsonString = preferences[NOTES_KEY] ?: "[]"
|
||||
try {
|
||||
json.decodeFromString<List<SerializableNote>>(jsonString).map {
|
||||
Note(it.id, it.categoryId, it.title, it.content, it.timestamp, it.isArchived, it.isDeleted, it.isPinned)
|
||||
Note(
|
||||
it.id,
|
||||
it.categoryId,
|
||||
it.title,
|
||||
it.content,
|
||||
it.timestamp,
|
||||
it.isArchived,
|
||||
it.isDeleted,
|
||||
it.isPinned
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
13
app/src/main/java/com/example/notesai/data/model/Category.kt
Normal file
13
app/src/main/java/com/example/notesai/data/model/Category.kt
Normal file
@ -0,0 +1,13 @@
|
||||
// File: data/model/Category.kt
|
||||
package com.example.notesai.data.model
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class Category(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val name: String,
|
||||
val gradientStart: Long,
|
||||
val gradientEnd: Long,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val isDeleted: Boolean = false // TAMBAHKAN INI
|
||||
)
|
||||
@ -0,0 +1,11 @@
|
||||
// File: data/model/ChatMessage.kt
|
||||
package com.example.notesai.data.model
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class ChatMessage(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val message: String,
|
||||
val isUser: Boolean,
|
||||
val timestamp: Long = System.currentTimeMillis()
|
||||
)
|
||||
15
app/src/main/java/com/example/notesai/data/model/Note.kt
Normal file
15
app/src/main/java/com/example/notesai/data/model/Note.kt
Normal file
@ -0,0 +1,15 @@
|
||||
// File: data/model/Note.kt
|
||||
package com.example.notesai.data.model
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class Note(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val categoryId: String,
|
||||
val title: String,
|
||||
val content: String,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val isArchived: Boolean = false,
|
||||
val isDeleted: Boolean = false,
|
||||
val isPinned: Boolean = false
|
||||
)
|
||||
@ -0,0 +1,69 @@
|
||||
package com.example.notesai.data.model
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@Serializable
|
||||
data class SerializableCategory(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val gradientStart: Long,
|
||||
val gradientEnd: Long,
|
||||
val timestamp: Long,
|
||||
val isDeleted: Boolean = false // TAMBAHKAN INI
|
||||
)
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@Serializable
|
||||
data class SerializableNote(
|
||||
val id: String,
|
||||
val categoryId: String,
|
||||
val title: String,
|
||||
val content: String,
|
||||
val timestamp: Long,
|
||||
val isArchived: Boolean,
|
||||
val isDeleted: Boolean,
|
||||
val isPinned: Boolean
|
||||
)
|
||||
|
||||
// Extension functions untuk konversi
|
||||
fun Category.toSerializable() = SerializableCategory(
|
||||
id = id,
|
||||
name = name,
|
||||
gradientStart = gradientStart,
|
||||
gradientEnd = gradientEnd,
|
||||
timestamp = timestamp,
|
||||
isDeleted = isDeleted // TAMBAHKAN INI
|
||||
)
|
||||
|
||||
fun SerializableCategory.toCategory() = Category(
|
||||
id = id,
|
||||
name = name,
|
||||
gradientStart = gradientStart,
|
||||
gradientEnd = gradientEnd,
|
||||
timestamp = timestamp,
|
||||
isDeleted = isDeleted // TAMBAHKAN INI
|
||||
)
|
||||
|
||||
fun Note.toSerializable() = SerializableNote(
|
||||
id = id,
|
||||
categoryId = categoryId,
|
||||
title = title,
|
||||
content = content,
|
||||
timestamp = timestamp,
|
||||
isArchived = isArchived,
|
||||
isDeleted = isDeleted,
|
||||
isPinned = isPinned
|
||||
)
|
||||
|
||||
fun SerializableNote.toNote() = Note(
|
||||
id = id,
|
||||
categoryId = categoryId,
|
||||
title = title,
|
||||
content = content,
|
||||
timestamp = timestamp,
|
||||
isArchived = isArchived,
|
||||
isDeleted = isDeleted,
|
||||
isPinned = isPinned
|
||||
)
|
||||
@ -0,0 +1,177 @@
|
||||
package com.example.notesai.presentation.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Archive
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun DrawerMenu(
|
||||
currentScreen: String,
|
||||
onDismiss: () -> Unit,
|
||||
onItemClick: (String) -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding() // Padding untuk status bar
|
||||
.background(Color.Black.copy(alpha = 0.5f))
|
||||
.clickable(
|
||||
onClick = onDismiss,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(250.dp)
|
||||
.align(Alignment.CenterStart)
|
||||
.clickable(
|
||||
onClick = {},
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
shape = RoundedCornerShape(topEnd = 0.dp, bottomEnd = 0.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1E293B)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 16.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Header Drawer dengan tombol close
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
||||
)
|
||||
)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Icon(
|
||||
Icons.Default.Create,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
"AI Notes",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
"Smart & Modern",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.White.copy(0.8f)
|
||||
)
|
||||
}
|
||||
|
||||
// // Tombol Close
|
||||
// IconButton(
|
||||
// onClick = onDismiss,
|
||||
// modifier = Modifier
|
||||
// .size(40.dp)
|
||||
// .background(
|
||||
// Color.White.copy(alpha = 0.2f),
|
||||
// shape = CircleShape
|
||||
// )
|
||||
// ) {
|
||||
// Icon(
|
||||
// Icons.Default.Close,
|
||||
// contentDescription = "Tutup Menu",
|
||||
// tint = Color.White,
|
||||
// modifier = Modifier.size(24.dp)
|
||||
// )
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Menu Items
|
||||
MenuItem(
|
||||
icon = Icons.Default.Home,
|
||||
text = "Beranda",
|
||||
isSelected = currentScreen == "main"
|
||||
) { onItemClick("main") }
|
||||
|
||||
MenuItem(
|
||||
icon = Icons.Default.Star,
|
||||
text = "Berbintang",
|
||||
isSelected = currentScreen == "starred"
|
||||
) { onItemClick("starred") }
|
||||
|
||||
MenuItem(
|
||||
icon = Icons.Default.Archive,
|
||||
text = "Arsip",
|
||||
isSelected = currentScreen == "archive"
|
||||
) { onItemClick("archive") }
|
||||
|
||||
MenuItem(
|
||||
icon = Icons.Default.Delete,
|
||||
text = "Sampah",
|
||||
isSelected = currentScreen == "trash"
|
||||
) { onItemClick("trash") }
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Footer
|
||||
Divider(
|
||||
color = Color.White.copy(alpha = 0.1f),
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Version 1.0.0",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.White.copy(alpha = 0.5f),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.example.notesai.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun EmptyState(
|
||||
icon: ImageVector,
|
||||
message: String,
|
||||
subtitle: String
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = Color(0xFF475569)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
message,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color(0xFF94A3B8),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF64748B)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.example.notesai.presentation.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun MenuItem(
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.background(
|
||||
if (isSelected) Color(0xFF334155) else Color.Transparent
|
||||
)
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
tint = if (isSelected) Color(0xFFA855F7) else Color(0xFF94A3B8)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = if (isSelected) Color.White else Color(0xFF94A3B8),
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
package com.example.notesai.presentation.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun ModernBottomBar(
|
||||
currentScreen: String,
|
||||
onHomeClick: () -> Unit,
|
||||
onAIClick: () -> Unit
|
||||
) {
|
||||
BottomAppBar(
|
||||
containerColor = Color.Transparent,
|
||||
modifier = Modifier
|
||||
.shadow(8.dp, RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF1E293B).copy(0.95f),
|
||||
Color(0xFF334155).copy(0.95f)
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable(onClick = onHomeClick)
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Home,
|
||||
contentDescription = "Home",
|
||||
tint = if (currentScreen == "main") Color(0xFF6366F1) else Color(0xFF94A3B8),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
"Home",
|
||||
color = if (currentScreen == "main") Color(0xFF6366F1) else Color(0xFF94A3B8),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = if (currentScreen == "main") FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable(onClick = onAIClick)
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Star,
|
||||
contentDescription = "AI Helper",
|
||||
tint = if (currentScreen == "ai") Color(0xFFFBBF24) else Color(0xFF94A3B8),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
"AI Helper",
|
||||
color = if (currentScreen == "ai") Color(0xFFFBBF24) else Color(0xFF94A3B8),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = if (currentScreen == "ai") FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package com.example.notesai.presentation.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ModernTopBar(
|
||||
title: String,
|
||||
showBackButton: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onMenuClick: () -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
searchQuery: String,
|
||||
onSearchQueryChange: (String) -> Unit,
|
||||
showSearch: Boolean
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
if (showSearch) {
|
||||
TextField(
|
||||
value = searchQuery,
|
||||
onValueChange = onSearchQueryChange,
|
||||
placeholder = { Text("Cari catatan...", color = Color.White.copy(0.6f)) },
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
cursorColor = Color.White,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = if (showBackButton) onBackClick else onMenuClick) {
|
||||
Icon(
|
||||
if (showBackButton) Icons.AutoMirrored.Filled.ArrowBack else Icons.Default.Menu,
|
||||
contentDescription = null,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onSearchClick) {
|
||||
Icon(
|
||||
if (showSearch) Icons.Default.Close else Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier
|
||||
.background(
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
package com.example.notesai.presentation.dialogs
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun CategoryDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String, Long, Long) -> Unit
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
var selectedGradient by remember { mutableStateOf(0) }
|
||||
|
||||
val gradients = listOf(
|
||||
Pair(0xFF6366F1L, 0xFFA855F7L),
|
||||
Pair(0xFFEC4899L, 0xFFF59E0BL),
|
||||
Pair(0xFF8B5CF6L, 0xFFEC4899L),
|
||||
Pair(0xFF06B6D4L, 0xFF3B82F6L),
|
||||
Pair(0xFF10B981L, 0xFF059669L),
|
||||
Pair(0xFFF59E0BL, 0xFFEF4444L),
|
||||
Pair(0xFF6366F1L, 0xFF8B5CF6L),
|
||||
Pair(0xFFEF4444L, 0xFFDC2626L)
|
||||
)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
containerColor = Color(0xFF1E293B),
|
||||
title = {
|
||||
Text(
|
||||
"Buat Kategori Baru",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Nama Kategori", color = Color(0xFF94A3B8)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
focusedContainerColor = Color(0xFF334155),
|
||||
unfocusedContainerColor = Color(0xFF334155),
|
||||
cursorColor = Color(0xFFA855F7),
|
||||
focusedIndicatorColor = Color(0xFFA855F7),
|
||||
unfocusedIndicatorColor = Color(0xFF64748B)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Text(
|
||||
"Pilih Gradient:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
gradients.chunked(4).forEach { row ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
row.forEachIndexed { index, gradient ->
|
||||
val globalIndex = gradients.indexOf(gradient)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(gradient.first),
|
||||
Color(gradient.second)
|
||||
)
|
||||
)
|
||||
)
|
||||
.clickable { selectedGradient = globalIndex },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (selectedGradient == globalIndex) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
if (name.isNotBlank()) {
|
||||
val gradient = gradients[selectedGradient]
|
||||
onSave(name, gradient.first, gradient.second)
|
||||
}
|
||||
},
|
||||
enabled = name.isNotBlank(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
) {
|
||||
Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Batal", color = Color(0xFF94A3B8))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
package com.example.notesai.presentation.dialogs
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.notesai.data.model.Note
|
||||
|
||||
@Composable
|
||||
fun NoteDialog(
|
||||
note: Note?,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String, String) -> Unit,
|
||||
onDelete: (() -> Unit)?
|
||||
) {
|
||||
var title by remember { mutableStateOf(note?.title ?: "") }
|
||||
var content by remember { mutableStateOf(note?.content ?: "") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
containerColor = Color(0xFF1E293B),
|
||||
title = {
|
||||
Text(
|
||||
if (note == null) "Catatan Baru" else "Edit Catatan",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
label = { Text("Judul", color = Color(0xFF94A3B8)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
focusedContainerColor = Color(0xFF334155),
|
||||
unfocusedContainerColor = Color(0xFF334155),
|
||||
cursorColor = Color(0xFFA855F7),
|
||||
focusedIndicatorColor = Color(0xFFA855F7),
|
||||
unfocusedIndicatorColor = Color(0xFF64748B)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = content,
|
||||
onValueChange = { content = it },
|
||||
label = { Text("Isi Catatan", color = Color(0xFF94A3B8)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp),
|
||||
maxLines = 10,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
focusedContainerColor = Color(0xFF334155),
|
||||
unfocusedContainerColor = Color(0xFF334155),
|
||||
cursorColor = Color(0xFFA855F7),
|
||||
focusedIndicatorColor = Color(0xFFA855F7),
|
||||
unfocusedIndicatorColor = Color(0xFF64748B)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Row {
|
||||
if (onDelete != null) {
|
||||
TextButton(onClick = onDelete) {
|
||||
Text("Hapus", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Button(
|
||||
onClick = { if (title.isNotBlank()) onSave(title, content) },
|
||||
enabled = title.isNotBlank(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
) {
|
||||
Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Batal", color = Color(0xFF94A3B8))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,528 @@
|
||||
package com.example.notesai.presentation.screens.ai
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.notesai.data.model.Note
|
||||
import com.example.notesai.data.model.ChatMessage
|
||||
import com.example.notesai.data.model.Category
|
||||
import com.example.notesai.config.APIKey
|
||||
import com.example.notesai.presentation.screens.ai.components.ChatBubble
|
||||
import com.example.notesai.presentation.screens.ai.components.CompactStatItem
|
||||
import com.example.notesai.presentation.screens.ai.components.SuggestionChip
|
||||
import com.example.notesai.util.Constants.AppColors.Divider
|
||||
import com.google.ai.client.generativeai.GenerativeModel
|
||||
import com.google.ai.client.generativeai.type.generationConfig
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.collections.plus
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AIHelperScreen(
|
||||
categories: List<Category>,
|
||||
notes: List<Note>
|
||||
) {
|
||||
var prompt by remember { mutableStateOf("") }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var selectedCategory by remember { mutableStateOf<Category?>(null) }
|
||||
var showCategoryDropdown by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
var chatMessages by remember { mutableStateOf(listOf<ChatMessage>()) }
|
||||
var showCopiedMessage by remember { mutableStateOf(false) }
|
||||
var copiedMessageId by remember { mutableStateOf("") }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
// Inisialisasi Gemini Model
|
||||
val generativeModel = remember {
|
||||
GenerativeModel(
|
||||
modelName = "gemini-2.5-flash",
|
||||
apiKey = APIKey.GEMINI_API_KEY,
|
||||
generationConfig = generationConfig {
|
||||
temperature = 0.8f
|
||||
topK = 40
|
||||
topP = 0.95f
|
||||
maxOutputTokens = 4096
|
||||
candidateCount = 1
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Auto scroll ke bawah saat ada pesan baru
|
||||
LaunchedEffect(chatMessages.size) {
|
||||
if (chatMessages.isNotEmpty()) {
|
||||
delay(100)
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
// Header
|
||||
Card(
|
||||
modifier = Modifier.Companion.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.Companion.Transparent),
|
||||
shape = RoundedCornerShape(0.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.Companion.linearGradient(
|
||||
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
||||
)
|
||||
)
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.Companion.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFFFBBF24),
|
||||
modifier = Modifier.Companion.size(28.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.Companion.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
"AI Helper",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color.Companion.White,
|
||||
fontWeight = FontWeight.Companion.Bold
|
||||
)
|
||||
Text(
|
||||
"Powered by Gemini AI",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Companion.White.copy(0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Category Selector & Stats - Compact Version
|
||||
Column(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Category Selector
|
||||
Box {
|
||||
Card(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.clickable { showCategoryDropdown = !showCategoryDropdown },
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E293B)),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Companion.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.Companion.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF6366F1),
|
||||
modifier = Modifier.Companion.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.Companion.width(8.dp))
|
||||
Text(
|
||||
selectedCategory?.name ?: "Semua Kategori",
|
||||
color = Color.Companion.White,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF94A3B8)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showCategoryDropdown,
|
||||
onDismissRequest = { showCategoryDropdown = false },
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFF1E293B))
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Semua Kategori", color = Color.Companion.White) },
|
||||
onClick = {
|
||||
selectedCategory = null
|
||||
showCategoryDropdown = false
|
||||
}
|
||||
)
|
||||
categories.forEach { category ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(category.name, color = Color.Companion.White) },
|
||||
onClick = {
|
||||
selectedCategory = category
|
||||
showCategoryDropdown = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stats - Compact
|
||||
Spacer(modifier = Modifier.Companion.height(12.dp))
|
||||
val filteredNotes = if (selectedCategory != null) {
|
||||
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
|
||||
} else {
|
||||
notes.filter { !it.isArchived }
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.Companion.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
CompactStatItem(
|
||||
label = "Total",
|
||||
value = filteredNotes.size.toString(),
|
||||
color = Color(0xFF6366F1)
|
||||
)
|
||||
CompactStatItem(
|
||||
label = "Dipasang",
|
||||
value = filteredNotes.count { it.isPinned }.toString(),
|
||||
color = Color(0xFFFBBF24)
|
||||
)
|
||||
CompactStatItem(
|
||||
label = "Kategori",
|
||||
value = categories.size.toString(),
|
||||
color = Color(0xFFA855F7)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(color = Color(0xFF334155), thickness = 1.dp)
|
||||
|
||||
// Chat Area
|
||||
Column(
|
||||
modifier = Modifier.Companion
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
if (chatMessages.isEmpty()) {
|
||||
// Welcome State
|
||||
Column(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxSize()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.Companion.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.Companion.size(64.dp),
|
||||
tint = Color(0xFF6366F1).copy(0.5f)
|
||||
)
|
||||
Spacer(modifier = Modifier.Companion.height(16.dp))
|
||||
Text(
|
||||
"Mulai Percakapan",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color.Companion.White,
|
||||
fontWeight = FontWeight.Companion.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.Companion.height(8.dp))
|
||||
Text(
|
||||
"Tanyakan apa saja tentang catatan Anda",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF94A3B8),
|
||||
textAlign = TextAlign.Companion.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.Companion.height(24.dp))
|
||||
|
||||
// Suggestion Chips
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Companion.Start,
|
||||
modifier = Modifier.Companion.fillMaxWidth(0.8f)
|
||||
) {
|
||||
Text(
|
||||
"Contoh pertanyaan:",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFF64748B),
|
||||
modifier = Modifier.Companion.padding(bottom = 8.dp)
|
||||
)
|
||||
SuggestionChip("Analisis catatan saya", onSelect = { prompt = it })
|
||||
SuggestionChip("Buat ringkasan", onSelect = { prompt = it })
|
||||
SuggestionChip("Berikan saran organisasi", onSelect = { prompt = it })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Chat Messages
|
||||
Column(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
chatMessages.forEach { message ->
|
||||
ChatBubble(
|
||||
message = message,
|
||||
onCopy = {
|
||||
clipboardManager.setText(AnnotatedString(message.message))
|
||||
copiedMessageId = message.id
|
||||
showCopiedMessage = true
|
||||
scope.launch {
|
||||
delay(2000)
|
||||
showCopiedMessage = false
|
||||
}
|
||||
},
|
||||
showCopied = showCopiedMessage && copiedMessageId == message.id
|
||||
)
|
||||
Spacer(modifier = Modifier.Companion.height(12.dp))
|
||||
}
|
||||
|
||||
// Loading Indicator
|
||||
if (isLoading) {
|
||||
Row(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1E293B)
|
||||
),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.Companion.padding(16.dp),
|
||||
verticalAlignment = Alignment.Companion.CenterVertically
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.Companion.size(20.dp),
|
||||
color = Color(0xFF6366F1),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.Companion.width(12.dp))
|
||||
Text(
|
||||
"AI sedang berpikir...",
|
||||
color = Color(0xFF94A3B8),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error Message
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
Card(
|
||||
modifier = Modifier.Companion.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFFEF4444).copy(0.2f)
|
||||
),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.Companion.padding(12.dp),
|
||||
verticalAlignment = Alignment.Companion.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFFEF4444),
|
||||
modifier = Modifier.Companion.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.Companion.width(8.dp))
|
||||
Text(
|
||||
errorMessage,
|
||||
color = Color(0xFFEF4444),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.Companion.height(80.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Input Area
|
||||
Card(
|
||||
modifier = Modifier.Companion.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1E293B)
|
||||
),
|
||||
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.Companion.Bottom
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = prompt,
|
||||
onValueChange = { prompt = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Ketik pesan...",
|
||||
color = Color(0xFF64748B)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.Companion
|
||||
.weight(1f)
|
||||
.heightIn(min = 48.dp, max = 120.dp),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedTextColor = Color.Companion.White,
|
||||
unfocusedTextColor = Color.Companion.White,
|
||||
focusedContainerColor = Color(0xFF334155),
|
||||
unfocusedContainerColor = Color(0xFF334155),
|
||||
cursorColor = Color(0xFFA855F7),
|
||||
focusedIndicatorColor = Color(0xFF6366F1),
|
||||
unfocusedIndicatorColor = Color(0xFF475569)
|
||||
),
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(24.dp),
|
||||
maxLines = 4
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.Companion.width(12.dp))
|
||||
|
||||
// Send Button
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (prompt.isNotBlank() && !isLoading) {
|
||||
scope.launch {
|
||||
// Add user message
|
||||
chatMessages = chatMessages + ChatMessage(
|
||||
message = prompt,
|
||||
isUser = true
|
||||
)
|
||||
|
||||
val userPrompt = prompt
|
||||
prompt = ""
|
||||
isLoading = true
|
||||
errorMessage = ""
|
||||
|
||||
try {
|
||||
val filteredNotes = if (selectedCategory != null) {
|
||||
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
|
||||
} else {
|
||||
notes.filter { !it.isArchived }
|
||||
}
|
||||
|
||||
val notesContext = buildString {
|
||||
appendLine("Data catatan pengguna:")
|
||||
appendLine("Total catatan: ${filteredNotes.size}")
|
||||
appendLine("Kategori: ${selectedCategory?.name ?: "Semua"}")
|
||||
appendLine()
|
||||
appendLine("Daftar catatan:")
|
||||
filteredNotes.take(10).forEach { note ->
|
||||
appendLine("- Judul: ${note.title}")
|
||||
appendLine(" Isi: ${note.content.take(100)}")
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
|
||||
val fullPrompt =
|
||||
"$notesContext\n\nPertanyaan: $userPrompt\n\nBerikan jawaban dalam bahasa Indonesia yang natural dan membantu."
|
||||
|
||||
val result = generativeModel.generateContent(fullPrompt)
|
||||
val response = result.text ?: "Tidak ada respons dari AI"
|
||||
|
||||
// Add AI response
|
||||
chatMessages = chatMessages + ChatMessage(
|
||||
message = response,
|
||||
isUser = false
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "Error: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = Color.Companion.Transparent,
|
||||
modifier = Modifier.Companion
|
||||
.size(48.dp)
|
||||
.background(
|
||||
brush = Brush.Companion.linearGradient(
|
||||
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
||||
),
|
||||
shape = CircleShape
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Send,
|
||||
contentDescription = "Send",
|
||||
tint = Color.Companion.White,
|
||||
modifier = Modifier.Companion.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
package com.example.notesai.presentation.screens.ai.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AutoAwesome
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.notesai.data.model.ChatMessage
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun ChatBubble(
|
||||
message: ChatMessage,
|
||||
onCopy: () -> Unit,
|
||||
showCopied: Boolean
|
||||
) {
|
||||
val dateFormat = remember { SimpleDateFormat("HH:mm", Locale("id", "ID")) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = if (message.isUser) Arrangement.End else Arrangement.Start
|
||||
) {
|
||||
if (!message.isUser) {
|
||||
// Ganti ikon bintang dengan ikon robot/sparkles
|
||||
Icon(
|
||||
Icons.Default.AutoAwesome, // Atau bisa diganti dengan ikon lain seperti AutoAwesome
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF6366F1), // Warna ungu/biru untuk AI
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.padding(end = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(0.85f),
|
||||
horizontalAlignment = if (message.isUser) Alignment.End else Alignment.Start
|
||||
) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (message.isUser)
|
||||
Color(0xFF6366F1)
|
||||
else
|
||||
Color(0xFF1E293B)
|
||||
),
|
||||
shape = RoundedCornerShape(
|
||||
topStart = 16.dp,
|
||||
topEnd = 16.dp,
|
||||
bottomStart = if (message.isUser) 16.dp else 4.dp,
|
||||
bottomEnd = if (message.isUser) 4.dp else 16.dp
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
message.message,
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
dateFormat.format(Date(message.timestamp)),
|
||||
color = Color.White.copy(0.6f),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
|
||||
if (!message.isUser) {
|
||||
IconButton(
|
||||
onClick = onCopy,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy",
|
||||
tint = Color.White.copy(0.7f),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showCopied && !message.isUser) {
|
||||
Text(
|
||||
"✓ Disalin",
|
||||
color = Color(0xFF10B981),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(top = 4.dp, start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.example.notesai.presentation.screens.ai.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun CompactStatItem(label: String, value: String, color: Color) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = Color(0xFF1E293B),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
value,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = color,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFF94A3B8)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package com.example.notesai.presentation.screens.ai.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun StatItem(label: String, value: String, color: Color) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
Text(
|
||||
value,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = color,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFF94A3B8)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package com.example.notesai.presentation.screens.ai.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SuggestionChip(text: String, onSelect: (String) -> Unit) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.clickable { onSelect(text) },
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1E293B)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF6366F1),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text,
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.example.notesai.presentation.screens.archive
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Archive
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.notesai.data.model.Note
|
||||
import com.example.notesai.data.model.Category
|
||||
import com.example.notesai.presentation.components.EmptyState
|
||||
import com.example.notesai.presentation.screens.archive.components.ArchiveNoteCard
|
||||
|
||||
@Composable
|
||||
fun ArchiveScreen(
|
||||
notes: List<Note>,
|
||||
categories: List<Category>,
|
||||
onRestore: (Note) -> Unit,
|
||||
onDelete: (Note) -> Unit
|
||||
) {
|
||||
if (notes.isEmpty()) {
|
||||
EmptyState(
|
||||
icon = Icons.Default.Archive,
|
||||
message = "Arsip kosong",
|
||||
subtitle = "Catatan yang diarsipkan akan muncul di sini"
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(notes) { note ->
|
||||
val category = categories.find { it.id == note.categoryId }
|
||||
ArchiveNoteCard(
|
||||
note = note,
|
||||
categoryName = category?.name ?: "Unknown",
|
||||
onRestore = { onRestore(note) },
|
||||
onDelete = { onDelete(note) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
package com.example.notesai.presentation.screens.archive.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountBox
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.notesai.data.model.Note
|
||||
|
||||
@Composable
|
||||
fun ArchiveNoteCard(
|
||||
note: Note,
|
||||
categoryName: String,
|
||||
onRestore: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1E293B)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
note.title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
categoryName,
|
||||
color = Color(0xFF64748B),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (note.content.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
note.content,
|
||||
maxLines = 2,
|
||||
color = Color(0xFF94A3B8),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = onRestore) {
|
||||
Icon(
|
||||
Icons.Default.AccountBox,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = Color(0xFF10B981)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
TextButton(onClick = onDelete) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = Color(0xFFEF4444)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Hapus", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
// File: presentation/screens/main/MainScreen.kt
|
||||
package com.example.notesai.presentation.screens.main
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.notesai.data.model.Category
|
||||
import com.example.notesai.data.model.Note
|
||||
import com.example.notesai.presentation.components.EmptyState
|
||||
import com.example.notesai.presentation.screens.main.components.CategoryCard
|
||||
import com.example.notesai.presentation.screens.main.components.NoteCard
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
categories: List<Category>,
|
||||
notes: List<Note>,
|
||||
selectedCategory: Category?,
|
||||
searchQuery: String,
|
||||
onCategoryClick: (Category) -> Unit,
|
||||
onNoteClick: (Note) -> Unit,
|
||||
onPinToggle: (Note) -> Unit,
|
||||
onCategoryDelete: (Category) -> Unit,
|
||||
onCategoryEdit: (Category, String, Long, Long) -> Unit // Parameter baru
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
if (selectedCategory == null) {
|
||||
// Beranda: Tampilkan kategori dengan search filtering
|
||||
if (categories.isEmpty()) {
|
||||
EmptyState(
|
||||
icon = Icons.Default.Create,
|
||||
message = "Buat kategori pertama Anda",
|
||||
subtitle = "Tekan tombol + untuk memulai"
|
||||
)
|
||||
} else {
|
||||
// Filter kategori berdasarkan searchQuery
|
||||
val filteredCategories = if (searchQuery.isEmpty()) {
|
||||
categories
|
||||
} else {
|
||||
categories.filter {
|
||||
it.name.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredCategories.isEmpty()) {
|
||||
EmptyState(
|
||||
icon = Icons.Default.Search,
|
||||
message = "Kategori tidak ditemukan",
|
||||
subtitle = "Coba kata kunci lain"
|
||||
)
|
||||
} else {
|
||||
LazyVerticalStaggeredGrid(
|
||||
columns = StaggeredGridCells.Fixed(2),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalItemSpacing = 12.dp,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(filteredCategories) { category ->
|
||||
CategoryCard(
|
||||
category = category,
|
||||
noteCount = notes.count {
|
||||
it.categoryId == category.id &&
|
||||
!it.isDeleted &&
|
||||
!it.isArchived
|
||||
},
|
||||
onClick = { onCategoryClick(category) },
|
||||
onDelete = { onCategoryDelete(category) },
|
||||
onEdit = { name, gradientStart, gradientEnd ->
|
||||
onCategoryEdit(category, name, gradientStart, gradientEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val categoryNotes = notes
|
||||
.filter {
|
||||
it.categoryId == selectedCategory.id &&
|
||||
!it.isDeleted &&
|
||||
!it.isArchived &&
|
||||
(searchQuery.isEmpty() ||
|
||||
it.title.contains(searchQuery, ignoreCase = true) ||
|
||||
it.content.contains(searchQuery, ignoreCase = true))
|
||||
}
|
||||
.sortedWith(compareByDescending<Note> { it.isPinned }.thenByDescending { it.timestamp })
|
||||
|
||||
if (categoryNotes.isEmpty()) {
|
||||
EmptyState(
|
||||
icon = Icons.Default.Add,
|
||||
message = if (searchQuery.isEmpty()) "Belum ada catatan" else "Tidak ada hasil",
|
||||
subtitle = if (searchQuery.isEmpty()) "Tekan tombol + untuk membuat catatan" else "Coba kata kunci lain"
|
||||
)
|
||||
} else {
|
||||
LazyVerticalStaggeredGrid(
|
||||
columns = StaggeredGridCells.Fixed(2),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalItemSpacing = 12.dp,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(categoryNotes) { note ->
|
||||
NoteCard(
|
||||
note = note,
|
||||
onClick = { onNoteClick(note) },
|
||||
onPinClick = { onPinToggle(note) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,320 @@
|
||||
package com.example.notesai.presentation.screens.main.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.notesai.data.model.Category
|
||||
|
||||
@Composable
|
||||
fun CategoryCard(
|
||||
category: Category,
|
||||
noteCount: Int,
|
||||
onClick: () -> Unit,
|
||||
onDelete: () -> Unit = {},
|
||||
onEdit: (String, Long, Long) -> Unit = { _, _, _ -> }
|
||||
) {
|
||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||
var showEditDialog by remember { mutableStateOf(false) }
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
// Delete Confirmation Dialog
|
||||
if (showDeleteConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteConfirm = false },
|
||||
title = { Text("Pindahkan ke Sampah?", color = Color.White) },
|
||||
text = {
|
||||
Text(
|
||||
"Kategori '${category.name}' dan semua catatan di dalamnya akan dipindahkan ke sampah.",
|
||||
color = Color.White
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onDelete()
|
||||
showDeleteConfirm = false
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFEF4444)
|
||||
)
|
||||
) {
|
||||
Text("Hapus", color = Color.White)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(
|
||||
onClick = { showDeleteConfirm = false },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF64748B)
|
||||
)
|
||||
) {
|
||||
Text("Batal", color = Color.White)
|
||||
}
|
||||
},
|
||||
containerColor = Color(0xFF1E293B)
|
||||
)
|
||||
}
|
||||
|
||||
// Edit Dialog
|
||||
if (showEditDialog) {
|
||||
EditCategoryDialog(
|
||||
category = category,
|
||||
onDismiss = { showEditDialog = false },
|
||||
onSave = { name, gradientStart, gradientEnd ->
|
||||
onEdit(name, gradientStart, gradientEnd)
|
||||
showEditDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(category.gradientStart),
|
||||
Color(category.gradientEnd)
|
||||
)
|
||||
)
|
||||
)
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Column {
|
||||
Icon(
|
||||
Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = Color.White.copy(0.9f),
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
category.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
"$noteCount catatan",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.White.copy(0.8f)
|
||||
)
|
||||
}
|
||||
|
||||
// Menu Button (Titik Tiga)
|
||||
Box(
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { showMenu = true }
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = Color.White.copy(0.9f)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
modifier = Modifier.background(Color(0xFF1E293B))
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF6366F1),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text("Edit Kategori", color = Color.White)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showEditDialog = true
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFFEF4444),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text("Pindah ke Sampah", color = Color.White)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showDeleteConfirm = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditCategoryDialog(
|
||||
category: Category,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String, Long, Long) -> Unit
|
||||
) {
|
||||
val gradients = listOf(
|
||||
Pair(0xFF6366F1L, 0xFFA855F7L),
|
||||
Pair(0xFFEC4899L, 0xFFF59E0BL),
|
||||
Pair(0xFF8B5CF6L, 0xFFEC4899L),
|
||||
Pair(0xFF06B6D4L, 0xFF3B82F6L),
|
||||
Pair(0xFF10B981L, 0xFF059669L),
|
||||
Pair(0xFFF59E0BL, 0xFFEF4444L),
|
||||
Pair(0xFF6366F1L, 0xFF8B5CF6L),
|
||||
Pair(0xFFEF4444L, 0xFFDC2626L)
|
||||
)
|
||||
var name by remember { mutableStateOf(category.name) }
|
||||
var selectedGradient by remember {
|
||||
mutableStateOf(
|
||||
gradients.indexOfFirst {
|
||||
it.first == category.gradientStart && it.second == category.gradientEnd
|
||||
}.takeIf { it >= 0 } ?: 0
|
||||
)
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
containerColor = Color(0xFF1E293B),
|
||||
title = {
|
||||
Text(
|
||||
"Edit Kategori",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Nama Kategori", color = Color(0xFF94A3B8)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
focusedContainerColor = Color(0xFF334155),
|
||||
unfocusedContainerColor = Color(0xFF334155),
|
||||
cursorColor = Color(0xFFA855F7),
|
||||
focusedIndicatorColor = Color(0xFFA855F7),
|
||||
unfocusedIndicatorColor = Color(0xFF64748B)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Text(
|
||||
"Pilih Gradient:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
gradients.chunked(4).forEach { row ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
row.forEachIndexed { _, gradient ->
|
||||
val globalIndex = gradients.indexOf(gradient)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(gradient.first),
|
||||
Color(gradient.second)
|
||||
)
|
||||
)
|
||||
)
|
||||
.clickable { selectedGradient = globalIndex },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (selectedGradient == globalIndex) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
if (name.isNotBlank()) {
|
||||
val gradient = gradients[selectedGradient]
|
||||
onSave(name, gradient.first, gradient.second)
|
||||
}
|
||||
},
|
||||
enabled = name.isNotBlank(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
) {
|
||||
Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Batal", color = Color(0xFF94A3B8))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
package com.example.notesai.presentation.screens.main.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.outlined.StarBorder
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.notesai.data.model.Note
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NoteCard(
|
||||
note: Note,
|
||||
onClick: () -> Unit,
|
||||
onPinClick: () -> Unit
|
||||
) {
|
||||
val dateFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID"))
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1E293B)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
// Judul
|
||||
Text(
|
||||
note.title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
IconButton(
|
||||
onClick = onPinClick,
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
|
||||
contentDescription = "Pin",
|
||||
tint = if (note.isPinned) Color(0xFFFBBF24) else Color.Gray,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Deskripsi
|
||||
if (note.content.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "Deskripsi",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color(0xFF94A3B8),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
note.content,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = Color(0xFFCBD5E1),
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
HorizontalDivider(
|
||||
color = Color(0xFF334155),
|
||||
thickness = 1.dp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Timestamp
|
||||
Text(
|
||||
dateFormat.format(Date(note.timestamp)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFF64748B)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,250 @@
|
||||
package com.example.notesai.presentation.screens.note
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Archive
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.outlined.StarBorder
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.notesai.data.model.Note
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EditableFullScreenNoteView(
|
||||
note: Note,
|
||||
onBack: () -> Unit,
|
||||
onSave: (String, String) -> Unit,
|
||||
onArchive: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onPinToggle: () -> Unit
|
||||
) {
|
||||
var title by remember { mutableStateOf(note.title) }
|
||||
var content by remember { mutableStateOf(note.content) }
|
||||
var showArchiveDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
val dateFormat = remember { SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID")) }
|
||||
|
||||
// Dialog Konfirmasi Arsip
|
||||
if (showArchiveDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showArchiveDialog = false },
|
||||
title = {
|
||||
Text(
|
||||
text = "Arsipkan Catatan?",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = "Catatan ini akan dipindahkan ke arsip. Anda masih bisa mengaksesnya nanti.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (title.isNotBlank()) {
|
||||
onSave(title, content)
|
||||
}
|
||||
onArchive()
|
||||
showArchiveDialog = false
|
||||
}
|
||||
) {
|
||||
Text("Arsipkan", color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showArchiveDialog = false }) {
|
||||
Text("Batal", color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Dialog Konfirmasi Hapus
|
||||
if (showDeleteDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteDialog = false },
|
||||
title = {
|
||||
Text(
|
||||
text = "Hapus Catatan?",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = "Catatan ini akan dihapus permanen dan tidak bisa dikembalikan.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDelete()
|
||||
showDeleteDialog = false
|
||||
}
|
||||
) {
|
||||
Text("Hapus", color = Color(0xFFEF4444))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteDialog = false }) {
|
||||
Text("Batal", color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
if (title.isNotBlank()) {
|
||||
onSave(title, content)
|
||||
}
|
||||
onBack()
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali", tint = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
if (title.isNotBlank()) {
|
||||
onSave(title, content)
|
||||
}
|
||||
onPinToggle()
|
||||
}) {
|
||||
Icon(
|
||||
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
|
||||
contentDescription = "Pin Catatan",
|
||||
tint = if (note.isPinned) Color(0xFFFBBF24) else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { showArchiveDialog = true }) {
|
||||
Icon(Icons.Default.Archive, contentDescription = "Arsipkan", tint = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
IconButton(onClick = { showDeleteDialog = true }) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Hapus", tint = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
TextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
textStyle = MaterialTheme.typography.headlineLarge.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
"Judul",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = Color(0xFF64748B)
|
||||
)
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
cursorColor = Color(0xFFA855F7)
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = "Terakhir diubah: ${dateFormat.format(Date(note.timestamp))}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFF64748B)
|
||||
)
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(vertical = 20.dp),
|
||||
color = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
|
||||
TextField(
|
||||
value = content,
|
||||
onValueChange = { content = it },
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
lineHeight = 28.sp
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
"Mulai menulis...",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color(0xFF64748B)
|
||||
)
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
cursorColor = Color(0xFFA855F7)
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 400.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(100.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.example.notesai.presentation.screens.starred
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.notesai.presentation.components.EmptyState
|
||||
import com.example.notesai.presentation.screens.starred.components.StarredNoteCard
|
||||
import com.example.notesai.data.model.Note
|
||||
import com.example.notesai.data.model.Category
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StarredNotesScreen(
|
||||
notes: List<Note>,
|
||||
categories: List<Category>,
|
||||
onNoteClick: (Note) -> Unit,
|
||||
onMenuClick: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onUnpin: (Note) -> Unit
|
||||
) {
|
||||
val starredNotes = notes.filter { it.isPinned && !it.isArchived && !it.isDeleted }
|
||||
|
||||
if (starredNotes.isEmpty()) {
|
||||
EmptyState(
|
||||
icon = Icons.Default.Star,
|
||||
message = "Belum ada catatan berbintang",
|
||||
subtitle = "Catatan yang ditandai berbintang akan muncul di sini"
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(starredNotes) { note ->
|
||||
val category = categories.find { it.id == note.categoryId }
|
||||
StarredNoteCard(
|
||||
note = note,
|
||||
categoryName = category?.name ?: "Unknown",
|
||||
onClick = { onNoteClick(note) },
|
||||
onUnpin = { onUnpin(note) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package com.example.notesai.presentation.screens.starred.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.outlined.StarBorder
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.notesai.data.model.Note
|
||||
|
||||
@Composable
|
||||
fun StarredNoteCard(
|
||||
note: Note,
|
||||
categoryName: String,
|
||||
onClick: () -> Unit,
|
||||
onUnpin: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1E293B)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.Companion.padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.Companion.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Companion.Top
|
||||
) {
|
||||
Column(modifier = Modifier.Companion.weight(1f)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.Companion.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFFFBBF24),
|
||||
modifier = Modifier.Companion.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.Companion.width(8.dp))
|
||||
Text(
|
||||
note.title,
|
||||
fontWeight = FontWeight.Companion.Bold,
|
||||
color = Color.Companion.White,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.Companion.height(4.dp))
|
||||
Text(
|
||||
categoryName,
|
||||
color = Color(0xFF64748B),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (note.content.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.Companion.height(8.dp))
|
||||
Text(
|
||||
note.content,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Companion.Ellipsis,
|
||||
color = Color(0xFF94A3B8),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = onClick) {
|
||||
Icon(
|
||||
Icons.Default.Info,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.Companion.size(18.dp),
|
||||
tint = Color(0xFF6366F1)
|
||||
)
|
||||
Spacer(modifier = Modifier.Companion.width(4.dp))
|
||||
Text(
|
||||
"Lihat Detail",
|
||||
color = Color(0xFF6366F1),
|
||||
fontWeight = FontWeight.Companion.Bold
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.Companion.width(8.dp))
|
||||
TextButton(onClick = onUnpin) {
|
||||
Icon(
|
||||
Icons.Outlined.StarBorder,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.Companion.size(18.dp),
|
||||
tint = Color(0xFFFBBF24)
|
||||
)
|
||||
Spacer(modifier = Modifier.Companion.width(4.dp))
|
||||
Text(
|
||||
"Hapus Bintang",
|
||||
color = Color(0xFFFBBF24),
|
||||
fontWeight = FontWeight.Companion.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
// File: presentation/screens/trash/TrashScreen.kt
|
||||
package com.example.notesai.presentation.screens.trash
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.notesai.presentation.components.EmptyState
|
||||
import com.example.notesai.presentation.screens.trash.components.TrashNoteCard
|
||||
import com.example.notesai.presentation.screens.trash.components.TrashCategoryCard
|
||||
import com.example.notesai.data.model.Note
|
||||
import com.example.notesai.data.model.Category
|
||||
|
||||
@Composable
|
||||
fun TrashScreen(
|
||||
notes: List<Note>,
|
||||
categories: List<Category>,
|
||||
onRestoreNote: (Note) -> Unit,
|
||||
onDeleteNotePermanent: (Note) -> Unit,
|
||||
onRestoreCategory: (Category) -> Unit,
|
||||
onDeleteCategoryPermanent: (Category) -> Unit
|
||||
) {
|
||||
// Filter kategori dan note yang dihapus
|
||||
val deletedCategories = categories.filter { it.isDeleted }
|
||||
val deletedNotes = notes.filter { it.isDeleted }
|
||||
|
||||
if (deletedCategories.isEmpty() && deletedNotes.isEmpty()) {
|
||||
EmptyState(
|
||||
icon = Icons.Default.Delete,
|
||||
message = "Sampah kosong",
|
||||
subtitle = "Kategori dan catatan yang dihapus akan muncul di sini"
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Section: Kategori Terhapus
|
||||
if (deletedCategories.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
"Kategori Terhapus (${deletedCategories.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF94A3B8)
|
||||
)
|
||||
}
|
||||
|
||||
items(deletedCategories) { category ->
|
||||
val notesInCategory = notes.count {
|
||||
it.categoryId == category.id && it.isDeleted
|
||||
}
|
||||
TrashCategoryCard(
|
||||
category = category,
|
||||
noteCount = notesInCategory,
|
||||
onRestore = { onRestoreCategory(category) },
|
||||
onDeletePermanent = { onDeleteCategoryPermanent(category) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Section: Catatan Terhapus
|
||||
if (deletedNotes.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
"Catatan Terhapus (${deletedNotes.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF94A3B8)
|
||||
)
|
||||
}
|
||||
|
||||
items(deletedNotes) { note ->
|
||||
val category = categories.find { it.id == note.categoryId }
|
||||
TrashNoteCard(
|
||||
note = note,
|
||||
categoryName = category?.name ?: "Unknown",
|
||||
onRestore = { onRestoreNote(note) },
|
||||
onDeletePermanent = { onDeleteNotePermanent(note) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,160 @@
|
||||
// File: presentation/screens/trash/components/TrashCategoryCard.kt
|
||||
package com.example.notesai.presentation.screens.trash.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.Restore
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.notesai.data.model.Category
|
||||
|
||||
@Composable
|
||||
fun TrashCategoryCard(
|
||||
category: Category,
|
||||
noteCount: Int,
|
||||
onRestore: () -> Unit,
|
||||
onDeletePermanent: () -> Unit
|
||||
) {
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// Dialog konfirmasi hapus permanen
|
||||
if (showDeleteDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteDialog = false },
|
||||
title = { Text("Hapus Kategori Permanen?", color = Color.White) },
|
||||
text = {
|
||||
Text(
|
||||
"Kategori '${category.name}' dan $noteCount catatan di dalamnya akan dihapus permanen. Tindakan ini tidak dapat dibatalkan!",
|
||||
color = Color.White
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onDeletePermanent()
|
||||
showDeleteDialog = false
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFEF4444)
|
||||
)
|
||||
) {
|
||||
Text("Hapus Permanen", color = Color.White)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(
|
||||
onClick = { showDeleteDialog = false },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF64748B)
|
||||
)
|
||||
) {
|
||||
Text("Batal", color = Color.White)
|
||||
}
|
||||
},
|
||||
containerColor = Color(0xFF1E293B)
|
||||
)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1E293B)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
// Header dengan gradient
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(category.gradientStart).copy(alpha = 0.3f),
|
||||
Color(category.gradientEnd).copy(alpha = 0.3f)
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = Color.White.copy(0.9f),
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
category.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
"$noteCount catatan",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.White.copy(0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Info
|
||||
Text(
|
||||
"Kategori yang dihapus",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFF64748B)
|
||||
)
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = onRestore) {
|
||||
Icon(
|
||||
Icons.Default.Restore,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = Color(0xFF10B981)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
TextButton(onClick = { showDeleteDialog = true }) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = Color(0xFFEF4444)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Hapus Permanen", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
package com.example.notesai.presentation.screens.trash.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountBox
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.notesai.data.model.Note
|
||||
|
||||
@Composable
|
||||
fun TrashNoteCard(
|
||||
note: Note,
|
||||
categoryName: String,
|
||||
onRestore: () -> Unit,
|
||||
onDeletePermanent: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF7F1D1D).copy(0.2f)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
note.title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
categoryName,
|
||||
color = Color(0xFF64748B),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (note.content.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
note.content,
|
||||
maxLines = 2,
|
||||
color = Color(0xFF94A3B8),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = onRestore) {
|
||||
Icon(
|
||||
Icons.Default.AccountBox,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = Color(0xFF10B981)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
TextButton(onClick = onDeletePermanent) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = Color(0xFFEF4444)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Hapus Permanen", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
app/src/main/java/com/example/notesai/util/Constants.kt
Normal file
53
app/src/main/java/com/example/notesai/util/Constants.kt
Normal file
@ -0,0 +1,53 @@
|
||||
// File: util/Constants.kt
|
||||
package com.example.notesai.util
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
object Constants {
|
||||
// App Info
|
||||
const val APP_NAME = "AI Notes"
|
||||
const val APP_VERSION = "1.0.0"
|
||||
|
||||
// DataStore
|
||||
const val DATASTORE_NAME = "notes_prefs"
|
||||
const val DEBOUNCE_DELAY = 500L
|
||||
|
||||
// UI Constants
|
||||
const val MAX_NOTE_PREVIEW_LINES = 4
|
||||
const val MAX_CHAT_PREVIEW_LINES = 2
|
||||
const val GRID_COLUMNS = 2
|
||||
|
||||
// Gradients
|
||||
val GRADIENT_PRESETS = listOf(
|
||||
Pair(0xFF6366F1L, 0xFFA855F7L),
|
||||
Pair(0xFFEC4899L, 0xFFF59E0BL),
|
||||
Pair(0xFF8B5CF6L, 0xFFEC4899L),
|
||||
Pair(0xFF06B6D4L, 0xFF3B82F6L),
|
||||
Pair(0xFF10B981L, 0xFF059669L),
|
||||
Pair(0xFFF59E0BL, 0xFFEF4444L),
|
||||
Pair(0xFF6366F1L, 0xFF8B5CF6L),
|
||||
Pair(0xFFEF4444L, 0xFFDC2626L)
|
||||
)
|
||||
|
||||
// Colors
|
||||
object AppColors {
|
||||
val Primary = Color(0xFF6366F1)
|
||||
val Secondary = Color(0xFFA855F7)
|
||||
val Background = Color(0xFF0F172A)
|
||||
val Surface = Color(0xFF1E293B)
|
||||
val SurfaceVariant = Color(0xFF334155)
|
||||
val OnBackground = Color(0xFFE2E8F0)
|
||||
val OnSurface = Color(0xFFE2E8F0)
|
||||
val Success = Color(0xFF10B981)
|
||||
val Error = Color(0xFFEF4444)
|
||||
val Warning = Color(0xFFFBBF24)
|
||||
val TextSecondary = Color(0xFF94A3B8)
|
||||
val TextTertiary = Color(0xFF64748B)
|
||||
val Divider = Color(0xFF334155)
|
||||
}
|
||||
|
||||
// Animation
|
||||
const val ANIMATION_DURATION = 300
|
||||
const val FADE_IN_DURATION = 200
|
||||
const val FADE_OUT_DURATION = 200
|
||||
}
|
||||
42
app/src/main/java/com/example/notesai/util/DateFormatter.kt
Normal file
42
app/src/main/java/com/example/notesai/util/DateFormatter.kt
Normal file
@ -0,0 +1,42 @@
|
||||
// File: util/DateFormatter.kt
|
||||
package com.example.notesai.util
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
object DateFormatter {
|
||||
private val shortFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID"))
|
||||
private val longFormat = SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID"))
|
||||
private val timeOnlyFormat = SimpleDateFormat("HH:mm", Locale("id", "ID"))
|
||||
private val dateOnlyFormat = SimpleDateFormat("dd MMM yyyy", Locale("id", "ID"))
|
||||
|
||||
fun formatShort(timestamp: Long): String {
|
||||
return shortFormat.format(Date(timestamp))
|
||||
}
|
||||
|
||||
fun formatLong(timestamp: Long): String {
|
||||
return longFormat.format(Date(timestamp))
|
||||
}
|
||||
|
||||
fun formatTimeOnly(timestamp: Long): String {
|
||||
return timeOnlyFormat.format(Date(timestamp))
|
||||
}
|
||||
|
||||
fun formatDateOnly(timestamp: Long): String {
|
||||
return dateOnlyFormat.format(Date(timestamp))
|
||||
}
|
||||
|
||||
fun formatRelative(timestamp: Long): String {
|
||||
val now = System.currentTimeMillis()
|
||||
val diff = now - timestamp
|
||||
|
||||
return when {
|
||||
diff < 60000 -> "Baru saja"
|
||||
diff < 3600000 -> "${diff / 60000} menit yang lalu"
|
||||
diff < 86400000 -> "${diff / 3600000} jam yang lalu"
|
||||
diff < 604800000 -> "${diff / 86400000} hari yang lalu"
|
||||
else -> formatDateOnly(timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
31
app/src/main/java/com/example/notesai/util/Extensions.kt
Normal file
31
app/src/main/java/com/example/notesai/util/Extensions.kt
Normal file
@ -0,0 +1,31 @@
|
||||
// File: util/Extensions.kt
|
||||
package com.example.notesai.util
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
// String Extensions
|
||||
fun String.truncate(maxLength: Int, suffix: String = "..."): String {
|
||||
return if (this.length > maxLength) {
|
||||
this.substring(0, maxLength) + suffix
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
// Color Extensions
|
||||
fun Long.toColor(): Color = Color(this)
|
||||
|
||||
fun Color.withAlpha(alpha: Float): Color = this.copy(alpha = alpha)
|
||||
|
||||
// List Extensions
|
||||
fun <T> List<T>.replaceWhere(predicate: (T) -> Boolean, transform: (T) -> T): List<T> {
|
||||
return this.map { if (predicate(it)) transform(it) else it }
|
||||
}
|
||||
|
||||
fun <T> List<T>.removeWhere(predicate: (T) -> Boolean): List<T> {
|
||||
return this.filter { !predicate(it) }
|
||||
}
|
||||
|
||||
fun <T> List<T>.updateWhere(predicate: (T) -> Boolean, transform: (T) -> T): List<T> {
|
||||
return this.map { if (predicate(it)) transform(it) else it }
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
agp = "8.13.1"
|
||||
agp = "8.13.2"
|
||||
kotlin = "2.0.21"
|
||||
coreKtx = "1.10.1"
|
||||
junit = "4.13.2"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user