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