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 @@ + + + +