From ed21317c3e4d6b9b69ea8543696056b5eeb88d92 Mon Sep 17 00:00:00 2001 From: dendi <202310715051@mhs.ubharajaya.ac.id> Date: Thu, 11 Dec 2025 10:03:53 +0700 Subject: [PATCH] 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 --- .gitignore | 15 + .idea/.gitignore | 3 + .idea/.name | 1 + .idea/AndroidProjectSystem.xml | 6 + .idea/appInsightsSettings.xml | 26 + .idea/compiler.xml | 6 + .idea/deploymentTargetSelector.xml | 10 + .idea/deviceManager.xml | 13 + .idea/gradle.xml | 19 + .idea/inspectionProfiles/Project_Default.xml | 50 + .idea/markdown.xml | 8 + .idea/migrations.xml | 10 + .idea/misc.xml | 9 + .idea/runConfigurations.xml | 17 + .idea/studiobot.xml | 6 + .idea/vcs.xml | 6 + Readme.md | 182 ++ app/.gitignore | 1 + app/Readme.txt | 0 app/build.gradle.kts | 77 + app/proguard-rules.pro | 21 + .../notesai/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 28 + .../main/java/com/example/notesai/APIKEY.kt | 5 + .../com/example/notesai/DataStoreManager.kt | 116 + .../java/com/example/notesai/MainActivity.kt | 2136 +++++++++++++++++ .../res/drawable/ic_launcher_background.xml | 170 ++ .../res/drawable/ic_launcher_foreground.xml | 30 + app/src/main/res/layout/activity_main.xml | 19 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes app/src/main/res/values-night/themes.xml | 7 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 10 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../com/example/notesai/ExampleUnitTest.kt | 17 + build.gradle.kts | 10 + gradle.properties | 23 + gradle/libs.versions.toml | 26 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45457 bytes gradle/wrapper/gradle-wrapper.properties | 8 + gradlew | 251 ++ gradlew.bat | 94 + settings.gradle.kts | 24 + 56 files changed, 3536 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/appInsightsSettings.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/deviceManager.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/markdown.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/studiobot.xml create mode 100644 .idea/vcs.xml create mode 100644 Readme.md create mode 100644 app/.gitignore create mode 100644 app/Readme.txt create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/example/notesai/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/example/notesai/APIKEY.kt create mode 100644 app/src/main/java/com/example/notesai/DataStoreManager.kt create mode 100644 app/src/main/java/com/example/notesai/MainActivity.kt create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/example/notesai/ExampleUnitTest.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts 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 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 zcmV-U53%r4Nk&FS4*&pHMM6+kP&il$0000G0001A003VA06|PpNSy@$00HoY|G(*G z+qV7x14$dSO^Re!iqt-AAIE9iwr$(CZQJL$blA4B`>;C3fBY6Q8_YSjb2%a=fc}4E zrSzssacq<^nmW|Rs93PJni30R<8w<(bK_$LO4L?!_OxLl$}K$MUEllnMK|rg=f3;y z*?;3j|Nh>)p0JQ3A~rf(MibH2r+)3cyV1qF&;8m{w-S*y+0mM){KTK^M5}ksc`qX3 zy>rf^b>~l>SSHds8(I@hz3&PD@LmEs4&prkT=BjsBCXTMhN$_)+kvnl0bLKW5rEsj z*d#KXGDB4P&>etx0X+`R19yC=LS)j!mgs5M0L~+o-T~Jl!p!AJxnGAhV%~rhYUL4hlWhgES3Kb5oA&X z{}?3OBSS-{!v$nCIGj->(-TAG)8LR{htr41^gxsT8yqt2@DEG6Yl`Uma3Nd4;YUoW zTbkYl3CMU5ypMF3EIkYmWL|*BknM`0+Kq6CpvO(y$#j94e+q{vI{Zp8cV_6RK!`&C zob$*5Q|$IZ09dW=L!V zw@#2wviu|<#3lgGE8GEhcx+zBt`} zOwP8j9X%^f7i_bth4PiJ$LYtFJSCN$3xwDN;8mr*B;CJwBP2G0TMq0uNt7S^DO_wE zepk!Wrn#Z#03j{`c*Rf~y3o7?J}w?tEELRUR2cgxB*Y{LzA#pxHgf}q?u5idu>077 zd^=p)`nA}6e`|@`p?u}YU66PP_MA}Zqqe!c{nK&z%Jwq1N4e_q<#4g^xaz=ao;u|6 zwpRcW2Lax=ZGbx=Q*HhlJ`Ns#Y*r0*%!T?P*TTiX;rb)$CGLz=rSUum$)3Qyv{BL2 zO*=OI2|%(Yz~`pNEOnLp>+?T@glq-DujlIp?hdJeZ7ctP4_OKx|5@EOps3rr(pWzg zK4d3&oN-X2qN(d_MkfwB4I)_)!I_6nj2iA9u^pQ{;GckGLxBGrJUM2Wdda!k)Y>lq zmjws>dVQ*vW9lvEMkiN3wE-__6OWD0txS&Qn0n22cyj4Q*8(nG4!G{6OOwNvsrPIL zCl-$W9UwkEUVuLwyD%|inbOF*xMODZ4VMEVAq_zUxZ+K#Gdqf!DW$5f)?7UNOFMz! zrB~tuu=6X2FE(p^iqgxr+?ZK;=yz`e;C$#_@D9Lj-+TDVOrva>(#*PVbaHO>A)mhl z07OJWCqYC60518$!&c`eNBcBW%GnfaQ*$eazV^2_AW?j)h;J1nUjN(I9=0+!RVx~% z3@Tf!P0TE+98jA?WceK-}A1% zW!K)lyKcGqy#M~})315-A#2NXQ`?6NR#Apo=S!oF=JfpX>iR*49ec{7AN$xxpK{D$ z2d%Fz&rdfSqourN$~Y^NFIMV1CZ?J*bMx~H3k&meGtH@q9ra2vZxmA$S(#jaaj-g4 ztJmxG+DLV<*q<|sDXPp$X>E)#S}Vm&sRaO5P&goh2><}FEdZSXDqsL$06sAkh(e+v zAsBhKSRexgwg6tIy~GFJzaTxXD(}|+0eOwFDA%rn`X;MVwDHT9=4=g%OaJ9s%3b9>9EUTnnp0t;2Zpa{*>mk~hZqItE_!dQ zOtC>8`$l|mV43Jbudf0N6&&X;{=z}Zi}d1`2qmJ}i|0*GsulD3>GgQXHN)pkR6sf1 z?5ZU%&xtL}oH;YiAA)d*^Ndw2T$+Mjuzyzz@-SM`9df7LqTxLuIwC~S0092~+=qYv z@*ja;?Wt!T!{U?c*Z0YtGe)XbI&y-?B&G2$`JDM)(dIV9G`Sc#6?sI60de6kv+)Qb zUW~2|WjvJq3TA8`0+sWA3zRhY9a~ow)O~&StBkG2{*{TGiY~S8ep{V&Vo2l<6LWsu z^#p0-v*t2?3&aA1)ozu|%efSR=XnpX$lvTeRdKlvM!@|pM5p2w3u-6 zU>}t2xiYLS+{|%C65AzX+23Mtlq?BS&YdYcYsVjoiE&rT>;Necn6l^K)T^lmE`5u{ zm1i+-a-gc;Z&v-{;8r)z6NYfBUv+=_L}ef}qa9FX01)+Aaf+;xj(mL6|JUzGJR1|fnanb%?BPPIp>SCjP|8qE5qJ{=n5ZGw?81z3(k;pzH%1CtlX50{E7h)$h{qGKfzC`e2o`*IqA#tjA z`Fz&^%$b9F*N`)U-#6>a)Z`55`$Dd0cfcs0$d13^ONrdCu9xcv_=n#WQo8stcz3jP9|2EvdI-RhJM3%Q%oM&!OlShM|0 z?gz?wHZSnm45njLtsz8PVT1S&jAlbKg5kVam$p16=EK@Sj4EP0OtH zmJDmdc^v)x>56Qg_wmYHz6h)>kl_h$>0@J!ypv%APmjZTAQVLy6Fu50RGY&JAVNhx zrF_qG6`x9MkT;1SFWo$)l{M$;3qUDn9JwE}z zRl#E_bDRJFii61kPgBybIgp8dNW!Cc1b*^YYk-#oWLJvtM_v^hQx~9?8LD4VFFxBF z3MlrsSC%f9Oupn*ctPL0U1fwfX?`tRhPD{PSLFPQOmIt$mDy0SgpNVvHS+f#Do>h1Gn?LZU9(KaN>Q_=Y*_T zvtD7%_u^^+{g`0VGzg(VZrpVQ6Ub5M=tI_p7T93R8@3Zulu3|#{iNcu!oiHxZ4Rf*( zfmiN$$ru(*_Zqn=`Gq#OuHRTSwp7uH_SokR&|)RuW5yo=Z|_4?qU-JU+tpt>!B&Is z@N(=SG;bpVc;AO@zbmMM zScqq1)b-ZQIrs={oD}|?6y{$HNB1U0^LsBh8JI&3!GBZxOXI<}&5-$lgkAaYqhOTb z?2vEnZ$-kk;*M_17(upJF3%+iH*s0-r{vttXVB2OUwI1s^+G(Ft(U8gYFXC}#P&E^ z>T@C^tS`Z7{6HT4_nF~n>JlZtk5&qDBl6r|^kzQYe`wq!C)n@$c>WOPA61NDFj<<6 zGW71NMMhwAl!U-yqrq2xrSFqRCI8acw7?}3j;ynxo*-b7Co;g5r%^j=H@9({PXXBf z@r>U>>N;E)81wx`B4f%{PB~MHka_);%kBCb(d|Jy5!MqJ%2p`t&@L)4$T2j&-WHvG zv3(uyA_gwqNu(k?jQTtv3dgPKRZoH8prxe7>pQBW5L&dpumS&5Ld2?(sCpJjvc4L5 zEnh&?91WVm)ZdTj=fjJ$pPDdgAttLXuke+?KdKxu*;kTC(r!tQk6;gxj4h%FdHAt(^M3YvYj(!tOeN)+Hvj6+< zzyJRG?^lZfWuR#t!tUKP&(?%3v&Zd$R2YN>lB(Lq`OInY48%4%yTv2 zYe1{G`3)(PDEio5Y@-I5tUf`c%%OCJMtSW56g3iEg%3`$7XSJJHyA z<|7&N)5Xrlgv~%BO24eFd;Hd;uiK%D`EdK|quUeRZDqbh9l)%j%J#0lfrZumvA<_w zu&=AVvdChf6}eqh(bUz`(`Ue*p01{fBAcTgKyDYLs_I+YyJEk+rM@avU~>fB$n)HS zM7pfJydu`i%gfS<{PF94kZDv$t>06sAkheDzu40NJ$5CMW%n^Lls?8^p^QGWURbKu3ZduZQZ((s2? zzE`}<{;Zt7<$C|9R8A~DJ~@%x>TfP zF>TX8)@v|t)q4GjRt<}5s6hLHwRel7>V@&r-O|Av(yh;Q1A{E>Ir>p+%dHD|=l+lT zpr(Dg&>#Nu=!)6bCLr-ZS%|;h)Ij$+e@r8_{qO19QvDe=&1tmpY*0lcA^Cc-#{9fQ z<~$*<&P$Q<_jy#<$40PMofM7aQ}C=jphI`4kLg}Z7CIN#26D{-4v-_CA-LiE@(%{y!BzsU%gG`Q?sjLUf%qFSl0y)2#ae*+EI>s|i`d^V$Dn)qmzqRq6VJRY|{4ujsIU%#bnqU6MR&-1I_43=|5(6Jr;Jvert) zE?S|Tmn}Tv<-??sxV5@9t}3D=>YZ0JrQe$CO~|EY=Lj9RM&4svQHPQL6%pV5fPFiH zfXDx;l@~et{*{U*#c#Dvzu)|znDO7$#CRx)Z&yp-}SrD{&|(MQtfUz~n35@RLfUy=aqrhCX0M}J_r5QsK~NmRCR|Nm&L z41UdsLjWxSUlL41r^0K&nCCK>fdR-!MYjFg(z9_mF^C|#ZQw?`)f6uVzF^`bRnVY& zo}@M06J&_+>w9@jpaO4snmU;0t-(zYW1qVBHtuD!d?%?AtN7Plp><-1Y8Rqb20ZaP zTCgn*-Sri4Q8Xn>=gNaWQ57%!D35UkA@ksOlPB*Dvw}t02ENAqw|kFhn%ZyyW%+t{ zNdM!uqEM^;2}f+tECHbwLmH*!nZVrb$-az%t50Y2pg(HqhvY-^-lb}>^6l{$jOI6} zo_kBzj%8aX|6H5M0Y<)7pzz_wLkIpRm!;PzY)9+24wk2&TT{w--phDGDCOz{cN_ca zpnm7`$oDy=HX%0i-`769*0M6(e5j-?(?24%)<)&46y0e&6@HCDZAm9W6Ib#Y#BF6- z=30crHGg+RRTe%VBC>T00OV6F+gQDAK38Ne3N9bm|62tPccBJi)5{B z4zc^Db72XiBd}v$CF|yU{Z=M|DZ%-(XarYNclODlb1Kz1_EKLy(NSLCN`eUl(rBCL zT*jx@wNvze0|TSqgE(QArOZU)_?qH(sj#TwzElLs9q)(0u!_P|R%Cy_0JFQxgGV>1 zz4?_uq<8_gM0`c*Hh|;UMz~vrg1gQXp{ufg`hM_qU;U>+zmvc5blCLSq@PrEBSGR# z&8=2Z4uXN`F3p73ueD1l{s{k$WipAvSh5W7ABe?4)t;r@V?y`bNB5FvBuE|0VRTb< zM1Hn^?DSsJY+sX@T5xW=#>T9VEV|?<(=6|ge$X6Sb05!LFdjDcoq*gM(Zq=t;_)Le&jyt(&9jzR73noru`a# zN*<`KwGa^gZU3-)MSLF0aFag#f0<>E(bYTeHmtdbns#|I)-$)mJ`q9ctQ8g0=ET?| zdO}eZ*b_p>ygRTtR^5Ggdam=Zb5wmd{}np+Jn1d_=M`~P=M67jj})fH4ztb5yQqQW z^C|C&^LHAK-u+ooIK)yM)QM?t;|<{P;;{`p=BclzAN#JzL4jCwXkQB1Dy{=^KR`=~ zTrr)y7eiYBzSNs_DvO=4A6#EgGS-zY%Vi)N*Yb`U;6o}KR}dq{r9pT5wqZ@3NOE8- z9-(}D|Nc5732CSYQbL)!gPQ#RbD8BhK3dl{sUuPvei0tkvnJBxDEAYTesU8H$)g(Plra{VH(v3u^CO1~(+ zU0O7#)jaS4{NcwA+LuSm&VBcX2#Im3xg)W}ySNw%->orn1taZ&+d)}8gJTqA!u|5P z{yv?zol_3|(1(%M(EVU=cp?L`{Pi|ixk{U)*guFML3P!OSlz;zGA#T+E@8@cgQ_mv1o7RSU=Zo_82F?&&2r;WE z@wk}JHYEZ9nYUc(Vv~iTCa3u8e4q(yq<29VoNbKk|`mq%I6u)My=gPIDuUb&lzf4`MEA9^g8u z)vp8|$$HE9m_BTV?lOosIGa4jud=jIbw)O2eCMfyw2*S8?hjWw^nqws$O*M$3I1)x zR0PWFb3$ySOcGTe1dz%N0l;RPc`x%05FtT^f^j{YCP}*Q=lvp4$ZXrTZQHhO+w%wJn3c8j%+5C3UAFD&%8dBl_qi9D5g8fry}6Ev z2_Q~)5^N$!IU`BPh1O|=BxQ#*C5*}`lluC515$lxc-vNC)IgW=K|=z7o%cWFpndn= zX}f{`!VK02_kU+Q5a3m37J;c} zTzbxteE{GNf?yLt5X=Bzc-mio^Up0nunMCgp*ZJ;%MJvPM3QK)BryP(_v@ei4UvHr z6+sbCifQaOkL6-;5fL8$W($zZ_;CZp305C;~$hhRquZr-r)jjd1z z31%ZK{-(`P#|Um_Sivn@p$-vz46uqT>QG0B1w9znfS9A8PB2LaHdzA|_)yjXVR*l{ zkcu3@vEf7bxH0nkh`q?8FmoO_Ucui*>_a~P?qQrlZ9@+D7%MTpSnztpylXrt5!-k8_QPB?YL8Kx_On8WD zgT+111d(Op$^$&KLAN5+@?>f7F4~wFi(8TL8+szgVmcMDTp5l&k6~=rA{Dt}!gb^r zSWY<)M7D|Z2P0cEodj6E42PV>&>DFmQpgt)E-|#sSUU@uKed+F680H@<;-x{p|nuH4!_mn85rx>wz;0mPi2ZkL#k6;sznu?cXh!T0S>{w6 zL^gvR05NY64l*<+_L>On$rjx9!US;l;LX6@z}yi#2XHh)F@Oo+l)h%fq$v}DNmF2> zfs^_t0)3N-W<9-N?uedVv{)-J0W5mh#29QM5R5h&KuiRM=0Zvnf#lF=K#WlCgc#9c zS;qvh(P$!_a8JwyhI^ZJV2k+B6Z^64?w|1?5gyo6y{}923CRZfYVe1#?F% z7h2SUiNO3;T#JUOyovSs@@C1GtwipycA=*x5{BpIZ_#GCMuV8XK=x;qCNy{d7?wA~ zC+=vjls;ci&zW=6$H~4^K%v{p}Ab?U%C6Z4p%eC<3ExqU$XR<}LLF67A$Sr20DR_pJ3yeBa~ z^sw{V0FI5;UpwXsScYuhbqGQ`YQ25;6p6W^+tgL&;Ml;>S3CGpSZ>VrTn0m1$y$HU z&65)I!c?oREz};c=nLCliriqQX->4uivHTgd${GqeAlf*!P^B|jkU|*IdNP(&6C>4 zqOW$)Nw9nvjy^&`?E|gotDV{JmJ9Q~vuhy<`^C4XIUDt|j4o6rK^e8_(=YqC zuaR6TRVf@tUFHB079o4MBIh{M~4>WwnGgesQH*3?w(RA%hCZ*7)b!aNV=yOQ%o_Y=Lt0Sl*(9^jfRnC210Om$=y>*o|3z} zAR&vAdrB#mWoaB0fJSw9xw|Am$fzK>rx-~R#7IFSAwdu_EI|SRfB*yl0w8oX09H^q zAjl2?0I)v*odGJ40FVGaF&2qJq9Gv`>V>2r0|c`GX8h>CX8eHcOy>S0@<;M3<_6UM z7yCEpug5NZL!H_0>Hg_HasQGxR`rY&Z{geOy?N92Z z{lER^um|$*?*G63*njwc(R?NT)Bei*3jVzR>FWUDb^gKhtL4A=kE_1p-%Fo2`!8M} z(0AjuCiS;G{?*^1tB-uY%=)SRx&D)pK4u@>f6@KPe3}2j_har$>HqzH;UCR^ssFD0 z7h+VLO4o@_Yt>>AeaZKUxqyvxWCAjKB>qjQ30UA)#w z&=RmdwlT`7a8J8Yae=7*c8XL|{@%wA8uvCqfsNX^?UZsS>wX}QD{K}ad4y~iO*p%4 z_cS{u7Ek%?WV6em2(U9#d8(&JDirb^u~7wK4+xP$iiI6IlD|a&S)6o=kG;59N|>K1 zn(0mUqbG3YIY7dQd+*4~)`!S9m7H6HP6YcKHhBc#b%1L}VIisp%;TckEkcu0>lo@u995$<*Em;XNodjTiCdC%R+TX|_ZR#|1`RR|`^@Teh zl#w@8fI1FTx2Dy+{blUT{`^kY*V-AZUd?ZZqCS4gW(kY5?retkLbF=>p=59Nl|=sf zo1Pc|{{N4>5nt#627ylGF`3n>X%`w%bw-Y~zWM_{Si$dc82|=YhISal{N7OY?O`C4 zD|qb}6nLWJ`hUyL+E>-;ricg9J@ZNYP(x(Sct&OI$Y!QWr*=^VN;G3#i>^1n4e#Je zOVhbFbLpXVu*16enDM+ic;97@R~u&kh__kgP#!R`*rQEnA+_dLkNP~L`0alC|J;c; zeiK=s8;BsLE)KbG3BD&Br@(Ha@SBT&$?xX`=$;eeel=|R_dIr6-Ro?=HEjnsJ_b`1 zK6Yg^-6;^2aW!xeTK)A~3Rm|L^FCHB_I>jIju7ZGo&N_1*QHkxH2!!%@o4iZ?vntS;&zJdPe1dH#04YD93A44o-MpfD zP{rn_aq>U%RDvC2+bp;xPlsOzauIi3*Lf42`jVKKZCRuKdYhi>FDuL2l=v{$BCN#Q6796s%r-AG$Q^t(3c@ zD?w0UhYr11@feiyl9kY_@H8~|xlmO<8PfQmj1!$@WieW@VxR@Psxfe-v9WCi1+f>F4VL?0O~K7T?m4-u|pSkBpUJZZe*16_wAp zSYZ@;k`3;W3UHKUWc8QeI}0jH5Ly=cGWQPw(Kr2fm=-5L(d`lcXofy8tJY3@Tuadz zYWXR{mW7XT!RF#RVCe%}=tM*O6!AD3^(!8un~opNI%Uko7$5t@<8+?; zTxDys(MyyGsUjtSu9$+|_-t!U3fVb1dkK?l`17<+jfl=hrBHnDSV>^R1=TnQeyqbW z>ov#l%!1|S!1>8UUxIdhQq`_klcHVx0{?#>K3#$4GlXncwldt!g17TcvKq-jo_996 z>oA=tH9CqRl6Yw?Uc`am!V?lHJbizOJaVaScf1UP5e7Dbgabq=b!B~T&_F6?ooU>w%x0A zH~&MHJ=q`fCH{U<7MDXE4SD32cDZA)WJeWkllJ`UspWaS#eDe^kg^oU_A14UE9zG-a^g{xaXf$})Wik>gT zl#dkzGr(;h0JZDuFn(+k8wNq?PZ5grQ<+sM?wBGt@JnH6v0#or-5wBQWKU~(S_> zkE!tc*ZJ1Y&*p(xX84POb3cClRMd!^qJ#CAZfIepEj-<`VURS_yCz0(?*Ixcj4 z-!zV1_QZhpm=0<;*(nm+F>T=)o?ep@CK5I%g^VAA+RB25ab?7)A~z~egru=I1S|@v zH7tXV!0wmGS^qj#e+MY;C5eUjEAp$Y?LDkS^QPZ}8WN85?r$u<-Epi;yZ1|J2J`se z$D6DpH~2F=eI0B&=UFAUnJvZAmClJlK)sutJ?M>xpZiWV&0=G4MZP+x+p>EX=HbCz zxls%Mw?*u^;LbHWIWCyq+yi)`GmFn9J112CZda_u@YIP%i;srFg_paU02Ifij*7}l z&CF-(3|>*a|+vbNR`^RP=9G?ymEJ0Z~)d&c*UE$UMepZ zcITr{0WqhxkjUnM15js_gW=e3Uh|y6ZReaXHIz-=p`x5VvB&rH9y>Amv@^WmXFEw) zQXYrk3feir=a{jMQ+wDIkkFnZ$k{sJakHn*?u za%4b!00ev8NVLM1TY=cl?KB&55BY_MU-sg?c>=Dbz_W{(Z~c?HJi*XpYL)C6Bd8WH zt+v-#0&o~@t4qESi*)+eW%@VD0|o^yF)n0hME$UtXF$*Lvh}7sso{`|pn*JDIy5^Fm3s$5*zEE=?u5<=l8FJc3r%+H} zdfoNl2J0^~!-*mOL5o-x32|e0Im*E!yY7F7E5N)W3>+v_LBydlEx?4$RL5f2oYRD# zaR0wv(-p~wO0eLDl3K=%`{5+0Gd$ktO=W)gWlGZJ0`K z$_RNA=ckrfa;H0KA~dR^p�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 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 @@ + + + +