commit 98117731861f3327aa4688d8085f3f38768b9a2b
Author: FazriA <202310715082@mhs.ubharajaya.ac.id>
Date: Wed Dec 10 18:40:36 2025 +0700
Awal Commit Import Project
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/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/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/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/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/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..bec1c84
--- /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 = "AIzaSyBzC64RXsNtSERlts_FSd8HXKEpkLdT7-8"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/notesai/DataStoreManager.kt b/app/src/main/java/com/example/notesai/DataStoreManager.kt
new file mode 100644
index 0000000..216ec9f
--- /dev/null
+++ b/app/src/main/java/com/example/notesai/DataStoreManager.kt
@@ -0,0 +1,116 @@
+@file:OptIn(kotlinx.serialization.InternalSerializationApi::class)
+
+package com.example.notesai
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.map
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.Serializable
+import java.io.IOException
+
+val Context.dataStore: DataStore by preferencesDataStore(name = "notes_prefs")
+
+@Serializable
+data class SerializableCategory(
+ val id: String,
+ val name: String,
+ val gradientStart: Long,
+ val gradientEnd: Long,
+ val timestamp: Long
+)
+
+@Serializable
+data class SerializableNote(
+ val id: String,
+ val categoryId: String,
+ val title: String,
+ val content: String,
+ val timestamp: Long,
+ val isArchived: Boolean,
+ val isDeleted: Boolean,
+ val isPinned: Boolean
+)
+
+class DataStoreManager(private val context: Context) {
+ companion object {
+ val CATEGORIES_KEY = stringPreferencesKey("categories")
+ val NOTES_KEY = stringPreferencesKey("notes")
+ }
+
+ private val json = Json {
+ ignoreUnknownKeys = true
+ encodeDefaults = true
+ }
+
+ val categoriesFlow: Flow> = context.dataStore.data
+ .catch { exception ->
+ if (exception is IOException) {
+ emit(androidx.datastore.preferences.core.emptyPreferences())
+ } else {
+ throw exception
+ }
+ }
+ .map { preferences ->
+ val jsonString = preferences[CATEGORIES_KEY] ?: "[]"
+ try {
+ json.decodeFromString>(jsonString).map {
+ Category(it.id, it.name, it.gradientStart, it.gradientEnd, it.timestamp)
+ }
+ } catch (e: Exception) {
+ emptyList()
+ }
+ }
+
+ val notesFlow: Flow> = context.dataStore.data
+ .catch { exception ->
+ if (exception is IOException) {
+ emit(androidx.datastore.preferences.core.emptyPreferences())
+ } else {
+ throw exception
+ }
+ }
+ .map { preferences ->
+ val jsonString = preferences[NOTES_KEY] ?: "[]"
+ try {
+ json.decodeFromString>(jsonString).map {
+ Note(it.id, it.categoryId, it.title, it.content, it.timestamp, it.isArchived, it.isDeleted, it.isPinned)
+ }
+ } catch (e: Exception) {
+ emptyList()
+ }
+ }
+
+ suspend fun saveCategories(categories: List) {
+ try {
+ context.dataStore.edit { preferences ->
+ val serializable = categories.map {
+ SerializableCategory(it.id, it.name, it.gradientStart, it.gradientEnd, it.timestamp)
+ }
+ preferences[CATEGORIES_KEY] = json.encodeToString(serializable)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ suspend fun saveNotes(notes: List) {
+ try {
+ context.dataStore.edit { preferences ->
+ val serializable = notes.map {
+ SerializableNote(it.id, it.categoryId, it.title, it.content, it.timestamp, it.isArchived, it.isDeleted, it.isPinned)
+ }
+ preferences[NOTES_KEY] = json.encodeToString(serializable)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/notesai/MainActivity.kt b/app/src/main/java/com/example/notesai/MainActivity.kt
new file mode 100644
index 0000000..ef8f981
--- /dev/null
+++ b/app/src/main/java/com/example/notesai/MainActivity.kt
@@ -0,0 +1,2136 @@
+package com.example.notesai
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.animation.*
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
+import androidx.compose.foundation.lazy.staggeredgrid.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.launch
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.UUID
+import androidx.compose.material.icons.outlined.Star
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.HorizontalDivider as Divider
+import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material.icons.filled.Send
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import com.google.ai.client.generativeai.GenerativeModel
+import com.google.ai.client.generativeai.type.generationConfig
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.material.icons.filled.ContentCopy
+import androidx.compose.ui.text.style.TextAlign
+import kotlinx.coroutines.delay
+
+// Data Classes
+data class Category(
+ val id: String = UUID.randomUUID().toString(),
+ val name: String,
+ val gradientStart: Long,
+ val gradientEnd: Long,
+ val timestamp: Long = System.currentTimeMillis()
+)
+
+data class Note(
+ val id: String = UUID.randomUUID().toString(),
+ val categoryId: String,
+ val title: String,
+ val content: String,
+ val timestamp: Long = System.currentTimeMillis(),
+ val isArchived: Boolean = false,
+ val isDeleted: Boolean = false,
+ val isPinned: Boolean = false
+)
+
+data class ChatMessage(
+ val id: String = UUID.randomUUID().toString(),
+ val message: String,
+ val isUser: Boolean,
+ val timestamp: Long = System.currentTimeMillis()
+)
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MaterialTheme(
+ colorScheme = darkColorScheme(
+ primary = Color(0xFF6366F1),
+ secondary = Color(0xFFA855F7),
+ background = Color(0xFF0F172A),
+ surface = Color(0xFF1E293B),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onBackground = Color(0xFFE2E8F0),
+ onSurface = Color(0xFFE2E8F0)
+ )
+ ) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ NotesApp()
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun NotesApp() {
+ val context = androidx.compose.ui.platform.LocalContext.current
+ val dataStoreManager = remember { DataStoreManager(context) }
+ val scope = rememberCoroutineScope()
+
+ var categories by remember { mutableStateOf(listOf()) }
+ var notes by remember { mutableStateOf(listOf()) }
+ var selectedCategory by remember { mutableStateOf(null) }
+ var currentScreen by remember { mutableStateOf("main") }
+ var drawerState by remember { mutableStateOf(false) }
+ var showCategoryDialog by remember { mutableStateOf(false) }
+ var showNoteDialog by remember { mutableStateOf(false) }
+ var editingNote by remember { mutableStateOf(null) }
+ var searchQuery by remember { mutableStateOf("") }
+ var showSearch by remember { mutableStateOf(false) }
+ var showFullScreenNote by remember { mutableStateOf(false) }
+ var fullScreenNote by remember { mutableStateOf(null) }
+
+ // Load data dari DataStore - dengan error handling
+ LaunchedEffect(Unit) {
+ try {
+ dataStoreManager.categoriesFlow.collect { loadedCategories ->
+ categories = loadedCategories
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ try {
+ dataStoreManager.notesFlow.collect { loadedNotes ->
+ notes = loadedNotes
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ // Simpan categories dengan debounce
+ LaunchedEffect(categories.size) {
+ if (categories.isNotEmpty()) {
+ kotlinx.coroutines.delay(500)
+ try {
+ dataStoreManager.saveCategories(categories)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ // Simpan notes dengan debounce
+ LaunchedEffect(notes.size) {
+ if (notes.isNotEmpty()) {
+ kotlinx.coroutines.delay(500)
+ try {
+ dataStoreManager.saveNotes(notes)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ if (!showFullScreenNote) {
+ ModernTopBar(
+ title = when(currentScreen) {
+ "main" -> if (selectedCategory != null) selectedCategory!!.name else "AI Notes"
+ "ai" -> "AI Helper"
+ "archive" -> "Arsip"
+ "trash" -> "Sampah"
+ else -> "AI Notes"
+ },
+ showBackButton = (selectedCategory != null && currentScreen == "main") || currentScreen == "ai",
+ onBackClick = {
+ if (currentScreen == "ai") {
+ currentScreen = "main"
+ } else {
+ selectedCategory = null
+ }
+ },
+ onMenuClick = { drawerState = !drawerState },
+ onSearchClick = { showSearch = !showSearch },
+ searchQuery = searchQuery,
+ onSearchQueryChange = { searchQuery = it },
+ showSearch = showSearch && currentScreen == "main"
+ )
+ }
+ },
+ floatingActionButton = {
+ AnimatedVisibility(
+ visible = currentScreen == "main" && !showFullScreenNote,
+ enter = scaleIn() + fadeIn(),
+ exit = scaleOut() + fadeOut()
+ ) {
+ FloatingActionButton(
+ onClick = {
+ if (selectedCategory != null) {
+ editingNote = null
+ showNoteDialog = true
+ } else {
+ showCategoryDialog = true
+ }
+ },
+ containerColor = Color.Transparent,
+ modifier = Modifier
+ .shadow(8.dp, CircleShape)
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
+ ),
+ shape = CircleShape
+ )
+ ) {
+ Icon(
+ Icons.Default.Add,
+ contentDescription = if (selectedCategory != null) "Tambah Catatan" else "Tambah Kategori",
+ tint = Color.White
+ )
+ }
+ }
+ },
+ bottomBar = {
+ if (!showFullScreenNote) {
+ ModernBottomBar(
+ currentScreen = currentScreen,
+ onHomeClick = {
+ currentScreen = "main"
+ selectedCategory = null
+ },
+ onAIClick = { currentScreen = "ai" }
+ )
+ }
+ }
+ ) { padding ->
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (showFullScreenNote && fullScreenNote != null) {
+ EditableFullScreenNoteView(
+ note = fullScreenNote!!,
+ onBack = {
+ showFullScreenNote = false
+ fullScreenNote = null
+ },
+ onSave = { title, content ->
+ notes = notes.map {
+ if (it.id == fullScreenNote!!.id) it.copy(
+ title = title,
+ content = content,
+ timestamp = System.currentTimeMillis()
+ )
+ else it
+ }
+ fullScreenNote = fullScreenNote!!.copy(title = title, content = content)
+ },
+ onArchive = {
+ notes = notes.map {
+ if (it.id == fullScreenNote!!.id) it.copy(isArchived = true)
+ else it
+ }
+ showFullScreenNote = false
+ fullScreenNote = null
+ },
+ onDelete = {
+ notes = notes.map {
+ if (it.id == fullScreenNote!!.id) it.copy(isDeleted = true)
+ else it
+ }
+ showFullScreenNote = false
+ fullScreenNote = null
+ },
+ onPinToggle = {
+ notes = notes.map {
+ if (it.id == fullScreenNote!!.id) it.copy(isPinned = !it.isPinned)
+ else it
+ }
+ fullScreenNote = fullScreenNote!!.copy(isPinned = !fullScreenNote!!.isPinned)
+ }
+ )
+ } else {
+ Column(
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize()
+ ) {
+ when (currentScreen) {
+ "main" -> MainScreen(
+ categories = categories,
+ notes = notes,
+ selectedCategory = selectedCategory,
+ searchQuery = searchQuery,
+ onCategoryClick = { selectedCategory = it },
+ onNoteClick = { note ->
+ fullScreenNote = note
+ showFullScreenNote = true
+ },
+ onNoteLongClick = { note ->
+ notes = notes.map {
+ if (it.id == note.id) it.copy(isArchived = true)
+ else it
+ }
+ },
+ onPinToggle = { note ->
+ notes = notes.map {
+ if (it.id == note.id) it.copy(isPinned = !it.isPinned)
+ else it
+ }
+ },
+ onCategoryLongClick = { category ->
+ categories = categories.filter { it.id != category.id }
+ notes = notes.filter { it.categoryId != category.id }
+ }
+ )
+ "ai" -> AIHelperScreen(
+ categories = categories,
+ notes = notes.filter { !it.isDeleted }
+ )
+ "archive" -> ArchiveScreen(
+ notes = notes.filter { it.isArchived && !it.isDeleted },
+ categories = categories,
+ onRestore = { note ->
+ notes = notes.map {
+ if (it.id == note.id) it.copy(isArchived = false)
+ else it
+ }
+ },
+ onDelete = { note ->
+ notes = notes.map {
+ if (it.id == note.id) it.copy(isDeleted = true)
+ else it
+ }
+ }
+ )
+ "trash" -> TrashScreen(
+ notes = notes.filter { it.isDeleted },
+ categories = categories,
+ onRestore = { note ->
+ notes = notes.map {
+ if (it.id == note.id) it.copy(isDeleted = false, isArchived = false)
+ else it
+ }
+ },
+ onDeletePermanent = { note ->
+ notes = notes.filter { it.id != note.id }
+ }
+ )
+ }
+ }
+ }
+
+ // Drawer with Animation
+ AnimatedVisibility(
+ visible = drawerState,
+ enter = fadeIn() + slideInHorizontally(),
+ exit = fadeOut() + slideOutHorizontally()
+ ) {
+ DrawerMenu(
+ currentScreen = currentScreen,
+ onDismiss = { drawerState = false },
+ onItemClick = { screen ->
+ currentScreen = screen
+ selectedCategory = null
+ drawerState = false
+ showSearch = false
+ searchQuery = ""
+ }
+ )
+ }
+
+ // Dialogs
+ if (showCategoryDialog) {
+ CategoryDialog(
+ onDismiss = { showCategoryDialog = false },
+ onSave = { name, gradientStart, gradientEnd ->
+ categories = categories + Category(
+ name = name,
+ gradientStart = gradientStart,
+ gradientEnd = gradientEnd
+ )
+ showCategoryDialog = false
+ }
+ )
+ }
+
+ if (showNoteDialog && selectedCategory != null) {
+ NoteDialog(
+ note = editingNote,
+ onDismiss = {
+ showNoteDialog = false
+ editingNote = null
+ },
+ onSave = { title, content ->
+ if (editingNote != null) {
+ notes = notes.map {
+ if (it.id == editingNote!!.id)
+ it.copy(
+ title = title,
+ content = content,
+ timestamp = System.currentTimeMillis()
+ )
+ else it
+ }
+ } else {
+ notes = notes + Note(
+ categoryId = selectedCategory!!.id,
+ title = title,
+ content = content
+ )
+ }
+ showNoteDialog = false
+ editingNote = null
+ },
+ onDelete = if (editingNote != null) {
+ {
+ notes = notes.map {
+ if (it.id == editingNote!!.id) it.copy(isDeleted = true)
+ else it
+ }
+ showNoteDialog = false
+ editingNote = null
+ }
+ } else null
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun EditableFullScreenNoteView(
+ note: Note,
+ onBack: () -> Unit,
+ onSave: (String, String) -> Unit,
+ onArchive: () -> Unit,
+ onDelete: () -> Unit,
+ onPinToggle: () -> Unit
+) {
+ var title by remember { mutableStateOf(note.title) }
+ var content by remember { mutableStateOf(note.content) }
+ val dateFormat = remember { SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID")) }
+
+ Scaffold(
+ containerColor = MaterialTheme.colorScheme.background,
+ topBar = {
+ TopAppBar(
+ title = { },
+ navigationIcon = {
+ IconButton(onClick = {
+ if (title.isNotBlank()) {
+ onSave(title, content)
+ }
+ onBack()
+ }) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali", tint = MaterialTheme.colorScheme.onBackground)
+ }
+ },
+ actions = {
+ IconButton(onClick = {
+ if (title.isNotBlank()) {
+ onSave(title, content)
+ }
+ onPinToggle()
+ }) {
+ Icon(
+ if (note.isPinned) Icons.Default.Star else Icons.Default.Add,
+ contentDescription = "Pin Catatan",
+ tint = if (note.isPinned) Color(0xFFFBBF24) else MaterialTheme.colorScheme.onSurface
+ )
+ }
+ IconButton(onClick = {
+ if (title.isNotBlank()) {
+ onSave(title, content)
+ }
+ onArchive()
+ }) {
+ Icon(Icons.Default.Archive, contentDescription = "Arsipkan", tint = MaterialTheme.colorScheme.onSurface)
+ }
+ IconButton(onClick = onDelete) {
+ Icon(Icons.Default.Delete, contentDescription = "Hapus", tint = MaterialTheme.colorScheme.onSurface)
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = Color.Transparent
+ )
+ )
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = 20.dp)
+ ) {
+ TextField(
+ value = title,
+ onValueChange = { title = it },
+ textStyle = MaterialTheme.typography.headlineLarge.copy(
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onBackground
+ ),
+ placeholder = {
+ Text(
+ "Judul",
+ style = MaterialTheme.typography.headlineLarge,
+ color = Color(0xFF64748B)
+ )
+ },
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = Color.Transparent,
+ unfocusedContainerColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ cursorColor = Color(0xFFA855F7)
+ ),
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Text(
+ text = "Terakhir diubah: ${dateFormat.format(Date(note.timestamp))}",
+ style = MaterialTheme.typography.bodySmall,
+ color = Color(0xFF64748B)
+ )
+
+ Divider(
+ modifier = Modifier.padding(vertical = 20.dp),
+ color = MaterialTheme.colorScheme.surface
+ )
+
+ TextField(
+ value = content,
+ onValueChange = { content = it },
+ textStyle = MaterialTheme.typography.bodyLarge.copy(
+ color = MaterialTheme.colorScheme.onSurface,
+ lineHeight = 28.sp
+ ),
+ placeholder = {
+ Text(
+ "Mulai menulis...",
+ style = MaterialTheme.typography.bodyLarge,
+ color = Color(0xFF64748B)
+ )
+ },
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = Color.Transparent,
+ unfocusedContainerColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ cursorColor = Color(0xFFA855F7)
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 400.dp)
+ )
+
+ Spacer(modifier = Modifier.height(100.dp))
+ }
+ }
+}
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ModernTopBar(
+ title: String,
+ showBackButton: Boolean,
+ onBackClick: () -> Unit,
+ onMenuClick: () -> Unit,
+ onSearchClick: () -> Unit,
+ searchQuery: String,
+ onSearchQueryChange: (String) -> Unit,
+ showSearch: Boolean
+) {
+ TopAppBar(
+ title = {
+ if (showSearch) {
+ TextField(
+ value = searchQuery,
+ onValueChange = onSearchQueryChange,
+ placeholder = { Text("Cari catatan...", color = Color.White.copy(0.6f)) },
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = Color.Transparent,
+ unfocusedContainerColor = Color.Transparent,
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White,
+ cursorColor = Color.White,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent
+ ),
+ modifier = Modifier.fillMaxWidth()
+ )
+ } else {
+ Text(
+ title,
+ fontWeight = FontWeight.Bold,
+ fontSize = 22.sp
+ )
+ }
+ },
+ navigationIcon = {
+ IconButton(onClick = if (showBackButton) onBackClick else onMenuClick) {
+ Icon(
+ if (showBackButton) Icons.AutoMirrored.Filled.ArrowBack else Icons.Default.Menu,
+ contentDescription = null,
+ tint = Color.White
+ )
+ }
+ },
+ actions = {
+ IconButton(onClick = onSearchClick) {
+ Icon(
+ if (showSearch) Icons.Default.Close else Icons.Default.Search,
+ contentDescription = "Search",
+ tint = Color.White
+ )
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = Color.Transparent
+ ),
+ modifier = Modifier
+ .background(
+ brush = Brush.horizontalGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
+ )
+ )
+ )
+}
+
+@Composable
+fun ModernBottomBar(
+ currentScreen: String,
+ onHomeClick: () -> Unit,
+ onAIClick: () -> Unit
+) {
+ BottomAppBar(
+ containerColor = Color.Transparent,
+ modifier = Modifier
+ .shadow(8.dp, RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color(0xFF1E293B).copy(0.95f),
+ Color(0xFF334155).copy(0.95f)
+ )
+ ),
+ shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .weight(1f)
+ .clickable(onClick = onHomeClick)
+ .padding(vertical = 8.dp)
+ ) {
+ Icon(
+ Icons.Default.Home,
+ contentDescription = "Home",
+ tint = if (currentScreen == "main") Color(0xFF6366F1) else Color(0xFF94A3B8),
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ "Home",
+ color = if (currentScreen == "main") Color(0xFF6366F1) else Color(0xFF94A3B8),
+ style = MaterialTheme.typography.bodySmall,
+ fontWeight = if (currentScreen == "main") FontWeight.Bold else FontWeight.Normal
+ )
+ }
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .weight(1f)
+ .clickable(onClick = onAIClick)
+ .padding(vertical = 8.dp)
+ ) {
+ Icon(
+ Icons.Default.Star,
+ contentDescription = "AI Helper",
+ tint = if (currentScreen == "ai") Color(0xFFFBBF24) else Color(0xFF94A3B8),
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ "AI Helper",
+ color = if (currentScreen == "ai") Color(0xFFFBBF24) else Color(0xFF94A3B8),
+ style = MaterialTheme.typography.bodySmall,
+ fontWeight = if (currentScreen == "ai") FontWeight.Bold else FontWeight.Normal
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun MainScreen(
+ categories: List,
+ notes: List,
+ selectedCategory: Category?,
+ searchQuery: String,
+ onCategoryClick: (Category) -> Unit,
+ onNoteClick: (Note) -> Unit,
+ onNoteLongClick: (Note) -> Unit,
+ onPinToggle: (Note) -> Unit,
+ onCategoryLongClick: (Category) -> Unit
+) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ if (selectedCategory == null) {
+ if (categories.isEmpty()) {
+ EmptyState(
+ icon = Icons.Default.Create,
+ message = "Buat kategori pertama Anda",
+ subtitle = "Tekan tombol + untuk memulai"
+ )
+ } else {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(2),
+ contentPadding = PaddingValues(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalItemSpacing = 12.dp,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ items(categories) { category ->
+ CategoryCard(
+ category = category,
+ noteCount = notes.count { it.categoryId == category.id && !it.isDeleted && !it.isArchived },
+ onClick = { onCategoryClick(category) },
+ onLongClick = { onCategoryLongClick(category) }
+ )
+ }
+ }
+ }
+ } else {
+ val categoryNotes = notes
+ .filter {
+ it.categoryId == selectedCategory.id &&
+ !it.isDeleted &&
+ !it.isArchived &&
+ (searchQuery.isEmpty() ||
+ it.title.contains(searchQuery, ignoreCase = true) ||
+ it.content.contains(searchQuery, ignoreCase = true))
+ }
+ .sortedWith(compareByDescending { it.isPinned }.thenByDescending { it.timestamp })
+
+ if (categoryNotes.isEmpty()) {
+ EmptyState(
+ icon = Icons.Default.Add,
+ message = if (searchQuery.isEmpty()) "Belum ada catatan" else "Tidak ada hasil",
+ subtitle = if (searchQuery.isEmpty()) "Tekan tombol + untuk membuat catatan" else "Coba kata kunci lain"
+ )
+ } else {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(2),
+ contentPadding = PaddingValues(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalItemSpacing = 12.dp,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ items(categoryNotes) { note ->
+ NoteCard(
+ note = note,
+ onClick = { onNoteClick(note) },
+ onLongClick = { onNoteLongClick(note) },
+ onPinClick = { onPinToggle(note) }
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun CategoryCard(
+ category: Category,
+ noteCount: Int,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick
+ ),
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(containerColor = Color.Transparent),
+ elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ Color(category.gradientStart),
+ Color(category.gradientEnd)
+ )
+ )
+ )
+ .padding(20.dp)
+ ) {
+ Column {
+ Icon(
+ Icons.Default.Folder,
+ contentDescription = null,
+ tint = Color.White.copy(0.9f),
+ modifier = Modifier.size(32.dp)
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ category.name,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = Color.White
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ "$noteCount catatan",
+ style = MaterialTheme.typography.bodySmall,
+ color = Color.White.copy(0.8f)
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun NoteCard(
+ note: Note,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit,
+ onPinClick: () -> Unit
+) {
+ val dateFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID"))
+
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick
+ ),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.Top
+ ) {
+ Text(
+ note.title,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = Color.White,
+ modifier = Modifier.weight(1f)
+ )
+ IconButton(
+ onClick = onPinClick,
+ modifier = Modifier.size(24.dp)
+ ) {
+ Icon(
+ if (note.isPinned) Icons.Default.Star else Icons.Default.Add,
+ contentDescription = "Pin",
+ tint = if (note.isPinned) Color(0xFFFBBF24) else Color.Gray,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+
+ if (note.content.isNotEmpty()) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ note.content,
+ style = MaterialTheme.typography.bodyMedium,
+ maxLines = 6,
+ color = Color(0xFFCBD5E1),
+ lineHeight = 20.sp
+ )
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ dateFormat.format(Date(note.timestamp)),
+ style = MaterialTheme.typography.bodySmall,
+ color = Color(0xFF64748B)
+ )
+ }
+ }
+}
+
+@Composable
+fun DrawerMenu(
+ currentScreen: String,
+ onDismiss: () -> Unit,
+ onItemClick: (String) -> Unit
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black.copy(alpha = 0.6f))
+ .clickable(onClick = onDismiss)
+ ) {
+ Card(
+ modifier = Modifier
+ .fillMaxHeight()
+ .width(280.dp)
+ .clickable(enabled = false) {},
+ shape = RoundedCornerShape(0.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ )
+ ) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
+ )
+ )
+ .padding(32.dp)
+ ) {
+ Column {
+ Icon(
+ Icons.Default.Create,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(40.dp)
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ "AI Notes",
+ style = MaterialTheme.typography.headlineMedium,
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Smart & Modern",
+ style = MaterialTheme.typography.bodyMedium,
+ color = Color.White.copy(0.8f)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ MenuItem(
+ icon = Icons.Default.Home,
+ text = "Beranda",
+ isSelected = currentScreen == "main"
+ ) { onItemClick("main") }
+
+ MenuItem(
+ icon = Icons.Default.Archive,
+ text = "Arsip",
+ isSelected = currentScreen == "archive"
+ ) { onItemClick("archive") }
+
+ MenuItem(
+ icon = Icons.Default.Delete,
+ text = "Sampah",
+ isSelected = currentScreen == "trash"
+ ) { onItemClick("trash") }
+ }
+ }
+ }
+}
+
+@Composable
+fun MenuItem(
+ icon: ImageVector,
+ text: String,
+ isSelected: Boolean,
+ onClick: () -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .background(
+ if (isSelected) Color(0xFF334155) else Color.Transparent
+ )
+ .padding(horizontal = 20.dp, vertical = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ icon,
+ contentDescription = null,
+ tint = if (isSelected) Color(0xFFA855F7) else Color(0xFF94A3B8)
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(
+ text,
+ style = MaterialTheme.typography.bodyLarge,
+ color = if (isSelected) Color.White else Color(0xFF94A3B8),
+ fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
+ )
+ }
+}
+
+@Composable
+fun CategoryDialog(
+ onDismiss: () -> Unit,
+ onSave: (String, Long, Long) -> Unit
+) {
+ var name by remember { mutableStateOf("") }
+ var selectedGradient by remember { mutableStateOf(0) }
+
+ val gradients = listOf(
+ Pair(0xFF6366F1L, 0xFFA855F7L),
+ Pair(0xFFEC4899L, 0xFFF59E0BL),
+ Pair(0xFF8B5CF6L, 0xFFEC4899L),
+ Pair(0xFF06B6D4L, 0xFF3B82F6L),
+ Pair(0xFF10B981L, 0xFF059669L),
+ Pair(0xFFF59E0BL, 0xFFEF4444L),
+ Pair(0xFF6366F1L, 0xFF8B5CF6L),
+ Pair(0xFFEF4444L, 0xFFDC2626L)
+ )
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ containerColor = Color(0xFF1E293B),
+ title = {
+ Text(
+ "Buat Kategori Baru",
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ text = {
+ Column {
+ OutlinedTextField(
+ value = name,
+ onValueChange = { name = it },
+ label = { Text("Nama Kategori", color = Color(0xFF94A3B8)) },
+ modifier = Modifier.fillMaxWidth(),
+ colors = TextFieldDefaults.colors(
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White,
+ focusedContainerColor = Color(0xFF334155),
+ unfocusedContainerColor = Color(0xFF334155),
+ cursorColor = Color(0xFFA855F7),
+ focusedIndicatorColor = Color(0xFFA855F7),
+ unfocusedIndicatorColor = Color(0xFF64748B)
+ ),
+ shape = RoundedCornerShape(12.dp)
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+ Text(
+ "Pilih Gradient:",
+ style = MaterialTheme.typography.bodyMedium,
+ color = Color.White,
+ fontWeight = FontWeight.SemiBold
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ gradients.chunked(4).forEach { row ->
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ row.forEachIndexed { index, gradient ->
+ val globalIndex = gradients.indexOf(gradient)
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .clip(RoundedCornerShape(12.dp))
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ Color(gradient.first),
+ Color(gradient.second)
+ )
+ )
+ )
+ .clickable { selectedGradient = globalIndex },
+ contentAlignment = Alignment.Center
+ ) {
+ if (selectedGradient == globalIndex) {
+ Icon(
+ Icons.Default.Check,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ }
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ if (name.isNotBlank()) {
+ val gradient = gradients[selectedGradient]
+ onSave(name, gradient.first, gradient.second)
+ }
+ },
+ enabled = name.isNotBlank(),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.Transparent
+ ),
+ modifier = Modifier.background(
+ brush = Brush.linearGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
+ ),
+ shape = RoundedCornerShape(8.dp)
+ )
+ ) {
+ Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text("Batal", color = Color(0xFF94A3B8))
+ }
+ }
+ )
+}
+
+@Composable
+fun NoteDialog(
+ note: Note?,
+ onDismiss: () -> Unit,
+ onSave: (String, String) -> Unit,
+ onDelete: (() -> Unit)?
+) {
+ var title by remember { mutableStateOf(note?.title ?: "") }
+ var content by remember { mutableStateOf(note?.content ?: "") }
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ containerColor = Color(0xFF1E293B),
+ title = {
+ Text(
+ if (note == null) "Catatan Baru" else "Edit Catatan",
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ text = {
+ Column {
+ OutlinedTextField(
+ value = title,
+ onValueChange = { title = it },
+ label = { Text("Judul", color = Color(0xFF94A3B8)) },
+ modifier = Modifier.fillMaxWidth(),
+ colors = TextFieldDefaults.colors(
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White,
+ focusedContainerColor = Color(0xFF334155),
+ unfocusedContainerColor = Color(0xFF334155),
+ cursorColor = Color(0xFFA855F7),
+ focusedIndicatorColor = Color(0xFFA855F7),
+ unfocusedIndicatorColor = Color(0xFF64748B)
+ ),
+ shape = RoundedCornerShape(12.dp)
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ OutlinedTextField(
+ value = content,
+ onValueChange = { content = it },
+ label = { Text("Isi Catatan", color = Color(0xFF94A3B8)) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ maxLines = 10,
+ colors = TextFieldDefaults.colors(
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White,
+ focusedContainerColor = Color(0xFF334155),
+ unfocusedContainerColor = Color(0xFF334155),
+ cursorColor = Color(0xFFA855F7),
+ focusedIndicatorColor = Color(0xFFA855F7),
+ unfocusedIndicatorColor = Color(0xFF64748B)
+ ),
+ shape = RoundedCornerShape(12.dp)
+ )
+ }
+ },
+ confirmButton = {
+ Row {
+ if (onDelete != null) {
+ TextButton(onClick = onDelete) {
+ Text("Hapus", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ }
+ Button(
+ onClick = { if (title.isNotBlank()) onSave(title, content) },
+ enabled = title.isNotBlank(),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.Transparent
+ ),
+ modifier = Modifier.background(
+ brush = Brush.linearGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
+ ),
+ shape = RoundedCornerShape(8.dp)
+ )
+ ) {
+ Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
+ }
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text("Batal", color = Color(0xFF94A3B8))
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AIHelperScreen(
+ categories: List,
+ notes: List
+) {
+ var prompt by remember { mutableStateOf("") }
+ var isLoading by remember { mutableStateOf(false) }
+ var selectedCategory by remember { mutableStateOf(null) }
+ var showCategoryDropdown by remember { mutableStateOf(false) }
+ var errorMessage by remember { mutableStateOf("") }
+ var chatMessages by remember { mutableStateOf(listOf()) }
+ var showCopiedMessage by remember { mutableStateOf(false) }
+ var copiedMessageId by remember { mutableStateOf("") }
+
+ val scope = rememberCoroutineScope()
+ val clipboardManager = LocalClipboardManager.current
+ val scrollState = rememberScrollState()
+
+// Inisialisasi Gemini Model
+ val generativeModel = remember {
+ GenerativeModel(
+ modelName = "gemini-2.5-flash",
+ apiKey = APIKey.GEMINI_API_KEY,
+ generationConfig = generationConfig {
+ temperature = 0.8f
+ topK = 40
+ topP = 0.95f
+ maxOutputTokens = 4096
+ candidateCount = 1
+ }
+ )
+ }
+
+ // Auto scroll ke bawah saat ada pesan baru
+ LaunchedEffect(chatMessages.size) {
+ if (chatMessages.isNotEmpty()) {
+ delay(100)
+ scrollState.animateScrollTo(scrollState.maxValue)
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ ) {
+ // Header
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(containerColor = Color.Transparent),
+ shape = RoundedCornerShape(0.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
+ )
+ )
+ .padding(20.dp)
+ ) {
+ Column {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ Icons.Default.Star,
+ contentDescription = null,
+ tint = Color(0xFFFBBF24),
+ modifier = Modifier.size(28.dp)
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Column {
+ Text(
+ "AI Helper",
+ style = MaterialTheme.typography.titleLarge,
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Powered by Gemini AI",
+ style = MaterialTheme.typography.bodySmall,
+ color = Color.White.copy(0.8f)
+ )
+ }
+ }
+ }
+ }
+ }
+
+ // Category Selector & Stats - Compact Version
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.background)
+ .padding(16.dp)
+ ) {
+ // Category Selector
+ Box {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { showCategoryDropdown = !showCategoryDropdown },
+ colors = CardDefaults.cardColors(containerColor = Color(0xFF1E293B)),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ Icons.Default.Folder,
+ contentDescription = null,
+ tint = Color(0xFF6366F1),
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ selectedCategory?.name ?: "Semua Kategori",
+ color = Color.White,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ Icon(
+ Icons.Default.ArrowDropDown,
+ contentDescription = null,
+ tint = Color(0xFF94A3B8)
+ )
+ }
+ }
+
+ DropdownMenu(
+ expanded = showCategoryDropdown,
+ onDismissRequest = { showCategoryDropdown = false },
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color(0xFF1E293B))
+ ) {
+ DropdownMenuItem(
+ text = { Text("Semua Kategori", color = Color.White) },
+ onClick = {
+ selectedCategory = null
+ showCategoryDropdown = false
+ }
+ )
+ categories.forEach { category ->
+ DropdownMenuItem(
+ text = { Text(category.name, color = Color.White) },
+ onClick = {
+ selectedCategory = category
+ showCategoryDropdown = false
+ }
+ )
+ }
+ }
+ }
+
+ // Stats - Compact
+ Spacer(modifier = Modifier.height(12.dp))
+ val filteredNotes = if (selectedCategory != null) {
+ notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
+ } else {
+ notes.filter { !it.isArchived }
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ CompactStatItem(
+ label = "Total",
+ value = filteredNotes.size.toString(),
+ color = Color(0xFF6366F1)
+ )
+ CompactStatItem(
+ label = "Dipasang",
+ value = filteredNotes.count { it.isPinned }.toString(),
+ color = Color(0xFFFBBF24)
+ )
+ CompactStatItem(
+ label = "Kategori",
+ value = categories.size.toString(),
+ color = Color(0xFFA855F7)
+ )
+ }
+ }
+
+ Divider(color = Color(0xFF334155), thickness = 1.dp)
+
+ // Chat Area
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ ) {
+ if (chatMessages.isEmpty()) {
+ // Welcome State
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(32.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ Icons.Default.Star,
+ contentDescription = null,
+ modifier = Modifier.size(64.dp),
+ tint = Color(0xFF6366F1).copy(0.5f)
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ "Mulai Percakapan",
+ style = MaterialTheme.typography.titleLarge,
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ "Tanyakan apa saja tentang catatan Anda",
+ style = MaterialTheme.typography.bodyMedium,
+ color = Color(0xFF94A3B8),
+ textAlign = TextAlign.Center
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ // Suggestion Chips
+ Column(
+ horizontalAlignment = Alignment.Start,
+ modifier = Modifier.fillMaxWidth(0.8f)
+ ) {
+ Text(
+ "Contoh pertanyaan:",
+ style = MaterialTheme.typography.bodySmall,
+ color = Color(0xFF64748B),
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+ SuggestionChip("Analisis catatan saya", onSelect = { prompt = it })
+ SuggestionChip("Buat ringkasan", onSelect = { prompt = it })
+ SuggestionChip("Berikan saran organisasi", onSelect = { prompt = it })
+ }
+ }
+ } else {
+ // Chat Messages
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(scrollState)
+ .padding(16.dp)
+ ) {
+ chatMessages.forEach { message ->
+ ChatBubble(
+ message = message,
+ onCopy = {
+ clipboardManager.setText(AnnotatedString(message.message))
+ copiedMessageId = message.id
+ showCopiedMessage = true
+ scope.launch {
+ delay(2000)
+ showCopiedMessage = false
+ }
+ },
+ showCopied = showCopiedMessage && copiedMessageId == message.id
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ }
+
+ // Loading Indicator
+ if (isLoading) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ horizontalArrangement = Arrangement.Start
+ ) {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(20.dp),
+ color = Color(0xFF6366F1),
+ strokeWidth = 2.dp
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(
+ "AI sedang berpikir...",
+ color = Color(0xFF94A3B8),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+ }
+ }
+
+ // Error Message
+ if (errorMessage.isNotEmpty()) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFFEF4444).copy(0.2f)
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.Warning,
+ contentDescription = null,
+ tint = Color(0xFFEF4444),
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ errorMessage,
+ color = Color(0xFFEF4444),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(80.dp))
+ }
+ }
+ }
+
+ // Input Area
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.Bottom
+ ) {
+ OutlinedTextField(
+ value = prompt,
+ onValueChange = { prompt = it },
+ placeholder = {
+ Text(
+ "Ketik pesan...",
+ color = Color(0xFF64748B)
+ )
+ },
+ modifier = Modifier
+ .weight(1f)
+ .heightIn(min = 48.dp, max = 120.dp),
+ colors = TextFieldDefaults.colors(
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White,
+ focusedContainerColor = Color(0xFF334155),
+ unfocusedContainerColor = Color(0xFF334155),
+ cursorColor = Color(0xFFA855F7),
+ focusedIndicatorColor = Color(0xFF6366F1),
+ unfocusedIndicatorColor = Color(0xFF475569)
+ ),
+ shape = RoundedCornerShape(24.dp),
+ maxLines = 4
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ // Send Button
+ FloatingActionButton(
+ onClick = {
+ if (prompt.isNotBlank() && !isLoading) {
+ scope.launch {
+ // Add user message
+ chatMessages = chatMessages + ChatMessage(
+ message = prompt,
+ isUser = true
+ )
+
+ val userPrompt = prompt
+ prompt = ""
+ isLoading = true
+ errorMessage = ""
+
+ try {
+ val filteredNotes = if (selectedCategory != null) {
+ notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
+ } else {
+ notes.filter { !it.isArchived }
+ }
+
+ val notesContext = buildString {
+ appendLine("Data catatan pengguna:")
+ appendLine("Total catatan: ${filteredNotes.size}")
+ appendLine("Kategori: ${selectedCategory?.name ?: "Semua"}")
+ appendLine()
+ appendLine("Daftar catatan:")
+ filteredNotes.take(10).forEach { note ->
+ appendLine("- Judul: ${note.title}")
+ appendLine(" Isi: ${note.content.take(100)}")
+ appendLine()
+ }
+ }
+
+ val fullPrompt = "$notesContext\n\nPertanyaan: $userPrompt\n\nBerikan jawaban dalam bahasa Indonesia yang natural dan membantu."
+
+ val result = generativeModel.generateContent(fullPrompt)
+ val response = result.text ?: "Tidak ada respons dari AI"
+
+ // Add AI response
+ chatMessages = chatMessages + ChatMessage(
+ message = response,
+ isUser = false
+ )
+
+ } catch (e: Exception) {
+ errorMessage = "Error: ${e.message}"
+ } finally {
+ isLoading = false
+ }
+ }
+ }
+ },
+ containerColor = Color.Transparent,
+ modifier = Modifier
+ .size(48.dp)
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
+ ),
+ shape = CircleShape
+ )
+ ) {
+ Icon(
+ Icons.Default.Send,
+ contentDescription = "Send",
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ChatBubble(
+ message: ChatMessage,
+ onCopy: () -> Unit,
+ showCopied: Boolean
+) {
+ val dateFormat = remember { SimpleDateFormat("HH:mm", Locale("id", "ID")) }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = if (message.isUser) Arrangement.End else Arrangement.Start
+ ) {
+ if (!message.isUser) {
+ Icon(
+ Icons.Default.Star,
+ contentDescription = null,
+ tint = Color(0xFFFBBF24),
+ modifier = Modifier
+ .size(32.dp)
+ .padding(end = 8.dp)
+ )
+ }
+
+ Column(
+ modifier = Modifier.fillMaxWidth(0.85f),
+ horizontalAlignment = if (message.isUser) Alignment.End else Alignment.Start
+ ) {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = if (message.isUser)
+ Color(0xFF6366F1)
+ else
+ Color(0xFF1E293B)
+ ),
+ shape = RoundedCornerShape(
+ topStart = 16.dp,
+ topEnd = 16.dp,
+ bottomStart = if (message.isUser) 16.dp else 4.dp,
+ bottomEnd = if (message.isUser) 4.dp else 16.dp
+ )
+ ) {
+ Column(modifier = Modifier.padding(12.dp)) {
+ Text(
+ message.message,
+ color = Color.White,
+ style = MaterialTheme.typography.bodyMedium,
+ lineHeight = 20.sp
+ )
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ dateFormat.format(Date(message.timestamp)),
+ color = Color.White.copy(0.6f),
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.padding(top = 4.dp)
+ )
+
+ if (!message.isUser) {
+ IconButton(
+ onClick = onCopy,
+ modifier = Modifier.size(32.dp)
+ ) {
+ Icon(
+ Icons.Default.ContentCopy,
+ contentDescription = "Copy",
+ tint = Color.White.copy(0.7f),
+ modifier = Modifier.size(16.dp)
+ )
+ }
+ }
+ }
+ }
+ }
+
+ if (showCopied && !message.isUser) {
+ Text(
+ "✓ Disalin",
+ color = Color(0xFF10B981),
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.padding(top = 4.dp, start = 8.dp)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun CompactStatItem(label: String, value: String, color: Color) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .background(
+ color = Color(0xFF1E293B),
+ shape = RoundedCornerShape(8.dp)
+ )
+ .padding(horizontal = 12.dp, vertical = 8.dp)
+ ) {
+ Text(
+ value,
+ style = MaterialTheme.typography.titleMedium,
+ color = color,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ label,
+ style = MaterialTheme.typography.bodySmall,
+ color = Color(0xFF94A3B8)
+ )
+ }
+}
+
+@Composable
+fun SuggestionChip(text: String, onSelect: (String) -> Unit) {
+ Card(
+ modifier = Modifier
+ .padding(vertical = 4.dp)
+ .clickable { onSelect(text) },
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.Star,
+ contentDescription = null,
+ tint = Color(0xFF6366F1),
+ modifier = Modifier.size(16.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text,
+ color = Color.White,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+}
+
+@Composable
+fun StatItem(label: String, value: String, color: Color) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.padding(8.dp)
+ ) {
+ Text(
+ value,
+ style = MaterialTheme.typography.headlineMedium,
+ color = color,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ label,
+ style = MaterialTheme.typography.bodySmall,
+ color = Color(0xFF94A3B8)
+ )
+ }
+}
+
+@Composable
+fun ArchiveScreen(
+ notes: List,
+ categories: List,
+ onRestore: (Note) -> Unit,
+ onDelete: (Note) -> Unit
+) {
+ if (notes.isEmpty()) {
+ EmptyState(
+ icon = Icons.Default.Archive,
+ message = "Arsip kosong",
+ subtitle = "Catatan yang diarsipkan akan muncul di sini"
+ )
+ } else {
+ LazyColumn(
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ items(notes) { note ->
+ val category = categories.find { it.id == note.categoryId }
+ ArchiveNoteCard(
+ note = note,
+ categoryName = category?.name ?: "Unknown",
+ onRestore = { onRestore(note) },
+ onDelete = { onDelete(note) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ArchiveNoteCard(
+ note: Note,
+ categoryName: String,
+ onRestore: () -> Unit,
+ onDelete: () -> Unit
+) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF1E293B)
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ note.title,
+ fontWeight = FontWeight.Bold,
+ color = Color.White,
+ style = MaterialTheme.typography.titleMedium
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ categoryName,
+ color = Color(0xFF64748B),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+
+ if (note.content.isNotEmpty()) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ note.content,
+ maxLines = 2,
+ color = Color(0xFF94A3B8),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp),
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(onClick = onRestore) {
+ Icon(
+ Icons.Default.AccountBox,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = Color(0xFF10B981)
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ TextButton(onClick = onDelete) {
+ Icon(
+ Icons.Default.Delete,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = Color(0xFFEF4444)
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text("Hapus", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TrashScreen(
+ notes: List,
+ categories: List,
+ onRestore: (Note) -> Unit,
+ onDeletePermanent: (Note) -> Unit
+) {
+ if (notes.isEmpty()) {
+ EmptyState(
+ icon = Icons.Default.Delete,
+ message = "Sampah kosong",
+ subtitle = "Catatan yang dihapus akan muncul di sini"
+ )
+ } else {
+ LazyColumn(
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ items(notes) { note ->
+ val category = categories.find { it.id == note.categoryId }
+ TrashNoteCard(
+ note = note,
+ categoryName = category?.name ?: "Unknown",
+ onRestore = { onRestore(note) },
+ onDeletePermanent = { onDeletePermanent(note) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun TrashNoteCard(
+ note: Note,
+ categoryName: String,
+ onRestore: () -> Unit,
+ onDeletePermanent: () -> Unit
+) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFF7F1D1D).copy(0.2f)
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ note.title,
+ fontWeight = FontWeight.Bold,
+ color = Color.White,
+ style = MaterialTheme.typography.titleMedium
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ categoryName,
+ color = Color(0xFF64748B),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+
+ if (note.content.isNotEmpty()) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ note.content,
+ maxLines = 2,
+ color = Color(0xFF94A3B8),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp),
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(onClick = onRestore) {
+ Icon(
+ Icons.Default.AccountBox,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = Color(0xFF10B981)
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ TextButton(onClick = onDeletePermanent) {
+ Icon(
+ Icons.Default.Delete,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = Color(0xFFEF4444)
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text("Hapus Permanen", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun EmptyState(
+ icon: ImageVector,
+ message: String,
+ subtitle: String
+) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.padding(32.dp)
+ ) {
+ Icon(
+ icon,
+ contentDescription = null,
+ modifier = Modifier.size(80.dp),
+ tint = Color(0xFF475569)
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ message,
+ style = MaterialTheme.typography.titleLarge,
+ color = Color(0xFF94A3B8),
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ color = Color(0xFF64748B)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..86a5d97
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..3b9472b
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..c8524cd
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..4cf8926
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ AI Notes
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..2350d5d
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/example/notesai/ExampleUnitTest.kt b/app/src/test/java/com/example/notesai/ExampleUnitTest.kt
new file mode 100644
index 0000000..bb01bcf
--- /dev/null
+++ b/app/src/test/java/com/example/notesai/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.notesai
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..d8d7308
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,10 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ //id("com.android.application") version "8.2.0" apply false
+ id("com.android.library") version "8.2.0" apply false
+ //id("org.jetbrains.kotlin.android") version "2.0.0" apply false
+ id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
+ id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0" apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..8169e4b
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,26 @@
+[versions]
+agp = "8.13.1"
+kotlin = "2.0.21"
+coreKtx = "1.10.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+appcompat = "1.6.1"
+material = "1.10.0"
+activity = "1.8.0"
+constraintlayout = "2.1.4"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8bdaf60
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..3e87740
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,8 @@
+#Tue Dec 09 00:56:08 WIB 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..ef07e01
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..db3a6ac
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..8889210
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "notesai"
+include(":app")
+
\ No newline at end of file