commit ed21317c3e4d6b9b69ea8543696056b5eeb88d92
Author: dendi <202310715051@mhs.ubharajaya.ac.id>
Date: Thu Dec 11 10:03:53 2025 +0700
Sprint 1: Setup Proyek & Struktur Dasar
Setup awal proyek dengan Jetpack Compose dan Material3
Menambahkan skema warna mode gelap dengan warna gradien
Mengimplementasikan struktur navigasi utama menggunakan Scaffold
Membuat data class untuk Category, Note, dan ChatMessage
Sprint 2: Manajemen Kategori
Membuat CategoryDialog dengan pemilih warna gradien
Mengimplementasikan kartu kategori dengan layout staggered grid
Menambahkan fungsi hapus kategori melalui long press
Mengimplementasikan navigasi saat kategori ditekan dan menampilkan jumlah catatan
Sprint 3: Sistem Manajemen Catatan
Membuat dialog catatan untuk tambah/edit catatan
Mengimplementasikan kartu catatan dengan pratinjau isi dan timestamp
Menambahkan fitur pin/unpin catatan dengan sorting otomatis
Membuat tampilan catatan layar penuh dengan fitur auto-save
Menambahkan fitur pencarian catatan berdasarkan judul dan isi
Mengimplementasikan sistem arsip dan tempat sampah dengan opsi pemulihan
Menambahkan long press untuk mengarsipkan catatan dari layar utama
Sprint 4: UI Asisten AI
Membuat layar AI Helper dengan header gradien
Menambahkan dropdown pemilih kategori untuk filter konteks AI
Mengimplementasikan tampilan statistik ringkas (total catatan, pinned, kategori)
Membuat tampilan sambutan dengan suggestion chips untuk chat AI
Membuat area input chat dengan TextField multiline
Sprint 5: Integrasi Gemini AI
Menyiapkan SDK Gemini AI dengan konfigurasi API
Mengimplementasikan sistem pesan chat dengan pembeda user/AI
Membuat UI gelembung chat dengan gaya berbeda untuk pengirim
Menambahkan prompt engineering dengan membangun konteks dari catatan
Mengimplementasikan pengiriman pesan dan menampilkan respons AI
Menambahkan fitur salin ke clipboard untuk pesan AI
Mengimplementasikan error handling dan loading state saat request AI
Menambahkan auto-scroll ke bawah saat ada pesan baru
Sprint 6: Peningkatan UI/UX
Menambahkan animasi halus untuk drawer, FAB, dan transisi layar
Meningkatkan desain kartu dengan shadow dan elevasi lebih baik
Meningkatkan kontras warna dan keterbacaan teks
Mengoptimalkan tampilan empty state dengan ikon dan pesan informatif
Menambahkan indikator loading dan feedback visual di seluruh aplikasi
Memoles area input dan styling TextField
Mengimplementasikan background gradien untuk tombol dan header
Menambahkan pesan konfirmasi untuk aksi salin dengan auto-hide
Sprint 7: Penyimpanan Data (lanjutan)
Membuat DataStoreManager untuk penyimpanan lokal
Menambahkan data class Category dan Note versi Serializable
Mengimplementasikan categoriesFlow dan notesFlow dengan error handling
Menambahkan serialisasi/deserialisasi JSON menggunakan Kotlinx Serialization
Mengimplementasikan saveCategories dan saveNotes dengan try-catch
Menambahkan konfigurasi DataStore Preferences
Konfigurasi & Dependencies
Menambahkan permission INTERNET di AndroidManifest.xml
Mengonfigurasi dependency DataStore Preferences
Menambahkan plugin dan dependency Kotlinx Serialization
Menambahkan dependency Google Generative AI (Gemini) SDK
Mengonfigurasi plugin Kotlin Serialization di build.gradle
Menambahkan Material Icons Extended agar pilihan ikon lebih banyak
Menyiapkan Compose BOM untuk manajemen dependency
Mengonfigurasi minSdk 24 dan targetSdk 34
Menambahkan konfigurasi Compose compiler
Menyiapkan aturan proguard untuk mode rilis
Struktur Proyek
Membuat objek APIKey untuk konfigurasi API Gemini
Menyiapkan tema aplikasi di AndroidManifest
Mengonfigurasi dukungan vector drawable
Menambahkan dependency dan konfigurasi untuk testing
Menyiapkan packaging options untuk menghindari file META-INF
Implementasi Penyimpanan Data
Mengintegrasikan DataStoreManager ke dalam composable NotesApp
Menambahkan LaunchedEffect untuk memuat kategori saat aplikasi dibuka
Menambahkan LaunchedEffect untuk memuat catatan saat aplikasi dibuka
Mengimplementasikan auto-save dengan debounce 500ms untuk kategori
Mengimplementasikan auto-save dengan debounce 500ms untuk catatan
Menambahkan error handling untuk operasi baca DataStore
Menambahkan error handling untuk operasi tulis DataStore
Memperbaiki empty state ketika DataStore masih kosong
Perbaikan Bug Terkait Penyimpanan
Memperbaiki error parsing JSON dengan konfigurasi ignoreUnknownKeys
Menambahkan encodeDefaults ke konfigurasi JSON
Memperbaiki penanganan IOException pada alur DataStore
Menambahkan fallback emptyList untuk error decode JSON
Memperbaiki kategori tidak tersimpan setelah restart aplikasi
Memperbaiki catatan tidak tersimpan setelah restart aplikasi
Mengoptimalkan operasi edit pada DataStore
Testing & Validasi
Menguji operasi CRUD kategori beserta penyimpanannya
Menguji operasi CRUD catatan beserta penyimpanannya
Memastikan data dimuat dengan benar setelah restart aplikasi
Menguji operasi penyimpanan yang berjalan bersamaan
Memvalidasi format serialisasi JSON
Dokumentasi
Menambahkan komentar untuk implementasi DataStoreManager
Mendokumentasikan penggunaan data class Serializable
Menambahkan dokumentasi inline untuk transformasi Flow
Perbaikan Bug & Optimasi
Memperbaiki penghapusan kategori yang tidak menghapus catatan terkait
Memperbaiki pencarian yang tidak reset saat berpindah layar
Mengoptimalkan performa sorting catatan
Memperbaiki tampilan catatan layar penuh yang tidak memperbarui status pin
Menambahkan null check yang tepat untuk pencarian kategori
Memperbaiki perilaku scroll chat AI saat keyboard muncul
Meningkatkan efisiensi memori pada LazyColumn dan LazyGrid
Penyempurnaan Akhir
Memperbarui warna aplikasi dan konsistensi gradien
Menambahkan jarak dan padding yang lebih rapi di seluruh UI
Mengimplementasikan state hoisting dengan benar untuk performa
Menambahkan pesan error yang lebih jelas untuk pengguna
Penyempurnaan UI/UX final dan pembersihan kode
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..6e0286e
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+notesai
\ No newline at end of file
diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
new file mode 100644
index 0000000..371f2e2
--- /dev/null
+++ b/.idea/appInsightsSettings.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b86273d
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..639c779
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..f0c6ad0
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 0000000..c61ea33
--- /dev/null
+++ b/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..b2c751a
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml
new file mode 100644
index 0000000..539e3b8
--- /dev/null
+++ b/.idea/studiobot.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000..573d7b3
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,182 @@
+AI Notes - Changelog
+Tim Pengembang
+Dendi Yogia Pratama
+Raihan Ariq Muzaki
+Fazri Abdurahman
+
+Version 1.0.0 - Initial Release
+Sprint 1: Struktur Dasar Aplikasi
+Fitur yang Ditambahkan:
+✅ Implementasi struktur navigasi dasar aplikasi
+✅ Pembuatan menu drawer dengan navigasi ke berbagai screen
+✅ Implementasi screen Arsip untuk menyimpan catatan yang diarsipkan
+✅ Implementasi screen Sampah untuk catatan yang dihapus sementara
+✅ Sistem routing antar halaman (Beranda, Arsip, Sampah)
+✅ Bottom Navigation Bar dengan ikon Home dan AI Helper
+✅ Top App Bar dengan menu hamburger dan tombol search
+Technical:
+Setup Material3 Design System dengan Dark Theme
+Implementasi Color Scheme (Primary: #6366F1, Secondary: #A855F7)
+Gradient background untuk header dan komponen utama
+Data class untuk Category, Note, dan ChatMessage
+
+Sprint 2: Sistem Kategori
+Fitur yang Ditambahkan:
+✅ Pembuatan sistem kategori pada halaman beranda
+✅ Dialog untuk membuat kategori baru dengan:
+Input nama kategori
+Pemilihan 8 pilihan gradient warna yang berbeda
+Validasi input form
+✅ Tampilan kategori dalam bentuk Staggered Grid (2 kolom)
+✅ Category Card dengan:
+Icon folder
+Nama kategori
+Jumlah catatan di kategori tersebut
+Background gradient yang dapat dikustomisasi
+✅ Long press untuk menghapus kategori
+✅ Empty state ketika belum ada kategori
+Technical:
+Implementasi LazyVerticalStaggeredGrid untuk layout kategori
+Gradient color picker dengan 8 preset kombinasi warna
+State management untuk kategori menggunakan remember dan mutableStateOf
+
+Sprint 3: Sistem Catatan
+Fitur yang Ditambahkan:
+✅ Pembuatan catatan di dalam kategori yang dipilih
+✅ Dialog untuk membuat dan mengedit catatan dengan:
+Input judul catatan
+Input isi catatan (multiline)
+Tombol simpan dan batal
+Tombol hapus untuk catatan yang sudah ada
+✅ Note Card dengan informasi:
+Judul catatan
+Preview isi catatan (max 6 baris)
+Timestamp terakhir diubah
+Tombol pin/unpin catatan
+✅ Fitur Pin Note untuk menyematkan catatan penting di atas
+✅ Full-screen editable note view dengan:
+Edit judul dan konten langsung
+Auto-save saat kembali
+Tombol arsip, hapus, dan pin
+Timestamp terakhir diubah
+✅ Long press note untuk langsung mengarsipkan
+✅ Search functionality untuk mencari catatan berdasarkan judul/konten
+✅ Sorting otomatis: catatan yang dipasang di atas, lalu berdasarkan waktu
+Technical:
+Implementasi TextField dengan styling custom
+Date formatting menggunakan SimpleDateFormat
+Filter dan sort notes dengan sortedWith dan compareByDescending
+Edit in-place di full-screen note view
+
+Sprint 4: Fitur AI Assistant (Tahap Awal)
+Fitur yang Ditambahkan:
+✅ Halaman AI Helper dengan tampilan chat interface
+✅ Header AI Helper dengan ikon bintang dan badge "Powered by Gemini AI"
+✅ Category selector dropdown untuk filter catatan berdasarkan kategori
+✅ Statistik compact (Total catatan, Dipasang, Kategori)
+✅ Chat area dengan welcome state yang menampilkan:
+Icon dan greeting message
+3 suggestion chips untuk contoh pertanyaan
+✅ Input area dengan:
+TextField multiline untuk mengetik pesan
+Tombol send dengan gradient background
+Placeholder text yang informatif
+Technical:
+Setup struktur UI untuk chat interface
+Implementasi LaunchedEffect untuk auto-scroll
+State management untuk chat messages
+Dropdown menu untuk pemilihan kategori
+
+Sprint 5: Integrasi Gemini AI
+Fitur yang Ditambahkan:
+✅ Integrasi dengan Gemini 2.5 Flash API
+✅ Sistem prompt engineering dengan context catatan pengguna
+✅ Chat bubble untuk pesan user (kanan) dan AI (kiri)
+✅ Fitur copy response AI ke clipboard
+✅ Loading indicator saat AI sedang memproses
+✅ Error handling dengan tampilan error message yang informatif
+✅ Timestamp pada setiap pesan
+✅ Filter catatan berdasarkan kategori terpilih untuk konteks AI
+✅ Limit 10 catatan terbaru untuk konteks (optimasi token)
+Kemampuan AI:
+Menganalisis catatan pengguna
+Memberikan ringkasan
+Menjawab pertanyaan tentang catatan
+Memberikan saran organisasi
+Merespon dalam Bahasa Indonesia
+Technical:
+Implementasi GenerativeModel dari Google AI SDK
+Configuration: temperature 0.8, topK 40, topP 0.95, maxOutputTokens 4096
+Context building dengan informasi kategori dan catatan
+Async coroutine untuk API calls
+ClipboardManager untuk copy functionality
+
+Sprint 6: UI/UX Enhancement
+Perbaikan dan Peningkatan:
+✅ Refinement warna dan gradient di seluruh aplikasi
+✅ Smooth animations untuk:
+Drawer slide in/out
+FAB scale in/out
+Screen transitions
+✅ Improved shadows dan elevations
+✅ Better spacing dan padding consistency
+✅ Enhanced Card designs dengan rounded corners
+✅ Optimized text readability dengan proper color contrast
+✅ Visual feedback untuk:
+Button clicks
+Copy action (✓ Disalin message)
+Loading states
+✅ Compact stats layout di AI Helper
+✅ Improved empty states dengan icons dan descriptive messages
+✅ Better error messages dengan icon dan color coding
+Technical Polish:
+Optimized recomposition dengan proper state hoisting
+Memory efficient image loading
+Smooth scroll behavior
+Proper keyboard handling di input fields
+
+Sprint 7: Data Persistence
+Fitur yang Ditambahkan:
+✅ Implementasi DataStore untuk penyimpanan data lokal
+✅ Auto-save categories dan notes dengan debounce (500ms)
+✅ Data persistence saat aplikasi ditutup dan dibuka kembali
+✅ Error handling untuk operasi read/write
+✅ Flow-based data loading dengan LaunchedEffect
+Technical:
+Setup DataStoreManager dengan kategoriesFlow dan notesFlow
+Debounced save operations untuk efisiensi
+Try-catch blocks untuk semua operasi I/O
+Proper lifecycle handling
+
+Fitur Utama Aplikasi
+📝 Manajemen Catatan
+Buat kategori dengan gradient warna custom
+Buat, edit, dan hapus catatan
+Pin catatan penting
+Full-screen editing mode
+Search catatan
+Arsipkan catatan
+Sistem sampah dengan restore/delete permanent
+🤖 AI Assistant
+Chat interface dengan Gemini AI
+Analisis catatan otomatis
+Suggestion chips untuk quick questions
+Copy AI response ke clipboard
+Filter berdasarkan kategori
+Real-time statistics
+🎨 UI/UX
+Modern dark theme
+Gradient backgrounds
+Smooth animations
+Responsive layout
+Empty states yang informatif
+Loading indicators
+Error handling yang baik
+
+Known Issues & Future Improvements
+Planned Features (v1.1.0):
+Backup dan restore data
+Tags untuk catatan
+Rich text editor
+Dark theme toggle
+Multi-language support
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/Readme.txt b/app/Readme.txt
new file mode 100644
index 0000000..e69de29
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..d2413f1
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,77 @@
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("org.jetbrains.kotlin.plugin.compose") version "2.0.0"
+ id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0"
+}
+
+android {
+ namespace = "com.example.notesai"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.example.notesai"
+ minSdk = 24
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ compose = true
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+ implementation("androidx.activity:activity-compose:1.8.2")
+ implementation(platform("androidx.compose:compose-bom:2024.02.00"))
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.material:material-icons-extended-android:1.6.7")
+ implementation("com.google.android.material:material:1.9.0")
+ implementation("androidx.datastore:datastore-preferences:1.0.0")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
+ implementation("com.google.ai.client.generativeai:generativeai:0.9.0")
+ // Untuk integrasi Gemini AI (optional - uncomment jika sudah ada API key)
+ // implementation("com.google.ai.client.generativeai:generativeai:0.1.2")
+
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+ androidTestImplementation(platform("androidx.compose:compose-bom:2024.02.00"))
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/example/notesai/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/notesai/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..0f5d1e8
--- /dev/null
+++ b/app/src/androidTest/java/com/example/notesai/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.example.notesai
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.example.notesai", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..03bd995
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/notesai/APIKEY.kt b/app/src/main/java/com/example/notesai/APIKEY.kt
new file mode 100644
index 0000000..6464b76
--- /dev/null
+++ b/app/src/main/java/com/example/notesai/APIKEY.kt
@@ -0,0 +1,5 @@
+package com.example.notesai
+
+object APIKey {
+ const val GEMINI_API_KEY = "AIzaSyBbeLWEzIstib8j0uf3cF2Bdw0jTSNXGGU"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/notesai/DataStoreManager.kt b/app/src/main/java/com/example/notesai/DataStoreManager.kt
new file mode 100644
index 0000000..216ec9f
--- /dev/null
+++ b/app/src/main/java/com/example/notesai/DataStoreManager.kt
@@ -0,0 +1,116 @@
+@file:OptIn(kotlinx.serialization.InternalSerializationApi::class)
+
+package com.example.notesai
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.map
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.Serializable
+import java.io.IOException
+
+val Context.dataStore: DataStore by preferencesDataStore(name = "notes_prefs")
+
+@Serializable
+data class SerializableCategory(
+ val id: String,
+ val name: String,
+ val gradientStart: Long,
+ val gradientEnd: Long,
+ val timestamp: Long
+)
+
+@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
+)
+
+class DataStoreManager(private val context: Context) {
+ companion object {
+ val CATEGORIES_KEY = stringPreferencesKey("categories")
+ val NOTES_KEY = stringPreferencesKey("notes")
+ }
+
+ private val json = Json {
+ ignoreUnknownKeys = true
+ encodeDefaults = true
+ }
+
+ val categoriesFlow: Flow> = context.dataStore.data
+ .catch { exception ->
+ if (exception is IOException) {
+ emit(androidx.datastore.preferences.core.emptyPreferences())
+ } else {
+ throw exception
+ }
+ }
+ .map { preferences ->
+ val jsonString = preferences[CATEGORIES_KEY] ?: "[]"
+ try {
+ json.decodeFromString>(jsonString).map {
+ Category(it.id, it.name, it.gradientStart, it.gradientEnd, it.timestamp)
+ }
+ } catch (e: Exception) {
+ emptyList()
+ }
+ }
+
+ val notesFlow: Flow> = context.dataStore.data
+ .catch { exception ->
+ if (exception is IOException) {
+ emit(androidx.datastore.preferences.core.emptyPreferences())
+ } else {
+ throw exception
+ }
+ }
+ .map { preferences ->
+ val jsonString = preferences[NOTES_KEY] ?: "[]"
+ try {
+ json.decodeFromString>(jsonString).map {
+ Note(it.id, it.categoryId, it.title, it.content, it.timestamp, it.isArchived, it.isDeleted, it.isPinned)
+ }
+ } catch (e: Exception) {
+ emptyList()
+ }
+ }
+
+ suspend fun saveCategories(categories: List) {
+ try {
+ context.dataStore.edit { preferences ->
+ val serializable = categories.map {
+ SerializableCategory(it.id, it.name, it.gradientStart, it.gradientEnd, it.timestamp)
+ }
+ preferences[CATEGORIES_KEY] = json.encodeToString(serializable)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ suspend fun saveNotes(notes: List) {
+ try {
+ context.dataStore.edit { preferences ->
+ val serializable = notes.map {
+ SerializableNote(it.id, it.categoryId, it.title, it.content, it.timestamp, it.isArchived, it.isDeleted, it.isPinned)
+ }
+ preferences[NOTES_KEY] = json.encodeToString(serializable)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/notesai/MainActivity.kt b/app/src/main/java/com/example/notesai/MainActivity.kt
new file mode 100644
index 0000000..ef8f981
--- /dev/null
+++ b/app/src/main/java/com/example/notesai/MainActivity.kt
@@ -0,0 +1,2136 @@
+package com.example.notesai
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.animation.*
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
+import androidx.compose.foundation.lazy.staggeredgrid.items
+import androidx.compose.foundation.shape.CircleShape
+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.*
+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.draw.shadow
+import androidx.compose.ui.graphics.Brush
+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
+import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.launch
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.UUID
+import androidx.compose.material.icons.outlined.Star
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.HorizontalDivider as Divider
+import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material.icons.filled.Send
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import com.google.ai.client.generativeai.GenerativeModel
+import com.google.ai.client.generativeai.type.generationConfig
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.ui.text.style.TextAlign
+import kotlinx.coroutines.delay
+
+// Data Classes
+data class Category(
+ val id: String = UUID.randomUUID().toString(),
+ val name: String,
+ val gradientStart: Long,
+ val gradientEnd: Long,
+ val timestamp: Long = System.currentTimeMillis()
+)
+
+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
+)
+
+data class ChatMessage(
+ val id: String = UUID.randomUUID().toString(),
+ val message: String,
+ val isUser: Boolean,
+ val timestamp: Long = System.currentTimeMillis()
+)
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MaterialTheme(
+ colorScheme = darkColorScheme(
+ primary = Color(0xFF6366F1),
+ secondary = Color(0xFFA855F7),
+ background = Color(0xFF0F172A),
+ surface = Color(0xFF1E293B),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onBackground = Color(0xFFE2E8F0),
+ onSurface = Color(0xFFE2E8F0)
+ )
+ ) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ NotesApp()
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun NotesApp() {
+ val context = androidx.compose.ui.platform.LocalContext.current
+ val dataStoreManager = remember { DataStoreManager(context) }
+ val scope = rememberCoroutineScope()
+
+ var categories by remember { mutableStateOf(listOf()) }
+ var notes by remember { mutableStateOf(listOf()) }
+ var selectedCategory by remember { mutableStateOf(null) }
+ var currentScreen by remember { mutableStateOf("main") }
+ var drawerState by remember { mutableStateOf(false) }
+ var showCategoryDialog by remember { mutableStateOf(false) }
+ var showNoteDialog by remember { mutableStateOf(false) }
+ var editingNote by remember { mutableStateOf(null) }
+ var searchQuery by remember { mutableStateOf("") }
+ var showSearch by remember { mutableStateOf(false) }
+ var showFullScreenNote by remember { mutableStateOf(false) }
+ var fullScreenNote by remember { mutableStateOf(null) }
+
+ // Load data dari DataStore - dengan error handling
+ LaunchedEffect(Unit) {
+ try {
+ dataStoreManager.categoriesFlow.collect { loadedCategories ->
+ categories = loadedCategories
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ try {
+ dataStoreManager.notesFlow.collect { loadedNotes ->
+ notes = loadedNotes
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ // Simpan categories dengan debounce
+ LaunchedEffect(categories.size) {
+ if (categories.isNotEmpty()) {
+ kotlinx.coroutines.delay(500)
+ try {
+ dataStoreManager.saveCategories(categories)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ // Simpan notes dengan debounce
+ LaunchedEffect(notes.size) {
+ if (notes.isNotEmpty()) {
+ kotlinx.coroutines.delay(500)
+ try {
+ dataStoreManager.saveNotes(notes)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ if (!showFullScreenNote) {
+ ModernTopBar(
+ title = when(currentScreen) {
+ "main" -> if (selectedCategory != null) selectedCategory!!.name else "AI Notes"
+ "ai" -> "AI Helper"
+ "archive" -> "Arsip"
+ "trash" -> "Sampah"
+ else -> "AI Notes"
+ },
+ showBackButton = (selectedCategory != null && currentScreen == "main") || currentScreen == "ai",
+ onBackClick = {
+ if (currentScreen == "ai") {
+ currentScreen = "main"
+ } else {
+ selectedCategory = null
+ }
+ },
+ onMenuClick = { drawerState = !drawerState },
+ onSearchClick = { showSearch = !showSearch },
+ searchQuery = searchQuery,
+ onSearchQueryChange = { searchQuery = it },
+ showSearch = showSearch && currentScreen == "main"
+ )
+ }
+ },
+ floatingActionButton = {
+ AnimatedVisibility(
+ visible = currentScreen == "main" && !showFullScreenNote,
+ enter = scaleIn() + fadeIn(),
+ exit = scaleOut() + fadeOut()
+ ) {
+ FloatingActionButton(
+ onClick = {
+ if (selectedCategory != null) {
+ editingNote = null
+ showNoteDialog = true
+ } else {
+ showCategoryDialog = true
+ }
+ },
+ containerColor = Color.Transparent,
+ modifier = Modifier
+ .shadow(8.dp, CircleShape)
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
+ ),
+ shape = CircleShape
+ )
+ ) {
+ Icon(
+ Icons.Default.Add,
+ contentDescription = if (selectedCategory != null) "Tambah Catatan" else "Tambah Kategori",
+ tint = Color.White
+ )
+ }
+ }
+ },
+ bottomBar = {
+ if (!showFullScreenNote) {
+ ModernBottomBar(
+ currentScreen = currentScreen,
+ onHomeClick = {
+ currentScreen = "main"
+ selectedCategory = null
+ },
+ onAIClick = { currentScreen = "ai" }
+ )
+ }
+ }
+ ) { padding ->
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (showFullScreenNote && fullScreenNote != null) {
+ EditableFullScreenNoteView(
+ note = fullScreenNote!!,
+ onBack = {
+ showFullScreenNote = false
+ fullScreenNote = null
+ },
+ onSave = { title, content ->
+ notes = notes.map {
+ if (it.id == fullScreenNote!!.id) it.copy(
+ title = title,
+ content = content,
+ timestamp = System.currentTimeMillis()
+ )
+ else it
+ }
+ fullScreenNote = fullScreenNote!!.copy(title = title, content = content)
+ },
+ onArchive = {
+ notes = notes.map {
+ if (it.id == fullScreenNote!!.id) it.copy(isArchived = true)
+ else it
+ }
+ showFullScreenNote = false
+ fullScreenNote = null
+ },
+ onDelete = {
+ notes = notes.map {
+ if (it.id == fullScreenNote!!.id) it.copy(isDeleted = true)
+ else it
+ }
+ showFullScreenNote = false
+ fullScreenNote = null
+ },
+ onPinToggle = {
+ notes = notes.map {
+ if (it.id == fullScreenNote!!.id) it.copy(isPinned = !it.isPinned)
+ else it
+ }
+ fullScreenNote = fullScreenNote!!.copy(isPinned = !fullScreenNote!!.isPinned)
+ }
+ )
+ } else {
+ Column(
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize()
+ ) {
+ when (currentScreen) {
+ "main" -> MainScreen(
+ categories = categories,
+ notes = notes,
+ selectedCategory = selectedCategory,
+ searchQuery = searchQuery,
+ onCategoryClick = { selectedCategory = it },
+ onNoteClick = { note ->
+ fullScreenNote = note
+ showFullScreenNote = true
+ },
+ onNoteLongClick = { note ->
+ notes = notes.map {
+ if (it.id == note.id) it.copy(isArchived = true)
+ else it
+ }
+ },
+ onPinToggle = { note ->
+ notes = notes.map {
+ if (it.id == note.id) it.copy(isPinned = !it.isPinned)
+ else it
+ }
+ },
+ onCategoryLongClick = { category ->
+ categories = categories.filter { it.id != category.id }
+ notes = notes.filter { it.categoryId != category.id }
+ }
+ )
+ "ai" -> AIHelperScreen(
+ categories = categories,
+ notes = notes.filter { !it.isDeleted }
+ )
+ "archive" -> ArchiveScreen(
+ notes = notes.filter { it.isArchived && !it.isDeleted },
+ categories = categories,
+ onRestore = { note ->
+ notes = notes.map {
+ if (it.id == note.id) it.copy(isArchived = false)
+ else it
+ }
+ },
+ onDelete = { note ->
+ notes = notes.map {
+ if (it.id == note.id) it.copy(isDeleted = true)
+ else it
+ }
+ }
+ )
+ "trash" -> TrashScreen(
+ notes = notes.filter { it.isDeleted },
+ categories = categories,
+ onRestore = { note ->
+ notes = notes.map {
+ if (it.id == note.id) it.copy(isDeleted = false, isArchived = false)
+ else it
+ }
+ },
+ onDeletePermanent = { note ->
+ notes = notes.filter { it.id != note.id }
+ }
+ )
+ }
+ }
+ }
+
+ // Drawer with Animation
+ AnimatedVisibility(
+ visible = drawerState,
+ enter = fadeIn() + slideInHorizontally(),
+ exit = fadeOut() + slideOutHorizontally()
+ ) {
+ DrawerMenu(
+ currentScreen = currentScreen,
+ onDismiss = { drawerState = false },
+ onItemClick = { screen ->
+ currentScreen = screen
+ selectedCategory = null
+ drawerState = false
+ showSearch = false
+ searchQuery = ""
+ }
+ )
+ }
+
+ // Dialogs
+ if (showCategoryDialog) {
+ CategoryDialog(
+ onDismiss = { showCategoryDialog = false },
+ onSave = { name, gradientStart, gradientEnd ->
+ categories = categories + Category(
+ name = name,
+ gradientStart = gradientStart,
+ gradientEnd = gradientEnd
+ )
+ showCategoryDialog = false
+ }
+ )
+ }
+
+ if (showNoteDialog && selectedCategory != null) {
+ NoteDialog(
+ note = editingNote,
+ onDismiss = {
+ showNoteDialog = false
+ editingNote = null
+ },
+ onSave = { title, content ->
+ if (editingNote != null) {
+ notes = notes.map {
+ if (it.id == editingNote!!.id)
+ it.copy(
+ title = title,
+ content = content,
+ timestamp = System.currentTimeMillis()
+ )
+ else it
+ }
+ } else {
+ notes = notes + Note(
+ categoryId = selectedCategory!!.id,
+ title = title,
+ content = content
+ )
+ }
+ showNoteDialog = false
+ editingNote = null
+ },
+ onDelete = if (editingNote != null) {
+ {
+ notes = notes.map {
+ if (it.id == editingNote!!.id) it.copy(isDeleted = true)
+ else it
+ }
+ showNoteDialog = false
+ editingNote = null
+ }
+ } else null
+ )
+ }
+ }
+ }
+}
+
+@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) }
+ val dateFormat = remember { SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID")) }
+
+ 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.Default.Star else Icons.Default.Add,
+ contentDescription = "Pin Catatan",
+ tint = if (note.isPinned) Color(0xFFFBBF24) else MaterialTheme.colorScheme.onSurface
+ )
+ }
+ IconButton(onClick = {
+ if (title.isNotBlank()) {
+ onSave(title, content)
+ }
+ onArchive()
+ }) {
+ Icon(Icons.Default.Archive, contentDescription = "Arsipkan", tint = MaterialTheme.colorScheme.onSurface)
+ }
+ IconButton(onClick = onDelete) {
+ 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))
+ }
+ }
+}
+
+
+@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))
+ )
+ )
+ )
+}
+
+@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
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun MainScreen(
+ categories: List,
+ notes: List,
+ selectedCategory: Category?,
+ searchQuery: String,
+ onCategoryClick: (Category) -> Unit,
+ onNoteClick: (Note) -> Unit,
+ onNoteLongClick: (Note) -> Unit,
+ onPinToggle: (Note) -> Unit,
+ onCategoryLongClick: (Category) -> Unit
+) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ if (selectedCategory == null) {
+ if (categories.isEmpty()) {
+ EmptyState(
+ icon = Icons.Default.Create,
+ message = "Buat kategori pertama Anda",
+ subtitle = "Tekan tombol + untuk memulai"
+ )
+ } else {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(2),
+ contentPadding = PaddingValues(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalItemSpacing = 12.dp,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ items(categories) { category ->
+ CategoryCard(
+ category = category,
+ noteCount = notes.count { it.categoryId == category.id && !it.isDeleted && !it.isArchived },
+ onClick = { onCategoryClick(category) },
+ onLongClick = { onCategoryLongClick(category) }
+ )
+ }
+ }
+ }
+ } 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 { 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) },
+ onLongClick = { onNoteLongClick(note) },
+ onPinClick = { onPinToggle(note) }
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun CategoryCard(
+ category: Category,
+ noteCount: Int,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick
+ ),
+ 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)
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun NoteCard(
+ note: Note,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit,
+ onPinClick: () -> Unit
+) {
+ val dateFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID"))
+
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick
+ ),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.Top
+ ) {
+ Text(
+ note.title,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = Color.White,
+ modifier = Modifier.weight(1f)
+ )
+ IconButton(
+ onClick = onPinClick,
+ modifier = Modifier.size(24.dp)
+ ) {
+ Icon(
+ if (note.isPinned) Icons.Default.Star else Icons.Default.Add,
+ contentDescription = "Pin",
+ tint = if (note.isPinned) Color(0xFFFBBF24) else Color.Gray,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+
+ if (note.content.isNotEmpty()) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ note.content,
+ style = MaterialTheme.typography.bodyMedium,
+ maxLines = 6,
+ color = Color(0xFFCBD5E1),
+ lineHeight = 20.sp
+ )
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ dateFormat.format(Date(note.timestamp)),
+ style = MaterialTheme.typography.bodySmall,
+ color = Color(0xFF64748B)
+ )
+ }
+ }
+}
+
+@Composable
+fun DrawerMenu(
+ currentScreen: String,
+ onDismiss: () -> Unit,
+ onItemClick: (String) -> Unit
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black.copy(alpha = 0.6f))
+ .clickable(onClick = onDismiss)
+ ) {
+ Card(
+ modifier = Modifier
+ .fillMaxHeight()
+ .width(280.dp)
+ .clickable(enabled = false) {},
+ shape = RoundedCornerShape(0.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ )
+ ) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
+ )
+ )
+ .padding(32.dp)
+ ) {
+ Column {
+ Icon(
+ Icons.Default.Create,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(40.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)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ MenuItem(
+ icon = Icons.Default.Home,
+ text = "Beranda",
+ isSelected = currentScreen == "main"
+ ) { onItemClick("main") }
+
+ MenuItem(
+ icon = Icons.Default.Archive,
+ text = "Arsip",
+ isSelected = currentScreen == "archive"
+ ) { onItemClick("archive") }
+
+ MenuItem(
+ icon = Icons.Default.Delete,
+ text = "Sampah",
+ isSelected = currentScreen == "trash"
+ ) { onItemClick("trash") }
+ }
+ }
+ }
+}
+
+@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
+ )
+ }
+}
+
+@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))
+ }
+ }
+ )
+}
+
+@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))
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AIHelperScreen(
+ categories: List,
+ notes: List
+) {
+ var prompt by remember { mutableStateOf("") }
+ var isLoading by remember { mutableStateOf(false) }
+ var selectedCategory by remember { mutableStateOf(null) }
+ var showCategoryDropdown by remember { mutableStateOf(false) }
+ var errorMessage by remember { mutableStateOf("") }
+ var chatMessages by remember { mutableStateOf(listOf()) }
+ 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
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ ) {
+ // Header
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(containerColor = Color.Transparent),
+ shape = RoundedCornerShape(0.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
+ )
+ )
+ .padding(20.dp)
+ ) {
+ Column {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ Icons.Default.Star,
+ contentDescription = null,
+ tint = Color(0xFFFBBF24),
+ modifier = Modifier.size(28.dp)
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Column {
+ Text(
+ "AI Helper",
+ style = MaterialTheme.typography.titleLarge,
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Powered by Gemini AI",
+ style = MaterialTheme.typography.bodySmall,
+ color = Color.White.copy(0.8f)
+ )
+ }
+ }
+ }
+ }
+ }
+
+ // Category Selector & Stats - Compact Version
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.background)
+ .padding(16.dp)
+ ) {
+ // Category Selector
+ Box {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { showCategoryDropdown = !showCategoryDropdown },
+ colors = CardDefaults.cardColors(containerColor = Color(0xFF1E293B)),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ Icons.Default.Folder,
+ contentDescription = null,
+ tint = Color(0xFF6366F1),
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ selectedCategory?.name ?: "Semua Kategori",
+ color = Color.White,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ Icon(
+ Icons.Default.ArrowDropDown,
+ contentDescription = null,
+ tint = Color(0xFF94A3B8)
+ )
+ }
+ }
+
+ DropdownMenu(
+ expanded = showCategoryDropdown,
+ onDismissRequest = { showCategoryDropdown = false },
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color(0xFF1E293B))
+ ) {
+ DropdownMenuItem(
+ text = { Text("Semua Kategori", color = Color.White) },
+ onClick = {
+ selectedCategory = null
+ showCategoryDropdown = false
+ }
+ )
+ categories.forEach { category ->
+ DropdownMenuItem(
+ text = { Text(category.name, color = Color.White) },
+ onClick = {
+ selectedCategory = category
+ showCategoryDropdown = false
+ }
+ )
+ }
+ }
+ }
+
+ // Stats - Compact
+ Spacer(modifier = Modifier.height(12.dp))
+ val filteredNotes = if (selectedCategory != null) {
+ notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
+ } else {
+ notes.filter { !it.isArchived }
+ }
+
+ Row(
+ modifier = Modifier.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
+ .weight(1f)
+ .fillMaxWidth()
+ ) {
+ if (chatMessages.isEmpty()) {
+ // Welcome State
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(32.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ Icons.Default.Star,
+ contentDescription = null,
+ modifier = Modifier.size(64.dp),
+ tint = Color(0xFF6366F1).copy(0.5f)
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ "Mulai Percakapan",
+ style = MaterialTheme.typography.titleLarge,
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ "Tanyakan apa saja tentang catatan Anda",
+ style = MaterialTheme.typography.bodyMedium,
+ color = Color(0xFF94A3B8),
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ // Suggestion Chips
+ Column(
+ horizontalAlignment = Alignment.Start,
+ modifier = Modifier.fillMaxWidth(0.8f)
+ ) {
+ Text(
+ "Contoh pertanyaan:",
+ style = MaterialTheme.typography.bodySmall,
+ color = Color(0xFF64748B),
+ modifier = Modifier.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
+ .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.height(12.dp))
+ }
+
+ // Loading Indicator
+ if (isLoading) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(20.dp),
+ color = Color(0xFF6366F1),
+ strokeWidth = 2.dp
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(
+ "AI sedang berpikir...",
+ color = Color(0xFF94A3B8),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+ }
+ }
+
+ // Error Message
+ if (errorMessage.isNotEmpty()) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFFEF4444).copy(0.2f)
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.Warning,
+ contentDescription = null,
+ tint = Color(0xFFEF4444),
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ errorMessage,
+ color = Color(0xFFEF4444),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(80.dp))
+ }
+ }
+ }
+
+ // Input Area
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.Bottom
+ ) {
+ OutlinedTextField(
+ value = prompt,
+ onValueChange = { prompt = it },
+ placeholder = {
+ Text(
+ "Ketik pesan...",
+ color = Color(0xFF64748B)
+ )
+ },
+ modifier = Modifier
+ .weight(1f)
+ .heightIn(min = 48.dp, max = 120.dp),
+ colors = TextFieldDefaults.colors(
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White,
+ focusedContainerColor = Color(0xFF334155),
+ unfocusedContainerColor = Color(0xFF334155),
+ cursorColor = Color(0xFFA855F7),
+ focusedIndicatorColor = Color(0xFF6366F1),
+ unfocusedIndicatorColor = Color(0xFF475569)
+ ),
+ shape = RoundedCornerShape(24.dp),
+ maxLines = 4
+ )
+
+ Spacer(modifier = Modifier.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.Transparent,
+ modifier = Modifier
+ .size(48.dp)
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
+ ),
+ shape = CircleShape
+ )
+ ) {
+ Icon(
+ Icons.Default.Send,
+ contentDescription = "Send",
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+ }
+ }
+}
+
+@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) {
+ Icon(
+ Icons.Default.Star,
+ contentDescription = null,
+ tint = Color(0xFFFBBF24),
+ 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)
+ )
+ }
+ }
+ }
+}
+
+@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)
+ )
+ }
+}
+
+@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
+ )
+ }
+ }
+}
+
+@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)
+ )
+ }
+}
+
+@Composable
+fun ArchiveScreen(
+ notes: List,
+ categories: List,
+ 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) }
+ )
+ }
+ }
+ }
+}
+
+@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)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TrashScreen(
+ notes: List,
+ categories: List,
+ onRestore: (Note) -> Unit,
+ onDeletePermanent: (Note) -> Unit
+) {
+ if (notes.isEmpty()) {
+ EmptyState(
+ icon = Icons.Default.Delete,
+ message = "Sampah kosong",
+ subtitle = "Catatan yang dihapus 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 }
+ TrashNoteCard(
+ note = note,
+ categoryName = category?.name ?: "Unknown",
+ onRestore = { onRestore(note) },
+ onDeletePermanent = { onDeletePermanent(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)
+ }
+ }
+ }
+ }
+}
+
+@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)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..86a5d97
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..3b9472b
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..c8524cd
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..4cf8926
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ AI Notes
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..2350d5d
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/example/notesai/ExampleUnitTest.kt b/app/src/test/java/com/example/notesai/ExampleUnitTest.kt
new file mode 100644
index 0000000..bb01bcf
--- /dev/null
+++ b/app/src/test/java/com/example/notesai/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.notesai
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..d8d7308
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,10 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ //id("com.android.application") version "8.2.0" apply false
+ id("com.android.library") version "8.2.0" apply false
+ //id("org.jetbrains.kotlin.android") version "2.0.0" apply false
+ id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
+ id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0" apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..8169e4b
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,26 @@
+[versions]
+agp = "8.13.1"
+kotlin = "2.0.21"
+coreKtx = "1.10.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+appcompat = "1.6.1"
+material = "1.10.0"
+activity = "1.8.0"
+constraintlayout = "2.1.4"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8bdaf60
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..3e87740
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,8 @@
+#Tue Dec 09 00:56:08 WIB 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..ef07e01
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..db3a6ac
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..8889210
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "notesai"
+include(":app")
+
\ No newline at end of file