diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d2de60f..fa7bd09 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -8,8 +8,8 @@ plugins {
}
android {
- namespace = "com.babelsoftware.airnote"
- compileSdk = 36
+ namespace = "com.babelsoftware.loudly"
+ compileSdk = 35
flavorDimensions += "default"
buildFeatures {
@@ -19,28 +19,21 @@ android {
productFlavors {
create("default") {
dimension = "default"
- applicationId = "com.babelsoftware.airnote"
+ applicationId = "com.babelsoftware.loudly"
versionNameSuffix = "-default"
}
-
- create("accrescent") {
- dimension = "default"
- applicationId = "by.babelapps.airnote"
- versionNameSuffix = "-babelapps"
- }
}
defaultConfig {
- applicationId = "com.babelsoftware.airnote"
+ applicationId = "com.babelsoftware.loudly"
minSdk = 26
- targetSdk = 36
+ targetSdk = 35
versionCode = 1
versionName = "v1.6.1"
vectorDrawables {
useSupportLibrary = true
}
- // https://developer.android.com/guide/topics/resources/app-languages#gradle-config
resourceConfigurations.plus(
listOf("en", "ar", "de", "es", "fa", "fil", "fr", "hi", "it", "ja", "ru", "sk", "tr", "da", "nl", "pl", "tr", "uk", "vi", "ota", "pt-rBR", "sr", "zh-rCN")
)
@@ -84,46 +77,70 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
- buildToolsVersion = "36.0.0"
+ buildToolsVersion = "35.0.0"
}
dependencies {
- implementation(libs.androidx.biometric.ktx)
- implementation(libs.androidx.glance)
- implementation(libs.coil.compose)
- implementation(libs.hilt.navigation.compose)
- implementation(libs.androidx.glance.appwidget)
- implementation(libs.androidx.material3.window.size.class1.android)
- ksp(libs.androidx.room.compiler)
- ksp(libs.hilt.android.compiler)
- ksp(libs.hilt.compile)
-
- implementation("com.google.ai.client.generativeai:generativeai:0.9.0")
- implementation("androidx.security:security-crypto:1.1.0")
- implementation("com.udojava:EvalEx:2.7")
- implementation("androidx.activity:activity-compose:1.11.0")
- implementation("com.google.mlkit:translate:17.0.3")
- implementation("com.google.mlkit:language-id:17.0.6")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2")
- implementation("org.jsoup:jsoup:1.21.2")
- implementation("com.google.code.gson:gson:2.13.2")
- implementation(platform("io.ktor:ktor-bom:3.0.0-beta-2"))
- implementation("io.ktor:ktor-client-core")
- implementation("io.ktor:ktor-client-android")
- implementation("io.ktor:ktor-client-content-negotiation")
- implementation("io.ktor:ktor-client-logging")
- implementation("io.ktor:ktor-serialization-kotlinx-json")
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.hilt.navigation.compose)
- implementation(libs.hilt.android)
- implementation(libs.androidx.datastore.preferences)
+ // Room
implementation(libs.androidx.room.runtime)
- implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.room.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.androidx.material3)
- implementation(libs.androidx.activity.compose)
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.core.splashscreen)
- implementation(libs.androidx.navigation.compose)
-}
\ No newline at end of file
+ ksp(libs.androidx.room.compiler)
+
+ // Hilt
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.android.compiler)
+ ksp(libs.hilt.compiler)
+
+ // Coil
+ implementation(libs.coil.compose)
+ implementation(libs.coil.gif)
+
+ // Media3
+ implementation(libs.androidx.media3.exoplayer)
+ implementation(libs.androidx.media3.session)
+ implementation(libs.androidx.media3.ui)
+ implementation(libs.androidx.media3.common)
+
+ // Palette
+ implementation(libs.androidx.palette.ktx)
+
+ // JSON
+ implementation("com.google.code.gson:gson:2.10.1")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
+
+ // Coroutines
+ implementation(libs.kotlinx.coroutines.android)
+
+ // Network (Retrofit/OkHttp assumed if not using Ktor, but saw Ktor in AirNote. Loudly might differ. Sticking to generics)
+ // Actually the imports showed org.json but not retrofit explicitly in the snippets.
+ // I will add standard networking just in case
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+
+ // Image Cropper (from manifest)
+ implementation("com.vanniktech:android-image-cropper:4.5.0")
+
+ // Material 3 Window Size Class
+ implementation("androidx.compose.material3:material3-window-size-class:1.2.1")
+
+ // Squiggles
+ implementation("me.saket.squiggles:squiggles:0.2.0")
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+}
diff --git a/app/src/debug/res/values/app_name.xml b/app/src/debug/res/values/app_name.xml
new file mode 100644
index 0000000..a1d1e2f
--- /dev/null
+++ b/app/src/debug/res/values/app_name.xml
@@ -0,0 +1,4 @@
+
+
+ Loudly Debug
+
\ No newline at end of file
diff --git a/app/src/debug/res/xml-v25/shortcuts.xml b/app/src/debug/res/xml-v25/shortcuts.xml
new file mode 100644
index 0000000..1e6fe60
--- /dev/null
+++ b/app/src/debug/res/xml-v25/shortcuts.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/foss/java/com/babelsoftware/loudly/TranslationHelper.kt b/app/src/foss/java/com/babelsoftware/loudly/TranslationHelper.kt
new file mode 100644
index 0000000..62c698c
--- /dev/null
+++ b/app/src/foss/java/com/babelsoftware/loudly/TranslationHelper.kt
@@ -0,0 +1,8 @@
+package com.babelsoftware.loudly
+
+import com.babelsoftware.loudly.db.entities.LyricsEntity
+
+object TranslationHelper {
+ fun translate(lyrics: LyricsEntity): LyricsEntity = lyrics
+ fun clearModels() {}
+}
\ No newline at end of file
diff --git a/app/src/foss/java/com/babelsoftware/loudly/Utils.kt b/app/src/foss/java/com/babelsoftware/loudly/Utils.kt
new file mode 100644
index 0000000..28beb80
--- /dev/null
+++ b/app/src/foss/java/com/babelsoftware/loudly/Utils.kt
@@ -0,0 +1,5 @@
+package com.babelsoftware.loudly
+
+fun reportException(throwable: Throwable) {
+ throwable.printStackTrace()
+}
diff --git a/app/src/full/java/com/babelsoftware/loudly/TranslationHelper.kt b/app/src/full/java/com/babelsoftware/loudly/TranslationHelper.kt
new file mode 100644
index 0000000..59bed41
--- /dev/null
+++ b/app/src/full/java/com/babelsoftware/loudly/TranslationHelper.kt
@@ -0,0 +1,78 @@
+package com.babelsoftware.loudly
+
+import android.util.LruCache
+import com.babelsoftware.loudly.db.entities.LyricsEntity
+import com.babelsoftware.loudly.lyrics.LyricsUtils
+import com.github.houbb.opencc4j.util.ZhConverterUtil
+import com.google.mlkit.common.model.DownloadConditions
+import com.google.mlkit.common.model.RemoteModelManager
+import com.google.mlkit.nl.languageid.LanguageIdentification
+import com.google.mlkit.nl.translate.TranslateLanguage
+import com.google.mlkit.nl.translate.TranslateRemoteModel
+import com.google.mlkit.nl.translate.Translation
+import com.google.mlkit.nl.translate.TranslatorOptions
+import kotlinx.coroutines.tasks.await
+import java.util.Locale
+
+object TranslationHelper {
+ private const val MAX_CACHE_SIZE = 20
+ private val cache = LruCache(MAX_CACHE_SIZE)
+
+ suspend fun translate(lyrics: LyricsEntity): LyricsEntity {
+ cache[lyrics.id]?.let { return it }
+ val isSynced = lyrics.lyrics.startsWith("[")
+ val sourceLanguage = TranslateLanguage.fromLanguageTag(
+ LanguageIdentification.getClient().identifyLanguage(
+ lyrics.lyrics.lines().joinToString(separator = "\n") { it.replace("\\[\\d{2}:\\d{2}.\\d{2,3}\\] *".toRegex(), "") }
+ ).await()
+ )
+ val targetLanguage = TranslateLanguage.fromLanguageTag(
+ Locale.getDefault().toLanguageTag().substring(0..1)
+ )
+ return if (sourceLanguage == null || targetLanguage == null || sourceLanguage == targetLanguage) {
+ lyrics
+ } else {
+ val translator = Translation.getClient(
+ TranslatorOptions.Builder()
+ .setSourceLanguage(sourceLanguage)
+ .setTargetLanguage(targetLanguage)
+ .build()
+ )
+ translator.downloadModelIfNeeded(
+ DownloadConditions.Builder()
+ .requireWifi()
+ .build()
+ ).await()
+ val traditionalChinese = Locale.getDefault().toLanguageTag().replace("-Hant", "") == "zh-TW"
+ lyrics.copy(
+ lyrics = if (isSynced) {
+ LyricsUtils.parseLyrics(lyrics.lyrics, trim = true, multilineEnable = false).map {
+ val translated = translator.translate(it.text).await()
+ it.copy(
+ text = if (traditionalChinese) ZhConverterUtil.toTraditional(translated) else translated
+ )
+ }.joinToString(separator = "\n") {
+ "[%02d:%02d.%03d]${it.text}".format(it.time / 60000, (it.time / 1000) % 60, it.time % 1000)
+ }
+ } else {
+ lyrics.lyrics.lines()
+ .map {
+ val translated = translator.translate(it).await()
+ if (traditionalChinese) ZhConverterUtil.toTraditional(translated) else translated
+ }
+ .joinToString(separator = "\n")
+ }
+ )
+ }.also {
+ cache.put(it.id, it)
+ }
+ }
+
+ suspend fun clearModels() {
+ val modelManager = RemoteModelManager.getInstance()
+ val downloadedModels = modelManager.getDownloadedModels(TranslateRemoteModel::class.java).await()
+ downloadedModels.forEach {
+ modelManager.deleteDownloadedModel(it).await()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/full/java/com/babelsoftware/loudly/Utils.kt b/app/src/full/java/com/babelsoftware/loudly/Utils.kt
new file mode 100644
index 0000000..dc03b67
--- /dev/null
+++ b/app/src/full/java/com/babelsoftware/loudly/Utils.kt
@@ -0,0 +1,9 @@
+package com.babelsoftware.loudly
+
+import com.google.firebase.crashlytics.ktx.crashlytics
+import com.google.firebase.ktx.Firebase
+
+fun reportException(throwable: Throwable) {
+ Firebase.crashlytics.recordException(throwable)
+ throwable.printStackTrace()
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2ca4db1..396e576 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,91 +1,211 @@
-
+
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ android:theme="@style/Theme.Loudly"
+ tools:targetApi="tiramisu">
+
+ android:resource="@xml/widget_info" />
-
+
+
-
+
-
-
-
-
-
-
-
+ android:permission="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />
+
+
-
+ android:launchMode="singleTask"
+ android:theme="@style/Theme.Loudly"
+ android:windowSoftInputMode="adjustResize">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ android:foregroundServiceType="specialUse"
+ tools:ignore="ExportedService">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
index 5363c78..28f802b 100644
Binary files a/app/src/main/ic_launcher-playstore.png and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/com/babelsoftware/airnote/App.kt b/app/src/main/java/com/babelsoftware/airnote/App.kt
deleted file mode 100644
index b07d285..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/App.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.babelsoftware.airnote
-
-import android.app.Application
-import dagger.hilt.android.HiltAndroidApp
-
-@HiltAndroidApp
-class App : Application()
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/constant/Connection.kt b/app/src/main/java/com/babelsoftware/airnote/constant/Connection.kt
deleted file mode 100644
index 15a4075..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/constant/Connection.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.babelsoftware.airnote.constant
-
-object ConnectionConst {
- /**
- * Support mail.
- */
- const val SUPPORT_MAIL = "rrechzexe.dev@gmail.com"
-
- /**
- * Url of source code on Github.
- */
- const val GITHUB_SOURCE_CODE = "https://github.com/RRechz/AirNote"
-
- /**
- * Feature request github.
- */
- const val FEATURE_REQUEST = "https://t.me/by_BabelSoftware"
-
-
- /**
- * Support ko-fi.
- */
- const val SUPPORT_BUYMEACOFFEE = "https://buymeacoffee.com/dev_rrechz.kt"
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/constant/Database.kt b/app/src/main/java/com/babelsoftware/airnote/constant/Database.kt
deleted file mode 100644
index 08edb9d..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/constant/Database.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.constant
-
-object DatabaseConst {
- /**
- * Database version
- */
- const val NOTES_DATABASE_VERSION = 6
-
- /**
- * Exported notes room database name and extension
- */
- const val NOTES_DATABASE_FILE_NAME = "note-list.db"
-
- /**
- * Name of backup
- */
- const val NOTES_DATABASE_BACKUP_NAME = "AirNote"
-
- /**
- * Exported settings datastore name and extension
- */
- const val SETTINGS_DATASTORE_FILE_NAME = "settings"
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/constant/Support.kt b/app/src/main/java/com/babelsoftware/airnote/constant/Support.kt
deleted file mode 100644
index f717d7a..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/constant/Support.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.constant
-
-object SupportConst {
- /**
- * Address for Proton Wallet users
- */
- const val PROTON_WALLET_ADDRESS = "m06.burakozcan@proton.me"
-
- /**
- * Address of bitcoin account
- */
- const val BITCOIN_ADDRESS = "bc1qfg6y048gfg7a0v5cl47atejuk06ruv3xj4sng6"
-
- const val TERMS_EFFECTIVE_DATE = "2025/06/30" // Created of date
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/local/converter/Converters.kt b/app/src/main/java/com/babelsoftware/airnote/data/local/converter/Converters.kt
deleted file mode 100644
index e516fc2..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/local/converter/Converters.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.babelsoftware.airnote.data.local.converter
-
-import androidx.room.TypeConverter
-import com.babelsoftware.airnote.domain.model.Participant
-import java.util.Date
-
-class Converters {
- @TypeConverter
- fun fromTimestamp(value: Long?): Date? {
- return value?.let { Date(it) }
- }
-
- @TypeConverter
- fun dateToTimestamp(date: Date?): Long? {
- return date?.time
- }
-
- @TypeConverter
- fun fromParticipantName(name: String?): Participant? {
- return name?.let { Participant.valueOf(it) }
- }
-
- @TypeConverter
- fun participantToName(participant: Participant?): String? {
- return participant?.name
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/local/dao/AiChatDao.kt b/app/src/main/java/com/babelsoftware/airnote/data/local/dao/AiChatDao.kt
deleted file mode 100644
index a85ddfd..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/local/dao/AiChatDao.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.babelsoftware.airnote.data.local.dao
-
-import androidx.room.Dao
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import com.babelsoftware.airnote.domain.model.AiChatMessage
-import com.babelsoftware.airnote.domain.model.AiChatSession
-import com.babelsoftware.airnote.domain.model.Participant
-import kotlinx.coroutines.flow.Flow
-
-@Dao
-interface AiChatDao {
- // --- Session Operations ---
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insertSession(session: AiChatSession): Long
-
- @Query("SELECT * FROM ai_chat_sessions ORDER BY createdAt DESC")
- fun getAllSessions(): Flow>
-
- @Query("SELECT * FROM ai_chat_sessions WHERE id = :sessionId")
- suspend fun getSessionById(sessionId: Long): AiChatSession?
-
- @Query("UPDATE ai_chat_sessions SET aiMode = :aiMode WHERE id = :sessionId")
- suspend fun updateSessionMode(sessionId: Long, aiMode: String)
-
- @Query("DELETE FROM ai_chat_sessions WHERE id = :sessionId")
- suspend fun deleteSessionById(sessionId: Long)
-
- // --- Message Operations ---
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insertMessage(message: AiChatMessage): Long
-
- @Query("SELECT * FROM ai_chat_messages WHERE sessionId = :sessionId ORDER BY timestamp ASC")
- fun getMessagesForSession(sessionId: Long): Flow>
-
- @Query("UPDATE ai_chat_messages SET text = :text, isLoading = :isLoading, participant = :participant WHERE id = :id")
- suspend fun updateMessageById(
- id: Long,
- text: String,
- isLoading: Boolean,
- participant: Participant
- )
-
- @Query("DELETE FROM ai_chat_messages WHERE id = :id")
- suspend fun deleteMessageById(id: Long)
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/local/dao/NotesDao.kt b/app/src/main/java/com/babelsoftware/airnote/data/local/dao/NotesDao.kt
deleted file mode 100644
index 2e3953a..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/local/dao/NotesDao.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.babelsoftware.airnote.data.local.dao
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import androidx.room.Update
-import com.babelsoftware.airnote.domain.model.Note
-import kotlinx.coroutines.flow.Flow
-
-@Dao
-interface NoteDao {
-
- @Insert(onConflict = OnConflictStrategy.IGNORE)
- suspend fun addNote(note: Note)
-
- @Query("SELECT * FROM `notes-table`")
- fun getAllNotes(): Flow>
-
- @Update
- suspend fun updateNote(note: Note)
-
- @Delete
- suspend fun deleteNote(note: Note)
-
- @Query("SELECT * FROM `notes-table` WHERE id=:id")
- fun getNoteById(id: Int): Flow
-
- @Query("SELECT id FROM `notes-table` ORDER BY id DESC LIMIT 1")
- fun getLastNoteId(): Long?
-
- @Query("SELECT * FROM `notes-table` WHERE `foldered` = :folderId ORDER BY `created_at` DESC")
- fun getNotesByFolder(folderId: Long): Flow>
-
- @Query("UPDATE `notes-table` SET `foldered` = NULL WHERE `foldered` = :folderId")
- suspend fun unlinkNotesFromFolder(folderId: Long)
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/local/database/NoteDatabase.kt b/app/src/main/java/com/babelsoftware/airnote/data/local/database/NoteDatabase.kt
deleted file mode 100644
index 90661bf..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/local/database/NoteDatabase.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.babelsoftware.airnote.data.local.database
-
-import androidx.room.Database
-import androidx.room.RoomDatabase
-import androidx.room.TypeConverters
-import com.babelsoftware.airnote.constant.DatabaseConst
-import com.babelsoftware.airnote.data.local.converter.Converters
-import com.babelsoftware.airnote.data.local.dao.AiChatDao
-import com.babelsoftware.airnote.data.local.dao.NoteDao
-import com.babelsoftware.airnote.data.source.FolderDao
-import com.babelsoftware.airnote.domain.model.AiChatMessage
-import com.babelsoftware.airnote.domain.model.AiChatSession
-import com.babelsoftware.airnote.domain.model.Folder
-import com.babelsoftware.airnote.domain.model.Note
-
-@Database(
- entities = [
- Note::class,
- Folder::class,
- AiChatSession::class,
- AiChatMessage::class
- ],
- version = 9,
- exportSchema = false
-)
-@TypeConverters(Converters::class)
-abstract class NoteDatabase : RoomDatabase() {
-
- abstract fun noteDao(): NoteDao
- abstract fun folderDao(): FolderDao
- abstract fun aiChatDao(): AiChatDao
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/local/database/NoteDatabaseProvider.kt b/app/src/main/java/com/babelsoftware/airnote/data/local/database/NoteDatabaseProvider.kt
deleted file mode 100644
index 4c89b5d..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/local/database/NoteDatabaseProvider.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-package com.babelsoftware.airnote.data.local.database
-
-
-import android.app.Application
-import androidx.room.Room
-import androidx.room.migration.Migration
-import androidx.sqlite.db.SupportSQLiteDatabase
-import com.babelsoftware.airnote.constant.DatabaseConst
-import com.babelsoftware.airnote.data.local.dao.AiChatDao
-import com.babelsoftware.airnote.data.local.dao.NoteDao
-import com.babelsoftware.airnote.data.source.FolderDao
-
-class NoteDatabaseProvider(private val application: Application) {
-
- @Volatile
- private var database: NoteDatabase? = null
-
- fun instance(): NoteDatabase {
- return database ?: synchronized(this) {
- database ?: buildDatabase().also { database = it }
- }
- }
-
- private fun buildDatabase(): NoteDatabase {
- return Room.databaseBuilder(application.applicationContext,
- NoteDatabase::class.java,
- DatabaseConst.NOTES_DATABASE_FILE_NAME)
- .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_2_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
- .build()
- }
-
- @Synchronized
- fun close() {
- database?.close()
- database = null
- }
-
- fun noteDao(): NoteDao {
- return instance().noteDao()
- }
-
- fun folderDao(): FolderDao {
- return instance().folderDao()
- }
- fun aiChatDao(): AiChatDao {
- return instance().aiChatDao()
- }
-}
-
-private val MIGRATION_1_2 = object : Migration(1, 2) {
- override fun migrate(db: SupportSQLiteDatabase) {
- db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `created_at` INTEGER NOT NULL DEFAULT ${System.currentTimeMillis()}")
- }
-}
-
-private val MIGRATION_2_3 = object : Migration(2, 3) {
- override fun migrate(db: SupportSQLiteDatabase) {
- db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `pinned` INTEGER NOT NULL DEFAULT 0")
- }
-}
-
-private val MIGRATION_3_4 = object : Migration(3, 4) {
- override fun migrate(db: SupportSQLiteDatabase) {
- db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `encrypted` INTEGER NOT NULL DEFAULT 0")
- }
-}
-
-private val MIGRATION_2_4 = object : Migration(2, 4) {
- override fun migrate(db: SupportSQLiteDatabase) {
- db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `pinned` INTEGER NOT NULL DEFAULT 0")
- db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `encrypted` INTEGER NOT NULL DEFAULT 0")
- }
-}
-
-private val MIGRATION_4_5 = object : Migration(4, 5) {
- override fun migrate(db: SupportSQLiteDatabase) {
- db.execSQL("CREATE TABLE IF NOT EXISTS `Folder` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `color` TEXT NOT NULL, `createdAt` INTEGER NOT NULL)")
- db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `foldered` INTEGER DEFAULT NULL")
- }
-}
-
-private val MIGRATION_5_6 = object : Migration(5, 6) {
- override fun migrate(db: SupportSQLiteDatabase) {
- db.execSQL("CREATE TABLE `Folder_new` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `iconName` TEXT NOT NULL, `createdAt` INTEGER NOT NULL)")
- db.execSQL("INSERT INTO `Folder_new` (`id`, `name`, `iconName`, `createdAt`) SELECT `id`, `name`, 'Folder', `createdAt` FROM `Folder`")
- db.execSQL("DROP TABLE `Folder`")
- db.execSQL("ALTER TABLE `Folder_new` RENAME TO `Folder`")
- }
-}
-
-private val MIGRATION_6_7 = object : Migration(6, 7) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("""
- CREATE TABLE IF NOT EXISTS `ai_chat_sessions` (
- `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
- `title` TEXT NOT NULL,
- `createdAt` INTEGER NOT NULL,
- `aiMode` TEXT NOT NULL
- )
- """.trimIndent())
-
- database.execSQL("""
- CREATE TABLE IF NOT EXISTS `ai_chat_messages` (
- `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
- `sessionId` INTEGER NOT NULL,
- `text` TEXT NOT NULL,
- `participant` TEXT NOT NULL,
- `timestamp` INTEGER NOT NULL,
- `isLoading` INTEGER NOT NULL,
- FOREIGN KEY(`sessionId`) REFERENCES `ai_chat_sessions`(`id`) ON DELETE CASCADE
- )
- """.trimIndent())
- }
-}
-
-private val MIGRATION_7_8 = object : Migration(7, 8) {
- override fun migrate(db: SupportSQLiteDatabase) {
- db.execSQL("ALTER TABLE `ai_chat_sessions` ADD COLUMN `serviceName` TEXT NOT NULL DEFAULT 'GEMINI'")
- }
-}
-
-private val MIGRATION_8_9 = object : Migration(8, 9) {
- override fun migrate(db: SupportSQLiteDatabase) {
- db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `is_locked` INTEGER NOT NULL DEFAULT 0")
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/provider/StringProvider.kt b/app/src/main/java/com/babelsoftware/airnote/data/provider/StringProvider.kt
deleted file mode 100644
index a1579f7..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/provider/StringProvider.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.babelsoftware.airnote.data.provider
-
-import android.content.Context
-import dagger.hilt.android.qualifiers.ApplicationContext
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class StringProvider @Inject constructor(
- @ApplicationContext private val context: Context
-) {
- // Basit metinler için
- fun getString(stringResId: Int): String {
- return context.getString(stringResId)
- }
-
- // Formatlanmış metinler için
- fun getString(stringResId: Int, vararg formatArgs: Any): String {
- return context.getString(stringResId, *formatArgs)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/repository/AiModels.kt b/app/src/main/java/com/babelsoftware/airnote/data/repository/AiModels.kt
deleted file mode 100644
index 19c75f3..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/repository/AiModels.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.babelsoftware.airnote.data.repository
-
-import androidx.annotation.StringRes
-import com.babelsoftware.airnote.R
-
-data class GeminiModelInfo(
- val name: String,
- @StringRes val displayNameResId: Int
-)
-
-object GeminiModels {
- val supportedModels = listOf(
- GeminiModelInfo(
- name = "gemini-2.5-flash",
- displayNameResId = R.string.gemini_models_25
- ),
- GeminiModelInfo(
- name = "gemini-2.0-flash-001",
- displayNameResId = R.string.gemini_models_20
- ),
- GeminiModelInfo(
- name = "gemini-1.5-flash-latest",
- displayNameResId = R.string.gemini_models_15
- )
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/repository/FolderRepositoryImpl.kt b/app/src/main/java/com/babelsoftware/airnote/data/repository/FolderRepositoryImpl.kt
deleted file mode 100644
index e3cca8b..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/repository/FolderRepositoryImpl.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.data.repository
-
-import com.babelsoftware.airnote.data.source.FolderDao
-import com.babelsoftware.airnote.domain.model.Folder
-import com.babelsoftware.airnote.domain.repository.FolderRepository
-import kotlinx.coroutines.flow.Flow
-import javax.inject.Inject
-
-class FolderRepositoryImpl @Inject constructor(
- private val folderDao: FolderDao
-) : FolderRepository {
- override fun getAllFolders(): Flow> = folderDao.getAllFolders()
- override suspend fun insertFolder(folder: Folder) = folderDao.insertFolder(folder)
- override suspend fun updateFolder(folder: Folder) = folderDao.updateFolder(folder)
- override suspend fun deleteFolder(folder: Folder) = folderDao.deleteFolder(folder)
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/repository/GeminiRepository.kt b/app/src/main/java/com/babelsoftware/airnote/data/repository/GeminiRepository.kt
deleted file mode 100644
index 9276b32..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/repository/GeminiRepository.kt
+++ /dev/null
@@ -1,845 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- *
- *
- */
-
-package com.babelsoftware.airnote.data.repository
-
-import android.util.Log
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.data.provider.StringProvider
-import com.babelsoftware.airnote.domain.model.ChatMessage
-import com.babelsoftware.airnote.domain.model.Note
-import com.babelsoftware.airnote.domain.model.Participant
-import com.babelsoftware.airnote.domain.repository.SettingsRepository
-import com.google.ai.client.generativeai.GenerativeModel
-import com.google.ai.client.generativeai.type.Content
-import com.google.ai.client.generativeai.type.Part
-import com.google.ai.client.generativeai.type.TextPart
-import com.google.ai.client.generativeai.type.content
-import com.google.ai.client.generativeai.type.generationConfig
-import com.google.gson.annotations.SerializedName
-import com.google.mlkit.common.model.DownloadConditions
-import com.google.mlkit.common.model.RemoteModelManager
-import com.google.mlkit.nl.languageid.LanguageIdentification
-import com.google.mlkit.nl.translate.TranslateLanguage
-import com.google.mlkit.nl.translate.TranslateRemoteModel
-import com.google.mlkit.nl.translate.Translation
-import com.google.mlkit.nl.translate.TranslatorOptions
-import io.ktor.client.HttpClient
-import io.ktor.client.plugins.ClientRequestException
-import io.ktor.client.request.header
-import io.ktor.client.request.post
-import io.ktor.client.request.setBody
-import io.ktor.client.statement.bodyAsChannel
-import io.ktor.http.HttpStatusCode
-import io.ktor.utils.io.readUTF8Line
-import jakarta.inject.Inject
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.withContext
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-
-enum class AiMode {
- NOTE_ASSISTANT, // For factual, note-taking tasks.
- CREATIVE_MIND, // For brainstorming and creative writing.
- ACADEMIC_RESEARCHER,// For structured, evidence-based academic writing.
- PROFESSIONAL_STRATEGIST // For clear, goal-oriented business communication.
-}
-
-enum class AiAction {
- IMPROVE_WRITING,
- SUMMARIZE,
- MAKE_SHORTER,
- MAKE_LONGER,
- CHANGE_TONE,
- TRANSLATE
-}
-
-enum class AiTone {
- FORMAL,
- BALANCED,
- FRIENDLY
-}
-
-enum class AiAssistantAction {
- GIVE_IDEA,
- CONTINUE_WRITING,
- CHANGE_PERSPECTIVE,
- PROS_AND_CONS,
- CREATE_TODO_LIST,
- SIMPLIFY,
- SUGGEST_A_TITLE
-}
-
-data class AiActionPlan(
- @SerializedName("thought")
- val thought: String?,
-
- @SerializedName("actions")
- val actions: List,
-
- @SerializedName("response_message")
- val response_message: String
-)
-
-data class AiActionCommand(
- @SerializedName("action_type")
- val action_type: String,
- @SerializedName("title")
- val title: String?,
- @SerializedName("content")
- val content: String?,
- @SerializedName("tasks")
- val tasks: List?,
- @SerializedName("name")
- val name: String?,
- @SerializedName("iconName")
- val iconName: String?,
- @SerializedName("note_title")
- val note_title: String?,
- @SerializedName("folder_name")
- val folder_name: String?,
- @SerializedName("response")
- val response: String?,
- @SerializedName("new_content")
- val new_content: String?,
- @SerializedName("search_term")
- val search_term: String?
-)
-
-@Serializable
-private data class PplxMessage(
- @SerialName("role") val role: String,
- @SerialName("content") val content: String
-)
-
-@Serializable
-private data class PplxRequest(
- @SerialName("model") val model: String,
- @SerialName("stream") val stream: Boolean,
- @SerialName("messages") val messages: List
-)
-
-@Serializable
-private data class PplxSource(
- @SerialName("title") val title: String,
- @SerialName("url") val url: String
-)
-
-@Serializable
-private data class PplxDelta(
- @SerialName("content") val content: String? = null
-)
-
-@Serializable
-private data class PplxChoice(
- @SerialName("delta") val delta: PplxDelta? = null,
- @SerialName("sources") val sources: List? = null
-)
-
-@Serializable
-private data class PplxStreamResponse(
- @SerialName("choices") val choices: List
-)
-
-class GeminiRepository @Inject constructor(
- private val settingsRepository: SettingsRepository,
- private val stringProvider: StringProvider,
- private val httpClient: HttpClient,
- private val json: Json
-) {
- private companion object {
- const val DEFAULT_TEMPERATURE = 0.7f
- const val CREATIVE_TEMPERATURE = 0.9f
- const val PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions"
- }
- class ApiKeyMissingException(message: String) : Exception(message)
-
- suspend fun validateApiKey(apiKey: String, modelName: String): Result = withContext(Dispatchers.IO) {
- if (apiKey.isBlank()) {
- return@withContext Result.failure(ApiKeyMissingException("API key cannot be empty."))
- }
- return@withContext try {
- GenerativeModel(modelName = modelName, apiKey = apiKey).countTokens("test")
- Result.success(Unit)
- } catch (e: Exception) {
- e.printStackTrace()
- Result.failure(e)
- }
- }
-
- suspend fun validatePerplexityApiKey(apiKey: String): Result = withContext(Dispatchers.IO) {
- if (apiKey.isBlank()) {
- return@withContext Result.failure(ApiKeyMissingException("The Perplexity API key cannot be empty."))
- }
-
- val settings = settingsRepository.settings.first()
- val modelToValidate = settings.selectedPerplexityModelName
-
- try {
- val testRequest = PplxRequest(
- model = modelToValidate,
- stream = false,
- messages = listOf(PplxMessage(role = "user", content = "Hello"))
- )
-
- val response = httpClient.post(PERPLEXITY_API_URL) {
- header("Authorization", "Bearer $apiKey")
- setBody(testRequest)
- }
-
- return@withContext if (response.status == HttpStatusCode.OK) {
- Result.success(Unit)
- } else {
- Result.failure(Exception("Perplexity API'sine bağlanılamadı. Durum Kodu: ${response.status.value}"))
- }
- } catch (e: ClientRequestException) {
- if (e.response.status == HttpStatusCode.Unauthorized) {
- Result.failure(Exception("Perplexity API anahtarı geçersiz veya izni yok."))
- } else {
- Result.failure(e)
- }
- } catch (e: Exception) {
- e.printStackTrace()
- Result.failure(e)
- }
- }
-
-
- suspend fun processAiAction(text: String, action: AiAction, tone: AiTone? = null, apiKey: String, aiMode: AiMode = AiMode.NOTE_ASSISTANT): Result {
- if (apiKey.isBlank()) {
- return Result.failure(ApiKeyMissingException(stringProvider.getString(R.string.error_no_user_api_key)))
- }
- if (action == AiAction.CHANGE_TONE) {
- require(tone != null) { "CHANGE_TONE eylemi için bir ton belirtilmelidir." }
- }
-
- val modelName = settingsRepository.settings.first().selectedModelName
- val generativeModel: GenerativeModel = try {
- GenerativeModel(
- modelName = modelName,
- apiKey = apiKey,
- generationConfig = generationConfig {
- this.temperature = if (aiMode == AiMode.CREATIVE_MIND) CREATIVE_TEMPERATURE else DEFAULT_TEMPERATURE
- }
- )
- } catch (e: Exception) {
- e.printStackTrace()
- return Result.failure(e)
- }
-
- val systemPrompt = when (aiMode) {
- AiMode.NOTE_ASSISTANT -> stringProvider.getString(R.string.system_prompt_note_assistant)
- AiMode.CREATIVE_MIND -> stringProvider.getString(R.string.system_prompt_creative_mind)
- AiMode.ACADEMIC_RESEARCHER -> stringProvider.getString(R.string.system_prompt_academic_researcher)
- AiMode.PROFESSIONAL_STRATEGIST -> stringProvider.getString(R.string.system_prompt_professional_strategist)
- }
-
- val userPrompt = when (action) {
- AiAction.IMPROVE_WRITING -> stringProvider.getString(R.string.prompt_improve_writing, text)
- AiAction.SUMMARIZE -> stringProvider.getString(R.string.prompt_summarize, text)
- AiAction.MAKE_SHORTER -> stringProvider.getString(R.string.prompt_make_shorter, text)
- AiAction.MAKE_LONGER -> stringProvider.getString(R.string.prompt_make_longer, text)
- AiAction.CHANGE_TONE -> {
- val tonePromptResId = when (tone) {
- AiTone.FORMAL -> R.string.prompt_instruction_tone_formal
- AiTone.BALANCED -> R.string.prompt_instruction_tone_balanced
- AiTone.FRIENDLY -> R.string.prompt_instruction_tone_friendly
- null -> throw IllegalArgumentException("Tone cannot be null for CHANGE_TONE action")
- }
- stringProvider.getString(R.string.prompt_change_tone_template, stringProvider.getString(tonePromptResId), text)
- }
- AiAction.TRANSLATE -> "" // This action is handled on-device
- }
-
- val finalPrompt = "$systemPrompt\n\n---\n\n$userPrompt"
-
- return try {
- val response = generativeModel.generateContent(finalPrompt)
- response.text?.let { Result.success(it) } ?: Result.failure(Exception("API'den boş yanıt alındı."))
- } catch (e: Exception) {
- e.printStackTrace()
- Result.failure(e)
- }
- }
-
- fun generatePerplexityChatResponse(
- history: List,
- apiKey: String
- ): Flow = flow {
- if (apiKey.isBlank()) {
- throw ApiKeyMissingException("Perplexity API anahtarı ayarlanmamış.")
- }
-
- val settings = settingsRepository.settings.first()
- val modelToUse = settings.selectedPerplexityModelName
-
- val messages = mutableListOf()
- messages.add(PplxMessage(role = "system", content = "Be helpful and concise."))
- history.filter { !it.isLoading && it.participant != Participant.ERROR }.forEach { msg ->
- messages.add(PplxMessage(
- role = if (msg.participant == Participant.USER) "user" else "assistant",
- content = msg.text
- ))
- }
-
- val requestBody = PplxRequest(
- model = modelToUse,
- stream = true,
- messages = messages
- )
-
- val sourcesSet = mutableSetOf()
- val responseTextBuilder = StringBuilder()
-
- try {
- val response = httpClient.post(PERPLEXITY_API_URL) {
- header("Authorization", "Bearer $apiKey")
- setBody(requestBody)
- }
-
- val channel = response.bodyAsChannel()
-
- while (!channel.isClosedForRead) {
- val line = channel.readUTF8Line()
- if (line.isNullOrBlank()) continue
-
- if (line.startsWith("data:")) {
- val jsonData = line.substring(5).trim()
- if (jsonData == "[DONE]") {
- break
- }
-
- try {
- val streamResponse = json.decodeFromString(jsonData)
-
- val choice = streamResponse.choices.firstOrNull()
- if (choice != null) {
- val content = choice.delta?.content
- if (content != null) {
- responseTextBuilder.append(content)
- emit(content)
- }
-
- val sources = choice.sources
- if (!sources.isNullOrEmpty()) {
- sourcesSet.addAll(sources)
- }
- }
- } catch (e: Exception) {
- Log.w("PerplexityParseError", "JSON ayrıştırma hatası: $jsonData", e)
- }
- }
- }
-
- if (sourcesSet.isNotEmpty()) {
- val sourcesString = buildString {
- append("\n\n\n**Sources:**")
- sourcesSet.forEachIndexed { index, source ->
- append("\n[${index + 1}] ${source.title} (${source.url})")
- }
- }
- emit(sourcesString)
- }
-
- } catch (e: Exception) {
- Log.e("PerplexityRequestError", "Perplexity isteği başarısız", e)
- throw e
- }
-
- }.catch {
- Log.e("PerplexityFlowError", "Akış hatası", it)
- emit(stringProvider.getString(R.string.error_api_request_failed, it.message ?: "Bilinmeyen hata"))
- }.flowOn(Dispatchers.IO)
-
-
- fun processAssistantAction(
- noteName: String,
- noteDescription: String,
- action: AiAssistantAction,
- apiKey: String,
- aiMode: AiMode = AiMode.CREATIVE_MIND
- ): Flow = flow {
- if (apiKey.isBlank()) {
- throw ApiKeyMissingException(stringProvider.getString(R.string.error_no_user_api_key))
- }
-
- val modelName = settingsRepository.settings.first().selectedModelName
- val generativeModel = GenerativeModel(
- modelName = modelName,
- apiKey = apiKey,
- generationConfig = generationConfig {
- this.temperature = if (aiMode == AiMode.CREATIVE_MIND) CREATIVE_TEMPERATURE else DEFAULT_TEMPERATURE
- }
- )
-
- val systemPrompt = when (aiMode) {
- AiMode.NOTE_ASSISTANT -> stringProvider.getString(R.string.system_prompt_note_assistant)
- AiMode.CREATIVE_MIND -> stringProvider.getString(R.string.system_prompt_creative_mind)
- AiMode.ACADEMIC_RESEARCHER -> stringProvider.getString(R.string.system_prompt_academic_researcher)
- AiMode.PROFESSIONAL_STRATEGIST -> stringProvider.getString(R.string.system_prompt_professional_strategist)
- }
-
- val userPrompt = when (action) {
- AiAssistantAction.GIVE_IDEA -> stringProvider.getString(R.string.prompt_assistant_give_idea, noteName)
- AiAssistantAction.CONTINUE_WRITING -> stringProvider.getString(R.string.prompt_assistant_continue_writing, noteName, noteDescription)
- AiAssistantAction.CHANGE_PERSPECTIVE -> stringProvider.getString(R.string.prompt_assistant_change_perspective, noteName, noteDescription)
- AiAssistantAction.PROS_AND_CONS -> stringProvider.getString(R.string.prompt_assistant_pros_and_cons, noteDescription)
- AiAssistantAction.CREATE_TODO_LIST -> stringProvider.getString(R.string.prompt_assistant_create_todo, noteDescription)
- AiAssistantAction.SIMPLIFY -> stringProvider.getString(R.string.prompt_assistant_simplify, noteDescription)
- AiAssistantAction.SUGGEST_A_TITLE -> stringProvider.getString(R.string.prompt_assistant_suggest_title, noteDescription)
- }
-
- val finalPrompt = "$systemPrompt\n\n---\n\n$userPrompt"
-
- generativeModel.generateContentStream(finalPrompt).collect { chunk ->
- emit(chunk.text ?: "")
- }
- }.catch {
- emit(stringProvider.getString(R.string.error_api_request_failed, it.message ?: "Unknown error"))
- }
-
- fun generateChatResponse(
- history: List,
- apiKey: String,
- aiMode: AiMode = AiMode.NOTE_ASSISTANT,
- mentionedNote: com.babelsoftware.airnote.domain.model.Note? = null,
- attachment: Part? = null
- ): Flow = flow {
- if (apiKey.isBlank()) {
- throw ApiKeyMissingException(stringProvider.getString(R.string.error_no_user_api_key))
- }
-
- val modelName = settingsRepository.settings.first().selectedModelName
- val generativeModel = GenerativeModel(
- modelName = modelName,
- apiKey = apiKey,
- generationConfig = generationConfig {
- this.temperature = if (aiMode == AiMode.CREATIVE_MIND) CREATIVE_TEMPERATURE else DEFAULT_TEMPERATURE
- }
- )
-
- val baseSystemPrompt = when (aiMode) {
- AiMode.NOTE_ASSISTANT -> stringProvider.getString(R.string.system_prompt_note_assistant)
- AiMode.CREATIVE_MIND -> stringProvider.getString(R.string.system_prompt_creative_mind)
- AiMode.ACADEMIC_RESEARCHER -> stringProvider.getString(R.string.system_prompt_academic_researcher)
- AiMode.PROFESSIONAL_STRATEGIST -> stringProvider.getString(R.string.system_prompt_professional_strategist)
- }
-
- val chatHistoryForModel = mutableListOf()
- chatHistoryForModel.add(content("user") { text(baseSystemPrompt) })
- chatHistoryForModel.add(content("model") { text("OK.") })
-
- if (mentionedNote != null) {
- val noteContextPrompt = stringProvider.getString(
- R.string.prompt_mention_context,
- mentionedNote.name,
- mentionedNote.description
- )
- chatHistoryForModel.add(content("user") { text(noteContextPrompt) })
- val ackPrompt = stringProvider.getString(R.string.prompt_mention_ack, mentionedNote.name)
- chatHistoryForModel.add(content("model") { text(ackPrompt) })
- }
-
- val userHistoryContent = history
- .filter { it.participant != Participant.ERROR && !it.isLoading }
- .map { msg ->
- content(role = if (msg.participant == Participant.USER) "user" else "model") {
- text(msg.text)
- }
- }
- chatHistoryForModel.addAll(userHistoryContent)
-
-
- val lastMessageContent = chatHistoryForModel.lastOrNull() ?: return@flow
- val historyWithoutLast = chatHistoryForModel.dropLast(1)
- val chat = generativeModel.startChat(history = historyWithoutLast)
-
- val finalMessageToSend: Content
- if (attachment != null) {
- val lastUserText = (lastMessageContent.parts.firstOrNull() as? TextPart)?.text ?: ""
-
- finalMessageToSend = content(lastMessageContent.role) {
- part(attachment)
- text(lastUserText)
- }
- } else {
- finalMessageToSend = lastMessageContent
- }
-
- chat.sendMessageStream(finalMessageToSend).collect { chunk ->
- emit(chunk.text ?: "")
- }
- }.catch {
- emit(stringProvider.getString(R.string.error_api_request_failed, it.message ?: "Unknown error"))
- }
-
- suspend fun generateDraft(topic: String, apiKey: String, aiMode: AiMode = AiMode.NOTE_ASSISTANT): Result {
- if (apiKey.isBlank()) {
- return Result.failure(ApiKeyMissingException(stringProvider.getString(R.string.error_no_user_api_key)))
- }
-
- val modelName = settingsRepository.settings.first().selectedModelName
- val generativeModel: GenerativeModel = try {
- GenerativeModel(
- modelName = modelName,
- apiKey = apiKey,
- generationConfig = generationConfig {
- this.temperature = if (aiMode == AiMode.CREATIVE_MIND) CREATIVE_TEMPERATURE else DEFAULT_TEMPERATURE
- }
- )
- } catch (e: Exception) {
- e.printStackTrace()
- return Result.failure(e)
- }
-
- val systemPrompt = when (aiMode) {
- AiMode.NOTE_ASSISTANT -> stringProvider.getString(R.string.system_prompt_note_assistant)
- AiMode.CREATIVE_MIND -> stringProvider.getString(R.string.system_prompt_creative_mind)
- AiMode.ACADEMIC_RESEARCHER -> stringProvider.getString(R.string.system_prompt_academic_researcher)
- AiMode.PROFESSIONAL_STRATEGIST -> stringProvider.getString(R.string.system_prompt_professional_strategist)
- }
- val userPrompt = stringProvider.getString(R.string.prompt_assistant_draft_anything, topic)
- val finalPrompt = "$systemPrompt\n\n---\n\n$userPrompt"
-
- return try {
- val response = generativeModel.generateContent(finalPrompt)
- response.text?.let { Result.success(it) } ?: Result.failure(Exception("API'den boş yanıt alındı."))
- } catch (e: Exception) {
- e.printStackTrace()
- Result.failure(e)
- }
- }
-
- suspend fun generateDraftFromAttachment(
- prompt: String,
- attachment: Part,
- apiKey: String,
- aiMode: AiMode
- ): Result {
- if (apiKey.isBlank()) {
- return Result.failure(ApiKeyMissingException(stringProvider.getString(R.string.error_no_user_api_key)))
- }
-
- val generativeModel: GenerativeModel = try {
- val modelName = settingsRepository.settings.first().selectedModelName
- val temperature = if (aiMode == AiMode.CREATIVE_MIND) CREATIVE_TEMPERATURE else DEFAULT_TEMPERATURE
-
- GenerativeModel(
- modelName = modelName,
- apiKey = apiKey,
- generationConfig = generationConfig {
- this.temperature = temperature
- }
- )
- } catch (e: Exception) {
- e.printStackTrace()
- return Result.failure(e)
- }
-
- val systemPrompt = when (aiMode) {
- AiMode.NOTE_ASSISTANT -> stringProvider.getString(R.string.system_prompt_note_assistant)
- AiMode.CREATIVE_MIND -> stringProvider.getString(R.string.system_prompt_creative_mind)
- AiMode.ACADEMIC_RESEARCHER -> stringProvider.getString(R.string.system_prompt_academic_researcher)
- AiMode.PROFESSIONAL_STRATEGIST -> stringProvider.getString(R.string.system_prompt_professional_strategist)
- }
-
- val userPrompt = """
- Kullanıcı prompt'u: "$prompt"
-
- Ekli dosyayı/görseli analiz et ve bu prompt'a göre bir not taslağı oluştur.
- Cevabını MUTLAKA şu formatta ver:
- TITLE: [buraya başlığı yaz]
-
- CONTENT: [buraya not içeriğini yaz]
- """.trimIndent()
-
- val finalPrompt = "$systemPrompt\n\n---\n\n$userPrompt"
-
- val inputContent = content {
- part(attachment)
- text(finalPrompt)
- }
-
- return try {
- val response = generativeModel.generateContent(inputContent)
- response.text?.let { Result.success(it) } ?: Result.failure(Exception("API'den boş yanıt alındı."))
- } catch (e: Exception) {
- e.printStackTrace()
- Result.failure(e)
- }
- }
-
- suspend fun generateChatOrCommandResponse(
- noteContext: String,
- userRequest: String,
- chatHistory: List,
- apiKey: String
- ): Result = withContext(Dispatchers.IO) {
- if (apiKey.isBlank()) {
- return@withContext Result.failure(ApiKeyMissingException(stringProvider.getString(R.string.error_no_user_api_key)))
- }
-
- try {
- val modelName = settingsRepository.settings.first().selectedModelName
- val generativeModel = GenerativeModel(
- modelName = modelName,
- apiKey = apiKey,
- generationConfig = generationConfig {
- temperature = 0.4f
- }
- )
-
- val systemPrompt = buildString {
- append(stringProvider.getString(R.string.system_prompt_command_chat_header))
- append("\n\n")
- append(stringProvider.getString(R.string.system_prompt_intent_chat_desc))
- append("\n\n")
- append(stringProvider.getString(R.string.system_prompt_intent_edit_desc))
- append("\n\n")
- append(stringProvider.getString(R.string.system_prompt_json_instruction))
- }
-
- val historyForModel = mutableListOf()
- val initialContextPrompt = buildString {
- append(systemPrompt)
- append("\n\n")
- append(stringProvider.getString(R.string.system_prompt_context_header, noteContext))
- }
- historyForModel.add(content("user") { text(initialContextPrompt) })
- historyForModel.add(content("model") { text("OK. I am ready. Waiting for user request.") })
-
- val previousMessages = chatHistory
- .filter { !it.isLoading && it.participant != Participant.ERROR }
- .map { msg ->
- content(role = if (msg.participant == Participant.USER) "user" else "model") {
- text(msg.text)
- }
- }
- historyForModel.addAll(previousMessages)
-
- val finalRequestPrompt = stringProvider.getString(R.string.system_prompt_request_header, userRequest)
- val chat = generativeModel.startChat(history = historyForModel)
- val response = chat.sendMessage(finalRequestPrompt)
- val responseText = response.text
-
- if (responseText.isNullOrBlank()) {
- Result.failure(Exception("API'den boş yanıt alındı."))
- } else {
- val cleanJson = responseText.trim()
- .removePrefix("```json")
- .removePrefix("```")
- .removeSuffix("```")
- .trim()
-
- Result.success(cleanJson)
- }
-
- } catch (e: Exception) {
- e.printStackTrace()
- Result.failure(e)
- }
- }
-
- suspend fun generateActionPlan(
- userRequest: String,
- chatHistory: List,
- apiKey: String,
- aiMode: AiMode,
- mentionedNote: Note?
- ): Result = withContext(Dispatchers.IO) {
-
- if (apiKey.isBlank()) {
- return@withContext Result.failure(ApiKeyMissingException(stringProvider.getString(R.string.error_no_user_api_key)))
- }
-
- try {
- val modelName = settingsRepository.settings.first().selectedModelName
- val generativeModel = GenerativeModel(
- modelName = modelName,
- apiKey = apiKey,
- generationConfig = generationConfig {
- this.temperature = if (aiMode == AiMode.CREATIVE_MIND) 0.5f else 0.2f
- }
- )
- val systemPrompt = stringProvider.getString(R.string.system_prompt_automation_engine)
- val historyForModel = mutableListOf()
- historyForModel.add(content("user") { text(systemPrompt) })
- historyForModel.add(content("model") { text("OK. I am ready to generate JSON action plans.") })
-
- if (mentionedNote != null) {
- val noteContextPrompt = """
- --- CURRENT NOTE CONTEXT ---
- Title: "${mentionedNote.name}"
- Content: "${mentionedNote.description}"
- """.trimIndent()
- historyForModel.add(content("user") { text(noteContextPrompt) })
- historyForModel.add(content("model") { text("OK. Context for note '${mentionedNote.name}' has been loaded.") })
- }
-
- val previousMessages = chatHistory
- .filter { !it.isLoading && it.participant != Participant.ERROR }
- .map { msg ->
- content(role = if (msg.participant == Participant.USER) "user" else "model") {
- text(msg.text)
- }
- }
- historyForModel.addAll(previousMessages)
-
- val chat = generativeModel.startChat(history = historyForModel)
- val response = chat.sendMessage(userRequest)
- val responseText = response.text
-
- if (responseText.isNullOrBlank()) {
- Result.failure(Exception("AI'den boş yanıt alındı."))
- } else {
- val cleanJson = responseText.trim()
- .removePrefix("```json")
- .removePrefix("```")
- .removeSuffix("```")
- .trim()
-
- Log.d("GeminiRepository", "Raw JSON Plan: $cleanJson")
- Result.success(cleanJson)
- }
-
- } catch (e: Exception) {
- e.printStackTrace()
- Result.failure(e)
- }
- }
- val supportedLanguages = mapOf(
- TranslateLanguage.ENGLISH to "English",
- TranslateLanguage.TURKISH to "Türkçe",
- TranslateLanguage.GERMAN to "Deutsch",
- TranslateLanguage.FRENCH to "Français",
- TranslateLanguage.SPANISH to "Español",
- TranslateLanguage.ITALIAN to "Italiano",
- TranslateLanguage.JAPANESE to "日本語",
- TranslateLanguage.RUSSIAN to "Русский"
- )
-
- suspend fun getDownloadedModels(): Result> = withContext(Dispatchers.IO) {
- suspendCoroutine { continuation ->
- RemoteModelManager.getInstance().getDownloadedModels(TranslateRemoteModel::class.java)
- .addOnSuccessListener { models ->
- continuation.resume(Result.success(models.map { it.language }.toSet()))
- }
- .addOnFailureListener {
- continuation.resume(Result.failure(it))
- }
- }
- }
-
- suspend fun downloadLanguageModel(languageCode: String): Result = withContext(Dispatchers.IO) {
- suspendCoroutine { continuation ->
- val model = TranslateRemoteModel.Builder(languageCode).build()
- val conditions = DownloadConditions.Builder().requireWifi().build()
- RemoteModelManager.getInstance().download(model, conditions)
- .addOnSuccessListener {
- continuation.resume(Result.success(Unit))
- }
- .addOnFailureListener {
- continuation.resume(Result.failure(it))
- }
- }
- }
- suspend fun deleteLanguageModel(languageCode: String): Result = withContext(Dispatchers.IO) {
- suspendCoroutine { continuation ->
- val model = TranslateRemoteModel.Builder(languageCode).build()
- RemoteModelManager.getInstance().deleteDownloadedModel(model)
- .addOnSuccessListener {
- continuation.resume(Result.success(Unit))
- }
- .addOnFailureListener {
- continuation.resume(Result.failure(it))
- }
- }
- }
-
- private suspend fun identifyLanguage(text: String): Result = suspendCoroutine { continuation ->
- val languageIdentifier = LanguageIdentification.getClient()
- languageIdentifier.identifyLanguage(text)
- .addOnSuccessListener { languageCode ->
- if (languageCode == "und") {
- continuation.resume(Result.failure(Exception("The source language could not be identified.")))
- } else {
- continuation.resume(Result.success(languageCode))
- }
- languageIdentifier.close()
- }
- .addOnFailureListener {
- continuation.resume(Result.failure(it))
- languageIdentifier.close()
- }
- }
-
- suspend fun translateOnDevice(text: String, targetLanguage: String): Result = withContext(Dispatchers.IO) {
- if (text.isBlank()) return@withContext Result.success("")
-
- val langIdResult = identifyLanguage(text)
- if (langIdResult.isFailure) {
- return@withContext Result.failure(langIdResult.exceptionOrNull()!!)
- }
- val sourceLanguageCode = langIdResult.getOrNull()!!
-
- if (sourceLanguageCode.equals(targetLanguage, ignoreCase = true)) {
- return@withContext Result.failure(Exception("The note text is already in the language you want to translate."))
- }
-
- val options = TranslatorOptions.Builder()
- .setSourceLanguage(sourceLanguageCode)
- .setTargetLanguage(targetLanguage)
- .build()
- val translator = Translation.getClient(options)
-
- try {
- val downloadResult = suspendCoroutine> { continuation ->
- translator.downloadModelIfNeeded()
- .addOnSuccessListener { continuation.resume(Result.success(Unit)) }
- .addOnFailureListener { continuation.resume(Result.failure(it)) }
- }
- if (downloadResult.isFailure) {
- throw downloadResult.exceptionOrNull()!!
- }
-
- val originalLines = text.split('\n')
- val translatedLines = mutableListOf()
-
- for (line in originalLines) {
- if (line.isBlank()) {
- translatedLines.add(line)
- } else {
- val translatedLineResult = suspendCoroutine> { continuation ->
- translator.translate(line)
- .addOnSuccessListener { translatedText -> continuation.resume(Result.success(translatedText)) }
- .addOnFailureListener { exception -> continuation.resume(Result.failure(exception)) }
- }
-
- if (translatedLineResult.isSuccess) {
- translatedLines.add(translatedLineResult.getOrThrow())
- } else {
- throw translatedLineResult.exceptionOrNull()!!
- }
- }
- }
- Result.success(translatedLines.joinToString("\n"))
-
- } catch (e: Exception) {
- Log.e("TranslateError", "Satır satır çeviri sırasında hata oluştu", e)
- Result.failure(e)
- } finally {
- translator.close()
- }
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/repository/ImportExportRepository.kt b/app/src/main/java/com/babelsoftware/airnote/data/repository/ImportExportRepository.kt
deleted file mode 100644
index 944c70d..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/repository/ImportExportRepository.kt
+++ /dev/null
@@ -1,175 +0,0 @@
-package com.babelsoftware.airnote.data.repository
-
-import android.content.Context
-import android.net.Uri
-import android.provider.MediaStore
-import androidx.core.database.getStringOrNull
-import com.babelsoftware.airnote.constant.DatabaseConst
-import com.babelsoftware.airnote.data.local.database.NoteDatabaseProvider
-import com.babelsoftware.airnote.domain.model.Note
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExecutorCoroutineDispatcher
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.coroutines.withContext
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileOutputStream
-import java.io.IOException
-import java.security.InvalidKeyException
-import java.security.SecureRandom
-import java.security.spec.KeySpec
-import java.util.zip.ZipEntry
-import java.util.zip.ZipInputStream
-import java.util.zip.ZipOutputStream
-import javax.crypto.BadPaddingException
-import javax.crypto.Cipher
-import javax.crypto.IllegalBlockSizeException
-import javax.crypto.SecretKey
-import javax.crypto.SecretKeyFactory
-import javax.crypto.spec.IvParameterSpec
-import javax.crypto.spec.PBEKeySpec
-import javax.crypto.spec.SecretKeySpec
-
-sealed class BackupResult {
- data class Success(val message: String) : BackupResult()
- data class Error(val message: String) : BackupResult()
- object BadPassword : BackupResult()
-}
-
-class ImportExportRepository(
- private val provider: NoteDatabaseProvider,
- private val context: Context,
- private val mutex: Mutex,
- private val scope: CoroutineScope,
- private val dispatcher: ExecutorCoroutineDispatcher,
-) {
- private val salt = ByteArray(16).apply { SecureRandom().nextBytes(this) }
-
- private fun generateSecretKey(password: String, salt: ByteArray): SecretKey {
- val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
- val spec: KeySpec = PBEKeySpec(password.toCharArray(), salt, 65536, 256)
- return SecretKeySpec(factory.generateSecret(spec).encoded, "AES")
- }
-
- private fun encrypt(data: ByteArray, secretKey: SecretKey): ByteArray {
- val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
- val iv = ByteArray(16).apply { SecureRandom().nextBytes(this) }
- cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
- val encryptedData = cipher.doFinal(data)
- return iv + encryptedData
- }
-
- private fun decrypt(data: ByteArray, secretKey: SecretKey): ByteArray {
- val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
- val iv = data.copyOfRange(0, 16)
- val encryptedData = data.copyOfRange(16, data.size)
- cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
- return cipher.doFinal(encryptedData)
- }
- suspend fun exportBackup(uri: Uri, password: String?): BackupResult {
- return try {
- withContext(dispatcher + scope.coroutineContext) {
- mutex.withLock {
- provider.close()
-
- context.contentResolver.openOutputStream(uri)?.use { outputStream ->
- val databaseFile = context.getDatabasePath(DatabaseConst.NOTES_DATABASE_FILE_NAME)
- val tempZipFile = File.createTempFile("backup", ".zip", context.cacheDir)
-
- ZipOutputStream(FileOutputStream(tempZipFile)).use { zipOutputStream ->
- FileInputStream(databaseFile).use { inputStream ->
- val zipEntry = ZipEntry(databaseFile.name)
- zipOutputStream.putNextEntry(zipEntry)
- inputStream.copyTo(zipOutputStream)
- zipOutputStream.closeEntry()
- }
- }
-
- val zipData = tempZipFile.readBytes()
- if (password != null) {
- val secretKey = generateSecretKey(password, salt)
- val encryptedData = encrypt(zipData, secretKey)
- outputStream.write(salt + encryptedData)
- } else {
- outputStream.write(zipData)
- }
- tempZipFile.delete()
- }
- }
- }
- BackupResult.Success("Export successful")
- } catch (e: Exception) {
- BackupResult.Error("Export failed: ${e.message}")
- }
- }
-
- suspend fun importBackup(uri: Uri, password: String?): BackupResult {
- return try {
- withContext(dispatcher + scope.coroutineContext) {
- mutex.withLock {
- provider.close()
-
- context.contentResolver.openInputStream(uri)?.use { inputStream ->
- val inputData = inputStream.readBytes()
- val data: ByteArray
-
- if (password != null) {
- val salt = inputData.copyOfRange(0, 16)
- val encryptedData = inputData.copyOfRange(16, inputData.size)
- val secretKey = generateSecretKey(password, salt)
- data = decrypt(encryptedData, secretKey)
- } else {
- data = inputData
- }
-
- val tempZipFile = File.createTempFile("backup", ".zip", context.cacheDir)
- tempZipFile.writeBytes(data)
-
- ZipInputStream(FileInputStream(tempZipFile)).use { zipInputStream ->
- var entry: ZipEntry?
- while (zipInputStream.nextEntry.also { entry = it } != null) {
- val dbFile = context.getDatabasePath(DatabaseConst.NOTES_DATABASE_FILE_NAME)
- if (dbFile != null) {
- dbFile.delete()
-
- dbFile.outputStream().use { outputStream ->
- zipInputStream.copyTo(outputStream)
- }
- } else {
- println("Database is not in backup")
- }
- zipInputStream.closeEntry()
- println("Writing Backup Finished")
- }
- }
- tempZipFile.delete()
- }
- }
- }
- BackupResult.Success("Import successful")
- } catch (e: Exception) {
- when (e) {
- is BadPaddingException,
- is IllegalBlockSizeException,
- is InvalidKeyException -> BackupResult.BadPassword
- else -> BackupResult.Error("Import failed: ${e.message}")
- }
- }
- }
-
- fun importFile(uri: Uri): Note {
- val content = context.contentResolver.openInputStream(uri)?.use { fileInputStream ->
- fileInputStream.reader().buffered().readText()
- } ?: throw IOException("ContentResolver couldn't open InputStream to import file.")
-
- val name = context.contentResolver.query(uri, arrayOf(MediaStore.Files.FileColumns.DISPLAY_NAME), null, null)?.use { cursor ->
- if (cursor.moveToFirst()) cursor.getStringOrNull(0) else ""
- } ?: ""
-
- return Note(
- name = name,
- description = content,
- )
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/repository/NoteRepository.kt b/app/src/main/java/com/babelsoftware/airnote/data/repository/NoteRepository.kt
deleted file mode 100644
index 5312ba6..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/repository/NoteRepository.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.babelsoftware.airnote.data.repository
-
-import com.babelsoftware.airnote.data.local.database.NoteDatabaseProvider
-import com.babelsoftware.airnote.domain.model.Note
-import com.babelsoftware.airnote.domain.repository.NoteRepository
-import kotlinx.coroutines.flow.Flow
-import javax.inject.Inject
-
-class NoteRepositoryImpl @Inject constructor(
- private val provider: NoteDatabaseProvider
-) : NoteRepository {
- override fun getAllNotes(): Flow> {
- return provider.noteDao().getAllNotes()
- }
-
- override suspend fun addNote(note: Note) {
- provider.noteDao().addNote(note)
- }
-
- override suspend fun updateNote(note: Note) {
- provider.noteDao().updateNote(note)
- }
-
- override suspend fun deleteNote(note: Note) {
- provider.noteDao().deleteNote(note)
- }
-
- override fun getNoteById(id: Int): Flow {
- return provider.noteDao().getNoteById(id)
- }
-
- override fun getLastNoteId(): Long? {
- return provider.noteDao().getLastNoteId()
- }
-
- override suspend fun unlinkNotesFromFolder(folderId: Long) {
- provider.noteDao().unlinkNotesFromFolder(folderId)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/repository/SecureStorageRepository.kt b/app/src/main/java/com/babelsoftware/airnote/data/repository/SecureStorageRepository.kt
deleted file mode 100644
index 929bde5..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/repository/SecureStorageRepository.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.data.repository
-
-import android.content.Context
-import android.content.SharedPreferences
-import androidx.security.crypto.EncryptedSharedPreferences
-import androidx.security.crypto.MasterKeys
-import dagger.hilt.android.qualifiers.ApplicationContext
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class SecureStorageRepository @Inject constructor(
- @ApplicationContext private val context: Context
-) {
- companion object {
- private const val PREFERENCE_FILE_NAME = "airnote_secure_prefs"
- private const val KEY_USER_GEMINI_API = "user_gemini_api_key"
- private const val KEY_PERPLEXITY_API = "perplexity_api_key"
- }
-
- private val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
-
- private val sharedPreferences = EncryptedSharedPreferences.create(
- PREFERENCE_FILE_NAME,
- masterKeyAlias,
- context,
- EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
- EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
- )
-
- fun saveUserApiKey(apiKey: String) {
- with(sharedPreferences.edit()) {
- putString(KEY_USER_GEMINI_API, apiKey)
- commit()
- }
- }
-
- fun getUserApiKey(): String? {
- return sharedPreferences.getString(KEY_USER_GEMINI_API, null)
- }
-
- fun savePerplexityApiKey(apiKey: String) {
- with(sharedPreferences.edit()) {
- putString(KEY_PERPLEXITY_API, apiKey)
- commit()
- }
- }
-
- fun getPerplexityApiKey(): String? {
- return sharedPreferences.getString(KEY_PERPLEXITY_API, null)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/repository/SettingsRepository.kt b/app/src/main/java/com/babelsoftware/airnote/data/repository/SettingsRepository.kt
deleted file mode 100644
index fd945f0..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/repository/SettingsRepository.kt
+++ /dev/null
@@ -1,201 +0,0 @@
-package com.babelsoftware.airnote.data.repository
-
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.intPreferencesKey
-import androidx.datastore.preferences.core.longPreferencesKey
-import androidx.datastore.preferences.core.stringPreferencesKey
-import androidx.datastore.preferences.preferencesDataStore
-import com.babelsoftware.airnote.domain.model.Settings
-import com.babelsoftware.airnote.domain.repository.SettingsRepository
-import com.babelsoftware.airnote.widget.NotesWidgetReceiver
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-
-
-private const val PREFERENCES_NAME = "settingsupdated"
-
-private val Context.dataStore: DataStore by preferencesDataStore(name = PREFERENCES_NAME)
-
-private object PreferencesKeys {
- val DEFAULT_ROUTE_TYPE = stringPreferencesKey("default_route_type")
- val PASSCODE = stringPreferencesKey("passcode")
- val FINGERPRINT = booleanPreferencesKey("fingerprint")
- val PATTERN = stringPreferencesKey("pattern")
- val VIEW_MODE = booleanPreferencesKey("view_mode")
- val AUTOMATIC_THEME = booleanPreferencesKey("automatic_theme")
- val DARK_THEME = booleanPreferencesKey("dark_theme")
- val DYNAMIC_THEME = booleanPreferencesKey("dynamic_theme")
- val AMOLED_THEME = booleanPreferencesKey("amoled_theme")
- val MINIMALISTIC_MODE = booleanPreferencesKey("minimalistic_mode")
- val EXTREME_AMOLED_MODE = booleanPreferencesKey("extreme_amoled_mode")
- val IS_MARKDOWN_ENABLED = booleanPreferencesKey("is_markdown_enabled")
- val SCREEN_PROTECTION = booleanPreferencesKey("screen_protection")
- val ENCRYPT_BACKUP = booleanPreferencesKey("encrypt_backup")
- val SORT_DESCENDING = booleanPreferencesKey("sort_descending")
- val VAULT_SETTING_ENABLED = booleanPreferencesKey("vault_setting_enabled")
- val VAULT_ENABLED = booleanPreferencesKey("vault_enabled")
- val EDIT_MODE = booleanPreferencesKey("edit_mode")
- val GALLERY_SYNC = booleanPreferencesKey("gallery_sync")
- val SHOW_ONLY_TITLE = booleanPreferencesKey("show_only_title")
- val TERMS_OF_SERVICE = booleanPreferencesKey("terms_of_service")
- val USE_MONO_SPACE_FONT = booleanPreferencesKey("use_mono_space_font")
- val LOCK_IMMEDIATELY = booleanPreferencesKey("lock_immediately")
- val CORNER_RADIUS = intPreferencesKey("corner_radius")
- val DISABLE_SWIPE_IN_EDIT_MODE = booleanPreferencesKey("disable_swipe_in_edit_mode")
- val MAKE_SEARCH_BAR_LONGER = booleanPreferencesKey("make_search_bar_longer")
- val FONT_SIZE = intPreferencesKey("font_size")
- val SHOW_FOLDER_INDICATOR = booleanPreferencesKey("show_folder_indicator")
- val USE_AIRNOTE_API = booleanPreferencesKey("use_airnote_api")
- val SELECTED_MODEL_NAME = stringPreferencesKey("selected_model_name")
- val DESKTOP_MODE_ENABLED = booleanPreferencesKey("desktop_mode_enabled")
- val DESKTOP_MODE_AI_ENABLED = booleanPreferencesKey("desktop_mode_ai_enabled")
- val OPEN_TO_LAST_USED_FOLDER = booleanPreferencesKey("open_to_last_used_folder")
- val LAST_USED_FOLDER_ID = longPreferencesKey("last_used_folder_id")
-}
-
-class SettingsRepositoryImpl(private val context: Context) : SettingsRepository {
-
- override val settings: Flow = context.dataStore.data.map { preferences ->
- Settings(
- defaultRouteType = preferences[PreferencesKeys.DEFAULT_ROUTE_TYPE] ?: "Home",
- passcode = preferences[PreferencesKeys.PASSCODE],
- fingerprint = preferences[PreferencesKeys.FINGERPRINT] ?: false,
- pattern = preferences[PreferencesKeys.PATTERN],
- viewMode = preferences[PreferencesKeys.VIEW_MODE] ?: true,
- automaticTheme = preferences[PreferencesKeys.AUTOMATIC_THEME] ?: true,
- darkTheme = preferences[PreferencesKeys.DARK_THEME] ?: false,
- dynamicTheme = preferences[PreferencesKeys.DYNAMIC_THEME] ?: false,
- amoledTheme = preferences[PreferencesKeys.AMOLED_THEME] ?: false,
- minimalisticMode = preferences[PreferencesKeys.MINIMALISTIC_MODE] ?: false,
- extremeAmoledMode = preferences[PreferencesKeys.EXTREME_AMOLED_MODE] ?: false,
- isMarkdownEnabled = preferences[PreferencesKeys.IS_MARKDOWN_ENABLED] ?: true,
- screenProtection = preferences[PreferencesKeys.SCREEN_PROTECTION] ?: false,
- encryptBackup = preferences[PreferencesKeys.ENCRYPT_BACKUP] ?: false,
- sortDescending = preferences[PreferencesKeys.SORT_DESCENDING] ?: true,
- vaultSettingEnabled = preferences[PreferencesKeys.VAULT_SETTING_ENABLED] ?: false,
- vaultEnabled = preferences[PreferencesKeys.VAULT_ENABLED] ?: false,
- editMode = preferences[PreferencesKeys.EDIT_MODE] ?: false,
- gallerySync = preferences[PreferencesKeys.GALLERY_SYNC] ?: true,
- showOnlyTitle = preferences[PreferencesKeys.SHOW_ONLY_TITLE] ?: false,
- termsOfService = preferences[PreferencesKeys.TERMS_OF_SERVICE] ?: false,
- useMonoSpaceFont = preferences[PreferencesKeys.USE_MONO_SPACE_FONT] ?: false,
- lockImmediately = preferences[PreferencesKeys.LOCK_IMMEDIATELY] ?: true,
- cornerRadius = preferences[PreferencesKeys.CORNER_RADIUS] ?: 28,
- disableSwipeInEditMode = preferences[PreferencesKeys.DISABLE_SWIPE_IN_EDIT_MODE] ?: false,
- makeSearchBarLonger = preferences[PreferencesKeys.MAKE_SEARCH_BAR_LONGER] ?: false,
- fontSize = preferences[PreferencesKeys.FONT_SIZE] ?: 13,
- showFolderIndicator = preferences[PreferencesKeys.SHOW_FOLDER_INDICATOR] ?: false,
- useAirNoteApi = preferences[PreferencesKeys.USE_AIRNOTE_API] ?: false,
- selectedModelName = preferences[PreferencesKeys.SELECTED_MODEL_NAME] ?: "gemini-2.0-flash-001",
- desktopModeEnabled = preferences[PreferencesKeys.DESKTOP_MODE_ENABLED] ?: true,
- desktopModeAiEnabled = preferences[PreferencesKeys.DESKTOP_MODE_AI_ENABLED] ?: true,
- openToLastUsedFolder = preferences[PreferencesKeys.OPEN_TO_LAST_USED_FOLDER] ?: false,
- lastUsedFolderId = preferences[PreferencesKeys.LAST_USED_FOLDER_ID]
- )
- }
-
- override suspend fun update(settings: Settings) {
- context.dataStore.edit { preferences ->
- preferences[PreferencesKeys.DEFAULT_ROUTE_TYPE] = settings.defaultRouteType
- settings.passcode?.let { preferences[PreferencesKeys.PASSCODE] = it } ?: preferences.remove(PreferencesKeys.PASSCODE)
- preferences[PreferencesKeys.FINGERPRINT] = settings.fingerprint
- settings.pattern?.let { preferences[PreferencesKeys.PATTERN] = it } ?: preferences.remove(PreferencesKeys.PATTERN)
- preferences[PreferencesKeys.VIEW_MODE] = settings.viewMode
- preferences[PreferencesKeys.AUTOMATIC_THEME] = settings.automaticTheme
- preferences[PreferencesKeys.DARK_THEME] = settings.darkTheme
- preferences[PreferencesKeys.DYNAMIC_THEME] = settings.dynamicTheme
- preferences[PreferencesKeys.AMOLED_THEME] = settings.amoledTheme
- preferences[PreferencesKeys.MINIMALISTIC_MODE] = settings.minimalisticMode
- preferences[PreferencesKeys.EXTREME_AMOLED_MODE] = settings.extremeAmoledMode
- preferences[PreferencesKeys.IS_MARKDOWN_ENABLED] = settings.isMarkdownEnabled
- preferences[PreferencesKeys.SCREEN_PROTECTION] = settings.screenProtection
- preferences[PreferencesKeys.ENCRYPT_BACKUP] = settings.encryptBackup
- preferences[PreferencesKeys.SORT_DESCENDING] = settings.sortDescending
- preferences[PreferencesKeys.VAULT_SETTING_ENABLED] = settings.vaultSettingEnabled
- preferences[PreferencesKeys.VAULT_ENABLED] = settings.vaultEnabled
- preferences[PreferencesKeys.EDIT_MODE] = settings.editMode
- preferences[PreferencesKeys.GALLERY_SYNC] = settings.gallerySync
- preferences[PreferencesKeys.SHOW_ONLY_TITLE] = settings.showOnlyTitle
- preferences[PreferencesKeys.TERMS_OF_SERVICE] = settings.termsOfService
- preferences[PreferencesKeys.USE_MONO_SPACE_FONT] = settings.useMonoSpaceFont
- preferences[PreferencesKeys.LOCK_IMMEDIATELY] = settings.lockImmediately
- preferences[PreferencesKeys.CORNER_RADIUS] = settings.cornerRadius
- preferences[PreferencesKeys.DISABLE_SWIPE_IN_EDIT_MODE] = settings.disableSwipeInEditMode
- preferences[PreferencesKeys.MAKE_SEARCH_BAR_LONGER] = settings.makeSearchBarLonger
- preferences[PreferencesKeys.FONT_SIZE] = settings.fontSize
- preferences[PreferencesKeys.SHOW_FOLDER_INDICATOR] = settings.showFolderIndicator
- preferences[PreferencesKeys.USE_AIRNOTE_API] = settings.useAirNoteApi
- preferences[PreferencesKeys.SELECTED_MODEL_NAME] = settings.selectedModelName
- preferences[PreferencesKeys.DESKTOP_MODE_ENABLED] = settings.desktopModeEnabled
- preferences[PreferencesKeys.DESKTOP_MODE_AI_ENABLED] = settings.desktopModeAiEnabled
- preferences[PreferencesKeys.OPEN_TO_LAST_USED_FOLDER] = settings.openToLastUsedFolder
- settings.lastUsedFolderId?.let { preferences[PreferencesKeys.LAST_USED_FOLDER_ID] = it } ?: preferences.remove(PreferencesKeys.LAST_USED_FOLDER_ID)
- }
- }
-
- override suspend fun getPreferences(): Preferences {
- return context.dataStore.data.first()
- }
-
- override suspend fun putString(key: String, value: String) {
- val preferencesKey = stringPreferencesKey(key)
- context.dataStore.edit { preferences ->
- preferences[preferencesKey] = value
- }
- }
-
- override suspend fun putInt(key: String, value: Int) {
- val preferencesKey = intPreferencesKey(key)
- context.dataStore.edit { preferences ->
- preferences[preferencesKey] = value
- }
- }
-
- override suspend fun getString(key: String): String? {
- val preferencesKey = stringPreferencesKey(key)
- val preferences = context.dataStore.data.first()
- return preferences[preferencesKey]
- }
-
- override suspend fun getInt(key: String): Int? {
- val preferencesKey = intPreferencesKey(key)
- val preferences = context.dataStore.data.first()
- return preferences[preferencesKey]
- }
-
- override suspend fun putBoolean(key: String, value: Boolean) {
- val preferencesKey = booleanPreferencesKey(key)
- context.dataStore.edit { preferences ->
- preferences[preferencesKey] = value
- }
- }
-
- override suspend fun getBoolean(key: String): Boolean? {
- val preferencesKey = booleanPreferencesKey(key)
- val preferences = context.dataStore.data.first()
- return preferences[preferencesKey]
- }
-
- override suspend fun getEveryNotesWidget(): List> {
- val preferences = context.dataStore.data.first()
- val widgetPairs = mutableListOf>()
-
- preferences.asMap().forEach { entry ->
- val key = entry.key.name
-
- if (entry.key.name.startsWith(NotesWidgetReceiver.Companion.WIDGET_PREFERENCE)) {
- val widgetId = key.substringAfter(NotesWidgetReceiver.Companion.WIDGET_PREFERENCE).toIntOrNull()
- if (widgetId != null) {
- val value = entry.value as? Int ?: 0
- widgetPairs.add(widgetId to value)
- }
- }
- }
- return widgetPairs
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/data/source/FolderDao.kt b/app/src/main/java/com/babelsoftware/airnote/data/source/FolderDao.kt
deleted file mode 100644
index 62e5824..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/data/source/FolderDao.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.data.source
-
-import androidx.room.*
-import com.babelsoftware.airnote.domain.model.Folder
-import kotlinx.coroutines.flow.Flow
-
-@Dao
-interface FolderDao {
- @Query("SELECT * FROM folder ORDER BY createdAt DESC")
- fun getAllFolders(): Flow>
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insertFolder(folder: Folder)
-
- @Update
- suspend fun updateFolder(folder: Folder)
-
- @Delete
- suspend fun deleteFolder(folder: Folder)
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/di/DataModule.kt b/app/src/main/java/com/babelsoftware/airnote/di/DataModule.kt
deleted file mode 100644
index e5bf9af..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/di/DataModule.kt
+++ /dev/null
@@ -1,138 +0,0 @@
-package com.babelsoftware.airnote.di
-
-import android.app.Application
-import android.content.Context
-import android.os.Handler
-import android.os.Looper
-import com.babelsoftware.airnote.data.local.dao.AiChatDao
-import com.babelsoftware.airnote.data.local.database.NoteDatabase
-import com.babelsoftware.airnote.data.local.database.NoteDatabaseProvider
-import com.babelsoftware.airnote.data.repository.FolderRepositoryImpl
-import com.babelsoftware.airnote.data.repository.ImportExportRepository
-import com.babelsoftware.airnote.data.repository.NoteRepositoryImpl
-import com.babelsoftware.airnote.data.repository.SettingsRepositoryImpl
-import com.babelsoftware.airnote.data.source.FolderDao
-import com.babelsoftware.airnote.domain.repository.FolderRepository
-import com.babelsoftware.airnote.domain.repository.NoteRepository
-import com.babelsoftware.airnote.domain.repository.SettingsRepository
-import com.babelsoftware.airnote.domain.usecase.FolderUseCase
-import com.babelsoftware.airnote.presentation.components.EncryptionHelper
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExecutorCoroutineDispatcher
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.asCoroutineDispatcher
-import kotlinx.coroutines.sync.Mutex
-import java.util.concurrent.Executors
-import javax.inject.Qualifier
-import javax.inject.Singleton
-
-@Qualifier
-annotation class WidgetCoroutineScope
-
-@Module
-@InstallIn(SingletonComponent::class)
-object ApplicationModule {
-
- @Provides
- @Singleton
- @WidgetCoroutineScope
- fun providesWidgetCoroutineScope(): CoroutineScope = CoroutineScope(
- Executors.newSingleThreadExecutor().asCoroutineDispatcher(),
- )
-
- @Provides
- @Singleton
- fun provideNoteDatabaseProvider(application: Application): NoteDatabaseProvider =
- NoteDatabaseProvider(application)
-
- @Provides
- @Singleton
- fun provideCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
-
- @Provides
- fun provideMutex(): Mutex = Mutex()
-
- @Provides
- @Singleton
- fun provideExecutorCoroutineDispatcher(): ExecutorCoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
-
- @Provides
- @Singleton
- fun provideNoteRepository(noteDatabaseProvider: NoteDatabaseProvider): NoteRepository {
- return NoteRepositoryImpl(noteDatabaseProvider)
- }
-
- @Provides
- @Singleton
- fun provideSettingsRepository(@ApplicationContext context: Context): SettingsRepository {
- return SettingsRepositoryImpl(context)
- }
-
- @Provides
- @Singleton
- fun provideBackupRepository(
- noteDatabaseProvider: NoteDatabaseProvider,
- application: Application,
- mutex: Mutex,
- coroutineScope: CoroutineScope,
- executorCoroutineDispatcher: ExecutorCoroutineDispatcher,
- ): ImportExportRepository {
- return ImportExportRepository(
- provider = noteDatabaseProvider,
- context = application,
- mutex = mutex,
- scope = coroutineScope,
- dispatcher = executorCoroutineDispatcher
- )
- }
-
- @Provides
- @Singleton
- fun provideMutableVaultPassword(): StringBuilder {
- return StringBuilder()
- }
-
- @Provides
- @Singleton
- fun provideEncryptionHelper(mutableVaultPassword: StringBuilder): EncryptionHelper {
- return EncryptionHelper(mutableVaultPassword)
- }
-
- @Provides
- fun provideHandler(): Handler {
- return Handler(Looper.getMainLooper())
- }
-
- @Provides
- @Singleton
- fun provideFolderDao(noteDatabaseProvider: NoteDatabaseProvider): FolderDao {
- return noteDatabaseProvider.folderDao()
- }
-
- @Provides
- @Singleton
- fun provideFolderRepository(folderDao: FolderDao): FolderRepository {
- return FolderRepositoryImpl(folderDao)
- }
-
- @Provides
- @Singleton
- fun provideFolderUseCase(
- folderRepository: FolderRepository,
- noteRepository: NoteRepository
- ): FolderUseCase {
- return FolderUseCase(folderRepository, noteRepository)
- }
-
- @Provides
- @Singleton
- fun provideAiChatDao(noteDatabaseProvider: NoteDatabaseProvider): AiChatDao {
- return noteDatabaseProvider.aiChatDao()
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/di/NetworkModule.kt b/app/src/main/java/com/babelsoftware/airnote/di/NetworkModule.kt
deleted file mode 100644
index 75c5b06..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/di/NetworkModule.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.babelsoftware.airnote.di // Kendi paket yapınıza göre düzenleyin
-
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import io.ktor.client.HttpClient
-import io.ktor.client.engine.android.Android
-import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
-import io.ktor.client.plugins.defaultRequest
-import io.ktor.client.plugins.logging.LogLevel
-import io.ktor.client.plugins.logging.Logging
-import io.ktor.client.request.accept
-import io.ktor.http.ContentType
-import io.ktor.http.contentType
-import io.ktor.serialization.kotlinx.json.json
-import kotlinx.serialization.json.Json
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-object NetworkModule {
-
- @Provides
- @Singleton
- fun provideKotlinxJson(): Json {
- return Json {
- ignoreUnknownKeys = true
- isLenient = true
- }
- }
-
- @Provides
- @Singleton
- fun provideHttpClient(json: Json): HttpClient {
- return HttpClient(Android) {
- install(ContentNegotiation) {
- json(json)
- }
-
- install(Logging) {
- level = LogLevel.BODY
- }
-
- defaultRequest {
- contentType(ContentType.Application.Json)
- accept(ContentType.Application.Json)
- accept(ContentType.Text.EventStream)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/model/AiChatMessage.kt b/app/src/main/java/com/babelsoftware/airnote/domain/model/AiChatMessage.kt
deleted file mode 100644
index 8d72539..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/model/AiChatMessage.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.babelsoftware.airnote.domain.model
-
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.Index
-import androidx.room.PrimaryKey
-import java.util.Date
-
-@Entity(tableName = "ai_chat_messages",
- foreignKeys = [
- ForeignKey(
- entity = AiChatSession::class,
- parentColumns = ["id"],
- childColumns = ["sessionId"],
- onDelete = ForeignKey.CASCADE
- )
- ],
- indices = [Index(value = ["sessionId"])]
-)
-data class AiChatMessage(
- @PrimaryKey(autoGenerate = true) val id: Long = 0,
- val sessionId: Long,
- val text: String,
- val participant: Participant,
- val timestamp: Date = Date(),
- val isLoading: Boolean = false
-)
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/model/AiChatSession.kt b/app/src/main/java/com/babelsoftware/airnote/domain/model/AiChatSession.kt
deleted file mode 100644
index 63c99d6..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/model/AiChatSession.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.babelsoftware.airnote.domain.model
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import java.util.Date
-
-@Entity(tableName = "ai_chat_sessions")
-data class AiChatSession(
- @PrimaryKey(autoGenerate = true) val id: Long = 0,
- val title: String,
- val createdAt: Date = Date(),
- val aiMode: String,
- val serviceName: String
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/model/AiResponse.kt b/app/src/main/java/com/babelsoftware/airnote/domain/model/AiResponse.kt
deleted file mode 100644
index a6f7fb9..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/model/AiResponse.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.babelsoftware.airnote.domain.model
-
-import com.google.gson.annotations.SerializedName
-
-/**
- * Data class representing the JSON response from Gemini.
- * Depending on the model's “intent,” it carries either a chat response or new note content.
- */
-data class AiResponse(
- @SerializedName("intent") val intent: String,
- @SerializedName("response") val response: String? = null,
- @SerializedName("new_note_content") val newNoteContent: String? = null
-)
-
-/**
- * Defines the AI's possible intents
- */
-enum class AiIntent {
- CHAT,
- EDIT_NOTE,
- UNKNOWN
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/model/Chat.kt b/app/src/main/java/com/babelsoftware/airnote/domain/model/Chat.kt
deleted file mode 100644
index 384cdd4..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/model/Chat.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.babelsoftware.airnote.domain.model
-
-import androidx.compose.ui.graphics.vector.ImageVector
-
-// Indicates who the message belongs to (User or Model)
-enum class Participant {
- USER, MODEL, ERROR
-}
-
-// Data class representing a single chat message
-data class ChatMessage(
- val text: String,
- val participant: Participant,
- val isLoading: Boolean = false // Loading animation while waiting for the model response
-)
-
-data class AiSuggestion(
- val title: String,
- val icon: ImageVector,
- val action: () -> Unit // Tıklandığında ViewModel'deki hangi fonksiyonu çağıracağı
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/model/Folder.kt b/app/src/main/java/com/babelsoftware/airnote/domain/model/Folder.kt
deleted file mode 100644
index 9eaca7f..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/model/Folder.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.domain.model
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-
-@Entity
-data class Folder(
- @PrimaryKey(autoGenerate = true)
- val id: Long = 0,
- val name: String,
- val iconName: String = "Folder",
- val createdAt: Long = System.currentTimeMillis()
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/model/Note.kt b/app/src/main/java/com/babelsoftware/airnote/domain/model/Note.kt
deleted file mode 100644
index f1cb429..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/model/Note.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.babelsoftware.airnote.domain.model
-
-import androidx.room.ColumnInfo
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-
-@Entity(tableName = "notes-table")
-data class Note(
- @PrimaryKey(autoGenerate = true)
- val id: Int = 0,
-
- @ColumnInfo(name = "note-name")
- val name: String,
-
- @ColumnInfo(name = "note-description")
- val description: String,
-
- @ColumnInfo(name = "pinned")
- val pinned: Boolean = false,
-
- @ColumnInfo(name = "foldered")
- val folderId: Long? = null,
-
- @ColumnInfo(name = "encrypted")
- val encrypted: Boolean = false,
-
- @ColumnInfo(name = "is_locked")
- val isLocked: Boolean = false,
-
- @ColumnInfo(name = "created_at")
- val createdAt: Long = System.currentTimeMillis() // Default value is the current timestamp
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/model/Settings.kt b/app/src/main/java/com/babelsoftware/airnote/domain/model/Settings.kt
deleted file mode 100644
index 4557d81..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/model/Settings.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.babelsoftware.airnote.domain.model
-
-import com.babelsoftware.airnote.presentation.navigation.NavRoutes
-
-data class Settings(
- var defaultRouteType: String = NavRoutes.Home.route,
- var passcode: String? = null,
- var fingerprint: Boolean = false,
- var pattern: String? = null,
- var appLockEnabled: Boolean = false,
- val viewMode: Boolean = true,
- val automaticTheme: Boolean = true,
- val darkTheme: Boolean = false,
- var dynamicTheme: Boolean = false,
- var amoledTheme: Boolean = false,
- var minimalisticMode: Boolean = false,
- var extremeAmoledMode: Boolean = false,
- var isMarkdownEnabled: Boolean = true,
- var screenProtection: Boolean = false,
- var encryptBackup: Boolean = false,
- var sortDescending: Boolean = true,
- var vaultSettingEnabled: Boolean = false,
- var vaultEnabled: Boolean = false,
- var editMode: Boolean = false,
- var gallerySync: Boolean = true,
- var showOnlyTitle: Boolean = false,
- var termsOfService: Boolean = false,
- var useMonoSpaceFont: Boolean = false,
- var lockImmediately: Boolean = true,
- var cornerRadius: Int = 28,
- var disableSwipeInEditMode: Boolean = false,
- var makeSearchBarLonger: Boolean = false,
- var fontSize: Int = 13,
- val showFolderIndicator: Boolean = false,
- val useAirNoteApi: Boolean = false,
- val selectedModelName: String = "gemini-2.0-flash-001",
- val selectedPerplexityModelName: String = "pplx-7b-online",
- val isPerplexityEnabled: Boolean = false,
- val desktopModeEnabled: Boolean = true,
- val desktopModeAiEnabled: Boolean = true,
- val openToLastUsedFolder: Boolean = false,
- val lastUsedFolderId: Long? = null,
- var noteLockPassword: String? = null,
- var vaultEncryptionType: String = "AES-256"
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/repository/FolderRepository.kt b/app/src/main/java/com/babelsoftware/airnote/domain/repository/FolderRepository.kt
deleted file mode 100644
index d3d2b96..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/repository/FolderRepository.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.domain.repository
-
-import com.babelsoftware.airnote.domain.model.Folder
-import kotlinx.coroutines.flow.Flow
-
-interface FolderRepository {
- fun getAllFolders(): Flow>
- suspend fun insertFolder(folder: Folder)
- suspend fun updateFolder(folder: Folder)
- suspend fun deleteFolder(folder: Folder)
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/repository/NoteRepository.kt b/app/src/main/java/com/babelsoftware/airnote/domain/repository/NoteRepository.kt
deleted file mode 100644
index 7dd12e1..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/repository/NoteRepository.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.babelsoftware.airnote.domain.repository
-
-import com.babelsoftware.airnote.domain.model.Note
-import kotlinx.coroutines.flow.Flow
-
-interface NoteRepository {
- fun getAllNotes(): Flow>
- suspend fun addNote(note: Note)
- suspend fun updateNote(note: Note)
- suspend fun deleteNote(note: Note)
- suspend fun unlinkNotesFromFolder(folderId: Long)
- fun getNoteById(id: Int): Flow
- fun getLastNoteId(): Long?
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/repository/SettingsRepository.kt b/app/src/main/java/com/babelsoftware/airnote/domain/repository/SettingsRepository.kt
deleted file mode 100644
index 08bc471..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/repository/SettingsRepository.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.babelsoftware.airnote.domain.repository
-
-import androidx.datastore.preferences.core.Preferences
-import com.babelsoftware.airnote.domain.model.Settings
-import kotlinx.coroutines.flow.Flow
-
-interface SettingsRepository {
- val settings: Flow
-
- suspend fun update(settings: Settings)
-
- suspend fun getPreferences(): Preferences
- suspend fun putString(key: String, value: String)
- suspend fun putInt(key: String, value: Int)
- suspend fun getString(key: String): String?
- suspend fun getInt(key: String): Int?
- suspend fun putBoolean(key: String, value: Boolean)
- suspend fun getBoolean(key: String): Boolean?
- suspend fun getEveryNotesWidget(): List>
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/usecase/AiChatUseCase.kt b/app/src/main/java/com/babelsoftware/airnote/domain/usecase/AiChatUseCase.kt
deleted file mode 100644
index ea458bc..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/usecase/AiChatUseCase.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.babelsoftware.airnote.domain.usecase
-
-import com.babelsoftware.airnote.data.local.dao.AiChatDao
-import com.babelsoftware.airnote.data.repository.AiMode
-import com.babelsoftware.airnote.domain.model.AiChatMessage
-import com.babelsoftware.airnote.domain.model.AiChatSession
-import com.babelsoftware.airnote.domain.model.ChatMessage
-import com.babelsoftware.airnote.domain.model.Participant
-import kotlinx.coroutines.flow.Flow
-import javax.inject.Inject
-
-class AiChatUseCase @Inject constructor(
- private val aiChatDao: AiChatDao
-) {
-
- fun getAllSessions(): Flow> {
- return aiChatDao.getAllSessions()
- }
-
- fun getMessagesForSession(sessionId: Long): Flow> {
- return aiChatDao.getMessagesForSession(sessionId)
- }
-
- suspend fun startNewSession(title: String, aiMode: AiMode, serviceName: String): Long {
- val session = AiChatSession(
- title = title,
- aiMode = aiMode.name,
- serviceName = serviceName
- )
- return aiChatDao.insertSession(session)
- }
-
- suspend fun addMessageToSession(sessionId: Long, message: ChatMessage): Long {
- val aiChatMessage = AiChatMessage(
- sessionId = sessionId,
- text = message.text,
- participant = message.participant,
- isLoading = message.isLoading
- )
- return aiChatDao.insertMessage(aiChatMessage)
- }
-
- suspend fun updateMessageById(id: Long, text: String, isLoading: Boolean, isError: Boolean = false) {
- val participant = if (isError) Participant.ERROR else Participant.MODEL
- aiChatDao.updateMessageById(id, text, isLoading, participant)
- }
-
- suspend fun getSessionById(sessionId: Long): AiChatSession? {
- return aiChatDao.getSessionById(sessionId)
- }
-
- suspend fun updateSessionMode(sessionId: Long, aiMode: AiMode) {
- aiChatDao.updateSessionMode(sessionId, aiMode.name)
- }
-
- suspend fun deleteSessionById(sessionId: Long) {
- aiChatDao.deleteSessionById(sessionId)
- }
-
- suspend fun deleteMessageById(id: Long) {
- aiChatDao.deleteMessageById(id)
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/usecase/FolderUseCase.kt b/app/src/main/java/com/babelsoftware/airnote/domain/usecase/FolderUseCase.kt
deleted file mode 100644
index cc3073b..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/usecase/FolderUseCase.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.domain.usecase
-
-import com.babelsoftware.airnote.domain.model.Folder
-import com.babelsoftware.airnote.domain.repository.FolderRepository
-import com.babelsoftware.airnote.domain.repository.NoteRepository
-import kotlinx.coroutines.flow.Flow
-import javax.inject.Inject
-
-class FolderUseCase @Inject constructor(
- private val folderRepository: FolderRepository,
- private val noteRepository: NoteRepository
-) {
- fun getAllFolders(): Flow> {
- return folderRepository.getAllFolders()
- }
-
- suspend fun addFolder(folder: Folder) {
- folderRepository.insertFolder(folder)
- }
-
- suspend fun updateFolder(folder: Folder) {
- folderRepository.updateFolder(folder)
- }
-
- suspend fun deleteFolder(folder: Folder) {
- folderRepository.deleteFolder(folder)
- }
-
- suspend fun deleteFolderAndUnlinkNotes(folder: Folder) {
- noteRepository.unlinkNotesFromFolder(folder.id)
- folderRepository.deleteFolder(folder)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/usecase/ImportExportUseCase.kt b/app/src/main/java/com/babelsoftware/airnote/domain/usecase/ImportExportUseCase.kt
deleted file mode 100644
index d0c7d7c..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/usecase/ImportExportUseCase.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.babelsoftware.airnote.domain.usecase
-
-import android.net.Uri
-import android.util.Log
-import com.babelsoftware.airnote.data.repository.ImportExportRepository
-import com.babelsoftware.airnote.data.repository.NoteRepositoryImpl
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.NonCancellable
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.io.IOException
-import javax.inject.Inject
-
-data class ImportResult(
- val successful: Int,
- val total: Int,
-)
-
-class ImportExportUseCase @Inject constructor(
- private val noteRepository: NoteRepositoryImpl,
- private val coroutineScope: CoroutineScope,
- private val fileRepository: ImportExportRepository,
-)
-{
- /**
- * Import notes from text files.
- */
- fun importNotes(uris: List, onResult: (ImportResult) -> Unit) {
- coroutineScope.launch(NonCancellable + Dispatchers.IO) {
- var successful = 0
- uris.forEach{uri ->
- try {
- noteRepository.addNote(fileRepository.importFile(uri))
- successful++
- } catch (e: IOException) {
- Log.e(ImportExportUseCase::class.simpleName, e.message, e)
- }
- }
- withContext(Dispatchers.Main) {
- onResult(ImportResult(successful, uris.size))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/usecase/NoteUseCase.kt b/app/src/main/java/com/babelsoftware/airnote/domain/usecase/NoteUseCase.kt
deleted file mode 100644
index 3d17eb5..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/usecase/NoteUseCase.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-package com.babelsoftware.airnote.domain.usecase
-
-import android.content.Context
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.glance.appwidget.updateAll
-import com.babelsoftware.airnote.data.repository.NoteRepositoryImpl
-import com.babelsoftware.airnote.domain.model.Note
-import com.babelsoftware.airnote.presentation.components.DecryptionResult
-import com.babelsoftware.airnote.presentation.components.EncryptionHelper
-import com.babelsoftware.airnote.widget.NotesWidget
-import dagger.hilt.android.qualifiers.ApplicationContext
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.NonCancellable
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import javax.inject.Inject
-
-class NoteUseCase @Inject constructor(
- private val noteRepository: NoteRepositoryImpl,
- private val coroutineScope: CoroutineScope,
- private val encryptionHelper: EncryptionHelper,
- @ApplicationContext private val context: Context
-) {
- var notes: List by mutableStateOf(emptyList())
- private set
-
- var decryptionResult: DecryptionResult by mutableStateOf(DecryptionResult.LOADING)
-
- private var observeKeysJob: Job? = null
-
- fun observe() {
- observeNotes()
- }
-
- private fun observeNotes() {
- observeKeysJob?.cancel()
- observeKeysJob = coroutineScope.launch {
- getAllNotes().collectLatest { notes ->
- val hasUnencryptedNotes = notes.any { !it.encrypted }
- if (!hasUnencryptedNotes) this@NoteUseCase.decryptionResult = DecryptionResult.EMPTY
- val processedNotes = notes.mapNotNull { note ->
- if (note.encrypted) {
- val (decryptedNote, status) = decryptNote(note)
- this@NoteUseCase.decryptionResult = status
- if (status == DecryptionResult.SUCCESS) decryptedNote else null
- } else {
- note
- }
- }
- this@NoteUseCase.notes = processedNotes
- NotesWidget().updateAll(context)
- }
- }
- }
-
- private fun encryptNote(note: Note): Note {
- return if (note.encrypted) {
- note.copy(
- name = encryptionHelper.encrypt(note.name),
- description = encryptionHelper.encrypt(note.description),
- encrypted = true
- )
- } else {
- note
- }
- }
-
- private fun decryptNote(note: Note): Pair {
- val (decryptedName, nameResult) = encryptionHelper.decrypt(note.name)
- val (decryptedDescription, descriptionResult) = encryptionHelper.decrypt(note.description)
- return if (note.encrypted) {
- Pair(note.copy(
- name = decryptedName ?: "",
- description = decryptedDescription ?: "",
- ), descriptionResult)
- } else {
- Pair(note, DecryptionResult.SUCCESS)
- }
- }
-
- fun getAllNotes(): Flow> {
- return noteRepository.getAllNotes()
- }
-
- suspend fun addNote(note: Note) {
- val noteToSave = encryptNote(note)
- if (note.id == 0) {
- noteRepository.addNote(noteToSave)
- } else {
- noteRepository.updateNote(noteToSave)
- }
- }
-
- fun pinNote(note: Note) {
- coroutineScope.launch(NonCancellable + Dispatchers.IO) {
- addNote(note)
- }
- }
-
- fun deleteNoteById(id: Int) {
- coroutineScope.launch(NonCancellable + Dispatchers.IO) {
- val noteToDelete = noteRepository.getNoteById(id).first()
- noteRepository.deleteNote(noteToDelete)
- }
- }
-
- fun getNoteById(id: Int): Flow {
- return noteRepository.getNoteById(id)
- }
-
- fun getLastNoteId(onResult: (Long?) -> Unit) {
- coroutineScope.launch(NonCancellable + Dispatchers.IO) {
- val lastNoteId = noteRepository.getLastNoteId()
- withContext(Dispatchers.Main) {
- onResult(lastNoteId)
- }
- }
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/domain/usecase/SettingsUseCase.kt b/app/src/main/java/com/babelsoftware/airnote/domain/usecase/SettingsUseCase.kt
deleted file mode 100644
index 155b203..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/domain/usecase/SettingsUseCase.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.babelsoftware.airnote.domain.usecase
-
-import android.content.Context
-import androidx.datastore.preferences.core.stringPreferencesKey
-import com.babelsoftware.airnote.constant.ConnectionConst
-import com.babelsoftware.airnote.domain.model.Settings
-import com.babelsoftware.airnote.domain.repository.SettingsRepository
-import dagger.hilt.android.qualifiers.ApplicationContext
-import javax.inject.Inject
-
-class SettingsUseCase @Inject constructor(
- @ApplicationContext private val context: Context,
- private val settingsRepository: SettingsRepository,
-) {
-
- suspend fun loadSettingsFromRepository(): Settings {
- return Settings().apply {
- Settings::class.java.declaredFields.forEach { field ->
- field.isAccessible = true
- val settingName = field.name
- val defaultValue = field.get(this)
- val settingValue = getSettingValue(field.type, settingName, defaultValue)
- field.set(this, settingValue)
- }
- }
- }
-
- private suspend fun getSettingValue(fieldType: Class<*>, settingName: String, defaultValue: Any?): Any? {
- return try {
- when (fieldType) {
- Boolean::class.java -> settingsRepository.getBoolean(settingName) ?: defaultValue
- String::class.java -> settingsRepository.getString(settingName) ?: defaultValue
- Int::class.java -> settingsRepository.getInt(settingName) ?: defaultValue
- else -> defaultValue // Allow unsupported types to default to null or the defaultValue.
- }
- } catch (e: ClassCastException) {
- handleCorruptedPreference(settingName, e)
- defaultValue
- }
- }
-
- private fun handleCorruptedPreference(settingName: String, e: ClassCastException) {
- println("Corrupted preference. Contact support: ${ConnectionConst.SUPPORT_MAIL}")
- println("Invalid Key: $settingName")
- println(e.stackTraceToString())
- }
-
- suspend fun saveSettingsToRepository(settings: Settings) {
- Settings::class.java.declaredFields.forEach { field ->
- field.isAccessible = true
- val settingName = field.name
- val settingValue = field.get(settings)
- saveSettingValue(settingName, settingValue)
- }
- }
-
- private suspend fun saveSettingValue(settingName: String, settingValue: Any?) {
- when (settingValue) {
- is Boolean -> settingsRepository.putBoolean(settingName, settingValue)
- is String -> settingsRepository.putString(settingName, settingValue)
- is Int -> settingsRepository.putInt(settingName, settingValue)
- null -> settingsRepository.getPreferences().toMutablePreferences().remove(stringPreferencesKey(settingName))
- else -> throw IllegalArgumentException("Unsupported setting type: ${settingValue?.javaClass}")
- }
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/MainActivity.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/MainActivity.kt
deleted file mode 100644
index 7ed3f24..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/MainActivity.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.babelsoftware.airnote.presentation
-
-import android.content.Intent
-import android.os.Bundle
-import android.provider.MediaStore
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.navigation.NavController
-import androidx.navigation.NavHostController
-import androidx.navigation.NavOptionsBuilder
-import androidx.navigation.compose.rememberNavController
-import com.babelsoftware.airnote.presentation.navigation.AppNavHost
-import com.babelsoftware.airnote.presentation.navigation.NavRoutes
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.theme.LeafNotesTheme
-import dagger.hilt.android.AndroidEntryPoint
-
-fun NavOptionsBuilder.popUpToTop(navController: NavController) {
- popUpTo(navController.currentBackStackEntry?.destination?.route ?: return) {
- inclusive = true
- }
-}
-
-@AndroidEntryPoint
-class MainActivity : AppCompatActivity() {
- private lateinit var navController: NavHostController
- private var settingsViewModel: SettingsViewModel? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- installSplashScreen()
- enableEdgeToEdge()
-
- setContent {
- settingsViewModel = hiltViewModel()
- settingsViewModel!!.loadDefaultRoute()
-
- val noteId = intent?.getIntExtra("noteId", -1) ?: -1
- val isShareIntent = intent?.action == Intent.ACTION_SEND && "text/plain" == intent.type
- val startRoute = settingsViewModel!!.defaultRoute!!
-
- if (settingsViewModel!!.settings.value.gallerySync) {
- contentResolver.registerContentObserver(
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- true,
- settingsViewModel!!.galleryObserver
- )
- }
-
- LeafNotesTheme(settingsViewModel!!) {
-
- Surface(
- color = MaterialTheme.colorScheme.surfaceContainerLow,
- ) {
- navController = rememberNavController()
- AppNavHost(
- settingsModel = settingsViewModel!!,
- navController = navController,
- noteId = noteId,
- defaultRoute = startRoute,
- isShareIntent = isShareIntent
- )
- }
- }
- }
- }
-
- override fun onResume() {
- super.onResume()
- settingsViewModel?.let {
- if (it.defaultRoute != NavRoutes.Home.route) {
- if (it.settings.value.passcode != null || it.settings.value.fingerprint || it.settings.value.pattern != null) {
- if (it.settings.value.lockImmediately) {
- if (::navController.isInitialized) {
- navController.navigate(it.defaultRoute!!) { popUpToTop(navController) }
- }
- }
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/Animations.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/Animations.kt
deleted file mode 100644
index 2b77363..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/Animations.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.babelsoftware.airnote.presentation.components
-
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.scaleIn
-import androidx.compose.animation.scaleOut
-import androidx.compose.animation.slideInHorizontally
-import androidx.compose.animation.slideOutHorizontally
-
-private const val DEFAULT_FADE_DURATION = 300
-private const val DEFAULT_SCALE_DURATION = 400
-private const val DEFAULT_SLIDE_DURATION = 400
-private const val DEFAULT_INITIAL_SCALE = 0.9f
-
-fun getNoteEnterAnimation(): EnterTransition {
- return fadeIn(animationSpec = tween(DEFAULT_FADE_DURATION)) + scaleIn(
- initialScale = 0.9f,
- animationSpec = tween(DEFAULT_SCALE_DURATION)
- )
-}
-
-fun getNoteExitAnimation(slideDirection: Int): ExitTransition {
- return slideOutHorizontally(
- targetOffsetX = { slideDirection * it },
- animationSpec = tween(durationMillis = DEFAULT_SLIDE_DURATION)
- ) + fadeOut(animationSpec = tween(durationMillis = DEFAULT_FADE_DURATION))
-}
-
-fun defaultScreenEnterAnimation(): EnterTransition {
- return fadeIn(animationSpec = tween(DEFAULT_FADE_DURATION)) +
- scaleIn(
- initialScale = DEFAULT_INITIAL_SCALE,
- animationSpec = tween(DEFAULT_SCALE_DURATION)
- )
-}
-
-fun defaultScreenExitAnimation(): ExitTransition {
- return fadeOut(animationSpec = tween(DEFAULT_FADE_DURATION)) +
- scaleOut(
- targetScale = DEFAULT_INITIAL_SCALE,
- animationSpec = tween(DEFAULT_SCALE_DURATION)
- )
-}
-
-fun slideScreenEnterAnimation(): EnterTransition {
- return slideInHorizontally(
- initialOffsetX = { fullWidth -> fullWidth },
- animationSpec = tween(DEFAULT_SLIDE_DURATION)
- )
-}
-
-fun slideScreenExitAnimation(): ExitTransition {
- return slideOutHorizontally(
- targetOffsetX = { fullWidth -> fullWidth },
- animationSpec = tween(DEFAULT_SLIDE_DURATION)
- )
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/AutoFill.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/AutoFill.kt
deleted file mode 100644
index 4ca1d7d..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/AutoFill.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-package com.babelsoftware.airnote.presentation.components
-
-import android.view.autofill.AutofillManager
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.Autofill
-import androidx.compose.ui.autofill.AutofillNode
-import androidx.compose.ui.autofill.AutofillType
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.boundsInWindow
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalAutofill
-import androidx.compose.ui.platform.LocalAutofillTree
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalView
-import kotlin.math.roundToInt
-
-fun Modifier.connectNode(handler: AutoFillHandler): Modifier {
- return with(handler) { fillBounds() }
-}
-
-fun Modifier.defaultFocusChangeAutoFill(handler: AutoFillHandler): Modifier {
- return this.then(
- Modifier.onFocusChanged {
- if (it.isFocused) {
- handler.request()
- } else {
- handler.cancel()
- }
- }
- )
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-@Composable
-fun AutoFillRequestHandler(
- autofillTypes: List = listOf(),
- onFill: (String) -> Unit,
-): AutoFillHandler {
- val view = LocalView.current
- val context = LocalContext.current
- var isFillRecently = remember { false }
- val autoFillNode = remember {
- AutofillNode(
- autofillTypes = autofillTypes,
- onFill = {
- isFillRecently = true
- onFill(it)
- }
- )
- }
- val autofill = LocalAutofill.current
- LocalAutofillTree.current += autoFillNode
- return remember {
- object : AutoFillHandler {
- val autofillManager = context.getSystemService(AutofillManager::class.java)
- override fun requestManual() {
- autofillManager.requestAutofill(
- view,
- autoFillNode.id,
- autoFillNode.boundingBox?.toAndroidRect() ?: error("BoundingBox is not provided yet")
- )
- }
-
- override fun requestVerifyManual() {
- if (isFillRecently) {
- isFillRecently = false
- requestManual()
- }
- }
-
- override val autoFill: Autofill?
- get() = autofill
-
- override val autoFillNode: AutofillNode
- get() = autoFillNode
-
- override fun request() {
- autofill?.requestAutofillForNode(autofillNode = autoFillNode)
- }
-
- override fun cancel() {
- autofill?.cancelAutofillForNode(autofillNode = autoFillNode)
- }
-
- override fun Modifier.fillBounds(): Modifier {
- return this.then(
- Modifier.onGloballyPositioned {
- autoFillNode.boundingBox = it.boundsInWindow()
- })
- }
- }
- }
-}
-
-fun Rect.toAndroidRect(): android.graphics.Rect {
- return android.graphics.Rect(
- left.roundToInt(),
- top.roundToInt(),
- right.roundToInt(),
- bottom.roundToInt()
- )
-}
-
-interface AutoFillHandler {
- @OptIn(ExperimentalComposeUiApi::class)
- val autoFill: Autofill?
- @OptIn(ExperimentalComposeUiApi::class)
- val autoFillNode: AutofillNode
- fun requestVerifyManual()
- fun requestManual()
- fun request()
- fun cancel()
- fun Modifier.fillBounds(): Modifier
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/CustomActionButton.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/CustomActionButton.kt
deleted file mode 100644
index 7ab9951..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/CustomActionButton.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.babelsoftware.airnote.presentation.components
-
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.CheckCircle
-import androidx.compose.material.icons.rounded.Edit
-import androidx.compose.material3.ExtendedFloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-
-
-@Composable
-fun NotesButton(
- text: String,
- onClick: () -> Unit
-) {
- ExtendedFloatingActionButton(
- modifier = Modifier.imePadding(),
- shape = RoundedCornerShape(24.dp),
- onClick = { onClick() },
- icon = { Icon(Icons.Rounded.Edit, text) },
- text = { Text(text = text) },
- )
-}
-
-@Composable
-fun AgreeButton(
- text: String,
- modifier: Modifier,
- onClick: () -> Unit,
-) {
- ExtendedFloatingActionButton(
- modifier = Modifier.imePadding(),
- shape = RoundedCornerShape(24.dp),
- onClick = { onClick() },
- icon = { Icon(Icons.Rounded.CheckCircle, text) },
- text = { Text(text = text) },
- )
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/CustomActions.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/CustomActions.kt
deleted file mode 100644
index 4606036..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/CustomActions.kt
+++ /dev/null
@@ -1,158 +0,0 @@
-package com.babelsoftware.airnote.presentation.components
-
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.ArrowBack
-import androidx.compose.material.icons.automirrored.rounded.Redo
-import androidx.compose.material.icons.automirrored.rounded.Undo
-import androidx.compose.material.icons.outlined.PushPin
-import androidx.compose.material.icons.rounded.Close
-import androidx.compose.material.icons.rounded.Delete
-import androidx.compose.material.icons.rounded.Done
-import androidx.compose.material.icons.rounded.Lock
-import androidx.compose.material.icons.rounded.LockOpen
-import androidx.compose.material.icons.rounded.MoreVert
-import androidx.compose.material.icons.rounded.PushPin
-import androidx.compose.material.icons.rounded.SelectAll
-import androidx.compose.material.icons.rounded.Settings
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-
-@Composable
-fun CloseButton(
- contentDescription: String = "Close",
- onCloseClicked: () -> Unit
-) {
- IconButton(onClick = onCloseClicked) {
- Icon(
- imageVector = Icons.Rounded.Close,
- contentDescription = contentDescription,
- tint = MaterialTheme.colorScheme.onBackground
- )
- }
-}
-
-@Composable
-fun MoreButton(onClick: () -> Unit) {
- IconButton(onClick = { onClick() }) {
- Icon(Icons.Rounded.MoreVert, contentDescription = "Info")
- }
-}
-
-@Composable
-fun SaveButton(onSaveClicked: () -> Unit) {
- IconButton(onClick = onSaveClicked) {
- Icon(
- imageVector = Icons.Rounded.Done,
- contentDescription = "Done",
- tint = MaterialTheme.colorScheme.onBackground
- )
- }
-}
-
-@Composable
-fun UndoButton(onUndoClicked: () -> Unit) {
- IconButton(onClick = onUndoClicked) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.Undo,
- contentDescription = "Undo",
- tint = MaterialTheme.colorScheme.outlineVariant
- )
- }
-}
-
-@Composable
-fun RedoButton(onRedoClicked: () -> Unit) {
- IconButton(onClick = onRedoClicked) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.Redo,
- contentDescription = "Redo",
- tint = MaterialTheme.colorScheme.outlineVariant
- )
- }
-}
-
-@Composable
-fun NavigationIcon(onBackNavClicked: () -> Unit) {
- IconButton(onClick = onBackNavClicked) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
- contentDescription = "Back",
- tint = MaterialTheme.colorScheme.onBackground
- )
- }
-}
-
-@Composable
-fun SettingsButton(onSettingsClicked: () -> Unit) {
- IconButton(onClick = onSettingsClicked) {
- Icon(
- imageVector = Icons.Rounded.Settings,
- contentDescription = "Settings",
- tint = MaterialTheme.colorScheme.onBackground
- )
- }
-}
-
-@Composable
-fun VaultButton(vaultEnabled: Boolean,onVaultButtonClicked: () -> Unit) {
- IconButton(onClick = onVaultButtonClicked) {
- Icon(
- imageVector = if (vaultEnabled) Icons.Rounded.Lock else Icons.Rounded.LockOpen,
- contentDescription = "Vault",
- tint = MaterialTheme.colorScheme.onBackground
- )
- }
-}
-
-@Composable
-fun TitleText(titleText: String) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = titleText,
- modifier = Modifier.weight(1f)
- )
- }
-}
-
-@Composable
-fun PinButton(isPinned: Boolean, onClick: () -> Unit) {
- IconButton(onClick = { onClick() }) {
- Icon(if (isPinned) Icons.Rounded.PushPin else Icons.Outlined.PushPin, contentDescription = "Pin")
- }
-}
-
-@Composable
-fun DeleteButton(onClick: () -> Unit) {
- IconButton(
- onClick = { onClick() }
- ) {
- Icon(
- imageVector = Icons.Rounded.Delete,
- contentDescription = "Delete",
- )
- }
-}
-
-@Composable
-fun SelectAllButton(enabled: Boolean, onClick: () -> Unit) {
- if (enabled) {
- IconButton(
- onClick = { onClick() }
- ) {
- Icon(
- imageVector = Icons.Rounded.SelectAll,
- contentDescription = "Select All",
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/CustomScaffold.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/CustomScaffold.kt
deleted file mode 100644
index 7d8d68c..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/CustomScaffold.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.presentation.components
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-
-@Composable
-fun NotesScaffold(
- modifier: Modifier = Modifier,
- topBar : @Composable () -> Unit = {},
- floatingActionButton: @Composable () -> Unit = {},
- snackbarHost: @Composable () -> Unit = {},
- // DEĞİŞİKLİK: content parametresini tekrar orijinal, basit haline getiriyoruz.
- content: @Composable () -> Unit
-) {
- Scaffold(
- modifier = modifier,
- snackbarHost = snackbarHost,
- containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
- topBar = { topBar() },
- floatingActionButton = { floatingActionButton() },
- ) { innerPadding ->
- Box(modifier = Modifier.padding(innerPadding).consumeWindowInsets(innerPadding)) {
- content()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/EncryptionHelper.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/EncryptionHelper.kt
deleted file mode 100644
index 46f8172..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/EncryptionHelper.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-package com.babelsoftware.airnote.presentation.components
-
-import android.util.Base64
-import java.nio.charset.StandardCharsets
-import java.security.MessageDigest
-import javax.crypto.Cipher
-import javax.crypto.SecretKey
-import javax.crypto.spec.GCMParameterSpec
-import javax.crypto.spec.SecretKeySpec
-
-class EncryptionHelper(private val mutableVaultPassword: StringBuilder) {
-
- fun isPasswordEmpty(): Boolean {
- return mutableVaultPassword.isEmpty()
- }
-
- fun removePassword() {
- mutableVaultPassword.setLength(0)
- }
-
- fun setPassword(newPassword: String) {
- mutableVaultPassword.setLength(0)
- mutableVaultPassword.append(newPassword)
- }
-
- fun encrypt(data: String): String {
- val secretKey = generateSecretKey(mutableVaultPassword.toString())
- val cipher = Cipher.getInstance("AES/GCM/NoPadding")
- cipher.init(Cipher.ENCRYPT_MODE, secretKey)
- val ivBytes = cipher.iv
- val encryptedBytes = cipher.doFinal(data.toByteArray(StandardCharsets.UTF_8))
- return "${Base64.encodeToString(ivBytes, Base64.DEFAULT)}:${Base64.encodeToString(encryptedBytes, Base64.DEFAULT)}"
- }
-
- fun decrypt(data: String): Pair {
- if (mutableVaultPassword.isEmpty()) return Pair(null, DecryptionResult.LOADING)
- if (data.isBlank()) return Pair(null, DecryptionResult.BLANK_DATA)
-
- return try {
- val split = data.split(":")
- if (split.size != 2) return Pair(null, DecryptionResult.INVALID_DATA)
- val ivBytes = Base64.decode(split[0], Base64.DEFAULT)
- val encryptedBytes = Base64.decode(split[1], Base64.DEFAULT)
- val secretKey = generateSecretKey(mutableVaultPassword.toString())
- val cipher = Cipher.getInstance("AES/GCM/NoPadding")
- val spec = GCMParameterSpec(128, ivBytes)
- cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
- val decryptedBytes = cipher.doFinal(encryptedBytes)
- val decryptedData = String(decryptedBytes, StandardCharsets.UTF_8)
- Pair(decryptedData, DecryptionResult.SUCCESS)
- } catch (e: Exception) {
- Pair(null, DecryptionResult.BAD_PASSWORD)
- }
- }
-
- private fun generateSecretKey(password: String): SecretKey {
- val keySpec = SecretKeySpec(password.toByteArray(StandardCharsets.UTF_8), "AES")
- val sha256 = MessageDigest.getInstance("SHA-256")
- val keyBytes = sha256.digest(keySpec.encoded)
- return SecretKeySpec(keyBytes, "AES")
- }
-}
-
-enum class DecryptionResult {
- EMPTY,
- SUCCESS,
- INVALID_DATA,
- BLANK_DATA,
- BAD_PASSWORD,
- LOADING
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/ImageCleaner.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/ImageCleaner.kt
deleted file mode 100644
index 4312272..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/ImageCleaner.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.babelsoftware.airnote.presentation.components
-
-import android.content.Context
-import android.database.ContentObserver
-import android.net.Uri
-import android.os.Handler
-import dagger.hilt.android.qualifiers.ApplicationContext
-import java.io.File
-import javax.inject.Inject
-
-// GalleryObserver class
-class GalleryObserver @Inject constructor(
- handler: Handler,
- @ApplicationContext private val context: Context
-) : ContentObserver(handler) {
- override fun onChange(selfChange: Boolean, uri: Uri?) {
- super.onChange(selfChange, uri)
- uri?.let {
- deleteFileIfExists(context, getImageName(uri))
- }
- }
-}
-
-// Function to unregister the GalleryObserver
-fun unregisterGalleryObserver(context: Context, observer: GalleryObserver) {
- context.contentResolver.unregisterContentObserver(observer)
-}
-
-// Function to delete the file if it exists
-fun deleteFileIfExists(context: Context, fileName: String): Boolean {
- val appStorageDir = getExternalStorageDir(context)
- val file = File(appStorageDir, fileName)
- return if (file.exists()) {
- file.delete()
- } else {
- false
- }
-}
-
-// Function to get the external storage directory
-fun getExternalStorageDir(context: Context): File {
- return context.getExternalFilesDir(null) ?: throw IllegalStateException("External storage directory not found")
-}
-
-// Function to get the image name from the URI
-fun getImageName(uri: Uri?): String {
- return if (uri != null) {
- uri.lastPathSegment?.filter { it.isDigit() }?.takeLast(10) ?: (1000..99999).random().toString()
- } else {
- (1000..99999).random().toString()
- } + ".jpg"
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/NoteItem.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/NoteItem.kt
deleted file mode 100644
index 587ee3e..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/NoteItem.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.presentation.components
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import com.babelsoftware.airnote.domain.model.Folder
-import com.babelsoftware.airnote.domain.model.Note
-
-@Composable
-fun FolderIndicator(
- folderName: String,
- modifier: Modifier = Modifier
-) {
- Surface(
- modifier = modifier.padding(top = 8.dp, end = 8.dp),
- shape = RoundedCornerShape(percent = 50),
- color = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.9f),
- tonalElevation = 2.dp
- ) {
- Text(
- text = folderName,
- modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
- style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.onTertiaryContainer
- )
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun NoteItem(
- note: Note,
- allFolders: List,
- onClick: () -> Unit
-) {
- val folder = note.folderId?.let { id ->
- allFolders.find { it.id == id }
- }
-
- Card(
- onClick = onClick,
- modifier = Modifier.fillMaxWidth(),
- shape = MaterialTheme.shapes.large,
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceVariant
- )
- ) {
- Box(
- modifier = Modifier.fillMaxWidth()
- ) {
- Column(
- modifier = Modifier
- .padding(16.dp)
- .fillMaxWidth()
- ) {
- // DÜZELTME: 'note.title' yerine 'note.name' kullanıldı.
- Text(
- text = note.name, //
- style = MaterialTheme.typography.titleMedium,
- maxLines = 1,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- Spacer(modifier = Modifier.height(8.dp))
-
- // DÜZELTME: 'note.content' yerine 'note.description' kullanıldı.
- Text(
- text = note.description, //
- style = MaterialTheme.typography.bodyMedium,
- maxLines = 4,
- color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
- )
- }
-
- if (folder != null) {
- FolderIndicator(
- folderName = folder.name,
- modifier = Modifier.align(Alignment.TopEnd)
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/UpdateScreen.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/UpdateScreen.kt
deleted file mode 100644
index 246b5c2..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/UpdateScreen.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- *
- * This file and codes created by RRechz - Babel Software
- */
-
-package com.babelsoftware.airnote.presentation.components
-
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Button
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.util.ChangelogResult
-import com.babelsoftware.airnote.util.getChangelogFromGitHub
-import kotlinx.coroutines.launch
-
-@Composable
-fun UpdateScreen(
- latestVersion: String,
- onDismiss: () -> Unit,
- onNavigateToAbout: () -> Unit
-) {
- val context = LocalContext.current
- var changelog by remember { mutableStateOf("") }
- val scope = rememberCoroutineScope()
-
- LaunchedEffect(Unit) {
- scope.launch {
- changelog = when (val result = getChangelogFromGitHub()) {
- is ChangelogResult.Success -> {
- if (result.body.isNotBlank()) {
- result.body
- } else {
- context.getString(R.string.changelog_fetch_error)
- }
- }
- is ChangelogResult.Error -> {
- context.getString(R.string.changelog_fetch_error_with_message, result.exception.message ?: "Unknow error")
- }
- }
- }
- }
-
- AlertDialog(
- onDismissRequest = onDismiss,
- title = {
- Text(text = "${stringResource(R.string.newversion)} $latestVersion")
- },
- text = {
- LazyColumn(modifier = Modifier.padding(vertical = 8.dp)) {
- item {
- Text(text = changelog)
- }
- }
- },
- confirmButton = {
- Button(
- onClick = {
- onNavigateToAbout()
- onDismiss()
- }
- ) {
- Text(stringResource(id = R.string.update))
- }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) {
- Text(stringResource(id = R.string.cancel))
- }
- }
- )
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/Builder.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/Builder.kt
deleted file mode 100644
index e1879e0..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/Builder.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-package com.babelsoftware.airnote.presentation.components.markdown
-
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.font.FontWeight
-import java.text.BreakIterator
-import androidx.compose.ui.text.withStyle
-
-class MarkdownBuilder(internal val lines: List, private var lineProcessors: List) {
- var lineIndex = -1
-
- internal val content = mutableListOf()
-
- fun add(element: MarkdownElement) {
- content.add(element)
- }
-
- fun parse() {
- while (hasNextLine()) {
- val line = nextLine()
- val processor = lineProcessors.find { it.canProcessLine(line) }
- if (processor != null) {
- processor.processLine(line, this)
- } else {
- add(NormalText(line))
- }
- }
- }
-
- private fun hasNextLine(): Boolean = lineIndex + 1 < lines.size
-
- private fun nextLine(): String {
- lineIndex++
- return lines[lineIndex]
- }
-}
-
-/**
- * Splits the input string by the specified delimiter and returns a list of index pairs.
- * Each pair represents the start and end indices of segments between delimiters.
- */
-fun splitByDelimiter(input: String, delimiter: String): List> {
- val segments = mutableListOf>()
- var startIndex = 0
- var delimiterIndex = input.indexOf(delimiter, startIndex)
-
- while (delimiterIndex != -1) {
- if (startIndex != delimiterIndex) {
- segments.add(Pair(startIndex, delimiterIndex))
- } else {
- segments.add(Pair(startIndex, startIndex))
- }
- startIndex = delimiterIndex + delimiter.length
- delimiterIndex = input.indexOf(delimiter, startIndex)
- }
-
- if (startIndex < input.length) {
- segments.add(Pair(startIndex, input.length))
- } else if (startIndex == input.length) {
- segments.add(Pair(startIndex, startIndex))
- }
-
- // Only keep segments that are odd-indexed (i.e., inside delimiters)
- return segments.filterIndexed { index, _ -> index % 2 == 1 }
-}
-
-/**
- * Checks if a given index is within any of the provided segments.
- */
-fun isInSegments(index: Int, segments: List>): Boolean {
- return segments.any { segment -> index in segment.first until segment.second }
-}
-
-fun buildString(input: String, defaultFontWeight: FontWeight = FontWeight.Normal): AnnotatedString {
- val textStyleSegments: List = listOf(
- BoldSegment(),
- ItalicSegment(),
- HighlightSegment(),
- Strikethrough(),
- Underline(),
- CodeSegment()
- )
-
- val allSegments = textStyleSegments.associateWith { splitByDelimiter(input, it.delimiter) }
-
- fun getSpanStyle(index: Int): SpanStyle {
- val styles = textStyleSegments.filter { segment -> isInSegments(index, allSegments[segment]!!) }
- return styles.fold(SpanStyle(fontWeight = defaultFontWeight)) { acc, segment -> acc.merge(segment.getSpanStyle()) }
- }
-
- val annotatedString = buildAnnotatedString {
- val iterator = BreakIterator.getWordInstance()
- iterator.setText(input)
-
- var start = iterator.first()
- var end = iterator.next()
-
- while (end != BreakIterator.DONE) {
- val substring = input.substring(start, end)
-
- // Skip delimiters and check Arabic substrings
- val isDelimiter = textStyleSegments.any { segment ->
- // Only consider it a delimiter if it's the exact delimiter string
- // This ensures single characters like +, -, = are not filtered out
- segment.delimiter == substring
- }
-
- if (!isDelimiter) {
- withStyle(style = getSpanStyle(start)) {
- append(substring) // Append full words or graphemes
- }
- }
-
- start = end
- end = iterator.next()
- }
- }
- return annotatedString
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/InlineElements.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/InlineElements.kt
deleted file mode 100644
index 09f4d9e..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/InlineElements.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.babelsoftware.airnote.presentation.components.markdown
-
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextDecoration
-
-interface TextStyleSegment {
- val delimiter: String
- fun getSpanStyle(): SpanStyle
-}
-
-data class BoldSegment(override val delimiter: String = "**") : TextStyleSegment {
- override fun getSpanStyle() = SpanStyle(fontWeight = FontWeight.Bold)
-}
-
-data class ItalicSegment(override val delimiter: String = "*") : TextStyleSegment {
- override fun getSpanStyle() = SpanStyle(fontStyle = FontStyle.Italic)
-}
-
-data class HighlightSegment(override val delimiter: String = "==") : TextStyleSegment {
- override fun getSpanStyle() = SpanStyle(background = Color.Yellow.copy(alpha = 0.2f))
-}
-
-data class Strikethrough(override val delimiter: String = "~~") : TextStyleSegment {
- override fun getSpanStyle() = SpanStyle(textDecoration = TextDecoration.LineThrough)
-}
-
-data class Underline(override val delimiter: String = "_") : TextStyleSegment {
- override fun getSpanStyle() = SpanStyle(textDecoration = TextDecoration.Underline)
-}
-
-data class CodeSegment(override val delimiter: String = "`") : TextStyleSegment {
- override fun getSpanStyle() = SpanStyle(
- fontFamily = FontFamily.Monospace,
- background = Color.LightGray.copy(alpha = 0.3f)
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/LineProccesor.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/LineProccesor.kt
deleted file mode 100644
index 013e08e..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/LineProccesor.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-package com.babelsoftware.airnote.presentation.components.markdown
-
-interface MarkdownLineProcessor {
- fun canProcessLine(line: String): Boolean
- fun processLine(line: String, builder: MarkdownBuilder)
-}
-
-class CodeBlockProcessor : MarkdownLineProcessor {
- override fun canProcessLine(line: String): Boolean {
- return line.startsWith("```")
- }
-
- override fun processLine(line: String, builder: MarkdownBuilder) {
- val codeBlock = StringBuilder()
- var index = builder.lineIndex + 1
- var isEnded = false
-
- while (index < builder.lines.size) {
- val nextLine = builder.lines[index]
- if (nextLine == "```") {
- builder.lineIndex = index
- isEnded = true
- break
- }
- codeBlock.appendLine(nextLine)
- index++
- }
-
- builder.add(CodeBlock(codeBlock.toString(), isEnded, line))
- }
-}
-
-class CheckboxProcessor : MarkdownLineProcessor {
- override fun canProcessLine(line: String): Boolean = line.matches(Regex("^\\[[ xX]]( .*)?"))
-
- override fun processLine(line: String, builder: MarkdownBuilder) {
- val checked = line.contains(Regex("^\\[[Xx]]"))
- val text = line.replace(Regex("^\\[[ xX]] ?"), "").trim()
- builder.add(CheckboxItem(text, checked, builder.lineIndex))
- }
-}
-
-
-class HeadingProcessor : MarkdownLineProcessor {
- override fun canProcessLine(line: String): Boolean = line.startsWith("#")
-
- override fun processLine(line: String, builder: MarkdownBuilder) {
- val level = line.takeWhile { it == '#' }.length
- val text = line.drop(level).trim()
- builder.add(Heading(level, text))
- }
-}
-
-class QuoteProcessor : MarkdownLineProcessor {
- override fun canProcessLine(line: String): Boolean = line.trim().startsWith(">")
-
- override fun processLine(line: String, builder: MarkdownBuilder) {
- val level = line.takeWhile { it == '>' }.length
- val text = line.drop(level).trim()
- builder.add(Quote(level, text))
- }
-}
-
-class ListItemProcessor : MarkdownLineProcessor {
- override fun canProcessLine(line: String): Boolean {
- val trimmed = line.trim()
- return trimmed.startsWith("- ") ||
- trimmed.startsWith("+ ") ||
- trimmed.startsWith("* ")
- }
-
- override fun processLine(line: String, builder: MarkdownBuilder) {
- val trimmed = line.trim()
- val text = when {
- trimmed.startsWith("- ") -> trimmed.removePrefix("- ").trim()
- trimmed.startsWith("+ ") -> trimmed.removePrefix("+ ").trim()
- trimmed.startsWith("* ") -> trimmed.removePrefix("* ").trim()
- else -> trimmed // Shouldn't happen due to canProcessLine check
- }
- builder.add(ListItem(text))
- }
-}
-
-class ImageInsertionProcessor : MarkdownLineProcessor {
- override fun canProcessLine(line: String): Boolean {
- return line.trim().startsWith("!(") && line.trim().endsWith(")")
- }
-
- override fun processLine(line: String, builder: MarkdownBuilder) {
- val photoUri = line.substringAfter("!(", "").substringBefore(")")
- builder.add(ImageInsertion(photoUri))
- }
-}
-
-class LinkProcessor : MarkdownLineProcessor {
- private val urlPattern = Regex("(https?://[\\w-]+(\\.[\\w-]+)+[\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])")
- override fun canProcessLine(line: String): Boolean {
- return urlPattern.containsMatchIn(line)
- }
-
- override fun processLine(line: String, builder: MarkdownBuilder) {
- val matches = urlPattern.findAll(line)
- val urlRanges = mutableListOf>()
- for (match in matches) {
- val url = match.value
- val range = match.range
- urlRanges.add(Pair(url, range))
- }
- if (urlRanges.isNotEmpty()) {
- builder.add(Link(line, urlRanges))
- } else {
- builder.add(NormalText(line))
- }
- }
-}
-
-class HorizontalRuleProcessor : MarkdownLineProcessor {
- override fun canProcessLine(line: String): Boolean {
- return line.trim() == "---"
- }
-
- override fun processLine(line: String, builder: MarkdownBuilder) {
- builder.add(HorizontalRule(line))
- }
-}
-
-class TableProcessor : MarkdownLineProcessor {
- override fun canProcessLine(line: String): Boolean {
- return line.trim().startsWith("|")
- }
-
- override fun processLine(line: String, builder: MarkdownBuilder) {
- if (builder.lineIndex + 1 < builder.lines.size) {
- val nextLine = builder.lines[builder.lineIndex + 1]
- if (nextLine.trim().startsWith("|") && nextLine.contains("---")) {
- val headers = parseRow(line)
- builder.lineIndex++
-
- val rows = mutableListOf>()
- while (builder.lineIndex + 1 < builder.lines.size) {
- val nextRowLine = builder.lines[builder.lineIndex + 1]
- if (nextRowLine.trim().startsWith("|")) {
- builder.lineIndex++
- rows.add(parseRow(nextRowLine))
- } else {
- break
- }
- }
- builder.add(Table(headers, rows))
- return
- }
- }
- builder.add(NormalText(line))
- }
-
- private fun parseRow(row: String): List {
- return row.trim().trim('|').split('|').map { it.trim() }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/MarkdownText.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/MarkdownText.kt
deleted file mode 100644
index f08ccac..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/MarkdownText.kt
+++ /dev/null
@@ -1,521 +0,0 @@
-package com.babelsoftware.airnote.presentation.components.markdown
-
-
-import android.content.Intent
-import android.net.Uri
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.ClickableText
-import androidx.compose.foundation.text.selection.SelectionContainer
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.VerticalDivider
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextDecoration
-import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import coil.compose.AsyncImage
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.settings.shapeManager
-import com.babelsoftware.airnote.presentation.theme.linkColor
-
-@Composable
-fun MarkdownTable(
- headers: List,
- rows: List>,
- fontSize: TextUnit,
- weight: FontWeight
-) {
- val scrollState = rememberScrollState()
- val borderColor = MaterialTheme.colorScheme.outlineVariant
-
- Box(
- modifier = Modifier
- .padding(vertical = 8.dp)
- .fillMaxWidth()
- .border(1.dp, borderColor, RoundedCornerShape(4.dp))
- .horizontalScroll(scrollState)
- ) {
- Column {
- Row(modifier = Modifier.height(IntrinsicSize.Min)) {
- headers.forEachIndexed { index, header ->
- Box(
- modifier = Modifier
- .width(IntrinsicSize.Max)
- .padding(8.dp)
- .align(Alignment.CenterVertically)
- ) {
- Text(
- text = header,
- fontWeight = FontWeight.Bold,
- fontSize = fontSize,
- color = MaterialTheme.colorScheme.onSurface
- )
- }
- if (index < headers.size - 1) {
- VerticalDivider(
- color = borderColor,
- modifier = Modifier.fillMaxHeight()
- )
- }
- }
- }
- HorizontalDivider(color = borderColor)
- rows.forEachIndexed { rowIndex, row ->
- Row(modifier = Modifier.height(IntrinsicSize.Min)) {
- headers.indices.forEach { colIndex ->
- val cellText = row.getOrNull(colIndex) ?: ""
- Box(
- modifier = Modifier
- .width(IntrinsicSize.Max)
- .padding(8.dp)
- .align(Alignment.CenterVertically)
- ) {
- Text(
- text = cellText,
- fontSize = fontSize,
- fontWeight = weight,
- color = MaterialTheme.colorScheme.onSurface
- )
- }
- if (colIndex < headers.size - 1) {
- VerticalDivider(
- color = borderColor,
- modifier = Modifier.fillMaxHeight()
- )
- }
- }
- }
- if (rowIndex < rows.size - 1) {
- HorizontalDivider(color = borderColor)
- }
- }
- }
- }
-}
-
-@Composable
-fun MarkdownCodeBlock(
- color: Color,
- text: @Composable () -> Unit
-) {
- Box(
- modifier = Modifier.padding(top = 6.dp),
- content = {
- Surface(
- color = color,
- modifier = Modifier
- .clip(RoundedCornerShape(6.dp))
- .fillMaxWidth(),
- content = {
- text()
- }
- )
- }
- )
-}
-
-@Composable
-fun MarkdownQuote(content: String, fontSize: TextUnit) {
- Row(horizontalArrangement = Arrangement.Center) {
- Box(
- modifier = Modifier
- .height(22.dp)
- .width(6.dp)
- .background(
- MaterialTheme.colorScheme.surfaceContainerLow,
- RoundedCornerShape(16.dp)
- )
- )
- Spacer(modifier = Modifier.width(6.dp))
- Text(
- text = StyledText(
- text = " $content",
- baseStyle = SpanStyle(fontSize = fontSize)
- )
- )
- }
-}
-
-@Composable
-fun MarkdownCheck(content: @Composable () -> Unit, checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?) {
- Row(
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Checkbox(
- checked = checked,
- onCheckedChange = onCheckedChange,
- modifier = Modifier
- .padding(end = 12.dp)
- .size(20.dp)
- )
- content()
- }
-}
-
-@Composable
-fun MarkdownText(
- radius: Int,
- markdown: String,
- isPreview: Boolean = false,
- isEnabled: Boolean,
- modifier: Modifier = Modifier.fillMaxWidth(),
- weight: FontWeight = FontWeight.Normal,
- fontSize: TextUnit = 16.sp,
- spacing: Dp = 2.dp,
- onContentChange: (String) -> Unit = {},
- settingsViewModel: SettingsViewModel? = null
-) {
- if (!isEnabled) {
- StaticMarkdownText(
- markdown = markdown,
- modifier = modifier,
- weight = weight,
- fontSize = fontSize
- )
- return
- }
-
- val lines = markdown.lines()
- val lineProcessors = listOf(
- HeadingProcessor(),
- ListItemProcessor(),
- CodeBlockProcessor(),
- QuoteProcessor(),
- ImageInsertionProcessor(),
- CheckboxProcessor(),
- LinkProcessor(),
- HorizontalRuleProcessor(),
- TableProcessor()
- )
- val markdownBuilder = MarkdownBuilder(lines, lineProcessors)
- markdownBuilder.parse()
-
- MarkdownContent(
- radius = radius,
- isPreview = isPreview,
- content = markdownBuilder.content,
- modifier = modifier,
- spacing = spacing,
- weight = weight,
- fontSize = fontSize,
- lines = lines,
- onContentChange = onContentChange
- )
-}
-
-@Composable
-fun StaticMarkdownText(
- markdown: String,
- modifier: Modifier,
- weight: FontWeight,
- fontSize: TextUnit
-) {
- Text(
- text = markdown,
- fontSize = fontSize,
- fontWeight = weight,
- modifier = modifier
- )
-}
-
-@Composable
-fun MarkdownContent(
- radius: Int,
- isPreview: Boolean,
- content: List,
- modifier: Modifier,
- spacing: Dp,
- weight: FontWeight,
- fontSize: TextUnit,
- lines: List,
- onContentChange: (String) -> Unit
-) {
- if (isPreview) {
- Column(modifier = modifier) {
- content.take(4).forEachIndexed { index, _ ->
- RenderMarkdownElement(
- radius = radius,
- index = index,
- content = content,
- weight = weight,
- fontSize = fontSize,
- lines = lines,
- isPreview = true,
- onContentChange = onContentChange
- )
- }
- }
- } else {
- SelectionContainer {
- LazyColumn(modifier = modifier) {
- items(content.size) { index ->
- Spacer(modifier = Modifier.height(spacing))
- RenderMarkdownElement(
- radius = radius,
- content = content,
- index = index,
- weight = weight,
- fontSize = fontSize,
- lines = lines,
- isPreview = isPreview,
- onContentChange = onContentChange
- )
- }
- }
- }
- }
-}
-
-@Composable
-fun RenderMarkdownElement(
- radius: Int,
- content: List,
- index: Int,
- weight: FontWeight,
- fontSize: TextUnit,
- lines: List,
- isPreview: Boolean,
- onContentChange: (String) -> Unit
-) {
- val element = content[index]
- Row {
- when (element) {
- is Heading -> {
- val style = SpanStyle(
- fontWeight = weight,
- fontSize = when (element.level) {
- in 1..6 -> (28 - (2 * element.level) - fontSize.value / 3).sp
- else -> fontSize
- }
- )
- Text(
- text = StyledText(text = element.text, baseStyle = style),
- modifier = Modifier.padding(vertical = 10.dp)
- )
- }
-
- is CheckboxItem -> {
- MarkdownCheck(
- content = {
- Text(
- text = StyledText(
- text = element.text,
- baseStyle = SpanStyle(fontSize = fontSize, fontWeight = weight)
- )
- )
- },
- checked = element.checked,
- onCheckedChange = if (isPreview) null else { newChecked ->
- val newMarkdown = lines.toMutableList().apply {
- this[element.index] = if (newChecked) {
- "[X] ${element.text}"
- } else {
- "[ ] ${element.text}"
- }
- }
- onContentChange(newMarkdown.joinToString("\n"))
- }
- )
- }
-
- is ListItem -> {
- Text(
- text = StyledText(
- text = "• ${element.text}",
- baseStyle = SpanStyle(fontSize = fontSize, fontWeight = weight)
- )
- )
- }
-
- is Quote -> {
- MarkdownQuote(content = element.text, fontSize = fontSize)
- }
-
- is ImageInsertion -> {
- val modifier = if (isPreview) {
- Modifier.clip(shape = shapeManager(radius = radius))
- } else {
- Modifier
- .clip(shape = shapeManager(isBoth = true, radius = radius / 2))
- .clickable { /* Just for animation */ }
- }
- AsyncImage(
- model = element.photoUri,
- contentDescription = "Image",
- modifier = modifier
- )
- }
-
- is CodeBlock -> {
- if (element.isEnded) {
- MarkdownCodeBlock(color = MaterialTheme.colorScheme.surfaceContainerLow) {
- Text(
- text = element.code.dropLast(1),
- fontSize = fontSize,
- fontWeight = weight,
- fontFamily = FontFamily.Monospace,
- modifier = Modifier.padding(6.dp),
- )
- }
- } else {
- Text(
- text = element.firstLine,
- fontWeight = weight,
- fontSize = fontSize,
- )
- }
- }
-
- is Link -> {
- val context = LocalContext.current
- val baseStyle = SpanStyle(fontWeight = weight, fontSize = fontSize)
- val annotatedString = buildAnnotatedString {
- val fullText = element.fullText
- var lastIndex = 0
-
- val sortedRanges = element.urlRanges.sortedBy { it.second.first }
-
- for ((url, range) in sortedRanges) {
- if (range.first > lastIndex) {
- val textBefore = fullText.substring(lastIndex, range.first)
- append(StyledText(text = textBefore, baseStyle = baseStyle))
- }
- pushStringAnnotation("URL", url)
- withStyle(baseStyle.plus(SpanStyle(color = linkColor))) {
- append(url)
- }
- pop()
-
- lastIndex = range.last + 1
- }
-
- if (lastIndex < fullText.length) {
- val textAfter = fullText.substring(lastIndex)
- append(StyledText(text = textAfter, baseStyle = baseStyle))
- }
- }
-
- ClickableText(
- text = annotatedString,
- onClick = { offset: Int ->
- annotatedString.getStringAnnotations("URL", offset, offset)
- .firstOrNull()?.let { annotation ->
- val intent = Intent(Intent.ACTION_VIEW)
- intent.data = Uri.parse(annotation.item)
- context.startActivity(intent)
- }
- },
- style = TextStyle(
- fontSize = fontSize,
- color = MaterialTheme.colorScheme.onSurface
- )
- )
- }
-
- is HorizontalRule -> {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 8.dp)
- .height(1.dp)
- .background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f))
- )
- }
-
- is Table -> {
- MarkdownTable(
- headers = element.headers,
- rows = element.rows,
- fontSize = fontSize,
- weight = weight
- )
- }
-
- is NormalText -> {
- Text(
- text = StyledText(
- text = element.text,
- baseStyle = SpanStyle(fontSize = fontSize, fontWeight = weight)
- )
- )
- }
- }
- // Add new line to selectionContainer but don't render it
- if (content.lastIndex != index) {
- Text(
- text = "\n",
- maxLines = 1
- )
- }
- }
-}
-
-@Composable
-private fun StyledText(
- text: String,
- baseStyle: SpanStyle
-): AnnotatedString {
- val regex = Regex("(~~)(.*?)(~~)")
-
- return buildAnnotatedString {
- var lastIndex = 0
- val matches = regex.findAll(text)
-
- matches.forEach { match ->
- if (match.range.first > lastIndex) {
- withStyle(baseStyle) {
- append(text.substring(lastIndex, match.range.first))
- }
- }
-
- val content = match.groupValues[2]
- withStyle(baseStyle.plus(SpanStyle(textDecoration = TextDecoration.LineThrough))) {
- append(content)
- }
-
- lastIndex = match.range.last + 1
- }
-
- if (lastIndex < text.length) {
- withStyle(baseStyle) {
- append(text.substring(lastIndex))
- }
- }
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/RowElements.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/RowElements.kt
deleted file mode 100644
index 6e2ae91..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/RowElements.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package com.babelsoftware.airnote.presentation.components.markdown
-
-sealed interface MarkdownElement {
- fun render(builder: StringBuilder)
-}
-
-data class Heading(val level: Int, val text: String) : MarkdownElement {
- override fun render(builder: StringBuilder) {
- builder.append("#".repeat(level)).append(" $text\n\n")
- }
-}
-
-data class CheckboxItem(val text: String, var checked: Boolean = false, var index: Int) : MarkdownElement {
- override fun render(builder: StringBuilder) {
- builder.append("[${if (checked) "X" else " "}] $text\n")
- }
-}
-
-data class Quote(val level: Int, val text: String) : MarkdownElement {
- override fun render(builder: StringBuilder) {
- builder.append("> ${text}\n")
- }
-}
-
-data class ListItem(val text: String) : MarkdownElement {
- override fun render(builder: StringBuilder) {
- builder.append("- ${text}\n")
- }
-}
-
-data class CodeBlock(val code: String, val isEnded: Boolean = false, val firstLine : String) : MarkdownElement {
- override fun render(builder: StringBuilder) {
- builder.append("```")
- isEnded.let {
- builder.append(it)
- }
- builder.append("\n$code\n```\n")
- }
-}
-
-data class NormalText(val text: String) : MarkdownElement {
- override fun render(builder: StringBuilder) {
- builder.append("$text\n\n")
- }
-}
-
-data class ImageInsertion(val photoUri: String) : MarkdownElement {
- override fun render(builder: StringBuilder) {
- builder.append("!($photoUri)\n\n")
- }
-}
-
-data class Link(val fullText: String, val urlRanges: List>) : MarkdownElement {
- override fun render(builder: StringBuilder) {
- builder.append("$fullText\n\n")
- }
-}
-
-data class HorizontalRule(val fullText: String) : MarkdownElement {
- override fun render(builder: StringBuilder) {
- builder.append("---\n\n")
- }
-}
-
-data class Table(val headers: List, val rows: List>) : MarkdownElement {
- override fun render(builder: StringBuilder) {
- builder.append("| ${headers.joinToString(" | ")} |\n")
- builder.append("| ${headers.joinToString(" | ") { "---" }} |\n")
- rows.forEach { row ->
- builder.append("| ${row.joinToString(" | ")} |\n")
- }
- builder.append("\n")
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/WidgetText.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/WidgetText.kt
deleted file mode 100644
index a5d64df..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/components/markdown/WidgetText.kt
+++ /dev/null
@@ -1,307 +0,0 @@
-package com.babelsoftware.airnote.presentation.components.markdown
-
-import android.graphics.BitmapFactory
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.glance.GlanceModifier
-import androidx.glance.GlanceTheme
-import androidx.glance.Image
-import androidx.glance.ImageProvider
-import androidx.glance.LocalContext
-import androidx.glance.appwidget.CheckBox
-import androidx.glance.appwidget.cornerRadius
-import androidx.glance.appwidget.lazy.LazyColumn
-import androidx.glance.background
-import androidx.glance.layout.Alignment
-import androidx.glance.layout.Box
-import androidx.glance.layout.ContentScale
-import androidx.glance.layout.Row
-import androidx.glance.layout.Spacer
-import androidx.glance.layout.fillMaxWidth
-import androidx.glance.layout.height
-import androidx.glance.layout.padding
-import androidx.glance.layout.width
-import androidx.glance.layout.wrapContentHeight
-import androidx.glance.text.FontFamily
-import androidx.glance.text.FontWeight
-import androidx.glance.text.Text
-import androidx.glance.text.TextStyle
-import androidx.glance.unit.ColorProvider
-import com.babelsoftware.airnote.R
-import java.io.File
-
-
-@Composable
-fun WidgetText(
- modifier: GlanceModifier,
- markdown: String,
- weight: FontWeight = FontWeight.Normal,
- fontSize: TextUnit = 16.sp,
- color: ColorProvider,
- onContentChange: (String) -> Unit = {}
-) {
- val lines = markdown.lines()
- val lineProcessors = listOf(
- HeadingProcessor(),
- ListItemProcessor(),
- CodeBlockProcessor(),
- QuoteProcessor(),
- ImageInsertionProcessor(),
- CheckboxProcessor()
- )
- val markdownBuilder = MarkdownBuilder(lines, lineProcessors)
- markdownBuilder.parse()
- MarkdownWidgetContent(
- modifier = modifier,
- content = markdownBuilder.content,
- weight = weight,
- lines = lines,
- fontSize = fontSize,
- color = color,
- onContentChange = onContentChange
- )
-}
-
-
-
-
-@Composable
-fun MarkdownWidgetContent(
- modifier: GlanceModifier,
- content: List,
- weight: FontWeight,
- lines: List,
- color: ColorProvider,
- fontSize: TextUnit,
- onContentChange: (String) -> Unit
-) {
- LazyColumn(
- modifier = modifier
- ) {
- items(content.size) { index ->
- WidgetMarkdownElement(
- modifier = modifier,
- index = index,
- color = color,
- content = content,
- weight = weight,
- fontSize = fontSize,
- lines = lines,
- onContentChange = onContentChange
- )
- }
- }
-}
-
-
-@Composable
-fun WidgetMarkdownElement(
- modifier: GlanceModifier,
- lines: List,
- content: List,
- index: Int,
- color: ColorProvider,
- weight: FontWeight,
- fontSize: TextUnit,
- onContentChange: (String) -> Unit
-) {
- val element = content[index]
- val context = LocalContext.current
-
- Row(
- modifier = modifier
- ) {
- when (element) {
- is Heading -> {
- Text(
- text = element.text,
- style = TextStyle(
- fontSize = when (element.level) {
- in 1..6 -> (28 - (2 * element.level)).sp
- else -> fontSize
- },
- fontWeight = weight,
- color = color
- )
- )
- }
- is ImageInsertion -> {
- val file = File(element.photoUri)
- if (file.exists()) {
- var bitmap = BitmapFactory.decodeFile(element.photoUri)
- val maxFileSize = 15552000
- if (bitmap.byteCount < maxFileSize) {
- Image(
- provider = ImageProvider(bitmap),
- contentDescription = null,
- contentScale = ContentScale.Fit,
- modifier = GlanceModifier
- .padding(6.dp)
- .wrapContentHeight()
- )
- } else {
- Text(
- text = context.getString(R.string.unsuported_image_size),
- style = TextStyle(
- fontSize = fontSize,
- fontWeight = weight,
- color = GlanceTheme.colors.error
- )
- )
- }
- }
- }
-
- is NormalText -> {
- Text(
- text = element.text,
- style = TextStyle(
- fontSize = fontSize,
- fontWeight = weight,
- color = color
- )
- )
- }
- is CheckboxItem -> {
- MarkdownWidgetCheck(
- content = {
- Text(
- text = element.text,
- style = TextStyle(
- fontSize = fontSize,
- fontWeight = weight,
- color = color
- )
- )
- },
- checked = element.checked,
- onCheckedChange = { newChecked ->
- val newMarkdown = lines.toMutableList().apply {
- this[element.index] = if (newChecked) {
- "[X] ${element.text}"
- } else {
- "[ ] ${element.text}"
- }
- }
- onContentChange(newMarkdown.joinToString("\n"))
- }
- )
- }
- is ListItem -> {
- Text(
- text = "• ${element.text}",
- style = TextStyle(
- fontSize = fontSize,
- fontWeight = weight,
- color = color
- )
- )
- }
- is Quote -> {
- Row(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Box(
- modifier = GlanceModifier
- .height(22.dp)
- .width(6.dp)
- .background(GlanceTheme.colors.tertiary)
- .cornerRadius(12.dp)
- ) {}
- Spacer(modifier = GlanceModifier.width(6.dp))
- Text(
- text = element.text,
- style = TextStyle(
- fontSize = fontSize,
- fontWeight = weight,
- color = color
- )
- )
- }
- }
- is CodeBlock -> {
- if (element.isEnded) {
- MarkdownWidgetCodeBlock(color = GlanceTheme.colors.widgetBackground) {
- Text(
- text = element.code.dropLast(1),
- style = TextStyle(
- fontSize = fontSize,
- fontWeight = weight,
- fontFamily = FontFamily.Monospace,
- color = color
- )
- )
- }
- } else {
- Text(
- text = element.firstLine,
- style = TextStyle(
- fontSize = fontSize,
- fontWeight = weight,
- color = color
- )
- )
- }
- }
-
- is Link -> {
- Text(
- text = element.fullText,
- style = TextStyle(
- fontSize = fontSize,
- fontWeight = weight,
- color = color
- )
- )
- }
-
- is HorizontalRule -> TODO()
- is Table -> TODO()
- }
- }
-}
-
-
-
-
-@Composable
-fun MarkdownWidgetCheck(
- content: @Composable () -> Unit,
- checked: Boolean,
- onCheckedChange: (Boolean) -> Unit,
-) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- CheckBox(
- checked = checked,
- onCheckedChange = { onCheckedChange(!checked) })
-
- content()
- }
-}
-
-@Composable
-fun MarkdownWidgetCodeBlock(
- color: ColorProvider,
- text: @Composable () -> Unit
-) {
- Box(
- modifier = GlanceModifier.padding(top = 6.dp),
- content = {
- Box(
- modifier = GlanceModifier
- .padding(6.dp)
- .cornerRadius(6.dp)
- .background(color)
- .fillMaxWidth(),
- ) {
- text()
- }
- }
- )
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/navigation/AnimatedComposable.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/navigation/AnimatedComposable.kt
deleted file mode 100644
index 9403202..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/navigation/AnimatedComposable.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.babelsoftware.airnote.presentation.navigation
-
-import androidx.compose.animation.AnimatedVisibilityScope
-import androidx.compose.runtime.Composable
-import androidx.navigation.NamedNavArgument
-import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavDeepLink
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.composable
-import com.babelsoftware.airnote.presentation.components.defaultScreenEnterAnimation
-import com.babelsoftware.airnote.presentation.components.defaultScreenExitAnimation
-import com.babelsoftware.airnote.presentation.components.slideScreenEnterAnimation
-import com.babelsoftware.airnote.presentation.components.slideScreenExitAnimation
-
-fun NavGraphBuilder.animatedComposable(
- route: String,
- arguments: List = emptyList(),
- deepLinks: List = emptyList(),
- content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
-) = composable(
- route = route,
- arguments = arguments,
- deepLinks = deepLinks,
- enterTransition = { defaultScreenEnterAnimation() },
- exitTransition = { defaultScreenExitAnimation() },
- popEnterTransition = { defaultScreenEnterAnimation() },
- popExitTransition = { defaultScreenExitAnimation() },
- content = content
-)
-
-fun NavGraphBuilder.slideInComposable(
- route: String,
- arguments: List = emptyList(),
- deepLinks: List = emptyList(),
- content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
-) = composable(
- route = route,
- arguments = arguments,
- deepLinks = deepLinks,
- enterTransition = { slideScreenEnterAnimation() },
- exitTransition = { defaultScreenExitAnimation() },
- popEnterTransition = { defaultScreenEnterAnimation() },
- popExitTransition = { slideScreenExitAnimation() },
- content = content
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/navigation/NavHost.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/navigation/NavHost.kt
deleted file mode 100644
index 4464734..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/navigation/NavHost.kt
+++ /dev/null
@@ -1,138 +0,0 @@
-package com.babelsoftware.airnote.presentation.navigation
-
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.LocalActivity
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.NavHostController
-import androidx.navigation.NavType
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.rememberNavController
-import androidx.navigation.navArgument
-import com.babelsoftware.airnote.domain.model.Settings
-import com.babelsoftware.airnote.presentation.screens.edit.EditNoteView
-import com.babelsoftware.airnote.presentation.screens.home.HomeView
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.terms.TermsScreen
-
-@Composable
-fun AppNavHost(
- settingsModel: SettingsViewModel,
- navController: NavHostController = rememberNavController(),
- noteId: Int,
- defaultRoute: String,
- isShareIntent: Boolean
-) {
- val activity = LocalActivity.current as? ComponentActivity
- val settings by settingsModel.settings
-
- LaunchedEffect(key1 = isShareIntent, key2 = navController.currentDestination) {
- if (isShareIntent && navController.currentDestination?.route == defaultRoute) {
- val editRoute = NavRoutes.Edit.createRoute(id = 0, encrypted = false, folderId = null)
- navController.navigate(editRoute) {
- popUpTo(defaultRoute) { inclusive = true }
- }
- }
- }
-
- NavHost(
- navController = navController,
- startDestination = defaultRoute
- ) {
- addHomeRoute(navController, settingsModel, settings)
- addTermsRoute(settingsModel)
- addEditNoteRoute(settingsModel, noteId, activity)
- addSettingsScreens(settingsModel, navController)
- }
-}
-
-private fun NavGraphBuilder.addHomeRoute(
- navController: NavHostController,
- settingsModel: SettingsViewModel,
- settings: Settings
-) {
- animatedComposable(NavRoutes.Home.route) {
- HomeView(
- onSettingsClicked = { navController.navigate(NavRoutes.Settings.route) },
- onNoteClicked = { id, encrypted, folderId ->
- navController.navigate(
- NavRoutes.Edit.createRoute(
- id,
- encrypted,
- folderId
- )
- )
- },
- settingsModel = settingsModel,
- settings = settings,
- onNavigateToAbout = { navController.navigate(NavRoutes.About.route) },
- )
- }
-}
-
-private fun NavGraphBuilder.addTermsRoute(settingsModel: SettingsViewModel) {
- animatedComposable(NavRoutes.Terms.route) {
- TermsScreen(
- settingsModel
- )
- }
-}
-
-private fun NavGraphBuilder.addEditNoteRoute(
- settingsModel: SettingsViewModel,
- noteId: Int,
- activity: ComponentActivity?
-) {
- animatedComposable(
- route = NavRoutes.Edit.route,
- arguments = listOf(
- navArgument("id") { type = NavType.IntType },
- navArgument("encrypted") { type = NavType.BoolType },
- navArgument("folderId") {
- type = NavType.StringType
- nullable = true
- defaultValue = null
- }
- )
- ) { backStackEntry ->
- val id = backStackEntry.arguments?.getInt("id") ?: 0
- val encrypted = backStackEntry.arguments?.getBoolean("encrypted") ?: false
- val folderIdString = backStackEntry.arguments?.getString("folderId")
- val folderId = if (folderIdString == "null" || folderIdString == null) null else folderIdString.toLongOrNull()
-
- EditNoteView(
- settingsViewModel = settingsModel,
- id = if (noteId == -1) id else noteId,
- folderId = folderId,
- encrypted = encrypted,
- isWidget = noteId != -1
- ) {
- if (noteId == -1) {
- (backStackEntry.destination.parent?.findNode(NavRoutes.Home.route) != null).let {
- activity?.onBackPressedDispatcher?.onBackPressed()
- }
- } else {
- activity?.finish()
- }
- }
- }
-}
-
-private fun NavGraphBuilder.addSettingsScreens(
- settingsModel: SettingsViewModel,
- navController: NavHostController
-) {
- settingScreens.forEach { (route, screen) ->
- if (route == NavRoutes.Settings.route) {
- slideInComposable(route) {
- screen(settingsModel, navController)
- }
- } else {
- animatedComposable(route) {
- screen(settingsModel, navController)
- }
- }
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/navigation/NavRoutes.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/navigation/NavRoutes.kt
deleted file mode 100644
index e4312fb..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/navigation/NavRoutes.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.babelsoftware.airnote.presentation.navigation
-
-import androidx.compose.runtime.Composable
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.presentation.screens.settings.MainSettings
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.settings.AboutScreen
-import com.babelsoftware.airnote.presentation.screens.settings.settings.CloudScreen
-import com.babelsoftware.airnote.presentation.screens.settings.settings.ColorStylesScreen
-import com.babelsoftware.airnote.presentation.screens.settings.settings.LanguageScreen
-import com.babelsoftware.airnote.presentation.screens.settings.settings.MarkdownScreen
-import com.babelsoftware.airnote.presentation.screens.settings.settings.PrivacyScreen
-import com.babelsoftware.airnote.presentation.screens.settings.settings.SupportScreen
-import com.babelsoftware.airnote.presentation.screens.settings.settings.AirNoteAiSettingsScreen
-import com.babelsoftware.airnote.presentation.screens.settings.settings.DesktopModeSettingsScreen
-import com.babelsoftware.airnote.presentation.screens.settings.settings.ToolsScreen
-
-enum class ActionType {
- PASSCODE,
- FINGERPRINT,
- PATTERN
-}
-
-sealed class NavRoutes(val route: String) {
- data object Home : NavRoutes("home")
- data object Edit : NavRoutes("edit_note_screen/{id}/{encrypted}?folderId={folderId}") {
- fun createRoute(id: Int, encrypted: Boolean, folderId: Long? = null): String {
- val baseRoute = "edit_note_screen/$id/$encrypted"
- return if (folderId != null) {
- "$baseRoute?folderId=$folderId"
- } else {
- baseRoute
- }
- }
- }
- data object Terms : NavRoutes("terms")
- data object Settings : NavRoutes("settings")
- data object ColorStyles : NavRoutes("settings/color_styles")
- data object Language : NavRoutes("settings/language")
- data object Cloud : NavRoutes("settings/cloud")
- data object Privacy : NavRoutes("settings/privacy")
- data object Markdown : NavRoutes("settings/markdown")
- data object Tools : NavRoutes("settings/tools")
- data object History : NavRoutes("settings/history")
- data object Widgets : NavRoutes("settings/widgets")
- data object About : NavRoutes("settings/about")
- data object Support : NavRoutes("settings/support")
- data object AirNoteAiSettings : NavRoutes("settings/ai_settings")
- data object DesktopMode : NavRoutes("settings/desktop_mode")
- data object DesktopModeSettings : NavRoutes("settings/desktop_mode_settings")
- data object LockScreen : NavRoutes("settings/lock/{type}") {
- fun createRoute(action: ActionType?) = "settings/lock/$action"
- }
-}
-
-val settingScreens = mapOf Unit>(
- NavRoutes.Settings.route to { settings, navController -> MainSettings(settings, navController) },
- NavRoutes.ColorStyles.route to { settings, navController ->
- ColorStylesScreen(
- navController,
- settings
- )
- },
- NavRoutes.Language.route to { settings, navController ->
- LanguageScreen(
- navController,
- settings
- )
- },
- NavRoutes.Cloud.route to { settings, navController -> CloudScreen(navController, settings) },
- NavRoutes.Privacy.route to { settings, navController -> PrivacyScreen(navController, settings) },
- NavRoutes.Markdown.route to { settings, navController ->
- MarkdownScreen(
- navController,
- settings
- )
- },
- NavRoutes.Tools.route to { settings, navController -> ToolsScreen(navController, settings) },
- NavRoutes.About.route to { settings, navController -> AboutScreen(navController, settings) },
- NavRoutes.Support.route to { settings, navController -> SupportScreen(navController, settings) },
- NavRoutes.AirNoteAiSettings.route to { settings, navController ->
- AirNoteAiSettingsScreen(navController, settings)
- },
- NavRoutes.DesktopModeSettings.route to { settings, navController ->
- DesktopModeSettingsScreen(navController, settings)
- }
-)
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/EditScreen.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/EditScreen.kt
deleted file mode 100644
index 6495eca..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/EditScreen.kt
+++ /dev/null
@@ -1,1481 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.edit
-
-import android.content.Intent
-import android.icu.text.SimpleDateFormat
-import androidx.activity.compose.LocalActivity
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.animateContentSize
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInHorizontally
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutHorizontally
-import androidx.compose.animation.slideOutVertically
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.PagerState
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.Send
-import androidx.compose.material.icons.automirrored.rounded.ArrowBack
-import androidx.compose.material.icons.outlined.PushPin
-import androidx.compose.material.icons.rounded.ArrowDownward
-import androidx.compose.material.icons.rounded.ArrowUpward
-import androidx.compose.material.icons.rounded.AutoAwesome
-import androidx.compose.material.icons.rounded.Balance
-import androidx.compose.material.icons.rounded.BusinessCenter
-import androidx.compose.material.icons.rounded.Close
-import androidx.compose.material.icons.rounded.ContentCopy
-import androidx.compose.material.icons.rounded.Delete
-import androidx.compose.material.icons.rounded.Edit
-import androidx.compose.material.icons.rounded.EmojiEmotions
-import androidx.compose.material.icons.rounded.EmojiSymbols
-import androidx.compose.material.icons.rounded.Face
-import androidx.compose.material.icons.rounded.Info
-import androidx.compose.material.icons.rounded.Interests
-import androidx.compose.material.icons.rounded.Language
-import androidx.compose.material.icons.rounded.Numbers
-import androidx.compose.material.icons.rounded.PushPin
-import androidx.compose.material.icons.rounded.RemoveRedEye
-import androidx.compose.material.icons.rounded.Spellcheck
-import androidx.compose.material.icons.rounded.Summarize
-import androidx.compose.material.icons.rounded.Translate
-import androidx.compose.material.icons.rounded.Tune
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.CenterAlignedTopAppBar
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ElevatedCard
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.FilledIconButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.IconButtonDefaults
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.SegmentedButton
-import androidx.compose.material3.SingleChoiceSegmentedButtonRow
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.material3.TextField
-import androidx.compose.material3.TextFieldDefaults
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.platform.LocalLifecycleOwner
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.zIndex
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.data.repository.AiAction
-import com.babelsoftware.airnote.data.repository.AiAssistantAction
-import com.babelsoftware.airnote.data.repository.AiTone
-import com.babelsoftware.airnote.domain.model.ChatMessage
-import com.babelsoftware.airnote.domain.model.Participant
-import com.babelsoftware.airnote.presentation.components.MoreButton
-import com.babelsoftware.airnote.presentation.components.NavigationIcon
-import com.babelsoftware.airnote.presentation.components.NotesScaffold
-import com.babelsoftware.airnote.presentation.components.RedoButton
-import com.babelsoftware.airnote.presentation.components.SaveButton
-import com.babelsoftware.airnote.presentation.components.UndoButton
-import com.babelsoftware.airnote.presentation.components.markdown.MarkdownText
-import com.babelsoftware.airnote.presentation.screens.edit.components.CustomTextField
-import com.babelsoftware.airnote.presentation.screens.edit.components.TextFormattingToolbar
-import com.babelsoftware.airnote.presentation.screens.edit.model.EditViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.settings.shapeManager
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.copyToClipboard
-import com.babelsoftware.airnote.presentation.theme.FontUtils
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import java.util.Locale
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun EditNoteView(
- id: Int,
- folderId: Long?,
- settingsViewModel: SettingsViewModel,
- encrypted: Boolean = false,
- isWidget: Boolean = false,
- onClickBack: () -> Unit
-) {
- val viewModel: EditViewModel = hiltViewModel()
- viewModel.updateIsEncrypted(encrypted)
- val activity = LocalActivity.current
- val intent = activity?.intent
- val isLoading by viewModel.isLoading
-
- LaunchedEffect(key1 = id, key2 = intent) {
- if (id == 0 && intent?.action == Intent.ACTION_SEND && "text/plain" == intent.type) {
- viewModel.handleSharedIntent(intent)
- } else {
- viewModel.setupNoteData(id, folderId)
- }
- }
-
- ObserveLifecycleEvents(viewModel)
-
- val scope = rememberCoroutineScope()
- val snackbarHostState = remember { SnackbarHostState() }
-
- LaunchedEffect(key1 = true) {
- viewModel.uiEvent.collect { message ->
- snackbarHostState.showSnackbar(message)
- }
- }
-
- if (viewModel.isAiActionSheetVisible.value) {
- AiActionSheet(viewModel = viewModel)
- }
-
- // "Tüm Notu Çevir" için alt pencereyi geri getirdik.
- if (viewModel.isTranslateSheetVisible.value) {
- TranslateLanguageSheet(viewModel = viewModel)
- }
-
- if (viewModel.titleSuggestions.value.isNotEmpty()) {
- TitleSuggestionDialog(
- suggestions = viewModel.titleSuggestions.value,
- onDismiss = { viewModel.clearTitleSuggestions() },
- onSelect = { selectedTitle ->
- viewModel.updateNoteName(TextFieldValue(selectedTitle))
- viewModel.clearTitleSuggestions()
- }
- )
- }
-
- val pagerState = rememberPagerState(initialPage = if (id == 0 || isWidget || settingsViewModel.settings.value.editMode) 0 else 1, pageCount = { 2 })
-
-
- val coroutineScope = rememberCoroutineScope()
-
- NotesScaffold(
- snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
- topBar = {
- if (!isLoading && !settingsViewModel.settings.value.minimalisticMode) TopBar(
- pagerState,
- coroutineScope,
- onClickBack,
- viewModel
- )
- },
- content = {
- if (isLoading) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- CircularProgressIndicator()
- }
- } else {
- PagerContent(pagerState, viewModel, settingsViewModel, onClickBack)
- }
- }
- )
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun TopBarActions(pagerState: PagerState, onClickBack: () -> Unit, viewModel: EditViewModel) {
- val context = LocalContext.current
- val isAssistantStreaming = viewModel.isAiAssistantStreaming.value
- val isMenuExpanded = viewModel.isAiAssistantSheetVisible.value
-
- val actionButtonPlaceholder = @Composable {
- Box(modifier = Modifier.size(48.dp), contentAlignment = Alignment.Center) {
- if (isAssistantStreaming) {
- CircularProgressIndicator(
- modifier = Modifier.size(24.dp),
- strokeWidth = 2.5.dp
- )
- } else {
- IconButton(onClick = { viewModel.toggleAiAssistantSheet(true) }) {
- Icon(
- imageVector = Icons.Rounded.AutoAwesome,
- contentDescription = "AI Asistanı"
- )
- }
- }
- AiCommandMenu(
- viewModel = viewModel,
- expanded = isMenuExpanded,
- onDismiss = { viewModel.toggleAiAssistantSheet(false) }
- )
- }
- }
-
- when (pagerState.currentPage) {
-
- 0 -> { // Edit Mode
- Row(verticalAlignment = Alignment.CenterVertically) {
- actionButtonPlaceholder()
- SaveButton { onClickBack() }
- }
- }
- 1 -> { // Preview Mode
- Row(verticalAlignment = Alignment.CenterVertically) {
- actionButtonPlaceholder()
- MoreButton {
- viewModel.toggleEditMenuVisibility(true)
- }
- DropdownMenu(
- expanded = viewModel.isEditMenuVisible.value,
- onDismissRequest = { viewModel.toggleEditMenuVisibility(false) }
- ) {
- if (viewModel.noteId.value != 0) {
- DropdownMenuItem(
- text = { Text(stringResource(R.string.delete)) },
- leadingIcon = { Icon(Icons.Rounded.Delete, contentDescription = "Delete")},
- onClick = {
- viewModel.toggleEditMenuVisibility(false)
- viewModel.deleteNote(viewModel.noteId.value)
- onClickBack()
- }
- )
- }
- DropdownMenuItem(
- text = { Text(stringResource(id = R.string.pinned)) },
- leadingIcon = { Icon(if (viewModel.isPinned.value) Icons.Rounded.PushPin else Icons.Outlined.PushPin, contentDescription = "Pin")},
- onClick = { viewModel.toggleNotePin(!viewModel.isPinned.value) }
- )
- DropdownMenuItem(
- text = { Text(stringResource(R.string.copy)) },
- leadingIcon = { Icon(Icons.Rounded.ContentCopy, contentDescription = "Copy")},
- onClick = {
- copyToClipboard(context, viewModel.noteDescription.value.text)
- }
- )
- DropdownMenuItem(
- text = { Text(stringResource(R.string.information)) },
- leadingIcon = { Icon(Icons.Rounded.Info, contentDescription = "Information")},
- onClick = {
- viewModel.toggleEditMenuVisibility(false)
- viewModel.toggleNoteInfoVisibility(true)
- }
- )
- }
- }
- }
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun PagerContent(pagerState: PagerState, viewModel: EditViewModel, settingsViewModel: SettingsViewModel, onClickBack: () -> Unit) {
- HorizontalPager(
- state = pagerState,
- modifier = Modifier,
- userScrollEnabled = !settingsViewModel.settings.value.disableSwipeInEditMode
- ) { page ->
- when (page) {
- 0 -> EditScreen(viewModel, settingsViewModel, pagerState, onClickBack)
- 1 -> PreviewScreen(viewModel, settingsViewModel, pagerState, onClickBack)
- }
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
-@Composable
-fun TopBar(
- pagerState: PagerState,
- coroutineScope: CoroutineScope,
- onClickBack: () -> Unit,
- viewModel: EditViewModel
-) {
- CenterAlignedTopAppBar(
- colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
- title = {
- ModeButton(
- pagerState = pagerState,
- coroutineScope = coroutineScope,
- isUndoRedoVisible = viewModel.isDescriptionInFocus.value
- )
- },
- navigationIcon = {
- Row {
- NavigationIcon(onClickBack)
- if (pagerState.currentPage == 0 && viewModel.isDescriptionInFocus.value) {
- UndoButton { viewModel.undo() }
- }
- }
- },
- actions = {
- Row(verticalAlignment = Alignment.CenterVertically) {
- if (pagerState.currentPage == 0 && viewModel.isDescriptionInFocus.value) {
- RedoButton { viewModel.redo() }
- }
- TopBarActions(pagerState, onClickBack, viewModel)
- }
- }
- )
-}
-
-@Composable
-fun ObserveLifecycleEvents(viewModel: EditViewModel) {
- val lifecycleOwner = LocalLifecycleOwner.current
- DisposableEffect(lifecycleOwner) {
- val observer = LifecycleEventObserver { _, event ->
- if (event == Lifecycle.Event.ON_STOP) {
- viewModel.saveNote(viewModel.noteId.value)
- }
- }
- lifecycleOwner.lifecycle.addObserver(observer)
- onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun BottomModal(viewModel: EditViewModel, settingsViewModel: SettingsViewModel) {
- ModalBottomSheet(
- containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
- onDismissRequest = { viewModel.toggleNoteInfoVisibility(false) }
- ) {
- val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
-
- Column(
- modifier = Modifier.padding(20.dp, 0.dp, 20.dp, 20.dp)
- ) {
- SettingsBox(
- size = 8.dp,
- title = stringResource(R.string.created_time),
- icon = IconResource.Vector(Icons.Rounded.Numbers),
- actionType = ActionType.TEXT,
- radius = shapeManager(
- isFirst = true,
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- customText = sdf.format(viewModel.noteCreatedTime.value).toString()
- )
- SettingsBox(
- size = 8.dp,
- title = stringResource(R.string.words),
- icon = IconResource.Vector(Icons.Rounded.Numbers),
- radius = shapeManager(radius = settingsViewModel.settings.value.cornerRadius),
- actionType = ActionType.TEXT,
- customText = if (viewModel.noteDescription.value.text != "") viewModel.noteDescription.value.text.split(
- "\\s+".toRegex()
- ).size.toString() else "0"
- )
- SettingsBox(
- size = 8.dp,
- title = stringResource(R.string.characters),
- icon = IconResource.Vector(Icons.Rounded.Numbers),
- actionType = ActionType.TEXT,
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = true
- ),
- customText = viewModel.noteDescription.value.text.length.toString()
- )
- }
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun MinimalisticMode(
- alignment : Alignment.Vertical = Alignment.CenterVertically,
- viewModel: EditViewModel,
- modifier: Modifier = Modifier,
- isEnabled: Boolean, pagerState: PagerState,
- isExtremeAmoled: Boolean,
- showOnlyDescription: Boolean = false,
- onClickBack: () -> Unit, content: @Composable () -> Unit,
-) {
- val coroutineScope = rememberCoroutineScope()
- Row(
- verticalAlignment = alignment,
- modifier = modifier
- .fillMaxWidth()
- .then(if (showOnlyDescription) Modifier.padding(top = 8.dp) else Modifier)
- ) {
- if (!showOnlyDescription) {
- if (isEnabled) NavigationIcon(onClickBack)
- if (isEnabled && viewModel.isDescriptionInFocus.value) UndoButton { viewModel.undo() }
- content()
- if (isEnabled) TopBarActions(pagerState, onClickBack, viewModel)
- if (isEnabled) ModeButton(pagerState, coroutineScope, isUndoRedoVisible = viewModel.isDescriptionInFocus.value)
- } else {
- Column {
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (isEnabled) NavigationIcon(onClickBack)
- Spacer(modifier = Modifier.weight(1f))
- if (isEnabled) ModeButton(pagerState, coroutineScope, isUndoRedoVisible = viewModel.isDescriptionInFocus.value)
- if (isEnabled) TopBarActions(pagerState, onClickBack, viewModel)
- }
- content()
- }
- }
- }
-}
-
-
-
-@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
-@Composable
-fun EditScreen(viewModel: EditViewModel, settingsViewModel: SettingsViewModel, pagerState: PagerState, onClickBack: () -> Unit) {
-
- if (viewModel.isAiLoading.value) {
- LoadingOverlay()
- }
-
- if (viewModel.aiResultText.value != null) {
- AiResultDialog(viewModel = viewModel)
- }
-
- Column(
- modifier = Modifier
- .fillMaxSize()
- .imePadding()
- ) {
- Column(
- modifier = Modifier
- .weight(1f)
- .padding(16.dp, 16.dp, 16.dp, 0.dp)
- ) {
- MarkdownBox(
- isExtremeAmoled = settingsViewModel.settings.value.extremeAmoledMode,
- shape = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isFirst = true
- ),
- content = {
- MinimalisticMode(
- viewModel = viewModel,
- modifier = Modifier.padding(top = 2.dp),
- isEnabled = settingsViewModel.settings.value.minimalisticMode,
- pagerState = pagerState,
- isExtremeAmoled = settingsViewModel.settings.value.extremeAmoledMode,
- onClickBack = { onClickBack() }
- ) {
- println(settingsViewModel.settings.value.useMonoSpaceFont)
- CustomTextField(
- value = viewModel.noteName.value,
- modifier = Modifier.weight(1f),
- onValueChange = { viewModel.updateNoteName(it) },
- placeholder = stringResource(R.string.name),
- useMonoSpaceFont = settingsViewModel.settings.value.useMonoSpaceFont
- )
- }
- }
- )
- MarkdownBox(
- isExtremeAmoled = settingsViewModel.settings.value.extremeAmoledMode,
- shape = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = true
- ),
- modifier = Modifier
- .weight(1f)
- .onFocusChanged { focusState ->
- if (!viewModel.isMinimalAiUiVisible.value) {
- viewModel.toggleIsDescriptionInFocus(focusState.isFocused)
- }
- },
- content = {
- CustomTextField(
- value = viewModel.noteDescription.value,
- onValueChange = { viewModel.updateNoteDescription(it) },
- modifier = Modifier.fillMaxSize(),
- placeholder = stringResource(R.string.description),
- useMonoSpaceFont = settingsViewModel.settings.value.useMonoSpaceFont
- )
- }
- )
- }
- BottomBarContainer(viewModel, settingsViewModel)
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun BottomBarContainer(viewModel: EditViewModel, settingsViewModel: SettingsViewModel) {
- val isAiVisible by viewModel.isMinimalAiUiVisible
- val showBar = (viewModel.isDescriptionInFocus.value || isAiVisible) && settingsViewModel.settings.value.isMarkdownEnabled
-
- AnimatedVisibility(
- visible = showBar,
- enter = slideInVertically { it },
- exit = slideOutVertically { it }
- ) {
- AnimatedContent(
- targetState = isAiVisible,
- transitionSpec = {
- if (targetState) {
- (fadeIn(animationSpec = tween(220, delayMillis = 90)) + slideInVertically(initialOffsetY = { it }, animationSpec = tween(220, delayMillis = 90)))
- .togetherWith(fadeOut(animationSpec = tween(90)))
- } else {
- (fadeIn(animationSpec = tween(220, delayMillis = 90)) + slideInVertically(initialOffsetY = { -it }, animationSpec = tween(220, delayMillis = 90)))
- .togetherWith(fadeOut(animationSpec = tween(90)))
- }
- },
- label = "BottomBarAnimation"
- ) { targetState ->
- if (targetState) {
- MinimalAiChatInterface(viewModel = viewModel, settingsViewModel = settingsViewModel, isDreamJournalMode = viewModel.isDreamJournalMode.value)
- } else {
- TextFormattingToolbar(viewModel = viewModel)
- }
- }
- }
-}
-
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun PreviewScreen(viewModel: EditViewModel, settingsViewModel: SettingsViewModel, pagerState: PagerState, onClickBack: () -> Unit) {
- if (viewModel.isNoteInfoVisible.value) BottomModal(viewModel, settingsViewModel)
-
- val focusManager = LocalFocusManager.current
- focusManager.clearFocus()
- val showOnlyDescription = viewModel.noteName.value.text.isNotBlank()
-
- Column(
- modifier = Modifier.padding(16.dp),
- ) {
- if (showOnlyDescription) {
- MarkdownBox(
- isExtremeAmoled = settingsViewModel.settings.value.extremeAmoledMode,
- shape = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isFirst = true
- ),
- content = {
- MinimalisticMode(
- viewModel = viewModel,
- isEnabled = settingsViewModel.settings.value.minimalisticMode,
- pagerState = pagerState,
- isExtremeAmoled = settingsViewModel.settings.value.extremeAmoledMode,
- onClickBack = { onClickBack() }
- ) {
- MarkdownText(
- markdown = viewModel.noteName.value.text,
- isEnabled = settingsViewModel.settings.value.isMarkdownEnabled,
- weight = FontWeight.Bold,
- fontSize = FontUtils.getTitleFontSize(settingsViewModel),
- modifier = Modifier
- .padding(16.dp)
- .align(Alignment.CenterHorizontally),
- onContentChange = { viewModel.updateNoteName(TextFieldValue(text = it)) },
- radius = settingsViewModel.settings.value.cornerRadius,
- settingsViewModel = settingsViewModel
- )
- Spacer(modifier = Modifier.weight(1f))
- }
- }
- )
- }
- MarkdownBox(
- isExtremeAmoled = settingsViewModel.settings.value.extremeAmoledMode,
- shape = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = (showOnlyDescription),
- isBoth = (!showOnlyDescription)
- ),
- modifier = Modifier.fillMaxSize(),
- content = {
- MinimalisticMode(
- alignment = Alignment.Top,
- viewModel = viewModel,
- isExtremeAmoled = settingsViewModel.settings.value.extremeAmoledMode,
- isEnabled = settingsViewModel.settings.value.minimalisticMode && !showOnlyDescription,
- pagerState = pagerState,
- showOnlyDescription = !showOnlyDescription,
- onClickBack = { onClickBack() },
- ) {
- MarkdownText(
- radius = settingsViewModel.settings.value.cornerRadius,
- markdown = viewModel.noteDescription.value.text,
- isEnabled = settingsViewModel.settings.value.isMarkdownEnabled,
- fontSize = FontUtils.getBodyFontSize(settingsViewModel),
- modifier = Modifier
- .padding(
- 16.dp,
- top = if (showOnlyDescription) 16.dp else 6.dp,
- 16.dp,
- 16.dp
- )
- .weight(1f),
- onContentChange = { newText ->
- val synchronizedText = synchronizeStrikethroughs(newText)
- viewModel.updateNoteDescription(TextFieldValue(text = synchronizedText))
- },
- settingsViewModel = settingsViewModel
- )
- }
- }
- )
- }
-}
-
-@Composable
-fun MarkdownBox(
- isExtremeAmoled: Boolean,
- modifier: Modifier = Modifier,
- shape: RoundedCornerShape = RoundedCornerShape(0.dp),
- content: @Composable () -> Unit,
-) {
- ElevatedCard(
- shape = shape,
- modifier = modifier
- .clip(shape)
- .then(
- if (isExtremeAmoled) {
- Modifier.border(
- 1.5.dp,
- shape = shape,
- color = MaterialTheme.colorScheme.surfaceContainerHighest
- )
- } else Modifier
- ),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceContainer
- ),
- elevation = CardDefaults.cardElevation(defaultElevation = if (!isExtremeAmoled) 1.dp else 0.dp), // Shadow
-
- ) {
- content()
- }
- Spacer(modifier = Modifier.height(3.dp))
-}
-
-@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
-@Composable
-fun ModeButton(
- pagerState: PagerState,
- coroutineScope: CoroutineScope,
- isUndoRedoVisible: Boolean
-) {
- val options = listOf(
- stringResource(R.string.edit) to Icons.Rounded.Edit,
- stringResource(R.string.preview) to Icons.Rounded.RemoveRedEye
- )
-
- var widthPx by remember { mutableFloatStateOf(0f) }
- val density = LocalDensity.current
-
- Box(
- modifier = Modifier
- .height(44.dp)
- .clip(RoundedCornerShape(100))
- .background(MaterialTheme.colorScheme.surfaceVariant)
- .padding(4.dp)
- .onSizeChanged { widthPx = it.width.toFloat() }
- ) {
- val indicatorWidth = widthPx / 2
-
- val indicatorOffset by remember(pagerState, widthPx) {
- derivedStateOf {
- val pageOffset = pagerState.currentPage + pagerState.currentPageOffsetFraction
- pageOffset.coerceIn(0f, 1f) * (widthPx / 2)
- }
- }
-
- if (widthPx > 0) {
- Box(
- modifier = Modifier
- .fillMaxHeight()
- .width(with(density) { indicatorWidth.toDp() })
- .offset { IntOffset(x = indicatorOffset.toInt(), y = 0) }
- .shadow(2.dp, RoundedCornerShape(100))
- .clip(RoundedCornerShape(100))
- .background(MaterialTheme.colorScheme.surfaceContainerLow)
- )
- }
-
- Row(modifier = Modifier.fillMaxSize()) {
- options.forEachIndexed { index, (text, icon) ->
- val isSelected = pagerState.currentPage == index
- val contentColor = if (isSelected) {
- MaterialTheme.colorScheme.primary
- } else {
- MaterialTheme.colorScheme.onSurfaceVariant
- }
-
- Box(
- modifier = Modifier
- .weight(1f)
- .fillMaxHeight()
- .clip(RoundedCornerShape(100))
- .clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = null
- ) {
- coroutineScope.launch {
- pagerState.animateScrollToPage(index)
- }
- },
- contentAlignment = Alignment.Center
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center,
- modifier = Modifier.padding(horizontal = 8.dp)
- ) {
- Icon(
- imageVector = icon,
- contentDescription = text,
- modifier = Modifier.size(18.dp),
- tint = contentColor
- )
- AnimatedVisibility(visible = !isUndoRedoVisible) {
- Row {
- Spacer(modifier = Modifier.width(8.dp))
- Text(
- text = text,
- style = MaterialTheme.typography.labelLarge,
- fontWeight = FontWeight.Medium,
- color = contentColor,
- maxLines = 1
- )
- }
- }
- }
- }
- }
- }
- }
-}
-
-@Composable
-private fun TitleSuggestionDialog(
- suggestions: List,
- onDismiss: () -> Unit,
- onSelect: (String) -> Unit
-) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text(stringResource(R.string.ai_suggestion)) },
- text = {
- LazyColumn {
- items(suggestions) { suggestion ->
- Text(
- text = suggestion,
- modifier = Modifier
- .fillMaxWidth()
- .clickable { onSelect(suggestion) }
- .padding(vertical = 8.dp)
- )
- }
- }
- },
- confirmButton = {
- TextButton(onClick = onDismiss) {
- Text(stringResource(R.string.close))
- }
- }
- )
-}
-
-@Composable
-private fun CommandMenuItem(
- title: String,
- subtitle: String? = null,
- onClick: () -> Unit
-) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .clickable(onClick = onClick)
- .padding(horizontal = 16.dp, vertical = 12.dp)
- ) {
- Text(text = title, style = MaterialTheme.typography.bodyLarge)
- if (subtitle != null) {
- Spacer(modifier = Modifier.height(2.dp))
- Text(
- text = subtitle,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
-}
-
-@Composable
-fun AiCommandMenu(
- viewModel: EditViewModel,
- expanded: Boolean,
- onDismiss: () -> Unit
-) {
- DropdownMenu(
- expanded = expanded,
- onDismissRequest = onDismiss,
- modifier = Modifier.widthIn(min = 280.dp)
- ) {
- CommandMenuItem(
- title = stringResource(R.string.ai_command_pros_cons),
- onClick = { viewModel.executeAiAssistantAction(AiAssistantAction.PROS_AND_CONS) }
- )
- CommandMenuItem(
- title = stringResource(R.string.ai_command_todo),
- onClick = { viewModel.executeAiAssistantAction(AiAssistantAction.CREATE_TODO_LIST) }
- )
- CommandMenuItem(
- title = stringResource(R.string.ai_command_simpler),
- subtitle = stringResource(R.string.ai_command_simpler_subtitle),
- onClick = { viewModel.executeAiAssistantAction(AiAssistantAction.SIMPLIFY) }
- )
- CommandMenuItem(
- title = stringResource(R.string.ai_command_title),
- onClick = { viewModel.executeAiAssistantAction(AiAssistantAction.SUGGEST_A_TITLE) }
- )
- CommandMenuItem(
- title = stringResource(R.string.translate_all_note),
- onClick = {
- onDismiss()
- viewModel.onTranslateClicked(forSelection = false)
- }
- )
- }
-}
-
-@Composable
-private fun AiResultDialog(viewModel: EditViewModel) {
- val aiResult = viewModel.aiResultText.value ?: return
-
- AlertDialog(
- onDismissRequest = { viewModel.clearAiResult() },
- title = { Text(stringResource(R.string.ai_suggestion)) },
- text = { Text(aiResult) },
- confirmButton = {
- TextButton(onClick = { viewModel.replaceWithAiResult() }) {
- Text(stringResource(R.string.change))
- }
- },
- dismissButton = {
- TextButton(onClick = { viewModel.clearAiResult() }) {
- Text(stringResource(R.string.cancel))
- }
- }
- )
-}
-
-@Composable
-private fun LoadingOverlay() {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(Color.Black.copy(alpha = 0.5f))
- .clickable(enabled = false, onClick = {}) // Prevents back clicks
- .zIndex(10f), // To appear above everything
- contentAlignment = Alignment.Center
- ) {
- CircularProgressIndicator()
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun AiActionSheet(viewModel: EditViewModel) {
- if (!viewModel.isAiActionSheetVisible.value) return
-
- LaunchedEffect(viewModel.isAiActionSheetVisible.value) {
- if (viewModel.isAiActionSheetVisible.value) {
- delay(300L)
- viewModel.markSheetAsReadyForInteraction()
- }
- }
-
- val actions = AiAction.values()
-
- ModalBottomSheet(
- onDismissRequest = { viewModel.toggleAiActionSheet(false) }
- ) {
- LazyColumn {
- items(actions) { action ->
- ListItem(
- headlineContent = { Text(text = action.getDisplayName()) },
- leadingContent = {
- if (action == AiAction.CHANGE_TONE) {
- Icon(
- imageVector = Icons.Rounded.Tune,
- contentDescription = null
- )
- }
- },
- modifier = Modifier.clickable(
- enabled = viewModel.isSheetReadyForInteraction.value
- ) {
- if (viewModel.isSheetReadyForInteraction.value) {
- viewModel.executeAiAction(action = action, tone = null)
- }
- }
- )
- }
- }
- }
-}
-
-private enum class MinimalAiUiMode {
- MAIN,
- TONE,
- TRANSLATE
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun MinimalAiChatInterface(viewModel: EditViewModel, settingsViewModel: SettingsViewModel, isDreamJournalMode: Boolean = false) {
- val isTextSelected = viewModel.noteDescription.value.selection.collapsed.not()
- val isChatActive = viewModel.isMinimalChatActive.value
- val chatText = viewModel.minimalAiChatText.value
- val chatHistory = viewModel.minimalAiChatHistory.value
- val isLoading = viewModel.isMinimalAiLoading.value
- val chatScrollState = rememberScrollState()
- val cornerRadius = settingsViewModel.settings.value.cornerRadius
- var currentUiState by remember { mutableStateOf(MinimalAiUiMode.MAIN) }
-
- LaunchedEffect(isTextSelected) {
- if (!isTextSelected) {
- currentUiState = MinimalAiUiMode.MAIN
- }
- }
-
- LaunchedEffect(chatHistory.size) {
- chatScrollState.animateScrollTo(chatScrollState.maxValue)
- }
-
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 12.dp, vertical = 8.dp),
- shape = RoundedCornerShape(cornerRadius.dp),
- elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer)
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .animateContentSize()
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(start = 16.dp, end = 8.dp, top = 8.dp, bottom = 4.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (isDreamJournalMode) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(
- imageVector = Icons.Rounded.AutoAwesome,
- contentDescription = "AirNote Kai - Dream Edition",
- tint = Color(0xFFB39DDB),
- modifier = Modifier.size(20.dp)
- )
- Spacer(modifier = Modifier.width(8.dp))
- Text(
- text = stringResource(R.string.dream_journal_ai_title),
- style = MaterialTheme.typography.titleSmall,
- fontWeight = FontWeight.Bold,
- color = Color(0xFFB39DDB)
- )
- }
- } else {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(
- imageVector = Icons.Rounded.AutoAwesome,
- contentDescription = "AirNote Kai",
- tint = MaterialTheme.colorScheme.primary,
- modifier = Modifier.size(20.dp)
- )
- Spacer(modifier = Modifier.width(8.dp))
- Text(
- text = "AirNote Kai",
- style = MaterialTheme.typography.titleSmall,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.primary
- )
- }
- }
- Spacer(modifier = Modifier.weight(1f))
- IconButton(
- onClick = {
- viewModel.toggleMinimalAiUi(false)
- currentUiState = MinimalAiUiMode.MAIN
- },
- ) {
- Icon(Icons.Rounded.Close, contentDescription = stringResource(R.string.close))
- }
- }
-
- if (isDreamJournalMode) {
- AnimatedVisibility(visible = !isChatActive) {
- LazyRow(
- modifier = Modifier.fillMaxWidth(),
- contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- item {
- InputActionButton(
- text = stringResource(R.string.interpret_my_dream),
- icon = Icons.Rounded.Interests,
- enabled = true,
- onClick = {
- viewModel.executeDreamAnalysis()
- }
- )
- }
- item {
- InputActionButton(
- text = stringResource(R.string.explain_symbols),
- icon = Icons.Rounded.EmojiSymbols,
- enabled = true,
- onClick = {
- viewModel.executeDreamSymbolAnalysis()
- }
- )
- }
- item {
- InputActionButton(
- text = stringResource(R.string.emotional_analysis),
- icon = Icons.Rounded.EmojiEmotions,
- enabled = true,
- onClick = {
- viewModel.executeDreamEmotionAnalysis()
- }
- )
- }
- }
- }
- } else {
- AnimatedVisibility(visible = !isChatActive) {
- AnimatedContent(
- targetState = currentUiState,
- label = "AiActionsAnimation",
- transitionSpec = {
- if (targetState != MinimalAiUiMode.MAIN && initialState == MinimalAiUiMode.MAIN) {
- (slideInHorizontally { width -> width } + fadeIn()).togetherWith(slideOutHorizontally { width -> -width } + fadeOut())
- } else {
- (slideInHorizontally { width -> -width } + fadeIn()).togetherWith(slideOutHorizontally { width -> width } + fadeOut())
- }
- }
- ) { state ->
- when (state) {
- MinimalAiUiMode.MAIN -> {
- LazyRow(
- modifier = Modifier.fillMaxWidth(),
- contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- items(AiAction.values()) { action ->
- InputActionButton(
- text = action.getDisplayName(),
- icon = action.getIcon(),
- enabled = isTextSelected,
- onClick = {
- when (action) {
- AiAction.CHANGE_TONE -> currentUiState = MinimalAiUiMode.TONE
- AiAction.TRANSLATE -> {
- viewModel.onTranslateClicked(forSelection = true)
- currentUiState = MinimalAiUiMode.TRANSLATE
- }
- else -> viewModel.executeAiAction(action = action, tone = null)
- }
- }
- )
- }
- }
- }
- MinimalAiUiMode.TONE -> {
- LazyRow(
- modifier = Modifier.fillMaxWidth(),
- contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- item {
- FilledIconButton(
- onClick = { currentUiState = MinimalAiUiMode.MAIN },
- modifier = Modifier.size(34.dp),
- colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)
- ) {
- Icon(
- Icons.AutoMirrored.Rounded.ArrowBack,
- contentDescription = stringResource(R.string.back),
- modifier = Modifier.size(18.dp)
- )
- }
- }
- items(AiTone.values()) { tone ->
- InputActionButton(
- text = tone.getDisplayName(),
- icon = tone.getIcon(),
- enabled = true,
- onClick = {
- viewModel.executeAiAction(action = AiAction.CHANGE_TONE, tone = tone)
- currentUiState = MinimalAiUiMode.MAIN
- }
- )
- }
- }
- }
- MinimalAiUiMode.TRANSLATE -> {
- val languages = viewModel.downloadedLanguages.value
- LazyRow(
- modifier = Modifier.fillMaxWidth(),
- contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- item {
- FilledIconButton(
- onClick = { currentUiState = MinimalAiUiMode.MAIN },
- modifier = Modifier.size(34.dp),
- colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)
- ) {
- Icon(
- Icons.AutoMirrored.Rounded.ArrowBack,
- contentDescription = stringResource(R.string.back),
- modifier = Modifier.size(18.dp)
- )
- }
- }
- if (languages.isEmpty()) {
- item {
- Text(
- text = stringResource(R.string.no_downloaded_languages),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.padding(start = 8.dp)
- )
- }
- } else {
- items(languages) { (code, name) ->
- InputActionButton(
- text = name,
- icon = Icons.Rounded.Translate,
- enabled = true,
- onClick = {
- viewModel.executeTranslation(code)
- currentUiState = MinimalAiUiMode.MAIN
- }
- )
- }
- }
- }
- }
- }
- }
- }
- }
-
- AnimatedVisibility(visible = isChatActive) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .heightIn(max = 300.dp)
- .verticalScroll(chatScrollState)
- .padding(horizontal = 16.dp)
- ) {
- chatHistory.forEach { message ->
- ChatMessageItem(message = message)
- }
- }
- }
-
- Surface(
- modifier = Modifier.fillMaxWidth(),
- color = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = 0.5f)
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- TextField(
- value = chatText,
- onValueChange = { viewModel.updateMinimalAiChatText(it) },
- modifier = Modifier.weight(1f),
- placeholder = { Text(stringResource(R.string.ask_airnote_ai)) },
- colors = TextFieldDefaults.colors(
- focusedContainerColor = Color.Transparent,
- unfocusedContainerColor = Color.Transparent,
- disabledContainerColor = Color.Transparent,
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
- cursorColor = MaterialTheme.colorScheme.primary,
- ),
- maxLines = 5
- )
-
- Box(modifier = Modifier.size(48.dp), contentAlignment = Alignment.Center) {
- if (isLoading) {
- CircularProgressIndicator(modifier = Modifier.size(24.dp))
- } else {
- IconButton(
- onClick = { viewModel.sendMinimalAiMessage() },
- enabled = chatText.isNotBlank()
- ) {
- Icon(
- Icons.AutoMirrored.Filled.Send,
- contentDescription = stringResource(R.string.send),
- tint = if (chatText.isNotBlank()) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
- }
- }
- }
- }
- }
-}
-
-@Composable
-private fun InputActionButton(
- text: String,
- icon: ImageVector,
- onClick: () -> Unit,
- enabled: Boolean = true
-) {
- TextButton(
- onClick = onClick,
- enabled = enabled,
- shape = RoundedCornerShape(24.dp),
- colors = ButtonDefaults.textButtonColors(
- containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
- contentColor = MaterialTheme.colorScheme.onSurface,
- disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.5f),
- disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
- ),
- border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
- contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
- modifier = Modifier.alpha(if (enabled) 1f else 0.5f)
- ) {
- Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp))
- Spacer(Modifier.width(8.dp))
- Text(text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
- }
-}
-
-
-@Composable
-fun ChatMessageItem(message: ChatMessage) {
- val isUser = message.participant == Participant.USER
- val alignment = if (isUser) Alignment.CenterEnd else Alignment.CenterStart
- val bubbleColor = when (message.participant) {
- Participant.USER -> MaterialTheme.colorScheme.primaryContainer
- Participant.MODEL -> MaterialTheme.colorScheme.surfaceContainerHigh
- Participant.ERROR -> MaterialTheme.colorScheme.errorContainer
- }
- val textColor = when (message.participant) {
- Participant.USER -> MaterialTheme.colorScheme.onPrimaryContainer
- Participant.MODEL -> MaterialTheme.colorScheme.onSurface
- Participant.ERROR -> MaterialTheme.colorScheme.onErrorContainer
- }
- val bubbleShape = RoundedCornerShape(
- topStart = 16.dp,
- topEnd = 16.dp,
- bottomStart = if (isUser) 16.dp else 0.dp,
- bottomEnd = if (isUser) 0.dp else 16.dp
- )
-
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 4.dp),
- contentAlignment = alignment
- ) {
- if (message.isLoading) {
- CircularProgressIndicator(modifier = Modifier.size(24.dp))
- } else {
- Surface(
- color = bubbleColor,
- shape = bubbleShape,
- modifier = Modifier.widthIn(max = 300.dp)
- ) {
- Text(
- text = message.text.replace(Regex("[*#]"), "").trim(),
- style = MaterialTheme.typography.bodyLarge,
- color = textColor,
- modifier = Modifier.padding(12.dp)
- )
- }
- }
- }
-}
-
-@Composable
-fun AiAction.getIcon(): ImageVector {
- return when (this) {
- AiAction.IMPROVE_WRITING -> Icons.Rounded.Spellcheck
- AiAction.SUMMARIZE -> Icons.Rounded.Summarize
- AiAction.MAKE_SHORTER -> Icons.Rounded.ArrowDownward
- AiAction.MAKE_LONGER -> Icons.Rounded.ArrowUpward
- AiAction.CHANGE_TONE -> Icons.Rounded.Tune
- AiAction.TRANSLATE -> Icons.Rounded.Translate
- }
-}
-
-
-@Composable
-fun AiAction.getDisplayName(): String {
- return when (this) {
- AiAction.IMPROVE_WRITING -> stringResource(id = R.string.ai_action_improve_writing)
- AiAction.SUMMARIZE -> stringResource(id = R.string.ai_action_summarize)
- AiAction.MAKE_SHORTER -> stringResource(id = R.string.ai_action_make_shorter)
- AiAction.MAKE_LONGER -> stringResource(id = R.string.ai_action_make_longer)
- AiAction.CHANGE_TONE -> stringResource(id = R.string.ai_action_change_tone)
- AiAction.TRANSLATE -> stringResource(id = R.string.translate)
- }
-}
-
-@Composable
-fun AiTone.getIcon(): ImageVector {
- return when (this) {
- AiTone.FORMAL -> Icons.Rounded.BusinessCenter
- AiTone.BALANCED -> Icons.Rounded.Balance
- AiTone.FRIENDLY -> Icons.Rounded.Face
- }
-}
-
-@Composable
-fun AiTone.getDisplayName(): String {
- return when (this) {
- AiTone.FORMAL -> stringResource(id = R.string.ai_tone_formal)
- AiTone.BALANCED -> stringResource(id = R.string.ai_tone_balanced)
- AiTone.FRIENDLY -> stringResource(id = R.string.ai_tone_friendly)
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun TranslateLanguageSheet(viewModel: EditViewModel) {
- val languages = viewModel.downloadedLanguages.value
- ModalBottomSheet(onDismissRequest = { viewModel.toggleTranslateSheet(false) }) {
- if (languages.isEmpty()) {
- Text(
- text = stringResource(R.string.no_downloaded_languages),
- modifier = Modifier
- .padding(16.dp)
- .fillMaxWidth(),
- textAlign = TextAlign.Center
- )
- Spacer(modifier = Modifier.height(16.dp))
- } else {
- LazyColumn {
- items(languages) { (code, name) ->
- ListItem(
- headlineContent = { Text(name) },
- modifier = Modifier.clickable {
- viewModel.executeTranslation(code)
- }
- )
- }
- }
- Spacer(modifier = Modifier.height(16.dp))
- }
- }
-}
-
-private fun synchronizeStrikethroughs(newMarkdown: String): String {
- return newMarkdown.lines().joinToString("\n") { line ->
- val checkedRegex = Regex("""^(\s*\[[xX]\]\s+)(.*)""")
- val uncheckedRegex = Regex("""^(\s*\[ \]\s+)(.*)""")
- val checkedMatch = checkedRegex.find(line)
- val uncheckedMatch = uncheckedRegex.find(line)
-
- when {
- checkedMatch != null -> {
- val prefix = checkedMatch.groupValues[1]
- val content = checkedMatch.groupValues[2]
-
- if (content.startsWith("~~") && content.endsWith("~~")) {
- line
- } else {
- "$prefix~~$content~~"
- }
- }
- uncheckedMatch != null -> {
- val prefix = uncheckedMatch.groupValues[1]
- val content = uncheckedMatch.groupValues[2]
-
- if (content.startsWith("~~") && content.endsWith("~~")) {
- val strippedContent = content.substring(2, content.length - 2)
- "$prefix$strippedContent"
- } else {
- line
- }
- }
-
- else -> line
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/components/CustomIconButton.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/components/CustomIconButton.kt
deleted file mode 100644
index 0e3b52f..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/components/CustomIconButton.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.edit.components
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.ElevatedCard
-import androidx.compose.material3.Icon
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-
-@Composable
-fun CustomIconButton(
- shape: RoundedCornerShape,
- elevation: Dp,
- icon: ImageVector,
- onClick: () -> Unit
-) {
- ElevatedCard(
- shape = shape,
- elevation = CardDefaults.cardElevation(defaultElevation = elevation),
- modifier = Modifier
- .clip(shape)
- .clickable { onClick() }
- ) {
- Icon(
- imageVector = icon,
- contentDescription = "",
- modifier = Modifier.padding(16.dp),
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/components/CustomTextField.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/components/CustomTextField.kt
deleted file mode 100644
index 32b1ff2..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/components/CustomTextField.kt
+++ /dev/null
@@ -1,144 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.edit.components
-
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material3.LocalTextStyle
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
-import androidx.compose.material3.TextFieldDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.input.KeyboardCapitalization
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.text.input.VisualTransformation
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.autofill.AutofillType
-import com.babelsoftware.airnote.presentation.components.AutoFillRequestHandler
-import com.babelsoftware.airnote.presentation.components.connectNode
-import com.babelsoftware.airnote.presentation.components.defaultFocusChangeAutoFill
-
-@OptIn(ExperimentalComposeUiApi::class)
-@Composable
-fun CustomTextField(
- value: TextFieldValue,
- onValueChange: (TextFieldValue) -> Unit,
- placeholder: String,
- shape: RoundedCornerShape = RoundedCornerShape(0.dp),
- interactionSource: MutableInteractionSource = MutableInteractionSource(),
- singleLine: Boolean = false,
- modifier: Modifier = Modifier,
- hideContent: Boolean = false,
- useMonoSpaceFont: Boolean = false,
- autofillTypes: List? = null
-) {
- val autoFillHandler = if (autofillTypes != null) AutoFillRequestHandler(
- autofillTypes = autofillTypes,
- onFill = {
- onValueChange(TextFieldValue(it))
- }
- ) else null
-
- val visualTransformation = if (hideContent) {
- PasswordVisualTransformation()
- } else {
- VisualTransformation.None
- }
-
- // Determine if this is a password field based on autofill types or hideContent flag
- val isPasswordField = hideContent || (autofillTypes != null &&
- (autofillTypes.contains(AutofillType.Password) ||
- autofillTypes.contains(AutofillType.NewPassword)))
-
-
-
- TextField(
- value = value,
- textStyle = if (useMonoSpaceFont) LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) else LocalTextStyle.current,
- visualTransformation = visualTransformation,
- onValueChange = {
- onValueChange(it)
- if (it.text.isEmpty()) autoFillHandler?.requestVerifyManual()
- },
- interactionSource = interactionSource,
- keyboardOptions = KeyboardOptions(
- autoCorrect = !isPasswordField,
- keyboardType = if (isPasswordField) KeyboardType.Password else KeyboardType.Text,
- capitalization = if (isPasswordField) KeyboardCapitalization.None else KeyboardCapitalization.Sentences,
- ),
- modifier = modifier
- .fillMaxWidth()
- .clip(shape)
- .then(
- if (autoFillHandler != null) {
- Modifier.Companion
- .connectNode(handler = autoFillHandler)
- .defaultFocusChangeAutoFill(handler = autoFillHandler)
- } else Modifier
- ),
-
- singleLine = singleLine,
- colors = TextFieldDefaults.colors(
- focusedContainerColor = Color.Transparent,
- unfocusedContainerColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
- focusedIndicatorColor = Color.Transparent,
- ),
- placeholder = {
- Text(placeholder)
- }
- )
-}
-
-// CustomTextField.kt
-
-class UndoRedoState {
- var input by mutableStateOf(TextFieldValue(""))
- private val undoHistory = ArrayDeque()
- private val redoHistory = ArrayDeque()
-
- init {
- undoHistory.add(input)
- }
-
- fun onInput(value: TextFieldValue) {
- if (undoHistory.lastOrNull()?.text != value.text) {
- undoHistory.add(value)
- redoHistory.clear() // A new write action clears the fast-forward history.
- }
- input = value
- }
-
- fun undo() {
- if (undoHistory.size > 1) {
- val lastState = undoHistory.removeLastOrNull()
- lastState?.let {
- redoHistory.add(it)
- }
-
- val previousState = undoHistory.lastOrNull()
- previousState?.let {
- input = it
- }
- }
- }
-
- fun redo() {
- val redoState = redoHistory.removeLastOrNull()
- redoState?.let {
- undoHistory.add(it)
- input = it
- }
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/components/TextFormattingToolbar.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/components/TextFormattingToolbar.kt
deleted file mode 100644
index fd17cd6..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/components/TextFormattingToolbar.kt
+++ /dev/null
@@ -1,190 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.edit.components
-
-import android.content.Context
-import android.net.Uri
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.FormatListBulleted
-import androidx.compose.material.icons.rounded.AutoAwesome
-import androidx.compose.material.icons.rounded.CheckBox
-import androidx.compose.material.icons.rounded.Code
-import androidx.compose.material.icons.rounded.DataObject
-import androidx.compose.material.icons.rounded.FormatBold
-import androidx.compose.material.icons.rounded.FormatItalic
-import androidx.compose.material.icons.rounded.FormatListNumbered
-import androidx.compose.material.icons.rounded.FormatQuote
-import androidx.compose.material.icons.rounded.FormatUnderlined
-import androidx.compose.material.icons.rounded.Highlight
-import androidx.compose.material.icons.rounded.HorizontalRule
-import androidx.compose.material.icons.rounded.Image
-import androidx.compose.material.icons.rounded.Link
-import androidx.compose.material.icons.rounded.StrikethroughS
-import androidx.compose.material.icons.rounded.TableChart
-import androidx.compose.material.icons.rounded.Title
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
-import com.babelsoftware.airnote.presentation.components.getExternalStorageDir
-import com.babelsoftware.airnote.presentation.components.getImageName
-import com.babelsoftware.airnote.presentation.screens.edit.model.EditViewModel
-import java.io.File
-import java.io.FileOutputStream
-import java.io.InputStream
-
-data class ToolbarItem(
- val icon: ImageVector,
- val contentDescription: String,
- val color: Color,
- val onClickAction: () -> Unit,
-)
-
-@Composable
-fun TextFormattingToolbar(viewModel: EditViewModel) {
- val colorIcon = MaterialTheme.colorScheme.onSurfaceVariant
- val context = LocalContext.current
- val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
- uri?.let {
- val savedUri = saveImageToAppStorage(context, it)
- viewModel.insertText("!($savedUri)")
- }
- }
-
- val toolbarItems = remember {
- listOf(
- ToolbarItem(Icons.Rounded.FormatBold, "Bold", color = colorIcon) {
- viewModel.insertText("****", offset = -2, newLine = false)
- },
- ToolbarItem(Icons.Rounded.FormatItalic, "Italic", color = colorIcon) {
- viewModel.insertText("**", offset = -1, newLine = false)
- },
- ToolbarItem(Icons.Rounded.FormatUnderlined, "Underline", color = colorIcon) {
- viewModel.insertText("__", -1 , newLine = false)
- },
- ToolbarItem(Icons.Rounded.StrikethroughS, "Strikethrough", color = colorIcon) {
- viewModel.insertText("~~~~", -2 , newLine = false)
- },
- ToolbarItem(Icons.Rounded.Highlight, "Highlight", color = colorIcon) {
- viewModel.insertText("====", offset = -2, newLine = false)
- },
- ToolbarItem(Icons.Rounded.Title, "Header", color = colorIcon) {
- viewModel.insertText("# ")
- },
- ToolbarItem(Icons.Rounded.FormatQuote, "Quote", color = colorIcon) {
- viewModel.insertText("> ", newLine = true)
- },
- ToolbarItem(Icons.Rounded.HorizontalRule, "Horizontal Rule", color = colorIcon) {
- viewModel.insertText("\n---\n", newLine = true)
- },
- ToolbarItem(Icons.AutoMirrored.Rounded.FormatListBulleted, "Bullet List", color = colorIcon) {
- viewModel.insertText("- ")
- },
- ToolbarItem(Icons.Rounded.FormatListNumbered, "Numbered List", color = colorIcon) {
- viewModel.insertText("1. ")
- },
- ToolbarItem(Icons.Rounded.CheckBox, "Checkbox", color = colorIcon) {
- viewModel.insertText("[ ] ")
- },
- ToolbarItem(Icons.Rounded.Link, "Insert Link", color = colorIcon) {
- viewModel.insertText("[](url)", offset = -6, newLine = false)
- },
- ToolbarItem(Icons.Rounded.Image, "Insert Image", color = colorIcon) {
- launcher.launch("image/*")
- },
- ToolbarItem(Icons.Rounded.TableChart, "Insert Table", color = colorIcon) {
- viewModel.insertText("| Header | Header |\n| --- | --- |\n| Cell | Cell |", newLine = true)
- },
- ToolbarItem(Icons.Rounded.Code, "Code Block", color = colorIcon) {
- viewModel.insertText("```\n\n```", -4)
- },
- ToolbarItem(Icons.Rounded.DataObject, "Inline Code", color = colorIcon) {
- viewModel.insertText("``", -1, newLine = false)
- },
- )
- }
-
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 12.dp, vertical = 8.dp),
- elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer)
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 4.dp, vertical = 2.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- IconButton(
- onClick = { viewModel.toggleMinimalAiUi(true) },
- enabled = true
- ) {
- Icon(
- imageVector = Icons.Rounded.AutoAwesome,
- contentDescription = "Edit with AI",
- modifier = Modifier.size(18.dp),
- tint = MaterialTheme.colorScheme.primary
- )
- }
-
- Spacer(modifier = Modifier.width(8.dp))
-
- LazyRow(
- modifier = Modifier.weight(1f),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- items(toolbarItems) { item ->
- IconButton(
- onClick = { item.onClickAction.invoke() },
- ) {
- Icon(
- item.icon,
- contentDescription = item.contentDescription,
- modifier = Modifier.size(24.dp),
- tint = item.color,
- )
- }
- }
- }
- }
- }
-}
-
-private fun saveImageToAppStorage(context: Context, uri: Uri): String {
- val appStorageDir = getExternalStorageDir(context)
- if (!appStorageDir.exists()) {
- appStorageDir.mkdirs()
- }
- val imageFile = File(appStorageDir, getImageName(uri))
-
- val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
- inputStream?.use { input ->
- FileOutputStream(imageFile).use { output ->
- input.copyTo(output)
- }
- }
-
- inputStream?.close()
- return imageFile.path.toString()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/model/EditModel.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/model/EditModel.kt
deleted file mode 100644
index dceb683..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/edit/model/EditModel.kt
+++ /dev/null
@@ -1,970 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.edit.model
-
-import android.content.Intent
-import android.util.Patterns
-import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableLongStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.babelsoftware.airnote.domain.model.Note
-import com.babelsoftware.airnote.domain.usecase.NoteUseCase
-import com.babelsoftware.airnote.presentation.components.DecryptionResult
-import com.babelsoftware.airnote.presentation.components.EncryptionHelper
-import com.babelsoftware.airnote.presentation.screens.edit.components.UndoRedoState
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-import com.babelsoftware.airnote.data.repository.GeminiRepository
-import com.babelsoftware.airnote.data.repository.AiAction
-import com.babelsoftware.airnote.data.repository.AiAssistantAction
-import com.babelsoftware.airnote.data.repository.AiTone
-import com.babelsoftware.airnote.data.repository.SecureStorageRepository
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.data.provider.StringProvider
-import com.babelsoftware.airnote.domain.model.ChatMessage
-import com.babelsoftware.airnote.domain.model.Participant
-import com.babelsoftware.airnote.domain.model.AiResponse
-import com.babelsoftware.airnote.domain.usecase.FolderUseCase
-import com.google.gson.Gson
-import com.google.gson.JsonSyntaxException
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.withContext
-import org.jsoup.Jsoup
-import kotlin.onSuccess
-
-@HiltViewModel
-class EditViewModel @Inject constructor(
- private val noteUseCase: NoteUseCase,
- private val folderUseCase: FolderUseCase,
- private val encryption: EncryptionHelper,
- private val geminiRepository: GeminiRepository,
- private val secureStorageRepository: SecureStorageRepository,
- private val stringProvider: StringProvider
-) : ViewModel() {
- private val _noteName = mutableStateOf(TextFieldValue())
- val noteName: State get() = _noteName
-
- private val _isDescriptionInFocus = mutableStateOf(false)
- val isDescriptionInFocus: State get() = _isDescriptionInFocus
-
- private val _isEncrypted = mutableStateOf(false)
- val isEncrypted: State get() = _isEncrypted
-
- private val _noteDescription = mutableStateOf(TextFieldValue())
- val noteDescription: State get() = _noteDescription
-
- private val _noteId = mutableIntStateOf(0)
- val noteId: State get() = _noteId
-
- private val _noteCreatedTime = mutableLongStateOf(System.currentTimeMillis())
- val noteCreatedTime: State get() = _noteCreatedTime
-
- private val _isNoteInfoVisible = mutableStateOf(false)
- val isNoteInfoVisible: State get() = _isNoteInfoVisible
-
- private val _isEditMenuVisible = mutableStateOf(false)
- val isEditMenuVisible: State get() = _isEditMenuVisible
-
- private val _isPinned = mutableStateOf(false)
- val isPinned: State get() = _isPinned
-
- private val undoRedoState = UndoRedoState()
-
- private val _folderId = mutableStateOf(null)
- val folderId: State get() = _folderId
-
- private var isShareIntentHandled = false
-
- private val _isLoading = mutableStateOf(true) // Başlangıçta true
- val isLoading: State get() = _isLoading
-
- private val _isDreamJournalMode = mutableStateOf(false)
- val isDreamJournalMode: State get() = _isDreamJournalMode
-
- // --- AI'S STATES ---
- private suspend fun getApiKeyToUse(): String {
- return secureStorageRepository.getUserApiKey() ?: ""
- }
- private val _isAiActionSheetVisible = mutableStateOf(false)
- val isAiActionSheetVisible: State get() = _isAiActionSheetVisible
-
- private val _isSheetReadyForInteraction = mutableStateOf(false)
- val isSheetReadyForInteraction: State get() = _isSheetReadyForInteraction
-
- private val _isToneActionSheetVisible = mutableStateOf(false)
- val isToneActionSheetVisible: State get() = _isToneActionSheetVisible
-
- private val _aiResultText = mutableStateOf(null)
- val aiResultText: State = _aiResultText
-
- private val _isAiLoading = mutableStateOf(false)
- val isAiLoading: State get() = _isAiLoading
-
- // --- NEW MINIMAL AI CHAT STATES ---
- private val _isMinimalAiUiVisible = mutableStateOf(false)
- val isMinimalAiUiVisible: State get() = _isMinimalAiUiVisible
-
- private val _minimalAiChatText = mutableStateOf("")
- val minimalAiChatText: State get() = _minimalAiChatText
-
- private val _minimalAiChatHistory = mutableStateOf>(emptyList())
- val minimalAiChatHistory: State> = _minimalAiChatHistory
-
- private val _isMinimalChatActive = mutableStateOf(false)
- val isMinimalChatActive: State get() = _isMinimalChatActive
-
- private val _isMinimalAiLoading = mutableStateOf(false)
- val isMinimalAiLoading: State get() = _isMinimalAiLoading
- // --- END NEW STATES ---
-
- private val _isAiAssistantStreaming = mutableStateOf(false)
- val isAiAssistantStreaming: State get() = _isAiAssistantStreaming
- private var _lastSelection: TextRange? = null // Tentative texts to be sent to AI
- private val _uiEvent = Channel()
- val uiEvent = _uiEvent.receiveAsFlow()
-
- private val _isAiAssistantSheetVisible = mutableStateOf(false)
- val isAiAssistantSheetVisible: State get() = _isAiAssistantSheetVisible
- private val _titleSuggestions = mutableStateOf>(emptyList())
- val titleSuggestions: State> = _titleSuggestions
-
- private val _isTranslateSheetVisible = mutableStateOf(false)
- val isTranslateSheetVisible: State get() = _isTranslateSheetVisible
-
- private val _downloadedLanguages = mutableStateOf>>(emptyList())
- val downloadedLanguages: State>> = _downloadedLanguages
-
- private var _translationSelection: TextRange? = null
- private var _isTranslatingSelection = false // Çevirinin seçim için mi yoksa tüm not için mi olduğunu takip eder
- // --- AI'S STATES | END---
-
-
- // --- YENİ EKLENEN FONKSİYONLAR (PAYLAŞ VE LİNK) ---
-
- /**
- * "Link" adında bir klasör arar. Bulamazsa oluşturur ve ID'sini döndürür.
- * (Daha kararlı hale getirildi)
- */
- private suspend fun getOrCreateLinkFolderId(): Long? {
- val linkFolderName = "Link"
- val foldersFlow = folderUseCase.getAllFolders()
- var folders = foldersFlow.first() // Mevcut listeyi al
- var linkFolder = folders.find { it.name.equals(linkFolderName, ignoreCase = true) }
-
- if (linkFolder != null) {
- return linkFolder.id // Klasör zaten var, ID'sini döndür
- }
-
- // Klasör yok, oluşturalım.
- // iconName için HomeModel.kt'de kullanılan "Link" ikonunu varsayıyorum.
- val newFolder = com.babelsoftware.airnote.domain.model.Folder(name = linkFolderName, iconName = "Link")
-
- try {
- folderUseCase.addFolder(newFolder) // suspend fonksiyon, eklemenin bitmesini bekle
-
- // Ekleme bittiğine göre, veritabanı güncellenmiş olmalı.
- // Flow'dan en güncel listeyi tekrar çek.
- folders = foldersFlow.first()
- linkFolder = folders.find { it.name.equals(linkFolderName, ignoreCase = true) }
-
- return linkFolder?.id // Yeni oluşturulan klasörün ID'sini döndür
-
- } catch (e: Exception) {
- // Ekleme veya bulma sırasında hata oluştu
- e.printStackTrace()
- // Hata durumunda bile son bir kez daha dene
- folders = foldersFlow.first()
- linkFolder = folders.find { it.name.equals(linkFolderName, ignoreCase = true) }
- return linkFolder?.id
- }
- }
-
- /**
- * Gelen paylaşım verisini (link veya metin) işler.
- */
- fun handleSharedIntent(intent: Intent) {
- // Intent'in zaten işlenip işlenmediğini kontrol et
- if (isShareIntentHandled) return
-
- if (intent.action == Intent.ACTION_SEND && "text/plain" == intent.type) {
- // Intent'i işlendi olarak işaretle
- isShareIntentHandled = true
-
- viewModelScope.launch {
- val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return@launch
- val sharedTitle = intent.getStringExtra(Intent.EXTRA_SUBJECT)
-
- // 1. Paylaşılan metin bir URL mi kontrol et
- if (Patterns.WEB_URL.matcher(sharedText).matches()) {
- // EVET, BU BİR LİNK
-
- // 2. "Link" klasörünün ID'sini al veya oluştur
- val targetFolderId = getOrCreateLinkFolderId() // <-- Artık Long? döndürür
-
- if(targetFolderId != null) {
- updateFolderId(targetFolderId) // Notun folderId'sini ayarla
- } else {
- // Opsiyonel: Klasör oluşturma başarısız olursa kullanıcıyı bilgilendir
- _uiEvent.send("The link folder could not be created.")
- }
-
- // 3. Site başlığını çek
- _isAiLoading.value = true // Başlık çekilirken yükleniyor göster
- val title = fetchTitleFromUrl(sharedText) ?: sharedTitle ?: "Saved links"
- _isAiLoading.value = false
-
- // 4. ViewModel'i doldur
- updateNoteName(TextFieldValue(title))
- updateNoteDescription(TextFieldValue(sharedText))
-
- } else {
- // HAYIR, BU DÜZ METİN
- // Normal davran, klasör ataması yapma (varsayılan klasörde kalsın)
- val title = sharedTitle ?: sharedText.take(30)
- updateNoteName(TextFieldValue(title))
- updateNoteDescription(TextFieldValue(sharedText))
- }
- }
- }
- }
-
- /**
- * Asenkron olarak URL'den başlık çeker.
- * Bu bir ağ işlemidir, bu yüzden 'suspend' olmalıdır.
- */
- private suspend fun fetchTitleFromUrl(url: String): String? {
- return withContext(Dispatchers.IO) {
- try {
- val doc = Jsoup.connect(url)
- .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
- .timeout(5000) // 5 saniye zaman aşımı
- .get()
- // Başlığın boş olmadığını kontrol et
- doc.title().takeIf { it.isNotBlank() }
- } catch (e: Exception) {
- e.printStackTrace()
- null // Hata durumunda null döndür
- }
- }
- }
-
- // --- BİTİŞ: YENİ EKLENEN FONKSİYONLAR ---
-
-
- // --- AI FUNCTIONS ---
-
- // --- NEW MINIMAL AI CHAT FUNCTIONS ---
- fun toggleMinimalAiUi(isVisible: Boolean) {
- _isMinimalAiUiVisible.value = isVisible
- if (isVisible) {
- viewModelScope.launch {
- geminiRepository.getDownloadedModels()
- .onSuccess { downloadedCodes ->
- val availableLanguages = geminiRepository.supportedLanguages
- .filter { downloadedCodes.contains(it.key) }
- .map { it.key to it.value }
- _downloadedLanguages.value = availableLanguages
- }
- .onFailure {
- println("Dil modelleri yüklenemedi (Minimal UI): ${it.message}")
- }
- }
- } else {
- // Reset chat state when closing
- _minimalAiChatText.value = ""
- _minimalAiChatHistory.value = emptyList()
- _isMinimalChatActive.value = false
- _isMinimalAiLoading.value = false
- }
- }
-
- fun updateMinimalAiChatText(newText: String) {
- _minimalAiChatText.value = newText
- }
-
- fun sendMinimalAiMessage() {
- if (isMinimalAiLoading.value || minimalAiChatText.value.isBlank()) return
-
- val userMessageText = _minimalAiChatText.value
- _minimalAiChatText.value = ""
- val userMessage = ChatMessage(userMessageText, Participant.USER)
- val loadingMessage = ChatMessage("", Participant.MODEL, isLoading = true)
-
- _minimalAiChatHistory.value = _minimalAiChatHistory.value + userMessage + loadingMessage
- _isMinimalChatActive.value = true
- _isMinimalAiLoading.value = true
-
- viewModelScope.launch {
- try {
- val apiKey = getApiKeyToUse()
- val noteContext = noteDescription.value.text
- val currentHistoryForApi = _minimalAiChatHistory.value.dropLast(2)
- val result = geminiRepository.generateChatOrCommandResponse(
- noteContext = noteContext,
- userRequest = userMessageText,
- chatHistory = currentHistoryForApi,
- apiKey = apiKey
- )
-
- _isMinimalAiLoading.value = false
- _minimalAiChatHistory.value = _minimalAiChatHistory.value.filter { !it.isLoading }
-
- result.onSuccess { jsonResponse ->
- try {
- val aiResponse = Gson().fromJson(jsonResponse, AiResponse::class.java)
-
- when (aiResponse.intent) {
- "CHAT" -> {
- val responseText = aiResponse.response ?: "..."
- val modelMessage = ChatMessage(responseText, Participant.MODEL)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value + modelMessage
- }
- "EDIT_NOTE" -> {
- aiResponse.newNoteContent?.let { newContent ->
- undoRedoState.onInput(noteDescription.value)
- updateNoteDescription(TextFieldValue(newContent))
-
- val successMessage = ChatMessage(
- stringProvider.getString(R.string.chat_message_note_updated),
- Participant.MODEL
- )
- _minimalAiChatHistory.value = _minimalAiChatHistory.value + successMessage
- }
- }
- else -> {
- val errorMessage = ChatMessage("AI garip bir yanıt verdi: ${aiResponse.intent}", Participant.ERROR)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value + errorMessage
- }
- }
- } catch (e: JsonSyntaxException) {
- e.printStackTrace()
- val errorMessage = ChatMessage(stringProvider.getString(R.string.ai_error_invalid_json), Participant.ERROR)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value + errorMessage
- }
- }
-
- result.onFailure { e ->
- val errorMessage = ChatMessage(e.message ?: stringProvider.getString(R.string.error_unknown), Participant.ERROR)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value + errorMessage
- }
-
- } catch (e: Exception) {
- _isMinimalAiLoading.value = false
- _minimalAiChatHistory.value = _minimalAiChatHistory.value.filter { !it.isLoading }
- val errorMessage = ChatMessage(e.message ?: stringProvider.getString(R.string.error_unknown), Participant.ERROR)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value + errorMessage
- }
- }
- }
-
- fun toggleAiActionSheet(isVisible: Boolean) {
- if (isVisible) {
- _isSheetReadyForInteraction.value = false // Set the interaction status to “not ready” (false) each time the menu is opened.
- }
- _isAiActionSheetVisible.value = isVisible
- }
-
- fun markSheetAsReadyForInteraction() {
- _isSheetReadyForInteraction.value = true
- }
-
- fun toggleToneActionSheet(isVisible: Boolean) {
- _isToneActionSheetVisible.value = isVisible
- }
-
- fun clearAiResult() {
- _aiResultText.value = null
- _lastSelection = null // Reset stored selection
- }
-
- /**
- * Central function that runs all AI actions.
- * Operates only on the selected text.
- */
- fun executeAiAction(action: AiAction, tone: AiTone? = null) {
- val selection = _lastSelection ?: noteDescription.value.selection
- if (action != AiAction.CHANGE_TONE && action != AiAction.TRANSLATE) {
- toggleAiActionSheet(false)
- } else {
- }
- toggleToneActionSheet(false)
-
- if (action == AiAction.CHANGE_TONE && tone == null) {
- return
- }
-
- if (action == AiAction.TRANSLATE) {
- onTranslateClicked(forSelection = true)
- return
- }
-
- if (selection.collapsed) {
- viewModelScope.launch {
- _uiEvent.send(stringProvider.getString(R.string.ai_error_no_text_selected))
- }
- return
- }
-
- val selectedText = noteDescription.value.text.substring(selection.min, selection.max)
-
- if (selectedText.isNotBlank()) {
- viewModelScope.launch {
- _isAiLoading.value = true
- try {
- val apiKey = getApiKeyToUse()
- geminiRepository.processAiAction(selectedText, action, tone, apiKey)
- .onSuccess { responseText ->
- _lastSelection = selection
- _aiResultText.value = responseText
- }
- .onFailure { exception ->
- _uiEvent.send(exception.message ?: stringProvider.getString(R.string.error_unknown))
- }
- } catch (e: Exception) {
- _uiEvent.send(stringProvider.getString(R.string.error_unexpected_with_message, e.message ?: ""))
- } finally {
- _isAiLoading.value = false
- }
- }
- }
- }
-
- fun executeAiAssistantAction(action: AiAssistantAction) {
- toggleAiAssistantSheet(false)
-
- viewModelScope.launch {
- var fullResponse = StringBuilder()
- val apiKey = getApiKeyToUse()
-
- geminiRepository.processAssistantAction(
- noteName = noteName.value.text,
- noteDescription = noteDescription.value.text,
- action = action,
- apiKey = apiKey
- )
- .onStart {
- if (action != AiAssistantAction.SUGGEST_A_TITLE) _isAiAssistantStreaming.value = true
- }
- .onCompletion {
- _isAiAssistantStreaming.value = false
- }
- .collect { chunk ->
- if (chunk.startsWith("API isteği başarısız oldu") || chunk.startsWith("Kullanıcı API anahtarı bulunamadı")) {
- _uiEvent.send(chunk)
- this.coroutineContext.cancel()
- return@collect
- }
- fullResponse.append(chunk)
- if (action == AiAssistantAction.PROS_AND_CONS || action == AiAssistantAction.CREATE_TODO_LIST || action == AiAssistantAction.SIMPLIFY) {
- val currentText = _noteDescription.value.text
- val newText = currentText + chunk
- _noteDescription.value = TextFieldValue(text = newText, selection = TextRange(newText.length))
- }
- }
- val finalResponse = fullResponse.toString()
- when (action) {
- AiAssistantAction.CREATE_TODO_LIST -> {
- if (finalResponse.trim() == "NO_TASKS") {
- _uiEvent.send(stringProvider.getString(R.string.ai_error_no_tasks_for_todo))
- }
- }
- AiAssistantAction.SUGGEST_A_TITLE -> {
- val suggestions = finalResponse.lines().mapNotNull {
- it.replaceFirst(Regex("^\\d+\\.?\\s*"), "").trim().takeIf { s -> s.isNotEmpty() }
- }
- _titleSuggestions.value = suggestions
- }
- else -> {
- }
- }
- }
- }
-
- fun executeDreamAnalysis() {
- toggleMinimalAiUi(true)
- _isMinimalChatActive.value = true
-
- val dreamText = noteDescription.value.text
- if (dreamText.isBlank()) {
- viewModelScope.launch {
- _uiEvent.send(stringProvider.getString(R.string.dream_not_found))
- }
- return
- }
-
- val userMessageText = stringProvider.getString(R.string.interpret_my_dream)
- val userMessage = ChatMessage(userMessageText, Participant.USER)
- val loadingMessage = ChatMessage("", Participant.MODEL, isLoading = true)
-
- _minimalAiChatHistory.value = listOf(userMessage, loadingMessage)
- _isMinimalAiLoading.value = true
-
- val historyToSend = mutableListOf()
- val dreamPrompt = stringProvider.getString(R.string.prompt_dream_journal_header)
- historyToSend.add(ChatMessage("$dreamPrompt\"\"\"$dreamText\"\"\"\n\n$userMessageText", Participant.USER))
-
- viewModelScope.launch {
- try {
- val apiKey = getApiKeyToUse()
- val responseBuilder = StringBuilder()
-
- geminiRepository.generateChatResponse(historyToSend, apiKey)
- .onCompletion {
- _isMinimalAiLoading.value = false
- val finalResponse = ChatMessage(responseBuilder.toString(), Participant.MODEL, isLoading = false)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value.dropLast(1) + finalResponse
- }
- .collect { chunk ->
- responseBuilder.append(chunk)
- // Akıcı yanıtı arayüze yansıt
- _minimalAiChatHistory.value = _minimalAiChatHistory.value.dropLast(1) + ChatMessage(responseBuilder.toString(), Participant.MODEL, isLoading = true)
- }
-
- } catch (e: Exception) {
- _isMinimalAiLoading.value = false
- val errorMessage = ChatMessage(e.message ?: stringProvider.getString(R.string.error_unknown), Participant.ERROR, isLoading = false)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value.dropLast(1) + errorMessage
- }
- }
- }
-
- fun executeDreamSymbolAnalysis() {
- toggleMinimalAiUi(true)
- _isMinimalChatActive.value = true
-
- val dreamText = noteDescription.value.text
- if (dreamText.isBlank()) {
- viewModelScope.launch {
- _uiEvent.send(stringProvider.getString(R.string.dream_not_found))
- }
- return
- }
-
- val userMessageText = stringProvider.getString(R.string.explain_symbols)
- val userMessage = ChatMessage(userMessageText, Participant.USER)
- val loadingMessage = ChatMessage("", Participant.MODEL, isLoading = true)
-
- _minimalAiChatHistory.value = listOf(userMessage, loadingMessage)
- _isMinimalAiLoading.value = true
-
- val historyToSend = mutableListOf()
- val dreamPrompt = stringProvider.getString(R.string.prompt_dream_journal_symbol_analysis)
-
- historyToSend.add(ChatMessage("$dreamPrompt\"\"\"$dreamText\"\"\"\n\n$userMessageText", Participant.USER))
-
- viewModelScope.launch {
- try {
- val apiKey = getApiKeyToUse()
- val responseBuilder = StringBuilder()
-
- geminiRepository.generateChatResponse(historyToSend, apiKey)
- .onCompletion {
- _isMinimalAiLoading.value = false
- val finalResponse = ChatMessage(responseBuilder.toString(), Participant.MODEL, isLoading = false)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value.dropLast(1) + finalResponse
- }
- .collect { chunk ->
- responseBuilder.append(chunk)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value.dropLast(1) + ChatMessage(responseBuilder.toString(), Participant.MODEL, isLoading = true)
- }
-
- } catch (e: Exception) {
- _isMinimalAiLoading.value = false
- val errorMessage = ChatMessage(e.message ?: stringProvider.getString(R.string.error_unknown), Participant.ERROR, isLoading = false)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value.dropLast(1) + errorMessage
- }
- }
- }
-
- fun executeDreamEmotionAnalysis() {
- toggleMinimalAiUi(true)
- _isMinimalChatActive.value = true
-
- val dreamText = noteDescription.value.text
- if (dreamText.isBlank()) {
- viewModelScope.launch {
- _uiEvent.send(stringProvider.getString(R.string.dream_not_found))
- }
- return
- }
-
- val userMessageText = stringProvider.getString(R.string.emotional_analysis)
- val userMessage = ChatMessage(userMessageText, Participant.USER)
- val loadingMessage = ChatMessage("", Participant.MODEL, isLoading = true)
-
- _minimalAiChatHistory.value = listOf(userMessage, loadingMessage)
- _isMinimalAiLoading.value = true
-
- val historyToSend = mutableListOf()
- val dreamPrompt = stringProvider.getString(R.string.prompt_dream_journal_emotion_analysis)
-
- historyToSend.add(ChatMessage("$dreamPrompt\"\"\"$dreamText\"\"\"\n\n$userMessageText", Participant.USER))
-
- viewModelScope.launch {
- try {
- val apiKey = getApiKeyToUse()
- val responseBuilder = StringBuilder()
-
- geminiRepository.generateChatResponse(historyToSend, apiKey)
- .onCompletion {
- _isMinimalAiLoading.value = false
- val finalResponse = ChatMessage(responseBuilder.toString(), Participant.MODEL, isLoading = false)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value.dropLast(1) + finalResponse
- }
- .collect { chunk ->
- responseBuilder.append(chunk)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value.dropLast(1) + ChatMessage(responseBuilder.toString(), Participant.MODEL, isLoading = true)
- }
-
- } catch (e: Exception) {
- _isMinimalAiLoading.value = false
- val errorMessage = ChatMessage(e.message ?: stringProvider.getString(R.string.error_unknown), Participant.ERROR, isLoading = false)
- _minimalAiChatHistory.value = _minimalAiChatHistory.value.dropLast(1) + errorMessage
- }
- }
- }
-
- /**
- * Replaces AI generated text with the selected text.
- */
- fun replaceWithAiResult() {
- val originalText = noteDescription.value.text
- val selection = _lastSelection ?: return // Use stored selection
- val newText = _aiResultText.value ?: return
-
- val updatedText = originalText.replaceRange(selection.start, selection.end, newText)
- val newSelectionStart = selection.start + newText.length
- updateNoteDescription(
- TextFieldValue(
- text = updatedText,
- selection = TextRange(newSelectionStart)
- )
- )
- clearAiResult()
- }
-
- fun toggleAiAssistantSheet(isVisible: Boolean) {
- _isAiAssistantSheetVisible.value = isVisible
- }
-
- fun clearTitleSuggestions() {
- _titleSuggestions.value = emptyList()
- }
-
- fun toggleTranslateSheet(isVisible: Boolean) {
- _isTranslateSheetVisible.value = isVisible
- }
-
- fun onTranslateClicked(forSelection: Boolean) {
- _isTranslatingSelection = forSelection
- _translationSelection = if (forSelection) {
- noteDescription.value.selection.takeIf { !it.collapsed }
- } else {
- TextRange(0, noteDescription.value.text.length)
- }
-
- if (forSelection && _translationSelection == null) {
- viewModelScope.launch {
- _uiEvent.send(stringProvider.getString(R.string.ai_error_select_text_to_translate))
- }
- return
- }
-
- if (forSelection) {
- toggleMinimalAiUi(true)
- } else {
- viewModelScope.launch {
- _isAiLoading.value = true
- geminiRepository.getDownloadedModels()
- .onSuccess { downloadedCodes ->
- val availableLanguages = geminiRepository.supportedLanguages
- .filter { downloadedCodes.contains(it.key) }
- .map { it.key to it.value }
- _downloadedLanguages.value = availableLanguages
- toggleTranslateSheet(true)
- }
- .onFailure {
- _uiEvent.send(stringProvider.getString(R.string.error_fetching_downloaded_models, it.message ?: ""))
- }
- _isAiLoading.value = false
- }
- }
- }
-
- fun executeTranslation(targetLanguageCode: String) {
- if (_isTranslatingSelection) {
- } else {
- toggleTranslateSheet(false)
- }
-
- val selection = _translationSelection
- if (selection == null || selection.end > noteDescription.value.text.length) {
- return
- }
-
- val textToTranslate = noteDescription.value.text.substring(selection.start, selection.end)
- if (textToTranslate.isBlank()) return
-
- viewModelScope.launch {
- _isAiLoading.value = true
- geminiRepository.translateOnDevice(textToTranslate, targetLanguageCode)
- .onSuccess { translatedText ->
- val originalText = noteDescription.value.text
- val updatedText = originalText.replaceRange(selection.start, selection.end, translatedText)
- val newSelectionStart = selection.start + translatedText.length
- updateNoteDescription(
- TextFieldValue(
- text = updatedText,
- selection = TextRange(newSelectionStart)
- )
- )
-
- if (_isTranslatingSelection) {
- toggleMinimalAiUi(false)
- }
- }
- .onFailure {
- _uiEvent.send(stringProvider.getString(R.string.error_translation_failed, it.message ?: ""))
- }
- _isAiLoading.value = false
- }
- }
- // --- AI FUNCTIONS | END ---
-
- fun updateFolderId(newFolderId: Long?) {
- _folderId.value = newFolderId
- }
-
- fun saveNote(id: Int) {
- if (noteName.value.text.isNotEmpty() || noteDescription.value.text.isNotBlank()) {
- viewModelScope.launch {
- noteUseCase.addNote(
- Note(
- id = id,
- name = noteName.value.text,
- description = noteDescription.value.text,
- pinned = isPinned.value,
- folderId = folderId.value,
- encrypted = isEncrypted.value,
- createdAt = if (noteCreatedTime.value != 0L) noteCreatedTime.value else System.currentTimeMillis(),
- )
- )
- fetchLastNoteAndUpdate()
- }
- }
- }
-
- fun deleteNote(id: Int) {
- noteUseCase.deleteNoteById(id = id)
- }
-
- private fun syncNote(note: Note) {
- if (note.encrypted) {
- val (name, nameStatus) = encryption.decrypt(note.name)
- val (description, descriptionStatus) = encryption.decrypt(note.description)
- if (nameStatus == DecryptionResult.SUCCESS && descriptionStatus == DecryptionResult.SUCCESS) {
- updateNoteName(TextFieldValue(name!!, selection = TextRange(note.name.length)))
- updateNoteDescription(TextFieldValue(description!!, selection = TextRange(note.description.length)))
- }
- } else {
- updateNoteName(TextFieldValue(note.name, selection = TextRange(note.name.length)))
- updateNoteDescription(TextFieldValue(note.description, selection = TextRange(note.description.length)))
- }
- updateNoteCreatedTime(note.createdAt)
- updateNoteId(note.id)
- updateNotePin(note.pinned)
- updateIsEncrypted(note.encrypted)
- updateFolderId(note.folderId)
- viewModelScope.launch {
- // --- DÜZELTME BAŞLANGIÇ ---
- val dreamFolderName = stringProvider.getString(R.string.dream_journal)
- // --- DÜZELTME BİTİŞ ---
- try {
- val folders = folderUseCase.getAllFolders().first()
- val folder = folders.find { it.id == note.folderId }
- // --- DÜZELTME BAŞLANGIÇ ---
- if (folder?.name.equals(dreamFolderName, ignoreCase = true)) {
- // --- DÜZELTME BİTİŞ ---
- _isDreamJournalMode.value = true
- } else {
- _isDreamJournalMode.value = false
- }
- } catch (e: Exception) {
- _isDreamJournalMode.value = false
- } finally {
- _isLoading.value = false
- }
- }
- }
-
- fun setupNoteData(id : Int, folderIdFromNav: Long? = null) {
- _isLoading.value = true
- if (id != 0) {
- viewModelScope.launch {
- noteUseCase.getNoteById(id).collectLatest { note ->
- if (note != null) {
- syncNote(note)
- } else {
- _isLoading.value = false
- }
- }
- }
- } else {
- updateFolderId(folderIdFromNav)
- if (folderIdFromNav != null) {
- viewModelScope.launch {
- val dreamFolderName = stringProvider.getString(R.string.dream_journal)
- try {
- val folders = folderUseCase.getAllFolders().first()
- val folder = folders.find { it.id == folderIdFromNav }
- if (folder?.name.equals(dreamFolderName, ignoreCase = true)) {
- _isDreamJournalMode.value = true
- }
- } catch (e: Exception) {
- _isDreamJournalMode.value = false
- } finally {
- _isLoading.value = false
- }
- }
- } else {
- _isLoading.value = false
- }
- }
- }
-
- private fun fetchLastNoteAndUpdate() {
- if (noteName.value.text.isNotEmpty() || noteDescription.value.text.isNotBlank()) {
- if (noteId.value == 0) {
- viewModelScope.launch {
- noteUseCase.getLastNoteId { lastId ->
- viewModelScope.launch {
- setupNoteData(lastId?.toInt() ?: 1)
- }
- }
- }
- }
- }
- }
-
- fun toggleEditMenuVisibility(value: Boolean) {
- _isEditMenuVisible.value = value
- }
-
- fun toggleNoteInfoVisibility(value: Boolean) {
- _isNoteInfoVisible.value = value
- }
-
- fun toggleIsDescriptionInFocus(value: Boolean) {
- _isDescriptionInFocus.value = value
- }
-
- fun toggleNotePin(value: Boolean) {
- _isPinned.value = value
- }
-
- fun updateNoteName(newName: TextFieldValue) {
- _noteName.value = newName
- undoRedoState.onInput(newName)
- }
-
- fun updateIsEncrypted(value: Boolean) {
- _isEncrypted.value = value
- }
-
- fun updateNoteDescription(newDescription: TextFieldValue) {
- _noteDescription.value = newDescription
- if (!newDescription.selection.collapsed) {
- _lastSelection = newDescription.selection
- }
- undoRedoState.onInput(newDescription)
- }
-
- private fun updateNoteCreatedTime(newTime: Long) {
- _noteCreatedTime.longValue = newTime
- }
-
- private fun updateNotePin(pinned: Boolean) {
- _isPinned.value = pinned
- }
-
- fun updateNoteId(newId: Int) {
- _noteId.intValue = newId
- }
-
- fun undo() {
- undoRedoState.undo()
- _noteDescription.value = undoRedoState.input
- }
-
- fun redo() {
- undoRedoState.redo()
- _noteDescription.value = undoRedoState.input
- }
-
- private fun isSelectorAtStartOfNonEmptyLine(): Boolean {
- val text = _noteDescription.value.text
- val selectionStart = _noteDescription.value.selection.start
-
- if (selectionStart == 0) {
- return true
- }
- return text[selectionStart - 1] == '\n'
- }
-
-
- private fun getIntRangeForCurrentLine(): IntRange {
- val text = _noteDescription.value.text
- val selectionStart = _noteDescription.value.selection.start
- val selectionEnd = _noteDescription.value.selection.end
- var lineStart = selectionStart
- var lineEnd = selectionEnd
-
- while (lineStart > 0 && text[lineStart - 1] != '\n') {
- lineStart--
- }
-
- while (lineEnd < text.length && text[lineEnd] != '\n') {
- lineEnd++
- }
- return IntRange(lineStart, lineEnd - 1);
- }
-
- fun insertText(insertText: String, offset: Int = 1, newLine: Boolean = true) {
- val currentText = _noteDescription.value.text
- val resultSelectionIndex: Int
- val rangeOfCurrentLine = getIntRangeForCurrentLine()
- val updatedText = if (!rangeOfCurrentLine.isEmpty()) {
- val currentLineContents = currentText.substring(rangeOfCurrentLine)
- val newLine = if (isSelectorAtStartOfNonEmptyLine()) {
- insertText + currentLineContents
- } else {
- if (newLine) {
- currentLineContents + "\n" + insertText
- } else {
- currentLineContents + insertText
- }
- }
- resultSelectionIndex = rangeOfCurrentLine.first + newLine.length - 1
- currentText.replaceRange(rangeOfCurrentLine, newLine)
- } else {
- resultSelectionIndex = (currentText + insertText).length
- currentText + insertText
- }
-
- _noteDescription.value = TextFieldValue(
- text = updatedText,
- selection = TextRange(resultSelectionIndex + offset)
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/AiInterface.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/AiInterface.kt
deleted file mode 100644
index 7b82d82..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/AiInterface.kt
+++ /dev/null
@@ -1,1917 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.home
-
-import android.content.ActivityNotFoundException
-import android.net.Uri
-import android.provider.MediaStore
-import android.widget.Toast
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.ClickableText
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.Chat
-import androidx.compose.material.icons.automirrored.filled.Send
-import androidx.compose.material.icons.automirrored.rounded.Notes
-import androidx.compose.material.icons.filled.AttachFile
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.Description
-import androidx.compose.material.icons.filled.Image
-import androidx.compose.material.icons.filled.Language
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.material.icons.rounded.AddComment
-import androidx.compose.material.icons.rounded.AutoAwesome
-import androidx.compose.material.icons.rounded.Edit
-import androidx.compose.material.icons.rounded.ExpandMore
-import androidx.compose.material.icons.rounded.FilePresent
-import androidx.compose.material.icons.rounded.GolfCourse
-import androidx.compose.material.icons.rounded.History
-import androidx.compose.material.icons.rounded.Notes
-import androidx.compose.material.icons.rounded.Psychology
-import androidx.compose.material.icons.rounded.School
-import androidx.compose.material.icons.rounded.Search
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.IconButtonDefaults
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.material3.TextField
-import androidx.compose.material3.TextFieldDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.StrokeCap
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalHapticFeedback
-import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextDecoration
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.window.Dialog
-import androidx.hilt.navigation.compose.hiltViewModel
-import coil.compose.AsyncImage
-import coil.compose.rememberAsyncImagePainter
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.data.repository.AiMode
-import com.babelsoftware.airnote.domain.model.AiChatSession
-import com.babelsoftware.airnote.domain.model.Participant
-import com.babelsoftware.airnote.domain.model.Settings
-import com.babelsoftware.airnote.presentation.screens.home.viewmodel.AiService
-import com.babelsoftware.airnote.presentation.screens.home.viewmodel.DraftedNote
-import com.babelsoftware.airnote.presentation.screens.home.viewmodel.HomeViewModel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlin.random.Random
-
-// =================================================================
-// === Enhanced AirNote Kai AI Interface (with THEME) ======================
-// =================================================================
-
-@OptIn(ExperimentalAnimationApi::class)
-@Composable
-fun AiChatContainer(viewModel: HomeViewModel, settings: Settings) {
- val isDark = isSystemInDarkTheme()
- val backgroundBrush = if (isDark) {
- Brush.verticalGradient(
- colors = listOf(Color(0xFF10141C), Color(0xFF0A0D12))
- )
- } else {
- Brush.verticalGradient(
- colors = listOf(MaterialTheme.colorScheme.surface, MaterialTheme.colorScheme.surfaceContainerLowest)
- )
- }
-
- val showHistoryScreen by viewModel.showAiHistoryScreen
- val allSessions by viewModel.allChatSessions.collectAsState()
-
- Column(
- modifier = Modifier
- .fillMaxSize()
- .background(backgroundBrush)
- ) {
- AiTopBar(
- viewModel = viewModel,
- showHistory = showHistoryScreen,
- onToggleHistory = { viewModel.toggleAiHistoryScreen(!showHistoryScreen) },
- isPerplexityEnabled = settings.isPerplexityEnabled
- )
-
- AnimatedContent(targetState = showHistoryScreen, label = "AiScreenAnimation") { showHistory ->
- if (showHistory) {
- AiHistoryScreen(
- sessions = allSessions,
- onNewChatClicked = { viewModel.startNewChat() },
- onSessionClicked = { viewModel.loadChatSession(it) },
- onDeleteSession = { viewModel.deleteChatSession(it.id) }
- )
- } else {
- AiMainContent(viewModel = viewModel)
- }
- }
- }
-}
-
-@Composable
-fun AiMainContent(viewModel: HomeViewModel) {
- val chatState by viewModel.chatState.collectAsState()
- var text by remember { mutableStateOf("") }
- val isChatActive = chatState.hasStartedConversation || chatState.messages.isNotEmpty()
- val allNotes by viewModel.allNotesForAi.collectAsState()
- val isDark = isSystemInDarkTheme()
- val selectedService by viewModel.selectedAiService.collectAsState()
- val showAttachmentButton = selectedService == AiService.GEMINI
- val placeholderText = if (selectedService == AiService.GEMINI) {
- if (chatState.isAwaitingDraftTopic) stringResource(R.string.draft_topic_placeholder) else stringResource(R.string.ask_airnote_ai)
- } else {
- stringResource(R.string.ask_perplexity_placeholder)
- }
-
- val (mentionQuery, showMentionSuggestions) = remember(text) {
- val cursorPosition = text.length
- val atIndex = text.lastIndexOf('@', cursorPosition - 1)
-
- if (atIndex != -1) {
- val query = text.substring(atIndex + 1, cursorPosition)
- if (" " !in query && query.length < 30) {
- Pair(query, true)
- } else {
- Pair("", false)
- }
- } else {
- Pair("", false)
- }
- }
-
- val mentionSuggestions = remember(mentionQuery, allNotes, showMentionSuggestions) {
- if (showMentionSuggestions) {
- allNotes.filter {
- it.name.contains(mentionQuery, ignoreCase = true)
- }.take(5)
- } else {
- emptyList()
- }
- }
-
- if (viewModel.showAskQuestionDialog.value) {
- AskAiQuestionDialog(
- onDismiss = { viewModel.onDismissQuestionDialog() },
- onConfirm = { question ->
- viewModel.sendMessage(question)
- viewModel.onDismissQuestionDialog()
- }
- )
- }
-
- if (viewModel.showCreateDraftDialog.value) {
- CreateDraftDialog(
- onDismiss = { viewModel.onDismissCreateDraftDialog() },
- onConfirm = { topic ->
- viewModel.generateDraft(topic)
- viewModel.onDismissCreateDraftDialog()
- }
- )
- }
-
- val onSendMessage = { message: String ->
- if (message.isNotBlank() || (chatState.pendingAttachmentUri != null && showAttachmentButton)) {
- if (chatState.isAwaitingDraftTopic) {
- viewModel.generateDraft(message)
- } else {
- viewModel.sendMessage(message)
- }
- text = ""
- }
- }
-
- Column(modifier = Modifier.fillMaxSize()) {
- Box(modifier = Modifier.weight(1f)) {
- val (isLoading, isAnalyzingImage) = remember(chatState) {
- val loadingMessage = chatState.messages.any { it.isLoading }
- val imageUri = chatState.analyzingImageUri
- (loadingMessage to (imageUri != null))
- }
-
- when {
- chatState.latestDraft != null && chatState.latestDraft?.sourceImageUri != null -> {
- DraftDisplayWithImage(
- draft = chatState.latestDraft!!,
- onSave = { viewModel.saveDraftedNote() },
- onRegenerate = { viewModel.regenerateDraft() }
- )
- }
-
- isLoading && isAnalyzingImage -> {
- ImageAnalysisScreen(imageUri = chatState.analyzingImageUri!!)
- }
-
- chatState.latestDraft != null -> {
- DraftDisplay(
- draft = chatState.latestDraft,
- onSave = { viewModel.saveDraftedNote() },
- onRegenerate = { viewModel.regenerateDraft() }
- )
- }
- isChatActive -> {
- ChatScreenContent(
- messages = chatState.messages,
- topicForLoading = if (
- chatState.messages.lastOrNull()?.isLoading == true &&
- chatState.messages.getOrNull(chatState.messages.size - 2)?.participant == Participant.USER
- ) {
- chatState.messages.getOrNull(chatState.messages.size - 2)?.text
- } else {
- null
- }
- )
- }
- else -> {
- NewAiHomeScreen(viewModel = viewModel)
- }
- }
- }
-
- AnimatedVisibility(visible = mentionSuggestions.isNotEmpty() && showAttachmentButton) {
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp)
- .offset(y = (-8).dp),
- shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
- color = if (isDark) Color.White.copy(alpha = 0.15f) else MaterialTheme.colorScheme.surfaceContainerHigh,
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.2f) else MaterialTheme.colorScheme.outlineVariant)
- ) {
- LazyColumn(
- modifier = Modifier
- .heightIn(max = 150.dp)
- .padding(vertical = 8.dp)
- ) {
- items(items = mentionSuggestions, key = { it.id }) { note ->
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- val atIndex = text.lastIndexOf('@')
- text = text.substring(0, atIndex + 1) + note.name + " "
- }
- .padding(horizontal = 16.dp, vertical = 12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- Icons.Rounded.FilePresent,
- contentDescription = null,
- tint = if (isDark) Color.White.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.size(18.dp)
- )
- Spacer(Modifier.width(12.dp))
- Text(
- text = note.name,
- color = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- }
- }
- }
- }
- }
-
- AnimatedVisibility(visible = chatState.pendingAttachmentUri != null && showAttachmentButton) {
- AttachedFileChip(
- uri = chatState.pendingAttachmentUri,
- mimeType = chatState.pendingAttachmentMimeType,
- onRemove = { viewModel.onAttachmentRemoved() }
- )
- }
-
- val showInputBar = chatState.latestDraft == null && chatState.analyzingImageUri == null
- val isLoading = chatState.messages.any { it.isLoading }
-
- if (showInputBar) {
- if (isChatActive) {
- RedesignedChatInputBar(
- text = text,
- onValueChange = { text = it },
- onSendMessage = { onSendMessage(text) },
- onImagePickerClicked = { viewModel.onAttachmentIconClicked() },
- enabled = !isLoading,
- showAttachmentButton = showAttachmentButton,
- placeholderText = placeholderText
- )
- } else {
- PreChatInputBar(
- text = text,
- onValueChange = { text = it },
- onSendMessage = onSendMessage,
- onImagePickerClicked = { viewModel.onAttachmentIconClicked() },
- enabled = !isLoading,
- showAttachmentButton = showAttachmentButton
- )
- }
- AiDisclaimerText()
- }
- }
-}
-
-@Composable
-fun AttachedFileChip(
- uri: Uri?,
- mimeType: String?,
- onRemove: () -> Unit
-) {
- if (uri == null || mimeType == null) return
-
- val isDark = isSystemInDarkTheme()
- val context = LocalContext.current
-
- val displayName = remember(uri, mimeType) {
- if (mimeType.startsWith("image/")) {
- "Image"
- } else {
- try {
- context.contentResolver.query(uri, arrayOf(MediaStore.Images.Media.DISPLAY_NAME), null, null, null)
- ?.use { cursor ->
- if (cursor.moveToFirst()) {
- cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))
- } else {
- "File"
- }
- } ?: "File"
- } catch (e: Exception) {
- "File"
- }
- }
- }
-
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp)
- .offset(y = (-8).dp),
- shape = RoundedCornerShape(12.dp),
- color = if (isDark) Color.White.copy(alpha = 0.15f) else MaterialTheme.colorScheme.surfaceContainerHigh,
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.2f) else MaterialTheme.colorScheme.outlineVariant)
- ) {
- Row(
- modifier = Modifier
- .padding(horizontal = 12.dp, vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) {
- Icon(
- imageVector = if (mimeType.startsWith("image/")) Icons.Default.Image else Icons.Default.Description,
- contentDescription = "File type",
- tint = if (isDark) Color.White.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant
- )
- Spacer(Modifier.width(8.dp))
- Text(
- text = displayName,
- color = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- }
- IconButton(onClick = onRemove, modifier = Modifier.size(24.dp)) {
- Icon(
- Icons.Default.Close,
- contentDescription = "Remove file",
- tint = if (isDark) Color.White.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
- }
-}
-
-
-@Composable
-fun ImageAnalysisScreen(imageUri: Uri) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp),
- contentAlignment = Alignment.Center
- ) {
- AsyncImage(
- model = imageUri,
- contentDescription = stringResource(R.string.image_being_analyzed),
- modifier = Modifier
- .fillMaxSize()
- .clip(RoundedCornerShape(16.dp)),
- contentScale = ContentScale.Fit
- )
-
- MagicAnalysisAnimation()
- }
-}
-
-@Composable
-fun MagicAnalysisAnimation() {
- val infiniteTransition = rememberInfiniteTransition(label = "analysis_animation")
- val scanLinePosition by infiniteTransition.animateFloat(
- initialValue = 0f,
- targetValue = 1f,
- animationSpec = infiniteRepeatable(
- animation = tween(2000, easing = LinearEasing),
- repeatMode = RepeatMode.Restart
- ),
- label = "scan_line"
- )
-
- val particles = remember {
- List(50) {
- Pair(
- Random.nextFloat(), // x
- Random.nextFloat() // y
- )
- }
- }
- val particleAlpha by infiniteTransition.animateFloat(
- initialValue = 0f,
- targetValue = 1f,
- animationSpec = infiniteRepeatable(
- animation = tween(1000, easing = LinearEasing),
- repeatMode = RepeatMode.Reverse
- ),
- label = "particles"
- )
-
- val primaryColor = if (isSystemInDarkTheme()) Color(0xFF33A2FF) else MaterialTheme.colorScheme.primary
-
- Canvas(modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(16.dp))) {
- val canvasWidth = size.width
- val canvasHeight = size.height
- val y = scanLinePosition * canvasHeight
-
- drawLine(
- brush = Brush.verticalGradient(
- colors = listOf(Color.Transparent, primaryColor.copy(alpha = 0.5f), Color.Transparent),
- startY = y - 20f,
- endY = y + 20f
- ),
- start = Offset(x = 0f, y = y),
- end = Offset(x = canvasWidth, y = y),
- strokeWidth = 4f,
- cap = StrokeCap.Round
- )
-
- particles.forEach { (x, y) ->
- drawCircle(
- color = primaryColor,
- radius = (Random.nextFloat() * 4f + 2f),
- center = Offset(x * canvasWidth, y * canvasHeight),
- alpha = particleAlpha * Random.nextFloat()
- )
- }
-
- val bracketSize = 30f
- val bracketStroke = 6f
-
- // Top left
- drawLine(primaryColor, Offset(0f, bracketSize), Offset(0f, 0f), bracketStroke)
- drawLine(primaryColor, Offset(0f, 0f), Offset(bracketSize, 0f), bracketStroke)
- // Top right
- drawLine(primaryColor, Offset(canvasWidth - bracketSize, 0f), Offset(canvasWidth, 0f), bracketStroke)
- drawLine(primaryColor, Offset(canvasWidth, 0f), Offset(canvasWidth, bracketSize), bracketStroke)
- // Lower Left
- drawLine(primaryColor, Offset(0f, canvasHeight - bracketSize), Offset(0f, canvasHeight), bracketStroke)
- drawLine(primaryColor, Offset(0f, canvasHeight), Offset(bracketSize, canvasHeight), bracketStroke)
- // Lower Right
- drawLine(primaryColor, Offset(canvasWidth - bracketSize, canvasHeight), Offset(canvasWidth, canvasHeight), bracketStroke)
- drawLine(primaryColor, Offset(canvasWidth, canvasHeight), Offset(canvasWidth, canvasHeight - bracketSize), bracketStroke)
- }
-}
-
-@Composable
-fun DraftDisplayWithImage(draft: DraftedNote, onSave: () -> Unit, onRegenerate: () -> Unit) {
- val isDark = isSystemInDarkTheme()
- val primaryAccentColor = if (isDark) Color(0xFF33A2FF) else MaterialTheme.colorScheme.primary
-
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp)
- ) {
- AsyncImage(
- model = draft.sourceImageUri,
- contentDescription = draft.title,
- modifier = Modifier
- .fillMaxWidth()
- .height(150.dp)
- .clip(RoundedCornerShape(16.dp)),
- contentScale = ContentScale.Crop
- )
- Spacer(modifier = Modifier.height(16.dp))
- Surface(
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(16.dp),
- color = if (isDark) Color.White.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceContainer,
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.1f) else MaterialTheme.colorScheme.outlineVariant)
- ) {
- LazyColumn(
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 20.dp)
- ) {
- item {
- Text(
- text = draft.title,
- style = MaterialTheme.typography.titleLarge,
- color = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(bottom = 12.dp)
- )
- }
- item {
- Text(
- text = draft.content,
- style = MaterialTheme.typography.bodyLarge,
- color = if (isDark) Color.White.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant,
- lineHeight = 24.sp
- )
- }
- }
- }
- Spacer(modifier = Modifier.height(16.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- OutlinedButton(
- onClick = onRegenerate,
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp),
- border = BorderStroke(1.dp, primaryAccentColor)
- ) {
- Icon(Icons.Rounded.AutoAwesome, contentDescription = null, tint = primaryAccentColor)
- Spacer(modifier = Modifier.width(8.dp))
- Text(stringResource(R.string.regenerate_note), color = primaryAccentColor)
- }
- Button(
- onClick = onSave,
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp),
- colors = ButtonDefaults.buttonColors(
- containerColor = primaryAccentColor,
- contentColor = if (isDark) Color(0xFF10141C) else MaterialTheme.colorScheme.onPrimary
- )
- ) {
- Text(stringResource(R.string.save_note))
- }
- }
- }
-}
-
-
-@Composable
-fun NewAiHomeScreen(viewModel: HomeViewModel) {
- val chatState by viewModel.chatState.collectAsState()
- val suggestions by viewModel.suggestions.collectAsState()
-
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- contentPadding = PaddingValues(bottom = 16.dp)
- ) {
- item {
- Spacer(modifier = Modifier.height(64.dp))
- AiCentralGraphic(isThinking = chatState.messages.any { it.isLoading })
- Spacer(modifier = Modifier.height(24.dp))
- }
-
- val rows = (suggestions.size + 1) / 2
- items(rows) { rowIndex ->
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp, vertical = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- val itemIndex1 = rowIndex * 2
- Box(modifier = Modifier.weight(1f)) {
- ActionCard(
- text = suggestions[itemIndex1].title,
- icon = suggestions[itemIndex1].icon,
- onClick = suggestions[itemIndex1].action
- )
- }
-
- val itemIndex2 = itemIndex1 + 1
- Box(modifier = Modifier.weight(1f)) {
- if (itemIndex2 < suggestions.size) {
- ActionCard(
- text = suggestions[itemIndex2].title,
- icon = suggestions[itemIndex2].icon,
- onClick = suggestions[itemIndex2].action
- )
- }
- }
- }
- }
- }
-}
-
-
-@Composable
-fun ChatScreenContent(
- messages: List,
- topicForLoading: String?
-) {
- val listState = rememberLazyListState()
- val scope = rememberCoroutineScope()
-
- LaunchedEffect(messages.size) {
- if (messages.isNotEmpty()) {
- scope.launch {
- listState.animateScrollToItem(messages.size - 1)
- }
- }
- }
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(horizontal = 8.dp),
- state = listState,
- contentPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
- ) {
- items(items = messages, key = { it.hashCode() }) { message ->
- ChatMessageItem(message = message, topicForLoading = topicForLoading)
- }
- }
-}
-
-@Composable
-fun AiHistoryScreen(
- sessions: List,
- onNewChatClicked: () -> Unit,
- onSessionClicked: (AiChatSession) -> Unit,
- onDeleteSession: (AiChatSession) -> Unit
-) {
- val isDark = isSystemInDarkTheme()
-
- Column(modifier = Modifier.fillMaxSize()) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- OutlinedButton(
- onClick = onNewChatClicked,
- modifier = Modifier.fillMaxWidth(),
- shape = RoundedCornerShape(12.dp),
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.2f) else MaterialTheme.colorScheme.outline),
- colors = ButtonDefaults.outlinedButtonColors(
- contentColor = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface
- )
- ) {
- Icon(Icons.Rounded.AddComment, contentDescription = stringResource(R.string.new_chat))
- Spacer(modifier = Modifier.width(8.dp))
- Text(stringResource(R.string.new_chat))
- }
- Spacer(modifier = Modifier.height(24.dp))
- Text(
- stringResource(R.string.history),
- style = MaterialTheme.typography.titleMedium,
- color = if (isDark) Color.White.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- if (sessions.isEmpty()) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(bottom = 80.dp),
- contentAlignment = Alignment.Center
- ) {
- Text(
- stringResource(R.string.no_chat_history),
- color = if (isDark) Color.White.copy(alpha = 0.6f) else MaterialTheme.colorScheme.onSurfaceVariant,
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(horizontal = 32.dp)
- )
- }
- } else {
- LazyColumn(
- contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- items(items = sessions, key = { it.id }) { session ->
- HistoryItem(
- session = session,
- onClick = { onSessionClicked(session) },
- onDelete = { onDeleteSession(session) }
- )
- }
- }
- }
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun HistoryItem(session: AiChatSession, onClick: () -> Unit, onDelete: () -> Unit) {
- var showDeleteConfirm by remember { mutableStateOf(false) }
- val isDark = isSystemInDarkTheme()
-
- if (showDeleteConfirm) {
- AlertDialog(
- onDismissRequest = { showDeleteConfirm = false },
- title = { Text(stringResource(R.string.delete_chat)) },
- text = { Text(stringResource(R.string.delete_chat_description)) },
- confirmButton = {
- TextButton(
- onClick = {
- onDelete()
- showDeleteConfirm = false
- },
- colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
- ) {
- Text(stringResource(R.string.delete))
- }
- },
- dismissButton = {
- TextButton(onClick = { showDeleteConfirm = false }) {
- Text(stringResource(R.string.cancel))
- }
- }
- )
- }
-
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(12.dp))
- .combinedClickable(
- onClick = onClick,
- onLongClick = { showDeleteConfirm = true }
- ),
- color = if (isDark) Color.White.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceContainer,
- ) {
- Row(
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- val serviceIcon = if (session.serviceName == AiService.PERPLEXITY.name) {
- Icons.Rounded.Search
- } else {
- Icons.Rounded.AutoAwesome
- }
-
- Icon(
- imageVector = serviceIcon,
- contentDescription = session.serviceName,
- tint = if (isDark) Color.White.copy(alpha = 0.6f) else MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.size(20.dp)
- )
- Spacer(modifier = Modifier.width(16.dp))
- Text(
- text = session.title,
- color = if (isDark) Color.White.copy(alpha = 0.9f) else MaterialTheme.colorScheme.onSurface,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.weight(1f)
- )
- }
- }
-}
-
-@Composable
-fun AiTopBar(
- viewModel: HomeViewModel,
- showHistory: Boolean,
- onToggleHistory: () -> Unit,
- isPerplexityEnabled: Boolean
-) {
- val currentAiMode by viewModel.aiMode.collectAsState()
- var showModelMenu by remember { mutableStateOf(false) }
- val selectedService by viewModel.selectedAiService.collectAsState()
- var showServiceMenu by remember { mutableStateOf(false) }
- val isDark = isSystemInDarkTheme()
-
- val geminiIconUrl = "https://cdn-1.webcatalog.io/catalog/google-bard/google-bard-icon-filled-256.webp?v=1760920816570"
- val perplexityIconUrl = "https://framerusercontent.com/images/gcMkPKyj2RX8EOEja8A1GWvCb7E.jpg?width=2000&height=2000"
-
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
- AnimatedVisibility(visible = selectedService == AiService.GEMINI) {
- Box {
- Row(
- modifier = Modifier
- .clip(CircleShape)
- .background(if (isDark) Color.White.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surfaceContainerHigh)
- .clickable { showModelMenu = true }
- .padding(horizontal = 12.dp, vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- imageVector = when (currentAiMode) {
- AiMode.NOTE_ASSISTANT -> Icons.AutoMirrored.Rounded.Notes
- AiMode.CREATIVE_MIND -> Icons.Rounded.Psychology
- AiMode.ACADEMIC_RESEARCHER -> Icons.Rounded.School
- AiMode.PROFESSIONAL_STRATEGIST -> Icons.Rounded.GolfCourse
- },
- contentDescription = "AI Model",
- tint = if (isDark) Color(0xFF33A2FF) else MaterialTheme.colorScheme.primary,
- modifier = Modifier.size(16.dp)
- )
- Spacer(modifier = Modifier.width(8.dp))
- Text(
- text = when (currentAiMode) {
- AiMode.NOTE_ASSISTANT -> stringResource(R.string.ai_mode_note_assistant)
- AiMode.CREATIVE_MIND -> stringResource(R.string.ai_mode_creative_mind)
- AiMode.ACADEMIC_RESEARCHER -> stringResource(R.string.ai_mode_academic_research)
- AiMode.PROFESSIONAL_STRATEGIST -> stringResource(R.string.ai_mode_professional_strategy)
- },
- color = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- fontWeight = FontWeight.SemiBold,
- fontSize = 14.sp
- )
- Icon(
- Icons.Rounded.ExpandMore,
- contentDescription = "Change Model",
- tint = if (isDark) Color.White.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
-
- DropdownMenu(
- expanded = showModelMenu,
- onDismissRequest = { showModelMenu = false },
- modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainerHigh)
- ) {
- DropdownMenuItem(
- text = { Text(stringResource(R.string.ai_mode_note_assistant)) },
- onClick = {
- viewModel.setAiMode(AiMode.NOTE_ASSISTANT)
- showModelMenu = false
- },
- leadingIcon = {
- Icon(
- Icons.Rounded.Notes,
- contentDescription = stringResource(R.string.ai_mode_note_assistant)
- )
- }
- )
- DropdownMenuItem(
- text = { Text(stringResource(R.string.ai_mode_creative_mind)) },
- onClick = {
- viewModel.setAiMode(AiMode.CREATIVE_MIND)
- showModelMenu = false
- },
- leadingIcon = {
- Icon(
- Icons.Rounded.Psychology,
- contentDescription = stringResource(R.string.ai_mode_creative_mind)
- )
- }
- )
- DropdownMenuItem(
- text = { Text(stringResource(R.string.ai_mode_academic_research)) },
- onClick = {
- viewModel.setAiMode(AiMode.ACADEMIC_RESEARCHER)
- showModelMenu = false
- },
- leadingIcon = {
- Icon(
- Icons.Rounded.School,
- contentDescription = stringResource(R.string.ai_mode_academic_research)
- )
- }
- )
- DropdownMenuItem(
- text = { Text(stringResource(R.string.ai_mode_professional_strategy)) },
- onClick = {
- viewModel.setAiMode(AiMode.PROFESSIONAL_STRATEGIST)
- showModelMenu = false
- },
- leadingIcon = {
- Icon(
- Icons.Rounded.GolfCourse,
- contentDescription = stringResource(R.string.ai_mode_professional_strategy)
- )
- }
- )
- }
- }
- }
-
- Box(modifier = Modifier.padding(start = if (selectedService == AiService.GEMINI) 8.dp else 0.dp)) {
- Row(
- modifier = Modifier
- .clip(CircleShape)
- .background(if (isDark) Color.White.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surfaceContainerHigh)
- .clickable { showServiceMenu = true }
- .padding(horizontal = 12.dp, vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Image(
- painter = rememberAsyncImagePainter(model = if (selectedService == AiService.GEMINI) geminiIconUrl else perplexityIconUrl),
- contentDescription = "AI Service",
- modifier = Modifier
- .size(20.dp)
- .clip(CircleShape),
- contentScale = ContentScale.Crop
- )
-
- Spacer(modifier = Modifier.width(8.dp))
- Text(
- text = if (selectedService == AiService.GEMINI) "Gemini" else "Perplexity",
- color = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- fontWeight = FontWeight.SemiBold,
- fontSize = 14.sp
- )
- Icon(
- Icons.Rounded.ExpandMore,
- contentDescription = "Change Service",
- tint = if (isDark) Color.White.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
-
- DropdownMenu(
- expanded = showServiceMenu,
- onDismissRequest = { showServiceMenu = false },
- modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainerHigh)
- ) {
- DropdownMenuItem(
- text = { Text("Gemini") },
- onClick = {
- viewModel.selectAiService(AiService.GEMINI)
- showServiceMenu = false
- },
- leadingIcon = {
- Image(
- painter = rememberAsyncImagePainter(model = geminiIconUrl),
- contentDescription = "Gemini",
- modifier = Modifier.size(24.dp).clip(CircleShape),
- contentScale = ContentScale.Crop
- )
- }
- )
- if (isPerplexityEnabled) {
- DropdownMenuItem(
- text = { Text("Perplexity") },
- onClick = {
- viewModel.selectAiService(AiService.PERPLEXITY)
- showServiceMenu = false
- },
- leadingIcon = {
- Image(
- painter = rememberAsyncImagePainter(model = perplexityIconUrl),
- contentDescription = "Perplexity",
- modifier = Modifier.size(24.dp).clip(CircleShape),
- contentScale = ContentScale.Crop
- )
- }
- )
- }
- }
- }
- }
- Spacer(modifier = Modifier.weight(1f))
- IconButton(onClick = onToggleHistory) {
- Icon(
- if (showHistory) Icons.AutoMirrored.Filled.Chat else Icons.Rounded.History,
- contentDescription = "Toggle History",
- tint = if (isDark) Color.White.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
-}
-
-@Composable
-fun AiCentralGraphic(isThinking: Boolean) {
- val infiniteTransition = rememberInfiniteTransition(label = "orb_animation")
- val rotation by infiniteTransition.animateFloat(
- initialValue = 0f,
- targetValue = 360f,
- animationSpec = infiniteRepeatable(tween(if (isThinking) 2000 else 10000, easing = LinearEasing), RepeatMode.Restart),
- label = "orb_rotation"
- )
- val scale by animateFloatAsState(
- targetValue = if (isThinking) 1.1f else 1.0f,
- animationSpec = spring(
- dampingRatio = Spring.DampingRatioMediumBouncy,
- stiffness = Spring.StiffnessLow
- ),
- label = "orb_scale"
- )
-
- val isDark = isSystemInDarkTheme()
- val primaryColor = if (isDark) Color(0xFF33A2FF) else MaterialTheme.colorScheme.primary
-
- Box(
- modifier = Modifier.size(120.dp),
- contentAlignment = Alignment.Center
- ) {
- Canvas(modifier = Modifier.fillMaxSize()) {
- drawCircle(
- brush = Brush.radialGradient(
- colors = listOf(primaryColor.copy(alpha = 0.1f), Color.Transparent),
- ),
- radius = (size.minDimension / 2.0f) * scale
- )
- }
- Box(
- modifier = Modifier
- .size(120.dp)
- .graphicsLayer {
- rotationZ = rotation
- }
- .border(
- BorderStroke(
- 2.dp,
- Brush.sweepGradient(
- 0.0f to Color.Transparent,
- 0.7f to primaryColor,
- 1.0f to Color.Transparent
- )
- ),
- CircleShape
- ),
- )
- Icon(
- imageVector = Icons.Rounded.AutoAwesome,
- contentDescription = "AI Core",
- tint = primaryColor,
- modifier = Modifier.size(50.dp)
- )
- }
-}
-
-@Composable
-fun ActionCard(text: String, icon: ImageVector, onClick: () -> Unit) {
- val isDark = isSystemInDarkTheme()
- Surface(
- onClick = onClick,
- shape = RoundedCornerShape(16.dp),
- color = if (isDark) Color.White.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceContainer,
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.1f) else MaterialTheme.colorScheme.outlineVariant),
- modifier = Modifier
- .fillMaxWidth()
- .height(100.dp)
- ) {
- Column(
- modifier = Modifier
- .padding(16.dp)
- .fillMaxSize(),
- horizontalAlignment = Alignment.Start,
- verticalArrangement = Arrangement.SpaceBetween
- ) {
- Icon(
- imageVector = icon,
- contentDescription = text,
- tint = if (isDark) Color.White.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant
- )
- Text(
- text = text,
- color = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.bodyMedium,
- fontWeight = FontWeight.SemiBold,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis
- )
- }
- }
-}
-
-
-@Composable
-fun ChatMessageItem(
- message: com.babelsoftware.airnote.domain.model.ChatMessage,
- topicForLoading: String?
-) {
- val isUser = message.participant == Participant.USER
- val isDark = isSystemInDarkTheme()
-
- Row(
- modifier = Modifier
- .padding(vertical = 4.dp, horizontal = 8.dp)
- .fillMaxWidth(),
- horizontalArrangement = if (isUser) Arrangement.End else Arrangement.Start
- ) {
- if (message.isLoading) {
- if (message.text.startsWith("// Generating Note...")) {
- TerminalLoadingIndicator(topic = topicForLoading ?: "")
- } else {
- TypingIndicator()
- }
- } else {
- val bubbleShape = RoundedCornerShape(
- topStart = 16.dp,
- topEnd = 16.dp,
- bottomStart = if (isUser) 16.dp else 0.dp,
- bottomEnd = if (isUser) 0.dp else 16.dp
- )
-
- val textColor = if (isDark) {
- if (message.participant == Participant.ERROR) Color.White else Color.White.copy(alpha = 0.9f)
- } else {
- when (message.participant) {
- Participant.USER -> MaterialTheme.colorScheme.onPrimaryContainer
- Participant.MODEL -> MaterialTheme.colorScheme.onSurfaceVariant
- Participant.ERROR -> MaterialTheme.colorScheme.onErrorContainer
- }
- }
-
- Box(
- modifier = Modifier
- .clip(bubbleShape)
- .then(
- if (isDark) {
- Modifier
- .background(
- brush = when (message.participant) {
- Participant.USER -> Brush.verticalGradient(
- colors = listOf(
- Color(0xFF33A2FF).copy(alpha = 0.4f),
- Color(0xFF33A2FF).copy(alpha = 0.1f)
- )
- )
-
- Participant.MODEL -> Brush.verticalGradient(
- colors = listOf(
- Color.White.copy(alpha = 0.15f),
- Color.White.copy(alpha = 0.05f)
- )
- )
-
- Participant.ERROR -> Brush.verticalGradient(
- colors = listOf(
- MaterialTheme.colorScheme.error.copy(alpha = 0.4f),
- MaterialTheme.colorScheme.error.copy(alpha = 0.2f)
- )
- )
- }
- )
- .border(
- width = 1.dp,
- brush = Brush.verticalGradient(
- colors = listOf(
- Color.White.copy(alpha = 0.3f),
- Color.Transparent
- )
- ),
- shape = bubbleShape
- )
- } else {
- Modifier.background(
- color = when (message.participant) {
- Participant.USER -> MaterialTheme.colorScheme.primaryContainer
- Participant.MODEL -> MaterialTheme.colorScheme.surfaceVariant
- Participant.ERROR -> MaterialTheme.colorScheme.errorContainer
- }
- )
- }
- )
- .padding(12.dp)
- ) {
- RenderMessageWithSources(
- fullText = message.text.replace(Regex("[*#]"), "").trim(),
- baseColor = textColor,
- linkColor = if (isDark) Color(0xFF33A2FF) else MaterialTheme.colorScheme.primary
- )
- }
- }
- }
-}
-
-@Composable
-private fun RenderMessageWithSources(
- fullText: String,
- baseColor: Color,
- linkColor: Color
-) {
- val uriHandler = LocalUriHandler.current
- val context = LocalContext.current
- val sourcesHeaderText = "**Sources:**"
- val parts = fullText.split(sourcesHeaderText, limit = 2)
- val mainText = parts[0]
- val sourcesText = if (parts.size > 1) parts[1] else null
- val sourceRegex = remember { Regex("""\[\d+\]\s*(.*?)\s*\((https?://[^\s)]+)\)""") }
-
- val annotatedString = buildAnnotatedString {
- withStyle(style = SpanStyle(color = baseColor)) {
- append(mainText)
- }
- if (sourcesText != null) {
- withStyle(style = SpanStyle(color = baseColor, fontWeight = FontWeight.Bold)) {
- append("\n\n\nSources:")
- }
-
- var lastIndex = 0
- sourceRegex.findAll(sourcesText).forEach { matchResult ->
- val title = matchResult.groupValues[1]
- val url = matchResult.groupValues[2]
- withStyle(style = SpanStyle(color = baseColor)) {
- append(sourcesText.substring(lastIndex, matchResult.range.first))
- }
- pushStringAnnotation(tag = "URL", annotation = url)
- withStyle(style = SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)) {
- append("\n[${matchResult.value.substring(1,2)}] $title")
- }
- pop()
-
- lastIndex = matchResult.range.last + 1
- }
- if (lastIndex < sourcesText.length) {
- withStyle(style = SpanStyle(color = baseColor)) {
- append(sourcesText.substring(lastIndex))
- }
- }
- }
- }
-
- ClickableText(
- text = annotatedString,
- style = MaterialTheme.typography.bodyLarge,
- onClick = { offset ->
- annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset)
- .firstOrNull()?.let { annotation ->
- try {
- uriHandler.openUri(annotation.item)
- } catch (e: ActivityNotFoundException) {
- Toast.makeText(context, "Web tarayıcısı bulunamadı.", Toast.LENGTH_SHORT).show()
- } catch (e: IllegalArgumentException) {
- Toast.makeText(context, "URL açılamadı.", Toast.LENGTH_SHORT).show()
- }
- }
- }
- )
-}
-
-@Composable
-fun RedesignedChatInputBar(
- text: String,
- onValueChange: (String) -> Unit,
- onSendMessage: () -> Unit,
- onImagePickerClicked: () -> Unit,
- enabled: Boolean,
- modifier: Modifier = Modifier,
- showAttachmentButton: Boolean,
- placeholderText: String
-) {
- val haptic = LocalHapticFeedback.current
- val isDark = isSystemInDarkTheme()
- val primaryAccentColor = if (isDark) Color(0xFF33A2FF) else MaterialTheme.colorScheme.primary
- val chatState by hiltViewModel().chatState.collectAsState()
-
- Surface(
- modifier = modifier
- .fillMaxWidth()
- .padding(16.dp),
- shape = RoundedCornerShape(24.dp),
- color = if (isDark) Color.White.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceContainer,
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.1f) else MaterialTheme.colorScheme.outline)
- ) {
- Row(
- modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- AnimatedVisibility(visible = showAttachmentButton) {
- IconButton(onClick = onImagePickerClicked, enabled = enabled) {
- Icon(
- Icons.Default.AttachFile,
- contentDescription = "Attach File",
- tint = if (isDark) Color.White.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
- TextField(
- value = text,
- onValueChange = onValueChange,
- modifier = Modifier.weight(1f),
- enabled = enabled,
- placeholder = {
- Text(placeholderText, color = if (isDark) Color.White.copy(alpha = 0.5f) else MaterialTheme.colorScheme.onSurfaceVariant)
- },
- colors = TextFieldDefaults.colors(
- focusedContainerColor = Color.Transparent,
- unfocusedContainerColor = Color.Transparent,
- disabledContainerColor = Color.Transparent,
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
- cursorColor = primaryAccentColor,
- focusedTextColor = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- unfocusedTextColor = if (isDark) Color.White.copy(alpha = 0.9f) else MaterialTheme.colorScheme.onSurface,
- disabledPlaceholderColor = if (isDark) Color.White.copy(alpha = 0.3f) else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
- )
- )
- AnimatedVisibility(visible = (text.isNotBlank() || chatState.pendingAttachmentUri != null) && enabled) {
- IconButton(
- onClick = {
- haptic.performHapticFeedback(HapticFeedbackType.LongPress)
- onSendMessage()
- },
- modifier = Modifier.padding(start = 8.dp),
- colors = IconButtonDefaults.iconButtonColors(
- containerColor = primaryAccentColor,
- contentColor = if (isDark) Color.White else MaterialTheme.colorScheme.onPrimary
- )
- ) {
- Icon(
- Icons.AutoMirrored.Filled.Send,
- contentDescription = "Send message"
- )
- }
- }
- }
- }
-}
-
-@Composable
-fun PreChatInputBar(
- text: String,
- onValueChange: (String) -> Unit,
- onSendMessage: (String) -> Unit,
- onImagePickerClicked: () -> Unit,
- enabled: Boolean,
- modifier: Modifier = Modifier,
- showAttachmentButton: Boolean
-) {
- val isDark = isSystemInDarkTheme()
- val primaryAccentColor = if (isDark) Color(0xFF33A2FF) else MaterialTheme.colorScheme.primary
- val chatState by hiltViewModel().chatState.collectAsState()
-
- Surface(
- modifier = modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp),
- shape = RoundedCornerShape(24.dp),
- color = if (isDark) Color.White.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceContainerHigh,
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.1f) else MaterialTheme.colorScheme.outline)
- ) {
- Column(
- modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- TextField(
- value = text,
- onValueChange = onValueChange,
- modifier = Modifier.weight(1f),
- enabled = enabled,
- placeholder = {
- Text(stringResource(R.string.ask_airnote_ai), color = if (isDark) Color.White.copy(alpha = 0.6f) else MaterialTheme.colorScheme.onSurfaceVariant)
- },
- leadingIcon = {
- Icon(Icons.Default.Search, contentDescription = null, tint = if (isDark) Color.White.copy(alpha = 0.6f) else MaterialTheme.colorScheme.onSurfaceVariant)
- },
- colors = TextFieldDefaults.colors(
- focusedContainerColor = Color.Transparent,
- unfocusedContainerColor = Color.Transparent,
- disabledContainerColor = Color.Transparent,
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
- cursorColor = primaryAccentColor,
- focusedTextColor = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- unfocusedTextColor = if (isDark) Color.White.copy(alpha = 0.9f) else MaterialTheme.colorScheme.onSurface,
- ),
- singleLine = true
- )
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
- ) {
- AnimatedVisibility(visible = showAttachmentButton) {
- IconButton(onClick = onImagePickerClicked, enabled = enabled) {
- Icon(
- Icons.Default.AttachFile,
- contentDescription = "Attach File",
- tint = if (isDark) Color.White.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
-
- val searchString = stringResource(R.string.ai_search)
- val thinkString = stringResource(R.string.ai_think)
-
- Row(
- modifier = Modifier.weight(1f),
- horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
- ) {
- InputActionButton(
- text = searchString,
- icon = Icons.Rounded.AutoAwesome,
- onClick = { onValueChange("$searchString: ") }
- )
- InputActionButton(
- text = thinkString,
- icon = Icons.Default.Language,
- onClick = { onValueChange("$thinkString: ") }
- )
- }
-
- IconButton(
- onClick = { if (text.isNotBlank() || chatState.pendingAttachmentUri != null) onSendMessage(text) },
- enabled = (text.isNotBlank() || chatState.pendingAttachmentUri != null) && enabled,
- modifier = Modifier
- .size(40.dp)
- .clip(CircleShape),
- colors = IconButtonDefaults.iconButtonColors(
- containerColor = primaryAccentColor,
- contentColor = if (isDark) Color.White else MaterialTheme.colorScheme.onPrimary,
- disabledContainerColor = (if (isDark) Color.White else MaterialTheme.colorScheme.onSurface).copy(alpha = 0.12f),
- disabledContentColor = (if (isDark) Color.White else MaterialTheme.colorScheme.onSurface).copy(alpha = 0.38f)
- )
- ) {
- Icon(
- Icons.AutoMirrored.Filled.Send,
- contentDescription = "Send"
- )
- }
- }
- }
- }
-}
-
-@Composable
-private fun InputActionButton(text: String, icon: ImageVector, onClick: () -> Unit) {
- val isDark = isSystemInDarkTheme()
- TextButton(
- onClick = onClick,
- shape = RoundedCornerShape(24.dp),
- colors = ButtonDefaults.textButtonColors(
- containerColor = if (isDark) Color.White.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surfaceContainer,
- contentColor = if (isDark) Color.White.copy(alpha = 0.9f) else MaterialTheme.colorScheme.onSurface
- ),
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.2f) else MaterialTheme.colorScheme.outlineVariant),
- contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
- ) {
- Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp))
- Spacer(Modifier.width(8.dp))
- Text(text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
- }
-}
-
-@Composable
-fun AiDisclaimerText() {
- val uriHandler = LocalUriHandler.current
- val disclaimerText = stringResource(R.string.ai_disclaimer_text)
- val linkText = stringResource(R.string.ai_disclaimer_link_text)
- val linkUrl = stringResource(R.string.ai_disclaimer_link_url)
- val isDark = isSystemInDarkTheme()
-
- val annotatedString = buildAnnotatedString {
- append(disclaimerText)
- pushStringAnnotation(tag = "URL", annotation = linkUrl)
- withStyle(
- style = SpanStyle(
- color = if (isDark) Color(0xFF33A2FF) else MaterialTheme.colorScheme.primary,
- textDecoration = TextDecoration.Underline
- )
- ) {
- append(linkText)
- }
- pop()
- }
-
- ClickableText(
- text = annotatedString,
- onClick = { offset ->
- annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset)
- .firstOrNull()?.let { annotation ->
- uriHandler.openUri(annotation.item)
- }
- },
- style = MaterialTheme.typography.bodySmall.copy(
- color = if (isDark) Color.White.copy(alpha = 0.6f) else MaterialTheme.colorScheme.onSurfaceVariant,
- textAlign = TextAlign.Center
- ),
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp)
- .padding(bottom = 8.dp)
- )
-}
-
-@Composable
-fun AskAiQuestionDialog(
- onDismiss: () -> Unit,
- onConfirm: (String) -> Unit
-) {
- var question by remember { mutableStateOf("") }
- val maxChars = 350
- val isDark = isSystemInDarkTheme()
- val primaryAccentColor = if (isDark) Color(0xFF33A2FF) else MaterialTheme.colorScheme.primary
-
- Dialog(onDismissRequest = onDismiss) {
- Surface(
- shape = RoundedCornerShape(24.dp),
- color = if (isDark) Color(0xFF10141C) else MaterialTheme.colorScheme.surfaceContainerHigh,
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.2f) else MaterialTheme.colorScheme.outlineVariant)
- ) {
- Column(
- modifier = Modifier.padding(24.dp)
- ) {
- Text(
- text = stringResource(R.string.ask_a_question_title),
- style = MaterialTheme.typography.headlineSmall,
- color = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- fontWeight = FontWeight.Bold
- )
- Spacer(modifier = Modifier.height(16.dp))
- TextField(
- value = question,
- onValueChange = { if (it.length <= maxChars) question = it },
- placeholder = { Text(stringResource(R.string.ask_a_question), color = if (isDark) Color.White.copy(alpha = 0.6f) else MaterialTheme.colorScheme.onSurfaceVariant) },
- modifier = Modifier
- .fillMaxWidth()
- .height(100.dp),
- colors = TextFieldDefaults.colors(
- focusedContainerColor = if (isDark) Color.White.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceContainer,
- unfocusedContainerColor = if (isDark) Color.White.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceContainer,
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
- cursorColor = primaryAccentColor,
- focusedTextColor = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- unfocusedTextColor = if (isDark) Color.White.copy(alpha = 0.9f) else MaterialTheme.colorScheme.onSurface,
- ),
- shape = RoundedCornerShape(16.dp)
- )
- Text(
- text = "${question.length} / $maxChars",
- textAlign = TextAlign.End,
- style = MaterialTheme.typography.bodySmall,
- color = if (isDark) Color.White.copy(alpha = 0.6f) else MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 8.dp)
- )
- Spacer(modifier = Modifier.height(24.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.End
- ) {
- TextButton(onClick = onDismiss) {
- Text(stringResource(R.string.cancel), color = if (isDark) Color.White.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant)
- }
- Spacer(modifier = Modifier.width(8.dp))
- Button(
- onClick = { onConfirm(question) },
- enabled = question.isNotBlank(),
- colors = ButtonDefaults.buttonColors(
- containerColor = primaryAccentColor,
- contentColor = if (isDark) Color(0xFF10141C) else MaterialTheme.colorScheme.onPrimary,
- disabledContainerColor = (if (isDark) Color.White else MaterialTheme.colorScheme.onSurface).copy(alpha = 0.12f)
- ),
- shape = RoundedCornerShape(12.dp)
- ) {
- Text(stringResource(R.string.send))
- }
- }
- }
- }
- }
-}
-
-@Composable
-fun CreateDraftDialog(
- onDismiss: () -> Unit,
- onConfirm: (String) -> Unit
-) {
- var topic by remember { mutableStateOf("") }
- val maxChars = 250
- val isDark = isSystemInDarkTheme()
- val primaryAccentColor = if (isDark) Color(0xFF33A2FF) else MaterialTheme.colorScheme.primary
-
- Dialog(onDismissRequest = onDismiss) {
- Surface(
- shape = RoundedCornerShape(24.dp),
- color = if (isDark) Color(0xFF10141C) else MaterialTheme.colorScheme.surfaceContainerHigh,
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.2f) else MaterialTheme.colorScheme.outlineVariant)
- ) {
- Column(
- modifier = Modifier.padding(24.dp)
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(
- imageVector = Icons.Rounded.Edit,
- contentDescription = null,
- tint = if (isDark) Color.White.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.size(28.dp)
- )
- Spacer(modifier = Modifier.width(12.dp))
- Text(
- text = stringResource(R.string.new_ai_note_draft),
- style = MaterialTheme.typography.headlineSmall,
- color = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- fontWeight = FontWeight.Bold
- )
- }
-
- Spacer(modifier = Modifier.height(16.dp))
- Text(
- text = stringResource(R.string.sample_question_request),
- style = MaterialTheme.typography.bodyMedium,
- color = if (isDark) Color.White.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant
- )
- Spacer(modifier = Modifier.height(16.dp))
-
- TextField(
- value = topic,
- onValueChange = { if (it.length <= maxChars) topic = it },
- placeholder = { Text(stringResource(R.string.example_question), color = if (isDark) Color.White.copy(alpha = 0.6f) else MaterialTheme.colorScheme.onSurfaceVariant) },
- modifier = Modifier.fillMaxWidth(),
- singleLine = true,
- colors = TextFieldDefaults.colors(
- focusedContainerColor = if (isDark) Color.White.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceContainer,
- unfocusedContainerColor = if (isDark) Color.White.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceContainer,
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
- cursorColor = primaryAccentColor,
- focusedTextColor = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- unfocusedTextColor = if (isDark) Color.White.copy(alpha = 0.9f) else MaterialTheme.colorScheme.onSurface,
- ),
- shape = RoundedCornerShape(16.dp)
- )
- Text(
- text = "${topic.length} / $maxChars",
- textAlign = TextAlign.End,
- style = MaterialTheme.typography.bodySmall,
- color = if (isDark) Color.White.copy(alpha = 0.6f) else MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
- )
- Spacer(modifier = Modifier.height(24.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.End
- ) {
- TextButton(onClick = onDismiss) {
- Text(stringResource(R.string.cancel), color = if (isDark) Color.White.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant)
- }
- Spacer(modifier = Modifier.width(8.dp))
- Button(
- onClick = { onConfirm(topic) },
- enabled = topic.isNotBlank(),
- colors = ButtonDefaults.buttonColors(
- containerColor = primaryAccentColor,
- contentColor = if (isDark) Color(0xFF10141C) else MaterialTheme.colorScheme.onPrimary,
- disabledContainerColor = (if (isDark) Color.White else MaterialTheme.colorScheme.onSurface).copy(alpha = 0.12f)
- ),
- shape = RoundedCornerShape(12.dp)
- ) {
- Text(stringResource(R.string.create))
- }
- }
- }
- }
- }
-}
-
-
-@Composable
-fun DraftDisplay(draft: DraftedNote?, onSave: () -> Unit, onRegenerate: () -> Unit) {
- if (draft == null) return
- val isDark = isSystemInDarkTheme()
- val primaryAccentColor = if (isDark) Color(0xFF33A2FF) else MaterialTheme.colorScheme.primary
-
- Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
- Column(
- modifier = Modifier.fillMaxSize()
- ) {
- Surface(
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(16.dp),
- color = if (isDark) Color.White.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceContainer,
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.1f) else MaterialTheme.colorScheme.outlineVariant)
- ) {
- LazyColumn(
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 20.dp)
- ) {
- item {
- Text(
- text = draft.title,
- style = MaterialTheme.typography.titleLarge,
- color = if (isDark) Color.White else MaterialTheme.colorScheme.onSurface,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(bottom = 12.dp)
- )
- }
- item {
- Text(
- text = draft.content,
- style = MaterialTheme.typography.bodyLarge,
- color = if (isDark) Color.White.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant,
- lineHeight = 24.sp
- )
- }
- }
- }
- Spacer(modifier = Modifier.height(16.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- OutlinedButton(
- onClick = onRegenerate,
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp),
- border = BorderStroke(1.dp, primaryAccentColor)
- ) {
- Icon(Icons.Rounded.AutoAwesome, contentDescription = null, tint = primaryAccentColor)
- Spacer(modifier = Modifier.width(8.dp))
- Text(stringResource(R.string.regenerate_note), color = primaryAccentColor)
- }
- Button(
- onClick = onSave,
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp),
- colors = ButtonDefaults.buttonColors(
- containerColor = primaryAccentColor,
- contentColor = if (isDark) Color(0xFF10141C) else MaterialTheme.colorScheme.onPrimary
- )
- ) {
- Text(stringResource(R.string.save_note))
- }
- }
- }
- }
-}
-
-@Composable
-private fun TypingIndicator() {
- val transition = rememberInfiniteTransition(label = "typing_indicator_transition")
- val dotSize = 8.dp
- val dotSpacing = 12.dp
-
- @Composable
- fun Dot(offsetY: Float) {
- Box(
- modifier = Modifier
- .size(dotSize)
- .offset(y = offsetY.dp)
- .background(
- color = (if (isSystemInDarkTheme()) Color.White else MaterialTheme.colorScheme.onSurfaceVariant).copy(
- alpha = 0.5f
- ),
- shape = CircleShape
- )
- )
- }
-
- val yOffset1 by transition.animateFloat(
- initialValue = 0f,
- targetValue = -10f,
- animationSpec = infiniteRepeatable(
- animation = tween(400),
- repeatMode = RepeatMode.Reverse
- ), label = "dot1_offset"
- )
- val yOffset2 by transition.animateFloat(
- initialValue = 0f,
- targetValue = -10f,
- animationSpec = infiniteRepeatable(
- animation = tween(400, delayMillis = 150),
- repeatMode = RepeatMode.Reverse
- ), label = "dot2_offset"
- )
- val yOffset3 by transition.animateFloat(
- initialValue = 0f,
- targetValue = -10f,
- animationSpec = infiniteRepeatable(
- animation = tween(400, delayMillis = 300),
- repeatMode = RepeatMode.Reverse
- ), label = "dot3_offset"
- )
-
- Row(
- modifier = Modifier.padding(vertical = 16.dp, horizontal = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(dotSpacing)
- ) {
- Dot(offsetY = yOffset1)
- Dot(offsetY = yOffset2)
- Dot(offsetY = yOffset3)
- }
-}
-
-@Composable
-fun TerminalLoadingIndicator(topic: String) {
- var displayedText by remember { mutableStateOf("") }
- val fullText = """
-[INFO] Connecting to AirNote's Kai AI services...
-[INFO] Authentication successful.
-[PROCESS] Analyzing topic: "$topic"
-[PROCESS] Generating content blocks...
-[PROCESS] Assembling draft...
-[SUCCESS] Note draft created.
- """.trimIndent()
-
- LaunchedEffect(Unit) {
- fullText.lines().forEach { line ->
- for (char in line) {
- displayedText += char
- delay(10)
- }
- displayedText += "\n"
- delay(280)
- }
- }
-
- val infiniteTransition = rememberInfiniteTransition(label = "cursor_blink")
- val cursorAlpha by infiniteTransition.animateFloat(
- initialValue = 0f,
- targetValue = 1f,
- animationSpec = infiniteRepeatable(
- animation = tween(500),
- repeatMode = RepeatMode.Reverse
- ), label = "cursor_alpha"
- )
-
- val isDark = isSystemInDarkTheme()
- val textColor = if (isDark) Color.Green.copy(alpha = 0.8f) else MaterialTheme.colorScheme.primary
-
- Surface(
- shape = RoundedCornerShape(16.dp),
- color = if (isDark) Color.Black.copy(alpha = 0.5f) else MaterialTheme.colorScheme.surfaceContainerLowest,
- border = BorderStroke(1.dp, if (isDark) Color.White.copy(alpha = 0.2f) else MaterialTheme.colorScheme.outlineVariant)
- ) {
- Row(modifier = Modifier.padding(12.dp)) {
- Text(
- text = displayedText,
- color = textColor,
- fontFamily = FontFamily.Monospace,
- fontSize = 14.sp,
- lineHeight = 20.sp
- )
- Text(
- text = "█",
- color = textColor.copy(alpha = cursorAlpha),
- fontFamily = FontFamily.Monospace,
- fontSize = 14.sp
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/HomeScreen.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/HomeScreen.kt
deleted file mode 100644
index b0300b0..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/HomeScreen.kt
+++ /dev/null
@@ -1,1362 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.home
-
-import android.app.Activity
-import android.net.Uri
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.animate
-import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.core.updateTransition
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInHorizontally
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutHorizontally
-import androidx.compose.animation.slideOutVertically
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.basicMarquee
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
-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.Chat
-import androidx.compose.material.icons.filled.AccountBalance
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Alarm
-import androidx.compose.material.icons.filled.Apartment
-import androidx.compose.material.icons.filled.Assessment
-import androidx.compose.material.icons.filled.AttachMoney
-import androidx.compose.material.icons.filled.Backup
-import androidx.compose.material.icons.filled.Book
-import androidx.compose.material.icons.filled.Bookmark
-import androidx.compose.material.icons.filled.Build
-import androidx.compose.material.icons.filled.Cake
-import androidx.compose.material.icons.filled.CameraAlt
-import androidx.compose.material.icons.filled.Campaign
-import androidx.compose.material.icons.filled.CardGiftcard
-import androidx.compose.material.icons.filled.Circle
-import androidx.compose.material.icons.filled.Cloud
-import androidx.compose.material.icons.filled.Code
-import androidx.compose.material.icons.filled.ContentCut
-import androidx.compose.material.icons.filled.Description
-import androidx.compose.material.icons.filled.DirectionsCar
-import androidx.compose.material.icons.filled.Eco
-import androidx.compose.material.icons.filled.Event
-import androidx.compose.material.icons.filled.Extension
-import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.material.icons.filled.FitnessCenter
-import androidx.compose.material.icons.filled.Flag
-import androidx.compose.material.icons.filled.Flight
-import androidx.compose.material.icons.filled.Folder
-import androidx.compose.material.icons.filled.FolderOpen
-import androidx.compose.material.icons.filled.Group
-import androidx.compose.material.icons.filled.Headset
-import androidx.compose.material.icons.filled.Home
-import androidx.compose.material.icons.filled.Image
-import androidx.compose.material.icons.filled.Key
-import androidx.compose.material.icons.filled.Lightbulb
-import androidx.compose.material.icons.filled.Link
-import androidx.compose.material.icons.filled.Lock
-import androidx.compose.material.icons.filled.Map
-import androidx.compose.material.icons.filled.Movie
-import androidx.compose.material.icons.filled.MusicNote
-import androidx.compose.material.icons.filled.Palette
-import androidx.compose.material.icons.filled.Person
-import androidx.compose.material.icons.filled.Pets
-import androidx.compose.material.icons.filled.PushPin
-import androidx.compose.material.icons.filled.Receipt
-import androidx.compose.material.icons.filled.Restaurant
-import androidx.compose.material.icons.filled.School
-import androidx.compose.material.icons.filled.Shield
-import androidx.compose.material.icons.filled.ShoppingCart
-import androidx.compose.material.icons.filled.Spa
-import androidx.compose.material.icons.filled.SportsEsports
-import androidx.compose.material.icons.filled.Star
-import androidx.compose.material.icons.filled.Store
-import androidx.compose.material.icons.filled.ThumbUp
-import androidx.compose.material.icons.filled.Visibility
-import androidx.compose.material.icons.filled.Warning
-import androidx.compose.material.icons.filled.Work
-import androidx.compose.material.icons.rounded.AutoAwesome
-import androidx.compose.material.icons.rounded.Edit
-import androidx.compose.material.icons.rounded.Lock
-import androidx.compose.material.icons.rounded.LockOpen
-import androidx.compose.material.icons.rounded.Search
-import androidx.compose.material.icons.rounded.Settings
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Badge
-import androidx.compose.material3.BadgedBox
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.FloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.IconButtonDefaults
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.SearchBar
-import androidx.compose.material3.SmallFloatingActionButton
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.material3.TextField
-import androidx.compose.material3.TextFieldDefaults
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.rememberModalBottomSheetState
-import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
-import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
-import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.ReadOnlyComposable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.rotate
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.domain.model.Folder
-import com.babelsoftware.airnote.domain.model.Note
-import com.babelsoftware.airnote.domain.model.Settings
-import com.babelsoftware.airnote.presentation.components.CloseButton
-import com.babelsoftware.airnote.presentation.components.DeleteButton
-import com.babelsoftware.airnote.presentation.components.NotesScaffold
-import com.babelsoftware.airnote.presentation.components.PinButton
-import com.babelsoftware.airnote.presentation.components.SelectAllButton
-import com.babelsoftware.airnote.presentation.components.TitleText
-import com.babelsoftware.airnote.presentation.components.UpdateScreen
-import com.babelsoftware.airnote.presentation.components.VaultButton
-import com.babelsoftware.airnote.presentation.components.defaultScreenEnterAnimation
-import com.babelsoftware.airnote.presentation.components.defaultScreenExitAnimation
-import com.babelsoftware.airnote.presentation.screens.home.desktop.DesktopHomeScreen
-import com.babelsoftware.airnote.presentation.screens.home.viewmodel.HomeViewModel
-import com.babelsoftware.airnote.presentation.screens.home.widgets.FolderActionBottomSheet
-import com.babelsoftware.airnote.presentation.screens.home.widgets.MoveToFolderDialog
-import com.babelsoftware.airnote.presentation.screens.home.widgets.NoteFilter
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.settings.PasswordPrompt
-import com.babelsoftware.airnote.presentation.screens.settings.settings.shapeManager
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3WindowSizeClassApi::class, ExperimentalAnimationApi::class)
-@Composable
-fun HomeView (
- viewModel: HomeViewModel = hiltViewModel(),
- settingsModel: SettingsViewModel,
- settings: Settings,
- onSettingsClicked: () -> Unit,
- onNoteClicked: (noteId: Int, isVault: Boolean, folderId: Long?) -> Unit,
- onNavigateToAbout: () -> Unit
-) {
- val context = LocalContext.current
- var showUnlockDialog by remember { mutableStateOf(null) }
- var showBulkUnlockDialog by remember { mutableStateOf(false) }
- val activity = context as? Activity
- val scope = rememberCoroutineScope()
- val snackbarHostState = remember { SnackbarHostState() }
- val allFolders by viewModel.allFolders.collectAsState()
- val notes by viewModel.displayedNotes.collectAsState()
- val selectedFolderId by viewModel.selectedFolderId.collectAsState()
- val query by viewModel.searchQuery.collectAsState()
- val isVaultMode by viewModel.isVaultMode.collectAsState()
- val liveSettings by settingsModel.settings
-
- if (activity != null) {
- val windowSizeClass = calculateWindowSizeClass(activity)
- val widthSizeClass = windowSizeClass.widthSizeClass
- val isCompact = widthSizeClass == WindowWidthSizeClass.Compact
- val shouldShowDesktopUi = !isCompact && settings.desktopModeEnabled
-
- if (shouldShowDesktopUi) {
- DesktopHomeScreen(
- viewModel = viewModel,
- settingsModel = settingsModel,
- settings = settings,
- onNoteClicked = onNoteClicked,
- onSettingsClicked = onSettingsClicked
- )
- } else {
- var showFolderSheet by remember { mutableStateOf(false) }
- val selectedFolder = remember(selectedFolderId, allFolders) {
- allFolders.find { it.id == selectedFolderId }
- }
- val imagePickerLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.GetContent(),
- onResult = { uri: Uri? ->
- if (uri != null) {
- val mimeType = context.contentResolver.getType(uri)
- viewModel.onAttachmentSelected(uri, mimeType ?: "image/*")
- }
- }
- )
-
- val filePickerLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.GetContent(),
- onResult = { uri: Uri? ->
- if (uri != null) {
- val mimeType = context.contentResolver.getType(uri)
- if (mimeType == "text/plain") {
- viewModel.onAttachmentSelected(uri, mimeType)
- } else {
- scope.launch {
- snackbarHostState.showSnackbar("Currently only '.txt' files are supported.")
- }
- }
- }
- }
- )
-
- val imageAnalysisLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.GetContent(),
- onResult = { uri: Uri? ->
- if (uri != null) {
- val mimeType = context.contentResolver.getType(uri)
- val analysisPrompt = context.getString(R.string.prompt_airnote_ai_analyzeimage)
-
- viewModel.analyzeFileAndCreateDraft(analysisPrompt, uri, mimeType ?: "image/*")
- }
- }
- )
-
- val fileAnalysisLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.GetContent(),
- onResult = { uri: Uri? ->
- if (uri != null) {
- val mimeType = context.contentResolver.getType(uri)
- if (mimeType == "text/plain") {
- val analysisPrompt = context.getString(R.string.prompt_airnote_ai_analyzeimage)
- viewModel.analyzeFileAndCreateDraft(analysisPrompt, uri, mimeType)
- } else {
- scope.launch {
- snackbarHostState.showSnackbar("Currently only '.txt' files are supported for analysis.")
- }
- }
- }
- }
- )
-
- var showAttachmentTypeSheet by remember { mutableStateOf(false) }
- if (showAttachmentTypeSheet) {
- AttachmentTypeBottomSheet(
- onDismiss = { showAttachmentTypeSheet = false },
- onImageClicked = {
- imagePickerLauncher.launch("image/*")
- },
- onFileClicked = {
- filePickerLauncher.launch("text/plain")
- }
- )
- }
-
- LaunchedEffect(Unit) {
- viewModel.uiActionChannel.collect { action ->
- when (action) {
- is HomeViewModel.UiAction.RequestImageForAnalysis -> {
- imageAnalysisLauncher.launch("image/*")
- }
- is HomeViewModel.UiAction.RequestFileForAnalysis -> {
- fileAnalysisLauncher.launch("text/plain")
- }
- is HomeViewModel.UiAction.RequestAttachmentType -> {
- showAttachmentTypeSheet = true
- }
- }
- }
- }
-
- LaunchedEffect(key1 = Unit) {
- settingsModel.checkForNewUpdate(context)
- }
-
- if (showFolderSheet) {
- FolderSelectionBottomSheet(
- allFolders = allFolders,
- selectedFolderId = selectedFolderId,
- onDismiss = { showFolderSheet = false },
- onFolderSelected = { folderId ->
- viewModel.selectFolder(folderId)
- showFolderSheet = false
- },
- onAddFolderClicked = {
- viewModel.setAddFolderDialogVisibility(true)
- },
- onFolderLongClick = { folder ->
- viewModel.onFolderLongPressed(folder)
- }
- )
- }
-
- if (settingsModel.showUpdateDialog.value) {
- UpdateScreen(
- latestVersion = settingsModel.latestVersion.value,
- onDismiss = { settingsModel.dismissUpdateDialog() },
- onNavigateToAbout = onNavigateToAbout
- )
- }
- val topBarColor = MaterialTheme.colorScheme.surfaceContainerLow
- val listState = rememberLazyStaggeredGridState()
-
- val isFabExtended by remember {
- derivedStateOf {
- listState.firstVisibleItemIndex == 0
- }
- }
- viewModel.setFabExtended(isFabExtended)
-
- val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
-
- if (viewModel.isAiChatSheetVisible.value) {
- ModalBottomSheet(
- onDismissRequest = { viewModel.toggleAiChatSheet(false) },
- sheetState = sheetState,
- modifier = Modifier.fillMaxWidth(),
- tonalElevation = 0.dp
- ) {
- AiChatContainer(viewModel = viewModel, settings = liveSettings)
- }
- }
-
- if (viewModel.isPasswordPromptVisible.value) {
- PasswordPrompt(
- context = context,
- text = stringResource(id = R.string.password_continue),
- settingsViewModel = settingsModel,
- onExit = { password ->
- if (password != null) {
- if (password.text.isNotBlank()) {
- viewModel.encryptionHelper.setPassword(password.text)
- viewModel.observeNotes()
- }
- }
- viewModel.toggleIsPasswordPromptVisible(false)
- }
- )
- }
-
- if (viewModel.isAddFolderDialogVisible.value) {
- AddFolderDialogWithIcons(
- onDismiss = { viewModel.setAddFolderDialogVisibility(false) },
- onConfirm = { name, iconName ->
- viewModel.addFolder(name, iconName)
- }
- )
- }
-
- viewModel.folderForAction.value?.let { folder ->
- if (!viewModel.showDeleteConfirmDialog.value) {
- FolderActionBottomSheet(
- folder = folder,
- onDismiss = { viewModel.onDismissFolderAction() },
- onEditClick = { viewModel.onEditFolderRequest() },
- onDeleteClick = {
- viewModel.onDeleteFolderRequest()
- }
- )
- }
- }
-
- viewModel.folderToEdit.value?.let { folder ->
- AddFolderDialogWithIcons(
- onDismiss = { viewModel.onDismissEditFolderDialog() },
- onConfirm = { name, iconName ->
- viewModel.updateFolder(name, iconName)
- },
- folderToEdit = folder
- )
- }
-
- if (viewModel.isMoveToFolderDialogVisible.value) {
- MoveToFolderDialog(
- folders = allFolders,
- onDismiss = { viewModel.setMoveToFolderDialogVisibility(false) },
- onFolderSelected = { folderId ->
- viewModel.moveSelectedNotesToFolder(folderId)
- }
- )
- }
-
- if (viewModel.showDeleteConfirmDialog.value) {
- viewModel.folderForAction.value?.let { folder ->
- AlertDialog(
- onDismissRequest = { viewModel.onDismissFolderAction() },
- title = { Text(stringResource(R.string.delete_folder)) },
- text = {
- Text(
- stringResource(
- R.string.delete_folder_confirmation,
- folder.name
- )
- )
- },
- confirmButton = {
- TextButton(onClick = { viewModel.confirmFolderDeletion() }) {
- Text(
- stringResource(R.string.delete),
- color = MaterialTheme.colorScheme.error
- )
- }
- },
- dismissButton = {
- TextButton(onClick = { viewModel.onDismissFolderAction() }) {
- Text(stringResource(R.string.cancel))
- }
- }
- )
- }
- }
-
- LaunchedEffect(key1 = true) {
- viewModel.uiEvent.collect { message ->
- scope.launch {
- snackbarHostState.showSnackbar(message)
- }
- }
- }
-
- if (settingsModel.databaseUpdate.value) viewModel.observeNotes()
- val containerColor = getContainerColor(settingsModel)
- val sortedNotes = remember(notes, settings.sortDescending) {
- notes.sortedWith(sorter(settings.sortDescending))
- }
-
- NotesScaffold(
- snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
- floatingActionButton = {
- AnimatedVisibility(
- visible = viewModel.isFabExtended.value,
- enter = slideInVertically(initialOffsetY = { it * 2 }),
- exit = slideOutVertically(targetOffsetY = { it * 2 })
- ) {
- MultiActionFloatingActionButton(
- onNewNoteClicked = { onNoteClicked(0, viewModel.isVaultMode.value, selectedFolderId) },
- onAskAiClicked = { viewModel.toggleAiChatSheet(true) },
- onDreamJournalClicked = { viewModel.onDreamJournalClicked(onNoteClicked) }
- )
- }
- },
- topBar = {
- AnimatedVisibility(
- visible = viewModel.selectedNotes.isNotEmpty(),
- enter = defaultScreenEnterAnimation(),
- exit = defaultScreenExitAnimation()
- ) {
- SelectedNotesTopAppBar(
- containerColor = topBarColor,
- selectedNotes = viewModel.selectedNotes,
- allNotes = notes,
- settingsModel = settingsModel,
- onPinClick = { viewModel.pinOrUnpinNotes() },
- onDeleteClick = { viewModel.toggleIsDeleteMode(true) },
- onSelectAllClick = { selectAllNotes(viewModel, notes) },
- onMoveToFolderClick = {
- viewModel.setMoveToFolderDialogVisibility(
- true
- )
- },
- onCloseClick = { viewModel.selectedNotes.clear() },
- onLockClick = {
- val anyLocked = viewModel.selectedNotes.any { it.isLocked }
- if (anyLocked) {
- if (settings.noteLockPassword.isNullOrBlank()) {
- scope.launch { snackbarHostState.showSnackbar(context.getString(R.string.first_set_a_note_password)) }
- } else {
- showBulkUnlockDialog = true
- }
- } else {
- if (settings.noteLockPassword.isNullOrBlank()) {
- scope.launch { snackbarHostState.showSnackbar(context.getString(R.string.first_set_a_password_in_the_settings)) }
- } else {
- viewModel.toggleLockForSelectedNotes()
- }
- }
- }
- )
- }
-
- AnimatedVisibility(
- visible = viewModel.selectedNotes.isEmpty(),
- enter = defaultScreenEnterAnimation(),
- exit = defaultScreenExitAnimation()
- ) {
- Column(
- modifier = Modifier.background(topBarColor)
- ) {
- NotesSearchBar(
- settingsModel = settingsModel,
- query = query,
- onQueryChange = { viewModel.changeSearchQuery(it) },
- onSettingsClick = onSettingsClicked,
- onClearClick = { viewModel.changeSearchQuery("") },
- onVaultClicked = {
- if (!isVaultMode) {
- viewModel.toggleIsPasswordPromptVisible(true)
- } else {
- viewModel.toggleIsVaultMode(false)
- viewModel.encryptionHelper.removePassword()
- }
- },
- isVaultMode = isVaultMode,
- selectedFolderName = selectedFolder?.name ?: stringResource(R.string.all_notes),
- selectedFolderIconName = selectedFolder?.iconName,
- onFoldersClicked = { showFolderSheet = true }
- )
- }
- }
- },
- content = {
- NoteFilter(
- listState = listState,
- settingsViewModel = settingsModel,
- containerColor = containerColor,
- shape = shapeManager(
- radius = settingsModel.settings.value.cornerRadius / 2,
- isBoth = true
- ),
- notes = sortedNotes,
- allFolders = allFolders,
- onNoteClicked = { noteId ->
- val clickedNote = viewModel.displayedNotes.value.find { it.id == noteId }
- if (clickedNote != null) {
- if (clickedNote.isLocked) {
- if (settings.noteLockPassword.isNullOrBlank()) {
- scope.launch { snackbarHostState.showSnackbar(context.getString(R.string.note_is_locked_but_no_password)) }
- } else {
- showUnlockDialog = noteId
- }
- } else {
- onNoteClicked(clickedNote.id, clickedNote.encrypted, clickedNote.folderId)
- }
- }
- },
- selectedNotes = viewModel.selectedNotes,
- viewMode = settingsModel.settings.value.viewMode,
- searchText = query.ifBlank { null },
- isDeleteMode = viewModel.isDeleteMode.value,
- onNoteUpdate = { note ->
- scope.launch(Dispatchers.IO) {
- viewModel.updateNote(note)
- }
- },
- onDeleteNote = {
- viewModel.toggleIsDeleteMode(false)
- viewModel.deleteNoteById(it)
- },
- )
- }
- )
- }
- var showUnlockDialog by remember { mutableStateOf(null) }
- if (showUnlockDialog != null) {
- var passwordInput by remember { mutableStateOf("") }
- AlertDialog(
- onDismissRequest = { showUnlockDialog = null },
- title = { Text(stringResource(R.string.unlock_note)) },
- text = {
- Column {
- Text(stringResource(R.string.enter_your_password_to_view_this_note))
- Spacer(modifier = Modifier.height(8.dp))
- OutlinedTextField(
- value = passwordInput,
- onValueChange = { passwordInput = it },
- label = { Text(stringResource(R.string.note_password)) },
- visualTransformation = PasswordVisualTransformation(),
- singleLine = true,
- modifier = Modifier.fillMaxWidth()
- )
- }
- },
- confirmButton = {
- Button(onClick = {
- if (passwordInput == settings.noteLockPassword) {
- val noteToOpen = notes.find { it.id == showUnlockDialog }
- if (noteToOpen != null) {
- onNoteClicked(noteToOpen.id, noteToOpen.encrypted, noteToOpen.folderId)
- }
- showUnlockDialog = null
- } else {
- scope.launch { snackbarHostState.showSnackbar("Wrong password!!") }
- }
- }) { Text("Aç") }
- },
- dismissButton = {
- TextButton(onClick = { showUnlockDialog = null }) { Text(stringResource(R.string.cancel)) }
- }
- )
- }
- if (showBulkUnlockDialog) {
- var passwordInput by remember { mutableStateOf("") }
- AlertDialog(
- onDismissRequest = { showBulkUnlockDialog = false },
- title = { Text(stringResource(R.string.unlock_selected_notes)) },
- text = {
- OutlinedTextField(
- value = passwordInput,
- onValueChange = { passwordInput = it },
- label = { Text(stringResource(R.string.note_password)) },
- visualTransformation = PasswordVisualTransformation(),
- singleLine = true
- )
- },
- confirmButton = {
- Button(onClick = {
- if (passwordInput == settings.noteLockPassword) {
- viewModel.toggleLockForSelectedNotes()
- showBulkUnlockDialog = false
- } else {
- scope.launch { snackbarHostState.showSnackbar("Wrong password!") }
- }
- }) { Text(stringResource(R.string.confirm)) }
- },
- dismissButton = {
- TextButton(onClick = { showBulkUnlockDialog = false }) { Text(stringResource(R.string.cancel)) }
- }
- )
- }
- } else {
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator()
- }
- }
-}
-
-@Composable
-@ReadOnlyComposable
-fun getContainerColor(settingsModel: SettingsViewModel): Color {
- return if (settingsModel.settings.value.extremeAmoledMode) Color.Black else MaterialTheme.colorScheme.surfaceContainerHigh
-}
-
-@Composable
-private fun MultiActionFloatingActionButton(
- onNewNoteClicked: () -> Unit,
- onAskAiClicked: () -> Unit,
- onDreamJournalClicked: () -> Unit
-) {
- var isExpanded by remember { mutableStateOf(false) }
- val transition = updateTransition(targetState = isExpanded, label = "fab_transition")
- val mainButtonRotation by transition.animateFloat(
- label = "fab_rotation",
- transitionSpec = { spring(stiffness = Spring.StiffnessMedium) }
- ) { expanded ->
- if (expanded) 45f else 0f
- }
- val secondaryButtonAlpha by transition.animateFloat(
- label = "fab_alpha",
- transitionSpec = { spring(stiffness = Spring.StiffnessMedium) }
- ) { expanded ->
- if (expanded) 1f else 0f
- }
- val secondaryButtonScale by transition.animateFloat(
- label = "fab_scale",
- transitionSpec = { spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow) }
- ) { expanded ->
- if (expanded) 1f else 0.5f
- }
- var hopTranslationY by remember { mutableStateOf(0f)
- }
-
- LaunchedEffect(isExpanded) {
- if (isExpanded) {
- delay(40)
- launch {
- animate(0f, -20f, animationSpec = spring(stiffness = Spring.StiffnessMedium)) { v, _ -> hopTranslationY = v }
- animate(-20f, 0f, animationSpec = spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessLow)) { v, _ -> hopTranslationY = v }
- }
- } else {
- launch {
- animate(hopTranslationY, 0f, animationSpec = tween(100)) { v, _ -> hopTranslationY = v }
- }
- }
- }
-
-
- Column(
- horizontalAlignment = Alignment.End,
- verticalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- AnimatedVisibility(
- visible = isExpanded,
- enter = fadeIn(animationSpec = tween(150)),
- exit = fadeOut(animationSpec = tween(150))
- ) {
- Column(
- horizontalAlignment = Alignment.End,
- verticalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- modifier = Modifier.graphicsLayer {
- alpha = secondaryButtonAlpha
- scaleX = secondaryButtonScale
- scaleY = secondaryButtonScale
- }
- ) {
- Surface(
- shape = RoundedCornerShape(12.dp),
- color = MaterialTheme.colorScheme.surfaceContainerHigh,
- tonalElevation = 2.dp
- ) {
- Text(
- text = stringResource(R.string.ai_button_texts),
- modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
- style = MaterialTheme.typography.labelLarge
- )
- }
- SmallFloatingActionButton(
- onClick = {
- onAskAiClicked()
- isExpanded = false
- },
- containerColor = MaterialTheme.colorScheme.tertiaryContainer,
- ) {
- Icon(
- imageVector = Icons.Rounded.AutoAwesome,
- contentDescription = stringResource(R.string.ai_button_texts),
- tint = MaterialTheme.colorScheme.onTertiaryContainer,
- modifier = Modifier.graphicsLayer {
- translationY = hopTranslationY
- }
- )
- }
- }
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- modifier = Modifier.graphicsLayer {
- alpha = secondaryButtonAlpha
- scaleX = secondaryButtonScale
- scaleY = secondaryButtonScale
- }
- ) {
- Surface(
- shape = RoundedCornerShape(12.dp),
- color = MaterialTheme.colorScheme.surfaceContainerHigh,
- tonalElevation = 2.dp
- ) {
- Text(
- text = stringResource(R.string.new_note),
- modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
- style = MaterialTheme.typography.labelLarge
- )
- }
- SmallFloatingActionButton(
- onClick = {
- onNewNoteClicked()
- isExpanded = false
- },
- containerColor = MaterialTheme.colorScheme.secondaryContainer
- ) {
- Icon(
- Icons.Rounded.Edit,
- contentDescription = stringResource(R.string.new_note),
- modifier = Modifier.graphicsLayer {
- translationY = hopTranslationY
- }
- )
- }
- }
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- modifier = Modifier.graphicsLayer {
- alpha = secondaryButtonAlpha
- scaleX = secondaryButtonScale
- scaleY = secondaryButtonScale
- }
- ) {
- Surface(
- shape = RoundedCornerShape(12.dp),
- color = MaterialTheme.colorScheme.surfaceContainerHigh,
- tonalElevation = 2.dp
- ) {
- Text(
- text = stringResource(R.string.dream_journal_new_note),
- modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
- style = MaterialTheme.typography.labelLarge
- )
- }
- SmallFloatingActionButton(
- onClick = {
- onDreamJournalClicked()
- isExpanded = false
- },
- containerColor = MaterialTheme.colorScheme.secondaryContainer
- ) {
- Icon(
- Icons.Default.Book,
- contentDescription = "Dream Journal",
- modifier = Modifier.graphicsLayer {
- translationY = hopTranslationY
- }
- )
- }
- }
- }
- }
-
- FloatingActionButton(
- onClick = { isExpanded = !isExpanded },
- containerColor = MaterialTheme.colorScheme.primary,
- ) {
- Icon(
- Icons.Default.Add,
- contentDescription = "Add",
- modifier = Modifier.rotate(mainButtonRotation)
- )
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun SelectedNotesTopAppBar(
- selectedNotes: List,
- allNotes: List,
- settingsModel: SettingsViewModel,
- onPinClick: () -> Unit,
- onDeleteClick: () -> Unit,
- onSelectAllClick: () -> Unit,
- onMoveToFolderClick: () -> Unit,
- containerColor: Color,
- onCloseClick: () -> Unit,
- onLockClick: () -> Unit = {},
-) {
- var deletelaert by remember {
- mutableStateOf(false)
- }
- AnimatedVisibility(visible = deletelaert) {
- AlertDialog(onDismissRequest = { deletelaert = false }, title = {
- Text(
- text = stringResource(id = R.string.alert_text)
- )
- }, confirmButton = {
- TextButton(onClick = { deletelaert=false
- onDeleteClick()
- }) {
- Text(text = stringResource(id = R.string.yes), color = MaterialTheme.colorScheme.error )
- }
- },
- dismissButton = {
- TextButton(onClick = { deletelaert = false }) {
- Text(text =stringResource(id = R.string.cancel))
- }
- })
-
- }
- TopAppBar(
- modifier = Modifier.padding(bottom = 36.dp),
- colors = TopAppBarDefaults.topAppBarColors(
- containerColor = containerColor
- ),
- title = { TitleText(titleText = selectedNotes.size.toString()) },
- navigationIcon = { CloseButton(onCloseClicked = onCloseClick) },
- actions = {
- Row {
- IconButton(onClick = onLockClick) {
- val allLocked = selectedNotes.all { it.isLocked }
- Icon(
- imageVector = if (allLocked) Icons.Rounded.LockOpen else Icons.Rounded.Lock,
- contentDescription = "Lock/Unlock"
- )
- }
- IconButton(onClick = onMoveToFolderClick) {
- Icon(Icons.Default.FolderOpen, contentDescription = "Move to Folder")
- }
- PinButton(isPinned = selectedNotes.all { it.pinned }, onClick = onPinClick)
- DeleteButton(onClick = { deletelaert = true })
- SelectAllButton(
- enabled = selectedNotes.size != allNotes.size,
- onClick = onSelectAllClick
- )
- }
- }
- )
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun NotesSearchBar(
- settingsModel: SettingsViewModel,
- query: String,
- onQueryChange: (String) -> Unit,
- onSettingsClick: () -> Unit,
- onVaultClicked: () -> Unit,
- onClearClick: () -> Unit,
- isVaultMode: Boolean,
- selectedFolderName: String,
- selectedFolderIconName: String?,
- onFoldersClicked: () -> Unit
-) {
- SearchBar(
- modifier = Modifier
- .fillMaxWidth()
- .padding(
- horizontal = if (settingsModel.settings.value.makeSearchBarLonger) 16.dp else 36.dp,
- vertical = 8.dp
- ),
- query = query,
- placeholder = { Text(stringResource(R.string.search)) },
- leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = "Search") },
- trailingIcon = {
- Row(verticalAlignment = Alignment.CenterVertically) {
- if (query.isNotBlank()) {
- CloseButton(contentDescription = "Clear", onCloseClicked = onClearClick)
- }
- if (settingsModel.settings.value.vaultSettingEnabled) {
- VaultButton(isVaultMode) { onVaultClicked() }
- }
- AnimatedVisibility(
- visible = query.isBlank(),
- enter = fadeIn() + slideInHorizontally { it },
- exit = fadeOut() + slideOutHorizontally { it }
- ) {
- Button(
- onClick = onFoldersClicked,
- shape = CircleShape,
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.surfaceVariant,
- contentColor = MaterialTheme.colorScheme.onSurfaceVariant
- ),
- modifier = Modifier
- .widthIn(max = 120.dp)
- .padding(horizontal = 4.dp),
- contentPadding = PaddingValues(horizontal = 12.dp)
- ) {
- Icon(
- imageVector = selectedFolderIconName?.let { iconName ->
- iconNameToVector(iconName)
- } ?: Icons.Default.FolderOpen,
- contentDescription = "Folders",
- modifier = Modifier.size(18.dp)
- )
-
- if (selectedFolderName != stringResource(R.string.all_notes)) {
- Spacer(modifier = Modifier.width(6.dp))
- Text(
- text = selectedFolderName,
- style = MaterialTheme.typography.labelLarge,
- maxLines = 1,
- modifier = Modifier.basicMarquee(
- iterations = Int.MAX_VALUE,
- initialDelayMillis = 1000
- )
- )
- }
- }
- }
- BadgedBox(
- badge = {
- if (settingsModel.updateAvailable.value) {
- Badge()
- }
- }
- ) {
- IconButton(onClick = onSettingsClick) {
- Icon(
- imageVector = Icons.Rounded.Settings,
- contentDescription = stringResource(R.string.screen_settings)
- )
- }
- }
- }
- },
- onQueryChange = onQueryChange,
- onSearch = onQueryChange,
- onActiveChange = {},
- active = false,
- ) {}
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun AttachmentTypeBottomSheet(
- onDismiss: () -> Unit,
- onImageClicked: () -> Unit,
- onFileClicked: () -> Unit
-) {
- ModalBottomSheet(
- onDismissRequest = onDismiss,
- sheetState = rememberModalBottomSheetState()
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 32.dp, top = 16.dp)
- ) {
- ListItem(
- headlineContent = { Text(stringResource(R.string.add_image)) },
- leadingContent = { Icon(Icons.Default.Image, contentDescription = "Image") },
- modifier = Modifier.clickable {
- onImageClicked()
- onDismiss()
- }
- )
- ListItem(
- headlineContent = { Text(stringResource(R.string.add_text_file)) },
- leadingContent = { Icon(Icons.Default.Description, contentDescription = "Fİle") },
- modifier = Modifier.clickable {
- onFileClicked()
- onDismiss()
- }
- )
- }
- }
-}
-
-private fun selectAllNotes(viewModel: HomeViewModel, allNotes: List) {
- allNotes.forEach {
- if (!viewModel.selectedNotes.contains(it)) {
- viewModel.selectedNotes.add(it)
- }
- }
-}
-
-fun sorter(descending: Boolean): Comparator {
- return if (descending) {
- compareByDescending { it.createdAt }
- } else {
- compareBy { it.createdAt }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
-@Composable
-fun FolderSelectionBottomSheet(
- allFolders: List,
- selectedFolderId: Long?,
- onDismiss: () -> Unit,
- onFolderSelected: (Long?) -> Unit,
- onAddFolderClicked: () -> Unit,
- onFolderLongClick: (Folder) -> Unit
-) {
- val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
- var searchQuery by remember { mutableStateOf("") }
-
- val filteredFolders = remember(searchQuery, allFolders) {
- if (searchQuery.isBlank()) {
- allFolders
- } else {
- allFolders.filter { folder ->
- folder.name.contains(searchQuery, ignoreCase = true)
- }
- }
- }
-
- ModalBottomSheet(
- onDismissRequest = onDismiss,
- sheetState = sheetState,
- shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
- containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
- scrimColor = Color.Black.copy(alpha = 0.6f)
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 16.dp)
- ) {
- TextField(
- value = searchQuery,
- onValueChange = { newQuery -> searchQuery = newQuery },
- placeholder = { Text(stringResource(R.string.search_folders)) },
- leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = null) },
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp),
- shape = CircleShape,
- colors = TextFieldDefaults.colors(
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent
- )
- )
-
- LazyColumn(
- modifier = Modifier
- .fillMaxWidth()
- .heightIn(max = 400.dp),
- contentPadding = PaddingValues(horizontal = 16.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- item {
- FolderListItem(
- name = stringResource(R.string.all_notes),
- iconName = "Inbox",
- isSelected = selectedFolderId == null,
- onClick = { onFolderSelected(null) }
- )
- }
- items(items = filteredFolders, key = { it.id }) { folder ->
- FolderListItem(
- name = folder.name,
- iconName = folder.iconName,
- isSelected = selectedFolderId == folder.id,
- onClick = { onFolderSelected(folder.id) },
- onLongClick = { onFolderLongClick(folder) }
- )
- }
-
- item {
- Button(
- onClick = onAddFolderClicked,
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 8.dp)
- ) {
- Icon(Icons.Default.Add, contentDescription = "Add New Folder")
- Spacer(modifier = Modifier.width(8.dp))
- Text(stringResource(R.string.add_new_folder))
- }
- }
- }
- }
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun FolderListItem(
- name: String,
- iconName: String,
- isSelected: Boolean,
- onClick: () -> Unit,
- onLongClick: (() -> Unit)? = null
-) {
- val selectedBorder = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null
- val cardColors = CardDefaults.cardColors(
- containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainer
- )
-
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .border(selectedBorder ?: BorderStroke(0.dp, Color.Transparent), RoundedCornerShape(16.dp))
- .combinedClickable(
- onClick = onClick,
- onLongClick = onLongClick
- ),
- shape = RoundedCornerShape(16.dp),
- colors = cardColors
- ) {
- Row(
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 20.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- imageVector = iconNameToVector(iconName),
- contentDescription = null,
- tint = MaterialTheme.colorScheme.primary
- )
- Spacer(modifier = Modifier.width(16.dp))
- Text(
- text = name,
- style = MaterialTheme.typography.bodyLarge,
- fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
- )
- }
- }
-}
-
-@Composable
-fun AddFolderDialogWithIcons(
- onDismiss: () -> Unit,
- onConfirm: (name: String, iconName: String) -> Unit,
- folderToEdit: Folder? = null
-) {
- var name by remember { mutableStateOf(folderToEdit?.name ?: "") }
- var selectedIconName by remember { mutableStateOf(folderToEdit?.iconName ?: "Folder") }
-
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text(if (folderToEdit == null) stringResource(R.string.new_folder) else stringResource(R.string.edit_folder)) },
- text = {
- Column {
- Text(
- text = stringResource(R.string.preview),
- style = MaterialTheme.typography.titleMedium,
- modifier = Modifier.padding(bottom = 8.dp)
- )
- FolderListItem(
- name = name.ifBlank { stringResource(R.string.folder_name) },
- iconName = selectedIconName,
- isSelected = false,
- onClick = {}
- )
- Spacer(modifier = Modifier.height(20.dp))
- OutlinedTextField(
- value = name,
- onValueChange = { newName -> name = newName },
- label = { Text(stringResource(R.string.folder_name)) },
- singleLine = true,
- modifier = Modifier.fillMaxWidth()
- )
- Spacer(modifier = Modifier.height(16.dp))
- Text(stringResource(R.string.choose_icon), style = MaterialTheme.typography.labelLarge)
- Spacer(modifier = Modifier.height(8.dp))
- IconPicker(
- selectedIconName = selectedIconName,
- onIconSelected = { selectedIconName = it }
- )
- }
- },
- confirmButton = {
- TextButton(
- onClick = {
- if (name.isNotBlank()) {
- onConfirm(name, selectedIconName)
- }
- },
- enabled = name.isNotBlank()
- ) {
- Text(stringResource(R.string.save))
- }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) {
- Text(stringResource(R.string.cancel))
- }
- }
- )
-}
-
-@Composable
-fun IconPicker(
- selectedIconName: String,
- onIconSelected: (String) -> Unit
-) {
- val iconList = remember { materialIconsList.keys.toList() }
- LazyVerticalGrid(
- columns = GridCells.Adaptive(minSize = 48.dp),
- modifier = Modifier.heightIn(max = 200.dp)
- ) {
- items(items = iconList, key = { it }) { iconName ->
- val isSelected = selectedIconName == iconName
- IconButton(
- onClick = { onIconSelected(iconName) },
- colors = IconButtonDefaults.iconButtonColors(
- containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent
- )
- ) {
- Icon(
- imageVector = materialIconsList.getValue(iconName),
- contentDescription = iconName,
- tint = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
- }
-}
-
-fun iconNameToVector(name: String): ImageVector {
- return materialIconsList[name] ?: Icons.Default.Folder
-}
-
-val materialIconsList = mapOf(
- "Folder" to Icons.Default.Folder,
- "Inbox" to Icons.Default.FolderOpen,
- "Bookmark" to Icons.Default.Bookmark,
- "Favorite" to Icons.Default.Favorite,
- "Home" to Icons.Default.Home,
- "Star" to Icons.Default.Star,
- "Event" to Icons.Default.Event,
- "Work" to Icons.Default.Work,
- "School" to Icons.Default.School,
- "Person" to Icons.Default.Person,
- "Group" to Icons.Default.Group,
- "ShoppingCart" to Icons.Default.ShoppingCart,
- "AttachMoney" to Icons.Default.AttachMoney,
- "FitnessCenter" to Icons.Default.FitnessCenter,
- "Travel" to Icons.Default.Flight,
- "MusicNote" to Icons.Default.MusicNote,
- "Movie" to Icons.Default.Movie,
- "Book" to Icons.Default.Book,
- "Code" to Icons.Default.Code,
- "Cloud" to Icons.Default.Cloud,
- "Lightbulb" to Icons.Default.Lightbulb,
- "Pets" to Icons.Default.Pets,
- "Build" to Icons.Default.Build,
- "Palette" to Icons.Default.Palette,
- "CardGiftcard" to Icons.Default.CardGiftcard,
- "DirectionsCar" to Icons.Default.DirectionsCar,
- "AccountBalance" to Icons.Default.AccountBalance,
- "Alarm" to Icons.Default.Alarm,
- "Apartment" to Icons.Default.Apartment,
- "Assessment" to Icons.Default.Assessment,
- "Backup" to Icons.Default.Backup,
- "Cake" to Icons.Default.Cake,
- "CameraAlt" to Icons.Default.CameraAlt,
- "Campaign" to Icons.Default.Campaign,
- "Chat" to Icons.AutoMirrored.Filled.Chat,
- "Circle" to Icons.Default.Circle,
- "ContentCut" to Icons.Default.ContentCut,
- "Eco" to Icons.Default.Eco,
- "Extension" to Icons.Default.Extension,
- "Flag" to Icons.Default.Flag,
- "Headset" to Icons.Default.Headset,
- "Key" to Icons.Default.Key,
- "Link" to Icons.Default.Link,
- "Lock" to Icons.Default.Lock,
- "Map" to Icons.Default.Map,
- "PushPin" to Icons.Default.PushPin,
- "Receipt" to Icons.Default.Receipt,
- "Restaurant" to Icons.Default.Restaurant,
- "Shield" to Icons.Default.Shield,
- "Spa" to Icons.Default.Spa,
- "SportsEsports" to Icons.Default.SportsEsports,
- "Store" to Icons.Default.Store,
- "ThumbUp" to Icons.Default.ThumbUp,
- "Visibility" to Icons.Default.Visibility,
- "Warning" to Icons.Default.Warning
-)
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/desktop/DesktopAiScreen.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/desktop/DesktopAiScreen.kt
deleted file mode 100644
index 9fe19a5..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/desktop/DesktopAiScreen.kt
+++ /dev/null
@@ -1,1503 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.home.desktop
-
-import android.net.Uri
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.ClickableText
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.Chat
-import androidx.compose.material.icons.automirrored.filled.Send
-import androidx.compose.material.icons.filled.AttachFile
-import androidx.compose.material.icons.filled.Language
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.material.icons.rounded.AddComment
-import androidx.compose.material.icons.rounded.AutoAwesome
-import androidx.compose.material.icons.rounded.Edit
-import androidx.compose.material.icons.rounded.ExpandMore
-import androidx.compose.material.icons.rounded.GolfCourse
-import androidx.compose.material.icons.rounded.History
-import androidx.compose.material.icons.rounded.Notes
-import androidx.compose.material.icons.rounded.Psychology
-import androidx.compose.material.icons.rounded.School
-import androidx.compose.material.icons.rounded.Search
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.IconButtonDefaults
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.material3.TextField
-import androidx.compose.material3.TextFieldDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.StrokeCap
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalHapticFeedback
-import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextDecoration
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
-import coil.compose.AsyncImage
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.data.repository.AiMode
-import com.babelsoftware.airnote.domain.model.AiChatSession
-import com.babelsoftware.airnote.domain.model.Participant
-import com.babelsoftware.airnote.presentation.screens.home.ActionCard
-import com.babelsoftware.airnote.presentation.screens.home.AiCentralGraphic
-import com.babelsoftware.airnote.presentation.screens.home.viewmodel.DraftedNote
-import com.babelsoftware.airnote.presentation.screens.home.viewmodel.HomeViewModel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlin.random.Random
-
-@Composable
-fun DesktopAiAssistantDialog(
- viewModel: HomeViewModel,
- onDismissRequest: () -> Unit
-) {
- Dialog(
- onDismissRequest = onDismissRequest,
- properties = DialogProperties(usePlatformDefaultWidth = false)
- ) {
- Card(
- shape = RoundedCornerShape(24.dp),
- modifier = Modifier
- .fillMaxWidth(0.8f)
- .fillMaxHeight(0.85f),
- elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
- ) {
- AiChatContainer(viewModel = viewModel)
- }
- }
-}
-
-// =================================================================
-// === Enhanced AirNote AI Interface
-// =================================================================
-
-@OptIn(ExperimentalAnimationApi::class)
-@Composable
-private fun AiChatContainer(viewModel: HomeViewModel) {
- val backgroundBrush = Brush.verticalGradient(
- colors = listOf(Color(0xFF10141C), Color(0xFF0A0D12))
- )
- val showHistoryScreen by viewModel.showAiHistoryScreen
- val allSessions by viewModel.allChatSessions.collectAsState()
-
- Column(
- modifier = Modifier
- .fillMaxSize()
- .background(backgroundBrush)
- ) {
- AiTopBar(
- viewModel = viewModel,
- showHistory = showHistoryScreen,
- onToggleHistory = { viewModel.toggleAiHistoryScreen(!showHistoryScreen) }
- )
-
- AnimatedContent(targetState = showHistoryScreen, label = "AiScreenAnimation") { showHistory ->
- if (showHistory) {
- AiHistoryScreen(
- sessions = allSessions,
- onNewChatClicked = { viewModel.startNewChat() },
- onSessionClicked = { viewModel.loadChatSession(it) },
- onDeleteSession = { viewModel.deleteChatSession(it.id) }
- )
- } else {
- AiMainContent(viewModel = viewModel)
- }
- }
- }
-}
-
-@Composable
-private fun AiMainContent(viewModel: HomeViewModel) {
- val chatState by viewModel.chatState.collectAsState()
- var text by remember { mutableStateOf("") }
- val isChatActive = chatState.hasStartedConversation || chatState.messages.isNotEmpty()
-
- if (viewModel.showAskQuestionDialog.value) {
- AskAiQuestionDialog(
- onDismiss = { viewModel.onDismissQuestionDialog() },
- onConfirm = { question ->
- viewModel.sendMessage(question)
- viewModel.onDismissQuestionDialog()
- }
- )
- }
-
- if (viewModel.showCreateDraftDialog.value) {
- CreateDraftDialog(
- onDismiss = { viewModel.onDismissCreateDraftDialog() },
- onConfirm = { topic ->
- viewModel.generateDraft(topic)
- viewModel.onDismissCreateDraftDialog()
- }
- )
- }
-
- val onSendMessage = { message: String ->
- if (message.isNotBlank()) {
- if (chatState.isAwaitingDraftTopic) {
- viewModel.generateDraft(message)
- } else {
- viewModel.sendMessage(message)
- }
- text = ""
- }
- }
-
- Column(modifier = Modifier.fillMaxSize()) {
- Box(modifier = Modifier.weight(1f)) {
- val (isLoading, isAnalyzingImage) = remember(chatState) {
- val loadingMessage = chatState.messages.any { it.isLoading }
- val imageUri = chatState.analyzingImageUri
- (loadingMessage to (imageUri != null))
- }
-
- when {
- chatState.latestDraft != null && chatState.latestDraft?.sourceImageUri != null -> {
- DraftDisplayWithImage(
- draft = chatState.latestDraft!!,
- onSave = { viewModel.saveDraftedNote() },
- onRegenerate = { viewModel.regenerateDraft() }
- )
- }
-
- isLoading && isAnalyzingImage -> {
- ImageAnalysisScreen(imageUri = chatState.analyzingImageUri!!)
- }
-
- chatState.latestDraft != null -> {
- DraftDisplay(
- draft = chatState.latestDraft,
- onSave = { viewModel.saveDraftedNote() },
- onRegenerate = { viewModel.regenerateDraft() }
- )
- }
- isChatActive -> {
- ChatScreenContent(
- messages = chatState.messages,
- topicForLoading = if (
- chatState.messages.lastOrNull()?.isLoading == true &&
- chatState.messages.getOrNull(chatState.messages.size - 2)?.participant == Participant.USER
- ) {
- chatState.messages.getOrNull(chatState.messages.size - 2)?.text
- } else {
- null
- }
- )
- }
- else -> {
- NewAiHomeScreen(viewModel = viewModel)
- }
- }
- }
-
- val showInputBar = chatState.latestDraft == null && chatState.analyzingImageUri == null
- val isLoading = chatState.messages.any { it.isLoading }
-
- if (showInputBar) {
- if (isChatActive) {
- RedesignedChatInputBar(
- text = text,
- onValueChange = { text = it },
- onSendMessage = { onSendMessage(text) },
- onImagePickerClicked = { viewModel.requestImageForAnalysis() },
- enabled = !isLoading,
- placeholderText = if (chatState.isAwaitingDraftTopic) stringResource(R.string.draft_topic_placeholder) else stringResource(R.string.ask_airnote_ai)
- )
- } else {
- PreChatInputBar(
- text = text,
- onValueChange = { text = it },
- onSendMessage = onSendMessage,
- onImagePickerClicked = { viewModel.requestImageForAnalysis() },
- enabled = !isLoading
- )
- }
- AiDisclaimerText()
- }
- }
-}
-
-@Composable
-private fun ImageAnalysisScreen(imageUri: Uri) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp),
- contentAlignment = Alignment.Center
- ) {
- AsyncImage(
- model = imageUri,
- contentDescription = stringResource(R.string.image_being_analyzed),
- modifier = Modifier
- .fillMaxSize()
- .clip(RoundedCornerShape(16.dp)),
- contentScale = ContentScale.Fit
- )
-
- MagicAnalysisAnimation()
- }
-}
-
-@Composable
-private fun MagicAnalysisAnimation() {
- val infiniteTransition = rememberInfiniteTransition(label = "analysis_animation")
- val scanLinePosition by infiniteTransition.animateFloat(
- initialValue = 0f,
- targetValue = 1f,
- animationSpec = infiniteRepeatable(
- animation = tween(2000, easing = LinearEasing),
- repeatMode = RepeatMode.Restart
- ),
- label = "scan_line"
- )
-
- val particles = remember {
- List(50) {
- Pair(
- Random.nextFloat(), // x
- Random.nextFloat() // y
- )
- }
- }
- val particleAlpha by infiniteTransition.animateFloat(
- initialValue = 0f,
- targetValue = 1f,
- animationSpec = infiniteRepeatable(
- animation = tween(1000, easing = LinearEasing),
- repeatMode = RepeatMode.Reverse
- ),
- label = "particles"
- )
-
- val primaryColor = Color(0xFF33A2FF)
-
- Canvas(modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(16.dp))) {
- val canvasWidth = size.width
- val canvasHeight = size.height
- val y = scanLinePosition * canvasHeight
-
- drawLine(
- brush = Brush.verticalGradient(
- colors = listOf(Color.Transparent, primaryColor.copy(alpha = 0.5f), Color.Transparent),
- startY = y - 20f,
- endY = y + 20f
- ),
- start = Offset(x = 0f, y = y),
- end = Offset(x = canvasWidth, y = y),
- strokeWidth = 4f,
- cap = StrokeCap.Round
- )
-
- particles.forEach { (x, y) ->
- drawCircle(
- color = primaryColor,
- radius = (Random.nextFloat() * 4f + 2f),
- center = Offset(x * canvasWidth, y * canvasHeight),
- alpha = particleAlpha * Random.nextFloat()
- )
- }
-
- val bracketSize = 30f
- val bracketStroke = 6f
-
- // Top left
- drawLine(primaryColor, Offset(0f, bracketSize), Offset(0f, 0f), bracketStroke)
- drawLine(primaryColor, Offset(0f, 0f), Offset(bracketSize, 0f), bracketStroke)
- // Top right
- drawLine(primaryColor, Offset(canvasWidth - bracketSize, 0f), Offset(canvasWidth, 0f), bracketStroke)
- drawLine(primaryColor, Offset(canvasWidth, 0f), Offset(canvasWidth, bracketSize), bracketStroke)
- // Lower Left
- drawLine(primaryColor, Offset(0f, canvasHeight - bracketSize), Offset(0f, canvasHeight), bracketStroke)
- drawLine(primaryColor, Offset(0f, canvasHeight), Offset(bracketSize, canvasHeight), bracketStroke)
- // Lower Right
- drawLine(primaryColor, Offset(canvasWidth - bracketSize, canvasHeight), Offset(canvasWidth, canvasHeight), bracketStroke)
- drawLine(primaryColor, Offset(canvasWidth, canvasHeight), Offset(canvasWidth, canvasHeight - bracketSize), bracketStroke)
- }
-}
-
-@Composable
-private fun DraftDisplayWithImage(draft: DraftedNote, onSave: () -> Unit, onRegenerate: () -> Unit) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp)
- ) {
- AsyncImage(
- model = draft.sourceImageUri,
- contentDescription = draft.title,
- modifier = Modifier
- .fillMaxWidth()
- .height(150.dp)
- .clip(RoundedCornerShape(16.dp)),
- contentScale = ContentScale.Crop
- )
- Spacer(modifier = Modifier.height(16.dp))
- Surface(
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(16.dp),
- color = Color.White.copy(alpha = 0.05f),
- border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f))
- ) {
- LazyColumn(
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 20.dp)
- ) {
- item {
- Text(
- text = draft.title,
- style = MaterialTheme.typography.titleLarge,
- color = Color.White,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(bottom = 12.dp)
- )
- }
- item {
- Text(
- text = draft.content,
- style = MaterialTheme.typography.bodyLarge,
- color = Color.White.copy(alpha = 0.8f),
- lineHeight = 24.sp
- )
- }
- }
- }
- Spacer(modifier = Modifier.height(16.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- OutlinedButton(
- onClick = onRegenerate,
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp),
- border = BorderStroke(1.dp, Color(0xFF33A2FF))
- ) {
- Icon(Icons.Rounded.AutoAwesome, contentDescription = null, tint = Color(0xFF33A2FF))
- Spacer(modifier = Modifier.width(8.dp))
- Text(stringResource(R.string.regenerate_note), color = Color(0xFF33A2FF))
- }
- Button(
- onClick = onSave,
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp),
- colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF33A2FF))
- ) {
- Text(stringResource(R.string.save_note), color = Color(0xFF10141C))
- }
- }
- }
-}
-
-@Composable
-fun NewAiHomeScreen(viewModel: HomeViewModel) {
- val chatState by viewModel.chatState.collectAsState()
- val suggestions by viewModel.suggestions.collectAsState()
-
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- contentPadding = PaddingValues(bottom = 16.dp)
- ) {
- item {
- Spacer(modifier = Modifier.height(64.dp))
- AiCentralGraphic(isThinking = chatState.messages.any { it.isLoading })
- Spacer(modifier = Modifier.height(24.dp))
- }
-
- val rows = (suggestions.size + 1) / 2
- items(rows) { rowIndex ->
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp, vertical = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- val itemIndex1 = rowIndex * 2
- Box(modifier = Modifier.weight(1f)) {
- ActionCard(
- text = suggestions[itemIndex1].title,
- icon = suggestions[itemIndex1].icon,
- onClick = suggestions[itemIndex1].action
- )
- }
-
- val itemIndex2 = itemIndex1 + 1
- Box(modifier = Modifier.weight(1f)) {
- if (itemIndex2 < suggestions.size) {
- ActionCard(
- text = suggestions[itemIndex2].title,
- icon = suggestions[itemIndex2].icon,
- onClick = suggestions[itemIndex2].action
- )
- }
- }
- }
- }
- }
-}
-
-
-@Composable
-private fun ChatScreenContent(
- messages: List,
- topicForLoading: String?
-) {
- val listState = rememberLazyListState()
- val scope = rememberCoroutineScope()
-
- LaunchedEffect(messages.size) {
- if (messages.isNotEmpty()) {
- scope.launch {
- listState.animateScrollToItem(messages.size - 1)
- }
- }
- }
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(horizontal = 8.dp),
- state = listState,
- contentPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
- ) {
- items(items = messages, key = { it.hashCode() }) { message ->
- ChatMessageItem(message = message, topicForLoading = topicForLoading)
- }
- }
-}
-
-@Composable
-private fun AiHistoryScreen(
- sessions: List,
- onNewChatClicked: () -> Unit,
- onSessionClicked: (AiChatSession) -> Unit,
- onDeleteSession: (AiChatSession) -> Unit
-) {
- Column(modifier = Modifier.fillMaxSize()) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- OutlinedButton(
- onClick = onNewChatClicked,
- modifier = Modifier.fillMaxWidth(),
- shape = RoundedCornerShape(12.dp),
- border = BorderStroke(1.dp, Color.White.copy(alpha = 0.2f)),
- colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White)
- ) {
- Icon(Icons.Rounded.AddComment, contentDescription = stringResource(R.string.new_chat))
- Spacer(modifier = Modifier.width(8.dp))
- Text(stringResource(R.string.new_chat))
- }
- Spacer(modifier = Modifier.height(24.dp))
- Text(
- stringResource(R.string.history),
- style = MaterialTheme.typography.titleMedium,
- color = Color.White.copy(alpha = 0.8f)
- )
- }
- if (sessions.isEmpty()) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(bottom = 80.dp),
- contentAlignment = Alignment.Center
- ) {
- Text(
- stringResource(R.string.no_chat_history),
- color = Color.White.copy(alpha = 0.6f),
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(horizontal = 32.dp)
- )
- }
- } else {
- LazyColumn(
- contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- items(items = sessions, key = { it.id }) { session ->
- HistoryItem(
- session = session,
- onClick = { onSessionClicked(session) },
- onDelete = { onDeleteSession(session) }
- )
- }
- }
- }
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class, ExperimentalFoundationApi::class)
-@Composable
-private fun HistoryItem(session: AiChatSession, onClick: () -> Unit, onDelete: () -> Unit) {
- var showDeleteConfirm by remember { mutableStateOf(false) }
-
- if (showDeleteConfirm) {
- AlertDialog(
- onDismissRequest = { showDeleteConfirm = false },
- title = { Text(stringResource(R.string.delete_chat)) },
- text = { Text(stringResource(R.string.delete_chat_description)) },
- confirmButton = {
- TextButton(
- onClick = {
- onDelete()
- showDeleteConfirm = false
- },
- colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
- ) {
- Text(stringResource(R.string.delete))
- }
- },
- dismissButton = {
- TextButton(onClick = { showDeleteConfirm = false }) {
- Text(stringResource(R.string.cancel))
- }
- }
- )
- }
-
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(12.dp))
- .combinedClickable(
- onClick = onClick,
- onLongClick = { showDeleteConfirm = true }
- ),
- color = Color.White.copy(alpha = 0.05f),
- ) {
- Row(
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- if (session.aiMode == AiMode.CREATIVE_MIND.name) Icons.Rounded.Psychology else Icons.Rounded.Notes,
- contentDescription = null,
- tint = Color.White.copy(alpha = 0.6f),
- modifier = Modifier.size(20.dp)
- )
- Spacer(modifier = Modifier.width(16.dp))
- Text(
- text = session.title,
- color = Color.White.copy(alpha = 0.9f),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.weight(1f)
- )
- }
- }
-}
-
-@Composable
-private fun AiTopBar(
- viewModel: HomeViewModel,
- showHistory: Boolean,
- onToggleHistory: () -> Unit
-) {
- val currentAiMode by viewModel.aiMode.collectAsState()
- var showModelMenu by remember { mutableStateOf(false) }
-
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Box {
- Row(
- modifier = Modifier
- .clip(CircleShape)
- .background(Color.White.copy(alpha = 0.1f))
- .clickable { showModelMenu = true }
- .padding(horizontal = 12.dp, vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- imageVector = when(currentAiMode) {
- AiMode.NOTE_ASSISTANT -> Icons.Rounded.Notes
- AiMode.CREATIVE_MIND -> Icons.Rounded.Psychology
- AiMode.ACADEMIC_RESEARCHER -> Icons.Rounded.School
- AiMode.PROFESSIONAL_STRATEGIST -> Icons.Rounded.GolfCourse
- },
- contentDescription = "AI Model",
- tint = Color(0xFF33A2FF),
- modifier = Modifier.size(16.dp)
- )
- Spacer(modifier = Modifier.width(8.dp))
- Text(
- text = when (currentAiMode) {
- AiMode.NOTE_ASSISTANT -> stringResource(R.string.ai_mode_note_assistant)
- AiMode.CREATIVE_MIND -> stringResource(R.string.ai_mode_creative_mind)
- AiMode.ACADEMIC_RESEARCHER -> stringResource(R.string.ai_mode_academic_research)
- AiMode.PROFESSIONAL_STRATEGIST -> stringResource(R.string.ai_mode_professional_strategy)
- },
- color = Color.White,
- fontWeight = FontWeight.SemiBold,
- fontSize = 14.sp
- )
- Icon(Icons.Rounded.ExpandMore, contentDescription = "Change Model", tint = Color.White.copy(alpha = 0.7f))
- }
-
- DropdownMenu(
- expanded = showModelMenu,
- onDismissRequest = { showModelMenu = false },
- modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainerHigh)
- ) {
- DropdownMenuItem(
- text = { Text(stringResource(R.string.ai_mode_note_assistant)) },
- onClick = {
- viewModel.setAiMode(AiMode.NOTE_ASSISTANT)
- showModelMenu = false
- },
- leadingIcon = {
- Icon(
- Icons.Rounded.Notes,
- contentDescription = stringResource(R.string.ai_mode_note_assistant)
- )
- }
- )
- DropdownMenuItem(
- text = { Text(stringResource(R.string.ai_mode_creative_mind)) },
- onClick = {
- viewModel.setAiMode(AiMode.CREATIVE_MIND)
- showModelMenu = false
- },
- leadingIcon = {
- Icon(
- Icons.Rounded.Psychology,
- contentDescription = stringResource(R.string.ai_mode_creative_mind)
- )
- }
- )
- }
- }
-
- IconButton(onClick = onToggleHistory) {
- Icon(
- if (showHistory) Icons.AutoMirrored.Filled.Chat else Icons.Rounded.History,
- contentDescription = "Toggle History",
- tint = Color.White.copy(alpha = 0.8f)
- )
- }
- }
-}
-
-@Composable
-private fun AiCentralGraphic(isThinking: Boolean) {
- val infiniteTransition = rememberInfiniteTransition(label = "orb_animation")
- val rotation by infiniteTransition.animateFloat(
- initialValue = 0f,
- targetValue = 360f,
- animationSpec = infiniteRepeatable(tween(if (isThinking) 2000 else 10000, easing = LinearEasing), RepeatMode.Restart),
- label = "orb_rotation"
- )
- val scale by animateFloatAsState(
- targetValue = if (isThinking) 1.1f else 1.0f,
- animationSpec = spring(
- dampingRatio = Spring.DampingRatioMediumBouncy,
- stiffness = Spring.StiffnessLow
- ),
- label = "orb_scale"
- )
-
- Box(
- modifier = Modifier.size(120.dp),
- contentAlignment = Alignment.Center
- ) {
- Canvas(modifier = Modifier.fillMaxSize()) {
- drawCircle(
- brush = Brush.radialGradient(
- colors = listOf(Color(0xFF33A2FF).copy(alpha = 0.1f), Color.Transparent),
- ),
- radius = (size.minDimension / 2.0f) * scale
- )
- }
- Box(
- modifier = Modifier
- .size(120.dp)
- .graphicsLayer {
- rotationZ = rotation
- }
- .border(
- BorderStroke(
- 2.dp,
- Brush.sweepGradient(
- 0.0f to Color.Transparent,
- 0.7f to Color(0xFF33A2FF),
- 1.0f to Color.Transparent
- )
- ),
- CircleShape
- ),
- )
- Icon(
- imageVector = Icons.Rounded.AutoAwesome,
- contentDescription = "AI Core",
- tint = Color(0xFF33A2FF),
- modifier = Modifier.size(50.dp)
- )
- }
-}
-
-@Composable
-private fun ActionCard(text: String, icon: ImageVector, onClick: () -> Unit) {
- Surface(
- onClick = onClick,
- shape = RoundedCornerShape(16.dp),
- color = Color.White.copy(alpha = 0.05f),
- border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f)),
- modifier = Modifier
- .fillMaxWidth()
- .height(100.dp)
- ) {
- Column(
- modifier = Modifier
- .padding(16.dp)
- .fillMaxSize(),
- horizontalAlignment = Alignment.Start,
- verticalArrangement = Arrangement.SpaceBetween
- ) {
- Icon(
- imageVector = icon,
- contentDescription = text,
- tint = Color.White.copy(alpha = 0.8f)
- )
- Text(
- text = text,
- color = Color.White,
- style = MaterialTheme.typography.bodyMedium,
- fontWeight = FontWeight.SemiBold,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis
- )
- }
- }
-}
-
-
-@Composable
-private fun ChatMessageItem(
- message: com.babelsoftware.airnote.domain.model.ChatMessage,
- topicForLoading: String?
-) {
- val isUser = message.participant == Participant.USER
- Row(
- modifier = Modifier
- .padding(vertical = 4.dp, horizontal = 8.dp)
- .fillMaxWidth(),
- horizontalArrangement = if (isUser) Arrangement.End else Arrangement.Start
- ) {
- if (message.isLoading) {
- if (message.text.startsWith("// Generating Note...")) {
- TerminalLoadingIndicator(topic = topicForLoading ?: "")
- } else if (message.text.isNotBlank()) {
- TypingIndicator()
- }
- } else {
- val bubbleShape = RoundedCornerShape(
- topStart = 16.dp,
- topEnd = 16.dp,
- bottomStart = if (isUser) 16.dp else 0.dp,
- bottomEnd = if (isUser) 0.dp else 16.dp
- )
-
- val backgroundBrush = when (message.participant) {
- Participant.USER -> Brush.verticalGradient(
- colors = listOf(
- Color(0xFF33A2FF).copy(alpha = 0.4f),
- Color(0xFF33A2FF).copy(alpha = 0.1f)
- )
- )
- Participant.MODEL -> Brush.verticalGradient(
- colors = listOf(
- Color.White.copy(alpha = 0.15f),
- Color.White.copy(alpha = 0.05f)
- )
- )
- Participant.ERROR -> Brush.verticalGradient(
- colors = listOf(
- MaterialTheme.colorScheme.error.copy(alpha = 0.4f),
- MaterialTheme.colorScheme.error.copy(alpha = 0.2f)
- )
- )
- }
-
- Box(
- modifier = Modifier
- .clip(bubbleShape)
- .background(brush = backgroundBrush)
- .border(
- width = 1.dp,
- brush = Brush.verticalGradient(
- colors = listOf(
- Color.White.copy(alpha = 0.3f),
- Color.Transparent
- )
- ),
- shape = bubbleShape
- )
- .padding(12.dp)
- ) {
- Text(
- text = message.text.replace(Regex("[*#]"), "").trim(),
- style = MaterialTheme.typography.bodyLarge,
- color = if (message.participant == Participant.ERROR) Color.White else Color.White.copy(alpha = 0.9f)
- )
- }
- }
- }
-}
-
-@Composable
-private fun RedesignedChatInputBar(
- text: String,
- onValueChange: (String) -> Unit,
- onSendMessage: () -> Unit,
- onImagePickerClicked: () -> Unit,
- enabled: Boolean,
- modifier: Modifier = Modifier,
- placeholderText: String
-) {
- val haptic = LocalHapticFeedback.current
-
- Surface(
- modifier = modifier
- .fillMaxWidth()
- .padding(16.dp),
- shape = RoundedCornerShape(24.dp),
- color = Color.White.copy(alpha = 0.05f),
- border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f))
- ) {
- Row(
- modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- IconButton(onClick = onImagePickerClicked, enabled = enabled) {
- Icon(
- Icons.Default.AttachFile,
- contentDescription = "Attach File",
- tint = Color.White.copy(alpha = 0.7f)
- )
- }
- TextField(
- value = text,
- onValueChange = onValueChange,
- modifier = Modifier.weight(1f),
- enabled = enabled,
- placeholder = {
- Text(placeholderText, color = Color.White.copy(alpha = 0.5f))
- },
- colors = TextFieldDefaults.colors(
- focusedContainerColor = Color.Transparent,
- unfocusedContainerColor = Color.Transparent,
- disabledContainerColor = Color.Transparent,
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
- cursorColor = Color(0xFF33A2FF),
- focusedTextColor = Color.White,
- unfocusedTextColor = Color.White.copy(alpha = 0.9f),
- disabledPlaceholderColor = Color.White.copy(alpha = 0.3f)
- )
- )
- AnimatedVisibility(visible = text.isNotBlank() && enabled) {
- IconButton(
- onClick = {
- haptic.performHapticFeedback(HapticFeedbackType.LongPress)
- onSendMessage()
- },
- modifier = Modifier.padding(start = 8.dp),
- colors = IconButtonDefaults.iconButtonColors(
- containerColor = Color(0xFF33A2FF),
- contentColor = Color.White
- )
- ) {
- Icon(
- Icons.AutoMirrored.Filled.Send,
- contentDescription = "Send message"
- )
- }
- }
- }
- }
-}
-
-@Composable
-private fun PreChatInputBar(
- text: String,
- onValueChange: (String) -> Unit,
- onSendMessage: (String) -> Unit,
- onImagePickerClicked: () -> Unit,
- enabled: Boolean,
- modifier: Modifier = Modifier,
-) {
- Surface(
- modifier = modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp),
- shape = RoundedCornerShape(24.dp),
- color = Color.White.copy(alpha = 0.05f),
- border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f))
- ) {
- Column(
- modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- TextField(
- value = text,
- onValueChange = onValueChange,
- modifier = Modifier.weight(1f),
- enabled = enabled,
- placeholder = {
- Text(stringResource(R.string.ask_airnote_ai), color = Color.White.copy(alpha = 0.6f))
- },
- leadingIcon = {
- Icon(Icons.Default.Search, contentDescription = null, tint = Color.White.copy(alpha = 0.6f))
- },
- colors = TextFieldDefaults.colors(
- focusedContainerColor = Color.Transparent,
- unfocusedContainerColor = Color.Transparent,
- disabledContainerColor = Color.Transparent,
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
- cursorColor = Color(0xFF33A2FF),
- focusedTextColor = Color.White,
- unfocusedTextColor = Color.White.copy(alpha = 0.9f),
- ),
- singleLine = true
- )
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
- ) {
- IconButton(onClick = onImagePickerClicked, enabled = enabled) {
- Icon(
- Icons.Default.AttachFile,
- contentDescription = "Attach File",
- tint = Color.White.copy(alpha = 0.8f)
- )
- }
-
- val searchString = stringResource(R.string.ai_search)
- val thinkString = stringResource(R.string.ai_think)
-
- Row(
- modifier = Modifier.weight(1f),
- horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
- ) {
- InputActionButton(
- text = searchString,
- icon = Icons.Rounded.AutoAwesome,
- onClick = { onValueChange("$searchString: ") }
- )
- InputActionButton(
- text = thinkString,
- icon = Icons.Default.Language,
- onClick = { onValueChange("$thinkString: ") }
- )
- }
-
- IconButton(
- onClick = { if (text.isNotBlank()) onSendMessage(text) },
- enabled = text.isNotBlank() && enabled,
- modifier = Modifier
- .size(40.dp)
- .clip(CircleShape),
- colors = IconButtonDefaults.iconButtonColors(
- containerColor = Color(0xFF33A2FF),
- contentColor = Color.White,
- disabledContainerColor = Color.White.copy(alpha = 0.1f),
- disabledContentColor = Color.White.copy(alpha = 0.4f)
- )
- ) {
- Icon(
- Icons.AutoMirrored.Filled.Send,
- contentDescription = "Send"
- )
- }
- }
- }
- }
-}
-
-@Composable
-private fun InputActionButton(text: String, icon: ImageVector, onClick: () -> Unit) {
- TextButton(
- onClick = onClick,
- shape = RoundedCornerShape(24.dp),
- colors = ButtonDefaults.textButtonColors(
- containerColor = Color.White.copy(alpha = 0.1f),
- contentColor = Color.White.copy(alpha = 0.9f)
- ),
- border = BorderStroke(1.dp, Color.White.copy(alpha = 0.2f)),
- contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
- ) {
- Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp))
- Spacer(Modifier.width(8.dp))
- Text(text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
- }
-}
-
-@Composable
-private fun AiDisclaimerText() {
- val uriHandler = LocalUriHandler.current
- val disclaimerText = stringResource(R.string.ai_disclaimer_text)
- val linkText = stringResource(R.string.ai_disclaimer_link_text)
- val linkUrl = stringResource(R.string.ai_disclaimer_link_url)
-
- val annotatedString = buildAnnotatedString {
- append(disclaimerText)
- pushStringAnnotation(tag = "URL", annotation = linkUrl)
- withStyle(
- style = SpanStyle(
- color = Color(0xFF33A2FF), // Link color
- textDecoration = TextDecoration.Underline
- )
- ) {
- append(linkText)
- }
- pop()
- }
-
- ClickableText(
- text = annotatedString,
- onClick = { offset ->
- annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset)
- .firstOrNull()?.let { annotation ->
- uriHandler.openUri(annotation.item)
- }
- },
- style = MaterialTheme.typography.bodySmall.copy(
- color = Color.White.copy(alpha = 0.6f),
- textAlign = TextAlign.Center
- ),
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp)
- .padding(bottom = 8.dp)
- )
-}
-
-@Composable
-private fun AskAiQuestionDialog(
- onDismiss: () -> Unit,
- onConfirm: (String) -> Unit
-) {
- var question by remember { mutableStateOf("") }
- val maxChars = 280
-
- Dialog(onDismissRequest = onDismiss) {
- Surface(
- shape = RoundedCornerShape(24.dp),
- color = Color(0xFF10141C),
- border = BorderStroke(1.dp, Color.White.copy(alpha = 0.2f))
- ) {
- Column(
- modifier = Modifier.padding(24.dp)
- ) {
- Text(
- text = stringResource(R.string.ask_a_question_title),
- style = MaterialTheme.typography.headlineSmall,
- color = Color.White,
- fontWeight = FontWeight.Bold
- )
- Spacer(modifier = Modifier.height(16.dp))
- TextField(
- value = question,
- onValueChange = { if (it.length <= maxChars) question = it },
- placeholder = { Text(stringResource(R.string.ask_a_question), color = Color.White.copy(alpha = 0.6f)) },
- modifier = Modifier
- .fillMaxWidth()
- .height(100.dp),
- colors = TextFieldDefaults.colors(
- focusedContainerColor = Color.White.copy(alpha = 0.05f),
- unfocusedContainerColor = Color.White.copy(alpha = 0.05f),
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
- cursorColor = Color(0xFF33A2FF),
- focusedTextColor = Color.White,
- unfocusedTextColor = Color.White.copy(alpha = 0.9f),
- ),
- shape = RoundedCornerShape(16.dp)
- )
- Text(
- text = "${question.length} / $maxChars",
- textAlign = TextAlign.End,
- style = MaterialTheme.typography.bodySmall,
- color = Color.White.copy(alpha = 0.6f),
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 8.dp)
- )
- Spacer(modifier = Modifier.height(24.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.End
- ) {
- TextButton(onClick = onDismiss) {
- Text(stringResource(R.string.cancel), color = Color.White.copy(alpha = 0.8f))
- }
- Spacer(modifier = Modifier.width(8.dp))
- Button(
- onClick = { onConfirm(question) },
- enabled = question.isNotBlank(),
- colors = ButtonDefaults.buttonColors(
- containerColor = Color(0xFF33A2FF),
- contentColor = Color(0xFF10141C),
- disabledContainerColor = Color.White.copy(alpha = 0.1f)
- ),
- shape = RoundedCornerShape(12.dp)
- ) {
- Text(stringResource(R.string.send))
- }
- }
- }
- }
- }
-}
-
-@Composable
-private fun CreateDraftDialog(
- onDismiss: () -> Unit,
- onConfirm: (String) -> Unit
-) {
- var topic by remember { mutableStateOf("") }
- val maxChars = 100
-
- Dialog(onDismissRequest = onDismiss) {
- Surface(
- shape = RoundedCornerShape(24.dp),
- color = Color(0xFF10141C),
- border = BorderStroke(1.dp, Color.White.copy(alpha = 0.2f))
- ) {
- Column(
- modifier = Modifier.padding(24.dp)
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(
- imageVector = Icons.Rounded.Edit,
- contentDescription = null,
- tint = Color.White.copy(alpha = 0.8f),
- modifier = Modifier.size(28.dp)
- )
- Spacer(modifier = Modifier.width(12.dp))
- Text(
- text = stringResource(R.string.new_ai_note_draft),
- style = MaterialTheme.typography.headlineSmall,
- color = Color.White,
- fontWeight = FontWeight.Bold
- )
- }
-
- Spacer(modifier = Modifier.height(16.dp))
- Text(
- text = stringResource(R.string.sample_question_request),
- style = MaterialTheme.typography.bodyMedium,
- color = Color.White.copy(alpha = 0.7f)
- )
- Spacer(modifier = Modifier.height(16.dp))
-
- TextField(
- value = topic,
- onValueChange = { if (it.length <= maxChars) topic = it },
- placeholder = { Text(stringResource(R.string.example_question), color = Color.White.copy(alpha = 0.6f)) },
- modifier = Modifier.fillMaxWidth(),
- singleLine = true,
- colors = TextFieldDefaults.colors(
- focusedContainerColor = Color.White.copy(alpha = 0.05f),
- unfocusedContainerColor = Color.White.copy(alpha = 0.05f),
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
- cursorColor = Color(0xFF33A2FF),
- focusedTextColor = Color.White,
- unfocusedTextColor = Color.White.copy(alpha = 0.9f),
- ),
- shape = RoundedCornerShape(16.dp)
- )
- Text(
- text = "${topic.length} / $maxChars",
- textAlign = TextAlign.End,
- style = MaterialTheme.typography.bodySmall,
- color = Color.White.copy(alpha = 0.6f),
- modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
- )
- Spacer(modifier = Modifier.height(24.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.End
- ) {
- TextButton(onClick = onDismiss) {
- Text(stringResource(R.string.cancel), color = Color.White.copy(alpha = 0.8f))
- }
- Spacer(modifier = Modifier.width(8.dp))
- Button(
- onClick = { onConfirm(topic) },
- enabled = topic.isNotBlank(),
- colors = ButtonDefaults.buttonColors(
- containerColor = Color(0xFF33A2FF),
- contentColor = Color(0xFF10141C),
- disabledContainerColor = Color.White.copy(alpha = 0.1f)
- ),
- shape = RoundedCornerShape(12.dp)
- ) {
- Text(stringResource(R.string.create))
- }
- }
- }
- }
- }
-}
-
-
-@Composable
-private fun DraftDisplay(draft: DraftedNote?, onSave: () -> Unit, onRegenerate: () -> Unit) {
- if (draft == null) return
-
- Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
- Column(
- modifier = Modifier.fillMaxSize()
- ) {
- Surface(
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(16.dp),
- color = Color.White.copy(alpha = 0.05f),
- border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f))
- ) {
- LazyColumn(
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 20.dp)
- ) {
- item {
- Text(
- text = draft.title,
- style = MaterialTheme.typography.titleLarge,
- color = Color.White,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(bottom = 12.dp)
- )
- }
- item {
- Text(
- text = draft.content,
- style = MaterialTheme.typography.bodyLarge,
- color = Color.White.copy(alpha = 0.8f),
- lineHeight = 24.sp
- )
- }
- }
- }
- Spacer(modifier = Modifier.height(16.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- OutlinedButton(
- onClick = onRegenerate,
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp),
- border = BorderStroke(1.dp, Color(0xFF33A2FF))
- ) {
- Icon(Icons.Rounded.AutoAwesome, contentDescription = null, tint = Color(0xFF33A2FF))
- Spacer(modifier = Modifier.width(8.dp))
- Text(stringResource(R.string.regenerate_note), color = Color(0xFF33A2FF))
- }
- Button(
- onClick = onSave,
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(12.dp),
- colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF33A2FF))
- ) {
- Text(stringResource(R.string.save_note), color = Color(0xFF10141C))
- }
- }
- }
- }
-}
-
-@Composable
-private fun TypingIndicator() {
- val transition = rememberInfiniteTransition(label = "typing_indicator_transition")
- val dotSize = 8.dp
- val dotSpacing = 12.dp
-
- @Composable
- fun Dot(offsetY: Float) {
- Box(
- modifier = Modifier
- .size(dotSize)
- .offset(y = offsetY.dp)
- .background(
- color = Color.White.copy(alpha = 0.5f),
- shape = CircleShape
- )
- )
- }
-
- val yOffset1 by transition.animateFloat(
- initialValue = 0f,
- targetValue = -10f,
- animationSpec = infiniteRepeatable(
- animation = tween(400),
- repeatMode = RepeatMode.Reverse
- ), label = "dot1_offset"
- )
- val yOffset2 by transition.animateFloat(
- initialValue = 0f,
- targetValue = -10f,
- animationSpec = infiniteRepeatable(
- animation = tween(400, delayMillis = 150),
- repeatMode = RepeatMode.Reverse
- ), label = "dot2_offset"
- )
- val yOffset3 by transition.animateFloat(
- initialValue = 0f,
- targetValue = -10f,
- animationSpec = infiniteRepeatable(
- animation = tween(400, delayMillis = 300),
- repeatMode = RepeatMode.Reverse
- ), label = "dot3_offset"
- )
-
- Row(
- modifier = Modifier.padding(vertical = 16.dp, horizontal = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(dotSpacing)
- ) {
- Dot(offsetY = yOffset1)
- Dot(offsetY = yOffset2)
- Dot(offsetY = yOffset3)
- }
-}
-
-@Composable
-private fun TerminalLoadingIndicator(topic: String) {
- var displayedText by remember { mutableStateOf("") }
- val fullText = """
-[INFO] Connecting to AirNote AI services...
-[INFO] Authentication successful.
-[PROCESS] Analyzing topic: "$topic"
-[PROCESS] Generating content blocks...
-[PROCESS] Assembling draft...
-[SUCCESS] Note draft created.
- """.trimIndent()
-
- LaunchedEffect(Unit) {
- fullText.lines().forEach { line ->
- for (char in line) {
- displayedText += char
- delay(10)
- }
- displayedText += "\n"
- delay(280)
- }
- }
-
- val infiniteTransition = rememberInfiniteTransition(label = "cursor_blink")
- val cursorAlpha by infiniteTransition.animateFloat(
- initialValue = 0f,
- targetValue = 1f,
- animationSpec = infiniteRepeatable(
- animation = tween(500),
- repeatMode = RepeatMode.Reverse
- ), label = "cursor_alpha"
- )
-
- Surface(
- shape = RoundedCornerShape(16.dp),
- color = Color.Black.copy(alpha = 0.5f),
- border = BorderStroke(1.dp, Color.White.copy(alpha = 0.2f))
- ) {
- Row(modifier = Modifier.padding(12.dp)) {
- Text(
- text = displayedText,
- color = Color.Green.copy(alpha = 0.8f),
- fontFamily = FontFamily.Monospace,
- fontSize = 14.sp,
- lineHeight = 20.sp
- )
- Text(
- text = "█",
- color = Color.Green.copy(alpha = cursorAlpha),
- fontFamily = FontFamily.Monospace,
- fontSize = 14.sp
- )
- }
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/desktop/DesktopHomeScreen.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/desktop/DesktopHomeScreen.kt
deleted file mode 100644
index 0aa2807..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/desktop/DesktopHomeScreen.kt
+++ /dev/null
@@ -1,823 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.home.desktop
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.LinearOutSlowInEasing
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.DriveFileMove
-import androidx.compose.material.icons.automirrored.rounded.LibraryBooks
-import androidx.compose.material.icons.automirrored.rounded.MenuOpen
-import androidx.compose.material.icons.automirrored.rounded.NoteAdd
-import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material.icons.rounded.Add
-import androidx.compose.material.icons.rounded.AllInbox
-import androidx.compose.material.icons.rounded.AutoAwesome
-import androidx.compose.material.icons.rounded.Code
-import androidx.compose.material.icons.rounded.Compress
-import androidx.compose.material.icons.rounded.Edit
-import androidx.compose.material.icons.rounded.Folder
-import androidx.compose.material.icons.rounded.FormatBold
-import androidx.compose.material.icons.rounded.FormatItalic
-import androidx.compose.material.icons.rounded.FormatListBulleted
-import androidx.compose.material.icons.rounded.FormatQuote
-import androidx.compose.material.icons.rounded.FormatUnderlined
-import androidx.compose.material.icons.rounded.HorizontalRule
-import androidx.compose.material.icons.rounded.KeyboardArrowDown
-import androidx.compose.material.icons.rounded.KeyboardArrowUp
-import androidx.compose.material.icons.rounded.Link
-import androidx.compose.material.icons.rounded.Menu
-import androidx.compose.material.icons.rounded.NoteAdd
-import androidx.compose.material.icons.rounded.Search
-import androidx.compose.material.icons.rounded.Settings
-import androidx.compose.material.icons.rounded.TheaterComedy
-import androidx.compose.material.icons.rounded.Title
-import androidx.compose.material3.Button
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.Divider
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.OutlinedTextFieldDefaults
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.material3.TextField
-import androidx.compose.material3.TextFieldDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.data.repository.AiAction
-import com.babelsoftware.airnote.data.repository.AiTone
-import com.babelsoftware.airnote.domain.model.Folder
-import com.babelsoftware.airnote.domain.model.Note
-import com.babelsoftware.airnote.presentation.screens.home.viewmodel.HomeViewModel
-import com.babelsoftware.airnote.presentation.screens.home.widgets.AddFolderDialog
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import kotlinx.coroutines.delay
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun DesktopHomeScreen(
- viewModel: HomeViewModel,
- settingsModel: SettingsViewModel,
- settings: com.babelsoftware.airnote.domain.model.Settings,
- onNoteClicked: (noteId: Int, isVault: Boolean, folderId: Long?) -> Unit,
- onSettingsClicked: () -> Unit,
-) {
- var showWelcomeAnimation by remember { mutableStateOf(true) }
-
- LaunchedEffect(Unit) {
- delay(3200L) // 3,2s
- showWelcomeAnimation = false
- }
-
- val notes by viewModel.displayedNotes.collectAsState()
- val selectedNote by viewModel.selectedNote.collectAsState()
- val folders by viewModel.allFolders.collectAsState()
- val selectedFolderId by viewModel.selectedFolderId.collectAsState()
- val searchQuery by viewModel.searchQuery.collectAsState()
- val noteForAction by viewModel.noteForAction
- val isAiChatSheetVisible by viewModel.isAiChatSheetVisible
- var isNavRailExpanded by remember { mutableStateOf(true) }
- val isAddFolderDialogVisible by viewModel.isAddFolderDialogVisible
-
- if (isAddFolderDialogVisible) {
- AddFolderDialog(
- onDismiss = { viewModel.setAddFolderDialogVisibility(false) },
- onConfirm = { name, color ->
- viewModel.addFolder(name, color)
- }
- )
- }
-
- if (isAiChatSheetVisible) {
- DesktopAiAssistantDialog(
- viewModel = viewModel,
- onDismissRequest = {
- viewModel.toggleAiChatSheet(false)
- viewModel.resetChatState()
- }
- )
- }
-
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.surfaceContainerLowest)
- ) {
- AnimatedVisibility(
- visible = !showWelcomeAnimation,
- enter = fadeIn(animationSpec = tween(durationMillis = 500, delayMillis = 200))
- ) {
- Row(
- modifier = Modifier
- .fillMaxSize()
- ) {
- DesktopNavRail(
- isExpanded = isNavRailExpanded,
- onToggleExpand = { isNavRailExpanded = !isNavRailExpanded },
- aiEnabled = settings.desktopModeAiEnabled,
- folders = folders,
- selectedFolderId = selectedFolderId,
- onFolderSelected = { folderId -> viewModel.selectFolder(folderId) },
- onAddNewNote = { viewModel.createNewNoteForDesktop() },
- onAddFolder = { viewModel.setAddFolderDialogVisibility(true) },
- onAskAi = { viewModel.toggleAiChatSheet(true) },
- onSettingsClicked = onSettingsClicked
- )
-
- NoteListPane(
- notes = notes,
- selectedNote = selectedNote,
- searchQuery = searchQuery,
- onQueryChange = { query -> viewModel.changeSearchQuery(query) },
- onNoteSelected = { note -> viewModel.selectNote(note) },
- onNewNoteClicked = { viewModel.createNewNoteForDesktop() },
- noteForAction = noteForAction,
- onNoteLongPressed = { note -> viewModel.onNoteLongPressed(note) },
- onDismissNoteAction = { viewModel.onDismissNoteAction() },
- onDeleteAction = { viewModel.deleteNoteAction() },
- onMoveAction = { viewModel.requestMoveNoteAction() },
- modifier = Modifier.weight(0.4f)
- )
-
- AnimatedContent(
- targetState = selectedNote,
- modifier = Modifier.weight(0.6f),
- transitionSpec = {
- (fadeIn(animationSpec = tween(300, easing = LinearOutSlowInEasing)) +
- slideInVertically(animationSpec = tween(300, easing = LinearOutSlowInEasing), initialOffsetY = { it / 8 }))
- .togetherWith(fadeOut(animationSpec = tween(300, easing = LinearOutSlowInEasing)))
- },
- label = "NoteDetailAnimation"
- ) { targetNote ->
- NoteDetailPane(
- note = targetNote,
- aiEnabled = settings.desktopModeAiEnabled,
- onAiAction = { action, tone -> viewModel.executeDesktopAiAction(action, tone) },
- onUpdateNote = { noteToUpdate, newName, newDescription ->
- viewModel.updateNoteDetails(noteToUpdate, newName, newDescription)
- }
- )
- }
- }
- }
- AnimatedVisibility(
- visible = showWelcomeAnimation,
- exit = fadeOut(animationSpec = tween(durationMillis = 500))
- ) {
- AnimatedWelcomeOverlay()
- }
- }
-}
-
-@Composable
-private fun DesktopNavRail(
- isExpanded: Boolean,
- onToggleExpand: () -> Unit,
- aiEnabled: Boolean,
- folders: List,
- selectedFolderId: Long?,
- onFolderSelected: (Long?) -> Unit,
- onAddNewNote: () -> Unit,
- onAddFolder: () -> Unit,
- onAskAi: () -> Unit,
- onSettingsClicked: () -> Unit
-) {
- val navRailBackground = Brush.verticalGradient(
- colors = listOf(
- MaterialTheme.colorScheme.surfaceContainer,
- MaterialTheme.colorScheme.surfaceContainerLow
- )
- )
-
- val animatedWidth by animateDpAsState(
- targetValue = if (isExpanded) 240.dp else 80.dp,
- label = "NavRailWidthAnimation",
- animationSpec = spring(stiffness = Spring.StiffnessMedium)
- )
-
- Surface(
- color = Color.Transparent,
- modifier = Modifier
- .fillMaxHeight()
- .width(animatedWidth)
- .background(navRailBackground)
- .padding(vertical = 10.dp, horizontal = 12.dp)
- ) {
- Column(horizontalAlignment = Alignment.Start) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(start = 4.dp, bottom = 16.dp)
- ) {
- IconButton(onClick = onToggleExpand) {
- Icon(
- imageVector = if (isExpanded) Icons.AutoMirrored.Rounded.MenuOpen else Icons.Rounded.Menu,
- contentDescription = "Toggle Navigation Rail",
- tint = MaterialTheme.colorScheme.onSurface
- )
- }
- AnimatedVisibility(visible = isExpanded) {
- Text(
- text = "AirNote DeX",
- style = MaterialTheme.typography.titleLarge,
- color = MaterialTheme.colorScheme.onSurface,
- modifier = Modifier.padding(start = 2.dp)
- )
- }
- }
-
- NavItem(isExpanded = isExpanded, text = stringResource(R.string.new_note), icon = Icons.Rounded.Edit, onClick = onAddNewNote)
- if (aiEnabled) {
- NavItem(isExpanded = isExpanded, text = stringResource(R.string.ai_button_texts), icon = Icons.Rounded.AutoAwesome, onClick = onAskAi)
- }
-
- Divider(modifier = Modifier.padding(vertical = 4.dp), color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
-
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- AnimatedVisibility(visible = isExpanded) {
- Text(
- text = stringResource(R.string.folders),
- style = MaterialTheme.typography.titleSmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- AnimatedVisibility(visible = isExpanded) {
- IconButton(onClick = onAddFolder, modifier = Modifier.size(24.dp)) {
- Icon(Icons.Rounded.Add, contentDescription = "Add New Folder", tint = MaterialTheme.colorScheme.onSurfaceVariant)
- }
- }
- }
-
- LazyColumn(
- modifier = Modifier.weight(1f),
- verticalArrangement = Arrangement.spacedBy(4.dp),
- contentPadding = PaddingValues(top = 8.dp)
- ) {
- item {
- NavItem(isExpanded = isExpanded, text = stringResource(R.string.all_notes), icon = Icons.Rounded.AllInbox, isSelected = selectedFolderId == null, onClick = { onFolderSelected(null) })
- }
- items(folders) { folder ->
- NavItem(isExpanded = isExpanded, text = folder.name, icon = Icons.Rounded.Folder, isSelected = selectedFolderId == folder.id, onClick = { onFolderSelected(folder.id) })
- }
- }
-
- NavItem(isExpanded = isExpanded, text = stringResource(R.string.screen_settings), icon = Icons.Rounded.Settings, onClick = onSettingsClicked)
- }
- }
-}
-
-@Composable
-private fun NavItem(
- isExpanded: Boolean,
- text: String,
- icon: ImageVector,
- isSelected: Boolean = false,
- onClick: () -> Unit
-) {
- val backgroundColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent
- val contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
-
- Surface(
- onClick = onClick,
- color = backgroundColor,
- shape = RoundedCornerShape(16.dp),
- modifier = Modifier
- .fillMaxWidth()
- .height(56.dp)
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(horizontal = 12.dp)
- ) {
- Icon(imageVector = icon, contentDescription = text, tint = contentColor)
- AnimatedVisibility(visible = isExpanded) {
- Row {
- Spacer(modifier = Modifier.width(16.dp))
- Text(text = text, color = contentColor, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium)
- }
- }
- }
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun NoteListPane(
- notes: List,
- selectedNote: Note?,
- searchQuery: String,
- onQueryChange: (String) -> Unit,
- onNoteSelected: (Note) -> Unit,
- onNewNoteClicked: () -> Unit,
- noteForAction: Note?,
- onNoteLongPressed: (Note) -> Unit,
- onDismissNoteAction: () -> Unit,
- onDeleteAction: () -> Unit,
- onMoveAction: () -> Unit,
- modifier: Modifier = Modifier
-) {
- Card(
- modifier = modifier
- .fillMaxHeight()
- .padding(top = 16.dp, bottom = 16.dp, start = 16.dp)
- .shadow(elevation = 8.dp, shape = RoundedCornerShape(topStart = 24.dp, bottomStart = 24.dp)),
- shape = RoundedCornerShape(topStart = 24.dp, bottomStart = 24.dp),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
- ) {
- Column {
- OutlinedTextField(
- value = searchQuery,
- onValueChange = onQueryChange,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- placeholder = { Text(stringResource(R.string.search_notes)) },
- leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = "Search") },
- shape = RoundedCornerShape(16.dp),
- singleLine = true,
- colors = OutlinedTextFieldDefaults.colors(
- focusedBorderColor = MaterialTheme.colorScheme.primary,
- unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
- )
- )
-
- if (notes.isEmpty() && searchQuery.isEmpty()) {
- EmptyNotesView(onNewNoteClicked = onNewNoteClicked)
- } else {
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp)
- ) {
- items(notes, key = { it.id }) { note ->
- val isSelected = selectedNote?.id == note.id
- Box {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- .shadow(elevation = if (isSelected) 4.dp else 0.dp, shape = RoundedCornerShape(16.dp))
- .combinedClickable(
- onClick = { onNoteSelected(note) },
- onLongClick = { onNoteLongPressed(note) }
- ),
- shape = RoundedCornerShape(16.dp),
- border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
- colors = if (isSelected) {
- CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
- } else {
- CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
- }
- ) {
- Column(Modifier.padding(16.dp)) {
- Text(text = note.name, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(6.dp))
- Text(
- text = note.description.take(120),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- maxLines = 3
- )
- }
- }
- DropdownMenu(
- expanded = noteForAction?.id == note.id,
- onDismissRequest = { onDismissNoteAction() }
- ) {
- DropdownMenuItem(
- text = { Text(stringResource(R.string.move_to_folder)) },
- onClick = { onMoveAction() },
- leadingIcon = { Icon(Icons.AutoMirrored.Filled.DriveFileMove, contentDescription = "Move to Folder") }
- )
- DropdownMenuItem(
- text = { Text(stringResource(R.string.delete), color = MaterialTheme.colorScheme.error) },
- onClick = { onDeleteAction() },
- leadingIcon = { Icon(Icons.Default.Delete, contentDescription = "Delete", tint = MaterialTheme.colorScheme.error) }
- )
- }
- }
- }
- }
- }
- }
- }
-}
-
-@Composable
-private fun NoteDetailPane(
- note: Note?,
- modifier: Modifier = Modifier,
- aiEnabled: Boolean,
- onAiAction: (AiAction, AiTone?) -> Unit,
- onUpdateNote: (noteToUpdate: Note, newName: String, newDescription: String) -> Unit
-) {
- var title by remember { mutableStateOf("") }
- var description by remember { mutableStateOf(TextFieldValue("")) }
- var isDescriptionFocused by remember { mutableStateOf(false) }
- var showToneMenu by remember { mutableStateOf(false) }
-
- LaunchedEffect(key1 = note) {
- if (note != null) {
- title = note.name
- description = TextFieldValue(note.description)
- } else {
- title = ""
- description = TextFieldValue("")
- }
- }
- LaunchedEffect(key1 = title, key2 = description) {
- if (note != null && (title != note.name || description.text != note.description)) {
- delay(750L)
- onUpdateNote(note, title, description.text)
- }
- }
-
- Box(
- modifier = modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp),
- contentAlignment = Alignment.TopCenter
- ) {
- Card(
- modifier = Modifier
- .widthIn(max = 800.dp)
- .fillMaxHeight()
- .shadow(elevation = 8.dp, shape = RoundedCornerShape(topEnd = 24.dp, bottomEnd = 24.dp)),
- shape = RoundedCornerShape(topEnd = 24.dp, bottomEnd = 24.dp),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer)
- ) {
- if (note != null) {
- Column(modifier = Modifier.fillMaxSize()) {
- TextField(
- value = title,
- onValueChange = { title = it },
- modifier = Modifier.fillMaxWidth(),
- textStyle = MaterialTheme.typography.headlineMedium.copy(
- color = MaterialTheme.colorScheme.onSurface
- ),
- placeholder = { Text(stringResource(R.string.title_dex)) },
- colors = TextFieldDefaults.colors(
- focusedContainerColor = Color.Transparent,
- unfocusedContainerColor = Color.Transparent,
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent
- )
- )
- Divider(modifier = Modifier.padding(horizontal = 16.dp))
- TextField(
- value = description,
- onValueChange = { description = it },
- modifier = Modifier
- .fillMaxWidth()
- .weight(1f)
- .onFocusChanged { focusState ->
- isDescriptionFocused = focusState.isFocused
- },
- textStyle = MaterialTheme.typography.bodyLarge.copy(
- color = MaterialTheme.colorScheme.onSurface
- ),
- placeholder = { Text(stringResource(R.string.write_your_note_here)) },
- colors = TextFieldDefaults.colors(
- focusedContainerColor = Color.Transparent,
- unfocusedContainerColor = Color.Transparent,
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent
- )
- )
-
- AnimatedVisibility(visible = isDescriptionFocused) {
- DesktopTextFormattingToolbar(
- currentValue = description,
- onValueChange = { newTextFieldValue ->
- description = newTextFieldValue
- }
- )
- }
-
- if (aiEnabled) {
- Surface(
- tonalElevation = 3.dp,
- modifier = Modifier.fillMaxWidth()
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp, vertical = 4.dp),
- horizontalArrangement = Arrangement.SpaceAround,
- verticalAlignment = Alignment.CenterVertically
- ) {
- AiActionButton(icon = Icons.Rounded.AutoAwesome, text = stringResource(R.string.ai_action_improve_writing)) {
- onAiAction(AiAction.IMPROVE_WRITING, null)
- }
- AiActionButton(icon = Icons.Rounded.Compress, text = stringResource(R.string.ai_action_summarize)) {
- onAiAction(AiAction.SUMMARIZE, null)
- }
- AiActionButton(icon = Icons.Rounded.KeyboardArrowDown, text = stringResource(R.string.ai_action_make_shorter)) {
- onAiAction(AiAction.MAKE_SHORTER, null)
- }
- AiActionButton(icon = Icons.Rounded.KeyboardArrowUp, text = stringResource(R.string.ai_action_make_longer)) {
- onAiAction(AiAction.MAKE_LONGER, null)
- }
- Box {
- AiActionButton(icon = Icons.Rounded.TheaterComedy, text = stringResource(R.string.ai_action_change_tone)) {
- showToneMenu = true
- }
- DropdownMenu(
- expanded = showToneMenu,
- onDismissRequest = { showToneMenu = false }
- ) {
- AiTone.values().forEach { tone ->
- DropdownMenuItem(
- text = { Text(tone.name.replaceFirstChar { it.titlecase() }) },
- onClick = {
- onAiAction(AiAction.CHANGE_TONE, tone)
- showToneMenu = false
- }
- )
- }
- }
- }
- }
- }
- }
- }
- } else {
- EmptySelectionView()
- }
- }
- }
-}
-
-@Composable
-private fun EmptyNotesView(onNewNoteClicked: () -> Unit) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.NoteAdd,
- contentDescription = "Empty Note Box",
- modifier = Modifier.size(80.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
- )
- Spacer(modifier = Modifier.height(24.dp))
- Text(
- text = stringResource(R.string.no_created_notes),
- style = MaterialTheme.typography.headlineSmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- Spacer(modifier = Modifier.height(32.dp))
- Button(onClick = onNewNoteClicked) {
- Icon(Icons.Rounded.Add, contentDescription = "New Note")
- Spacer(modifier = Modifier.width(8.dp))
- Text(stringResource(R.string.dex_create_new_note))
- }
- }
-}
-
-@Composable
-private fun EmptySelectionView() {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.LibraryBooks,
- contentDescription = "Note Not Selected",
- modifier = Modifier.size(80.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
- )
- Spacer(modifier = Modifier.height(24.dp))
- Text(
- text = stringResource(R.string.dex_start),
- style = MaterialTheme.typography.headlineSmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = stringResource(R.string.dex_note_preview),
- style = MaterialTheme.typography.bodyLarge,
- textAlign = TextAlign.Center,
- color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f)
- )
- }
-}
-
-@Composable
-private fun AiActionButton(icon: ImageVector, text: String, onClick: () -> Unit) {
- TextButton(onClick = onClick) {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Icon(imageVector = icon, contentDescription = text)
- Spacer(modifier = Modifier.height(2.dp))
- Text(text = text, style = MaterialTheme.typography.labelSmall)
- }
- }
-}
-
-@Composable
-private fun AnimatedWelcomeOverlay() {
- var iconScale by remember { mutableStateOf(0f) }
- var textAlpha by remember { mutableStateOf(0f) }
- var textOffsetY by remember { mutableStateOf(30f) }
-
- val animatedIconScale by animateFloatAsState(
- targetValue = iconScale,
- animationSpec = spring(
- dampingRatio = Spring.DampingRatioMediumBouncy,
- stiffness = 100f
- ), label = "icon_scale_anim"
- )
-
- val animatedTextAlpha by animateFloatAsState(
- targetValue = textAlpha,
- animationSpec = tween(durationMillis = 600, delayMillis = 200),
- label = "text_alpha_anim"
- )
-
- val animatedTextOffsetY by animateFloatAsState(
- targetValue = textOffsetY,
- animationSpec = tween(durationMillis = 600, delayMillis = 200),
- label = "text_offset_anim"
- )
-
- LaunchedEffect(Unit) {
- iconScale = 1f
- textAlpha = 1f
- textOffsetY = 0f
- }
-
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.surface),
- contentAlignment = Alignment.Center
- ) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Icon(
- painter = painterResource(id = R.drawable.ic_launcher_foreground),
- contentDescription = "AirNote Logo",
- modifier = Modifier
- .size(128.dp)
- .scale(animatedIconScale),
- tint = MaterialTheme.colorScheme.primary
- )
- Spacer(modifier = Modifier.height(16.dp))
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier
- .alpha(animatedTextAlpha)
- .offset(y = animatedTextOffsetY.dp)
- ) {
- Text(
- text = stringResource(R.string.welcome_to_airnote_dex),
- style = MaterialTheme.typography.headlineSmall,
- color = MaterialTheme.colorScheme.onSurface
- )
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = stringResource(R.string.airnote_dex_description),
- style = MaterialTheme.typography.bodyLarge,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
- }
-}
-
-@Composable
-private fun DesktopTextFormattingToolbar(
- currentValue: TextFieldValue,
- onValueChange: (TextFieldValue) -> Unit
-) {
- Surface(
- tonalElevation = 2.dp,
- modifier = Modifier.fillMaxWidth(),
- color = MaterialTheme.colorScheme.surfaceContainerHigh
- ) {
- LazyRow(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
- verticalAlignment = Alignment.CenterVertically
- ) {
- item { IconButton(onClick = { onValueChange(currentValue.applyMarkdown("**", "**")) }) { Icon(Icons.Rounded.FormatBold, "Thick") } }
- item { IconButton(onClick = { onValueChange(currentValue.applyMarkdown("*", "*")) }) { Icon(Icons.Rounded.FormatItalic, "Italic") } }
- item { IconButton(onClick = { onValueChange(currentValue.applyMarkdown("", "")) }) { Icon(Icons.Rounded.FormatUnderlined, "Underlined") } }
- item { IconButton(onClick = { onValueChange(currentValue.applyMarkdown("# ", isLinePrefix = true)) }) { Icon(Icons.Rounded.Title, "Title") } }
- item { IconButton(onClick = { onValueChange(currentValue.applyMarkdown("\n- ", "")) }) { Icon(Icons.Rounded.FormatListBulleted, "List") } }
- item { IconButton(onClick = { onValueChange(currentValue.applyMarkdown("> ", isLinePrefix = true)) }) { Icon(Icons.Rounded.FormatQuote, "Quote") } }
- item { IconButton(onClick = { onValueChange(currentValue.applyMarkdown("`", "`")) }) { Icon(Icons.Rounded.Code, "Code") } }
- item { IconButton(onClick = { onValueChange(currentValue.applyMarkdown("\n---\n", "")) }) { Icon(Icons.Rounded.HorizontalRule, "Bracket") } }
- item { IconButton(onClick = { onValueChange(currentValue.applyMarkdown("[", "]()")) }) { Icon(Icons.Rounded.Link, "Link") } }
- }
- }
-}
-
-private fun TextFieldValue.applyMarkdown(
- prefix: String,
- suffix: String = "",
- isLinePrefix: Boolean = false
-): TextFieldValue {
- val selection = this.selection
- val text = this.text
-
- if (isLinePrefix) {
- var lineStart = selection.start
- while (lineStart > 0 && text.getOrNull(lineStart - 1) != '\n') {
- lineStart--
- }
- val newText = text.substring(0, lineStart) + prefix + text.substring(lineStart)
- return this.copy(
- text = newText,
- selection = TextRange(selection.end + prefix.length)
- )
- } else if (selection.collapsed) {
- val newText = text.substring(0, selection.start) + prefix + suffix + text.substring(selection.end)
- return this.copy(
- text = newText,
- selection = TextRange(selection.start + prefix.length)
- )
- } else {
- val selectedText = text.substring(selection.min, selection.max)
- val newText = text.replaceRange(
- selection.min,
- selection.max,
- prefix + selectedText + suffix
- )
- return this.copy(
- text = newText,
- selection = TextRange(selection.end + prefix.length + suffix.length)
- )
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/viewmodel/HomeModel.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/viewmodel/HomeModel.kt
deleted file mode 100644
index 6f68b0d..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/viewmodel/HomeModel.kt
+++ /dev/null
@@ -1,1193 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.home.viewmodel
-
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.ImageDecoder
-import android.net.Uri
-import android.os.Build
-import android.provider.MediaStore
-import android.util.Log
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.AutoAwesome
-import androidx.compose.material.icons.rounded.Edit
-import androidx.compose.material.icons.rounded.ImageSearch
-import androidx.compose.material.icons.rounded.School
-import androidx.compose.material.icons.rounded.Search
-import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.res.stringResource
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.data.provider.StringProvider
-import com.babelsoftware.airnote.data.repository.AiAction
-import com.babelsoftware.airnote.data.repository.AiActionPlan
-import com.babelsoftware.airnote.data.repository.AiMode
-import com.babelsoftware.airnote.data.repository.AiTone
-import com.babelsoftware.airnote.data.repository.GeminiRepository
-import com.babelsoftware.airnote.data.repository.SecureStorageRepository
-import com.babelsoftware.airnote.domain.model.AiChatMessage
-import com.babelsoftware.airnote.domain.model.AiChatSession
-import com.babelsoftware.airnote.domain.model.AiSuggestion
-import com.babelsoftware.airnote.domain.model.ChatMessage
-import com.babelsoftware.airnote.domain.model.Folder
-import com.babelsoftware.airnote.domain.model.Note
-import com.babelsoftware.airnote.domain.model.Participant
-import com.babelsoftware.airnote.domain.repository.SettingsRepository
-import com.babelsoftware.airnote.domain.usecase.AiChatUseCase
-import com.babelsoftware.airnote.domain.usecase.FolderUseCase
-import com.babelsoftware.airnote.domain.usecase.NoteUseCase
-import com.babelsoftware.airnote.presentation.components.DecryptionResult
-import com.babelsoftware.airnote.presentation.components.EncryptionHelper
-import com.google.ai.client.generativeai.type.ImagePart
-import com.google.ai.client.generativeai.type.Part
-import com.google.ai.client.generativeai.type.TextPart
-import com.google.gson.Gson
-import com.google.gson.JsonSyntaxException
-import dagger.hilt.android.lifecycle.HiltViewModel
-import dagger.hilt.android.qualifiers.ApplicationContext
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-enum class AiService {
- GEMINI,
- PERPLEXITY
-}
-
-data class DraftedNote(
- val topic: String,
- val title: String,
- val content: String,
- val sourceImageUri: Uri? = null
-)
-
-data class ChatState(
- val messages: List = emptyList(),
- val isAwaitingDraftTopic: Boolean = false,
- val latestDraft: DraftedNote? = null,
- val hasStartedConversation: Boolean = false,
- val currentSessionId: Long? = null,
- val analyzingImageUri: Uri? = null,
- val pendingAttachmentUri: Uri? = null,
- val pendingAttachmentMimeType: String? = null
-)
-
-@HiltViewModel
-class HomeViewModel @Inject constructor(
- private val stringProvider: StringProvider,
- val encryptionHelper: EncryptionHelper,
- private val noteUseCase: NoteUseCase,
- private val folderUseCase: FolderUseCase,
- private val aiChatUseCase: AiChatUseCase,
- @ApplicationContext private val context: Context,
- private val geminiRepository: GeminiRepository,
- private val settingsRepository: SettingsRepository,
- private val secureStorageRepository: SecureStorageRepository
-) : ViewModel() {
- sealed class UiAction {
- object RequestImageForAnalysis : UiAction()
- object RequestFileForAnalysis : UiAction()
- object RequestAttachmentType: UiAction()
- }
- var selectedNotes = mutableStateListOf()
-
- private var _isDeleteMode = mutableStateOf(false)
- val isDeleteMode: State = _isDeleteMode
-
- private var _isPasswordPromptVisible = mutableStateOf(false)
- val isPasswordPromptVisible: State = _isPasswordPromptVisible
-
- private val _isVaultMode = MutableStateFlow(false)
- val isVaultMode: StateFlow = _isVaultMode.asStateFlow()
-
- private val _isAddFolderDialogVisible = mutableStateOf(false)
- val isAddFolderDialogVisible: State = _isAddFolderDialogVisible
-
- private val _isMoveToFolderDialogVisible = mutableStateOf(false)
- val isMoveToFolderDialogVisible: State = _isMoveToFolderDialogVisible
-
- private val _searchQuery = MutableStateFlow("")
- val searchQuery: StateFlow = _searchQuery.asStateFlow()
-
- private val _selectedFolderId = MutableStateFlow(null)
- val selectedFolderId: StateFlow = _selectedFolderId.asStateFlow()
-
- private val _folderForAction = mutableStateOf(null)
- val folderForAction: State = _folderForAction
-
- private val _showDeleteConfirmDialog = mutableStateOf(false)
- val showDeleteConfirmDialog: State = _showDeleteConfirmDialog
-
- private val _folderToEdit = mutableStateOf(null)
- val folderToEdit: State = _folderToEdit
-
- private val _noteForAction = mutableStateOf(null)
- val noteForAction: State = _noteForAction
-
- private val _uiEvent = Channel()
- val uiEvent = _uiEvent.receiveAsFlow()
-
- private suspend fun getApiKeyToUse(): String {
- return secureStorageRepository.getUserApiKey() ?: ""
- }
-
- private val _selectedNote = MutableStateFlow(null)
- val selectedNote: StateFlow = _selectedNote.asStateFlow()
-
- fun selectNote(note: Note?) {
- _selectedNote.value = note
- }
-
- // --- Global AI Chat States ---
- private val _isAiChatSheetVisible = mutableStateOf(false)
- val isAiChatSheetVisible: State = _isAiChatSheetVisible
-
- private val _isFabExtended = mutableStateOf(true)
- val isFabExtended: State = _isFabExtended
-
- private val _chatState = MutableStateFlow(ChatState())
- val chatState: StateFlow = _chatState.asStateFlow()
-
- private val _uiActionChannel = Channel()
- val uiActionChannel = _uiActionChannel.receiveAsFlow()
-
- private val _aiMode = MutableStateFlow(AiMode.NOTE_ASSISTANT)
- val aiMode: StateFlow = _aiMode.asStateFlow()
-
- private val _selectedAiService = MutableStateFlow(AiService.GEMINI)
- val selectedAiService: StateFlow = _selectedAiService.asStateFlow()
-
- val allChatSessions: StateFlow> = aiChatUseCase.getAllSessions()
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
- private val _showAiHistoryScreen = mutableStateOf(false)
- val showAiHistoryScreen: State = _showAiHistoryScreen
-
- private val _showAskQuestionDialog = mutableStateOf(false)
- val showAskQuestionDialog: State = _showAskQuestionDialog
-
- private val _showCreateDraftDialog = mutableStateOf(false)
- val showCreateDraftDialog: State = _showCreateDraftDialog
-
- val allNotesForAi: StateFlow> = noteUseCase.getAllNotes()
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
-
- /**
- * Finds the note mentioned in the message text.
- * Searches for an expression starting with “@” that matches the name of a note in the note list.
- * Sorts the notes in reverse order to check the longest-named notes first.
- */
- private fun findMentionedNote(message: String, allNotes: List): Note? {
- val sortedNotes = allNotes.sortedByDescending { it.name.length }
-
- for (note in sortedNotes) {
- val noteNamePattern = Regex.escape(note.name)
- val noteMentionRegex = Regex(
- pattern = "@$noteNamePattern(\\b|$)",
- option = RegexOption.IGNORE_CASE
- )
-
- if (noteMentionRegex.containsMatchIn(message)) {
- return note
- }
- }
-
- return null
- }
-
- fun onAskQuestionClicked() {
- _showAskQuestionDialog.value = true
- }
-
- fun onDismissQuestionDialog() {
- _showAskQuestionDialog.value = false
- }
-
- fun onDraftAnythingClicked() {
- _showCreateDraftDialog.value = true
- }
-
- fun onDismissCreateDraftDialog() {
- _showCreateDraftDialog.value = false
- }
-
- fun selectAiService(service: AiService) {
- _selectedAiService.value = service
- resetChatState()
- }
-
- val suggestions: StateFlow> =
- _selectedAiService.map { service ->
- if (service == AiService.PERPLEXITY) {
- listOf(
- AiSuggestion(
- title = stringProvider.getString(R.string.ask_ai),
- icon = Icons.Rounded.Search,
- action = { onAskQuestionClicked() }
- ),
- AiSuggestion(
- title = stringProvider.getString(R.string.ai_mode_academic_research),
- icon = Icons.Rounded.School,
- action = {
- sendMessage("What's the latest news?")
- }
- )
- )
- } else {
- listOf(
- AiSuggestion(
- title = stringProvider.getString(R.string.ask_ai),
- icon = Icons.Rounded.Search,
- action = { onAskQuestionClicked() }
- ),
- AiSuggestion(
- title = stringProvider.getString(R.string.make_note),
- icon = Icons.Rounded.Edit,
- action = { onDraftAnythingClicked() }
- ),
- AiSuggestion(
- title = stringProvider.getString(R.string.generate_ideas),
- icon = Icons.Rounded.AutoAwesome,
- action = {
- setAiMode(AiMode.CREATIVE_MIND)
- sendMessage(stringProvider.getString(R.string.generate_ideas_prompt))
- }
- ),
- AiSuggestion(
- title = stringProvider.getString(R.string.create_note_from_object),
- icon = Icons.Rounded.ImageSearch,
- action = { requestImageForAnalysis() },
- )
- )
- }
- }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
-
- init {
- _chatState.asStateFlow().flatMapLatest { state: ChatState ->
- state.currentSessionId?.let { sessionId: Long ->
- aiChatUseCase.getMessagesForSession(sessionId)
- } ?: flowOf(emptyList())
- }.onEach { dbMessages: List ->
- val chatMessages = dbMessages.map { dbMessage: AiChatMessage ->
- ChatMessage(dbMessage.text, dbMessage.participant, dbMessage.isLoading)
- }
- if (_chatState.value.latestDraft == null) {
- _chatState.value = _chatState.value.copy(messages = chatMessages)
- }
- }.launchIn(viewModelScope)
-
-
- noteUseCase.observe()
- viewModelScope.launch {
- val settings = settingsRepository.settings.first()
- if (settings.openToLastUsedFolder && settings.lastUsedFolderId != null) {
- _selectedFolderId.value = settings.lastUsedFolderId
- settingsRepository.update(settings.copy(lastUsedFolderId = null))
- }
- }
- }
-
- // --- Global AI Chat Functions ---
- fun setAiMode(mode: AiMode) {
- _aiMode.value = mode
- viewModelScope.launch {
- _chatState.value.currentSessionId?.let {
- aiChatUseCase.updateSessionMode(it, mode)
- }
- }
- }
-
- fun toggleAiHistoryScreen(show: Boolean) {
- _showAiHistoryScreen.value = show
- }
-
- fun startNewChat() {
- resetChatState()
- toggleAiHistoryScreen(false)
- }
-
- fun loadChatSession(session: AiChatSession) {
- viewModelScope.launch {
- _chatState.value = ChatState(
- currentSessionId = session.id,
- hasStartedConversation = true
- )
- setAiMode(AiMode.valueOf(session.aiMode))
- selectAiService(AiService.valueOf(session.serviceName))
- toggleAiHistoryScreen(false)
- }
- }
-
- fun deleteChatSession(sessionId: Long) {
- viewModelScope.launch {
- aiChatUseCase.deleteSessionById(sessionId)
- if (_chatState.value.currentSessionId == sessionId) {
- resetChatState()
- }
- }
- }
-
- fun toggleAiChatSheet(isVisible: Boolean) {
- _isAiChatSheetVisible.value = isVisible
- if (!isVisible) {
- resetChatState()
- toggleAiHistoryScreen(false)
- }
- }
-
- fun setFabExtended(isExtended: Boolean) {
- _isFabExtended.value = isExtended
- }
-
- fun onAttachmentIconClicked() {
- viewModelScope.launch {
- _uiActionChannel.send(UiAction.RequestAttachmentType)
- }
- }
-
- fun onAttachmentSelected(uri: Uri, mimeType: String?) {
- _chatState.value = _chatState.value.copy(
- pendingAttachmentUri = uri,
- pendingAttachmentMimeType = mimeType
- )
- }
-
- fun onAttachmentRemoved() {
- _chatState.value = _chatState.value.copy(
- pendingAttachmentUri = null,
- pendingAttachmentMimeType = null
- )
- }
-
- private val createNoteTriggers = setOf(
- stringProvider.getString(R.string.analyze_create_note),
- stringProvider.getString(R.string.analyze_analyze),
- stringProvider.getString(R.string.analyze_take_notes),
- stringProvider.getString(R.string.analyze_summarize_and_take_notes),
- stringProvider.getString(R.string.analyze_prepare_a_draft),
- stringProvider.getString(R.string.analyze_analyze_my_file_image))
-
- fun sendMessage(userMessage: String) = viewModelScope.launch {
- if (_selectedAiService.value == AiService.PERPLEXITY) {
- sendPerplexityChat(userMessage)
- return@launch
- }
-
- val currentState = _chatState.value
- val attachmentUri = currentState.pendingAttachmentUri
- val attachmentMime = currentState.pendingAttachmentMimeType
-
- if (attachmentUri != null && attachmentMime != null) {
- val isCreateNoteRequest = createNoteTriggers.any { userMessage.contains(it, ignoreCase = true) }
-
- if (isCreateNoteRequest) {
- analyzeFileAndCreateDraft(userMessage, attachmentUri, attachmentMime)
- } else {
- sendChatWithAttachment(userMessage, attachmentUri, attachmentMime)
- }
- } else {
- sendChatOnly(userMessage)
- }
- }
-
- private fun sendPerplexityChat(userMessage: String) = viewModelScope.launch {
- var sessionId = _chatState.value.currentSessionId
- if (sessionId == null) {
- val newSessionId = aiChatUseCase.startNewSession(
- title = userMessage.take(40),
- aiMode = _aiMode.value,
- serviceName = _selectedAiService.value.name
- )
- _chatState.value = _chatState.value.copy(currentSessionId = newSessionId, hasStartedConversation = true)
- sessionId = newSessionId
- }
-
- val userChatMessage = ChatMessage(userMessage, Participant.USER)
- aiChatUseCase.addMessageToSession(sessionId, userChatMessage)
- val loadingMessage = ChatMessage("", Participant.MODEL, isLoading = true)
- val loadingMessageId = aiChatUseCase.addMessageToSession(sessionId, loadingMessage)
- val historyForApi = (_chatState.value.messages + userChatMessage).filter { !it.isLoading }
-
- val perplexityApiKey = secureStorageRepository.getPerplexityApiKey() ?: ""
- if (perplexityApiKey.isBlank()) {
- aiChatUseCase.updateMessageById(loadingMessageId, "Perplexity API anahtarı ayarlanmamış.", false, true)
- return@launch
- }
-
- val responseBuilder = StringBuilder()
- try {
- geminiRepository.generatePerplexityChatResponse(historyForApi, perplexityApiKey)
- .collect { chunk ->
- responseBuilder.append(chunk)
- aiChatUseCase.updateMessageById(loadingMessageId, responseBuilder.toString(), true, false)
- }
- } catch (e: Exception) {
- Log.e("HomeViewModel_PplxChat", "Akış hatası: ${e.message}", e)
- aiChatUseCase.updateMessageById(loadingMessageId, e.message ?: "Bir hata oluştu", false, true)
- } finally {
- val fullResponse = responseBuilder.toString().trim()
- if (fullResponse.isNotBlank()) {
- aiChatUseCase.updateMessageById(loadingMessageId, fullResponse, false, false)
- } else {
- val currentMessages = aiChatUseCase.getMessagesForSession(sessionId).first()
- val msg = currentMessages.find { it.id == loadingMessageId }
- if (msg != null && !msg.text.isNotBlank() && msg.participant != Participant.ERROR) {
- aiChatUseCase.deleteMessageById(loadingMessageId)
- }
- }
- }
- }
-
- private val _lastActionContext = mutableStateOf(null)
- private val gson = Gson()
- private fun sendChatOnly(userMessage: String) = viewModelScope.launch {
- var sessionId = _chatState.value.currentSessionId
- if (sessionId == null) {
- val newSessionId = aiChatUseCase.startNewSession(
- title = userMessage.take(40),
- aiMode = _aiMode.value,
- serviceName = _selectedAiService.value.name
- )
- _chatState.value = _chatState.value.copy(currentSessionId = newSessionId, hasStartedConversation = true)
- sessionId = newSessionId
- }
-
- val userChatMessage = ChatMessage(userMessage, Participant.USER)
- aiChatUseCase.addMessageToSession(sessionId, userChatMessage)
- val loadingMessage = ChatMessage("", Participant.MODEL, isLoading = true)
- val loadingMessageId = aiChatUseCase.addMessageToSession(sessionId, loadingMessage)
- val historyForApi = (_chatState.value.messages + userChatMessage).filter { !it.isLoading }
- val allNotes = allNotesForAi.first()
- val contextFromMemory = _lastActionContext.value
- val contextFromMention = findMentionedNote(userMessage, allNotes)
- val mentionedNote = contextFromMemory ?: contextFromMention
-
- if (contextFromMemory != null) {
- _lastActionContext.value = null
- }
-
- try {
- val jsonResult = geminiRepository.generateActionPlan(
- userRequest = userMessage,
- chatHistory = historyForApi,
- apiKey = getApiKeyToUse(),
- aiMode = _aiMode.value,
- mentionedNote = mentionedNote
- )
-
- if (jsonResult.isSuccess) {
- val jsonString = jsonResult.getOrThrow()
- val plan = gson.fromJson(jsonString, AiActionPlan::class.java)
- var finalResponseMessage = plan.response_message
-
- for (action in plan.actions) {
- val targetNoteForModification = mentionedNote ?: allNotes.find {
- it.name.equals(action.note_title, ignoreCase = true)
- }
-
- when (action.action_type) {
-
- "CREATE_NOTE" -> {
- val title = action.title ?: stringProvider.getString(R.string.title_dex)
- val newNote = Note(
- name = title,
- description = action.content ?: "",
- folderId = selectedFolderId.value,
- encrypted = isVaultMode.value
- )
- noteUseCase.addNote(newNote)
- val createdNote = noteUseCase.getAllNotes().first().lastOrNull { it.name == title }
- _lastActionContext.value = createdNote
- }
-
- "CREATE_TODO_NOTE" -> {
- val todoContent = action.tasks?.joinToString("\n") { "[ ] $it" } ?: ""
- val title = action.title ?: stringProvider.getString(R.string.ai_command_todo)
- val newTodoNote = Note(
- name = title,
- description = todoContent,
- folderId = selectedFolderId.value,
- encrypted = isVaultMode.value
- )
- noteUseCase.addNote(newTodoNote)
-
- val createdTodoNote = noteUseCase.getAllNotes().first().lastOrNull { it.name == title }
- _lastActionContext.value = createdTodoNote
- }
-
- "CREATE_FOLDER" -> {
- val folderName = action.name ?: "New Folder"
- folderUseCase.addFolder(
- Folder(name = folderName, iconName = action.iconName ?: "Folder")
- )
- }
-
- "MOVE_NOTE_TO_FOLDER" -> {
- val targetFolder = folderUseCase.getAllFolders().first()
- .find { it.name.equals(action.folder_name, ignoreCase = true) }
-
- if (targetNoteForModification != null && targetFolder != null) {
- val movedNote = targetNoteForModification.copy(folderId = targetFolder.id)
- noteUseCase.addNote(movedNote)
- _lastActionContext.value = movedNote
- } else {
- Log.w("HomeViewModel", "Move action failed: Note or Folder not found.")
- finalResponseMessage = stringProvider.getString(R.string.ai_error_modification_failed)
- }
- }
-
- "APPEND_TO_NOTE" -> {
- val contentToAppend = action.content ?: ""
- if (targetNoteForModification != null) {
- val updatedNote = targetNoteForModification.copy(
- description = targetNoteForModification.description + "\n" + contentToAppend
- )
- noteUseCase.addNote(updatedNote)
- _lastActionContext.value = updatedNote
- } else {
- Log.w("HomeViewModel", "Append action failed: Note not found.")
- finalResponseMessage = stringProvider.getString(R.string.ai_error_modification_failed)
- }
- }
-
- "REWRITE_NOTE" -> {
- val newContent = action.new_content ?: ""
- if (targetNoteForModification != null) {
- val updatedNote = targetNoteForModification.copy(description = newContent)
- noteUseCase.addNote(updatedNote)
- _lastActionContext.value = updatedNote
- } else {
- Log.w("HomeViewModel", "Rewrite action failed: Note not found.")
- finalResponseMessage = stringProvider.getString(R.string.ai_error_modification_failed)
- }
- }
-
- "DELETE_NOTE" -> {
- if (targetNoteForModification != null) {
- noteUseCase.deleteNoteById(targetNoteForModification.id)
- _lastActionContext.value = null
- } else {
- Log.w("HomeViewModel", "Delete action failed: Note not found.")
- finalResponseMessage = stringProvider.getString(R.string.ai_error_modification_failed)
- }
- }
-
- "QUERY_NOTES" -> {
- val allFolders = folderUseCase.getAllFolders().first()
- var notesToSearch = allNotes
-
- if (!action.folder_name.isNullOrBlank()) {
- val targetFolder = allFolders.find { it.name.equals(action.folder_name, ignoreCase = true) }
- if (targetFolder != null) {
- notesToSearch = notesToSearch.filter { it.folderId == targetFolder.id }
- } else {
- finalResponseMessage = stringProvider.getString(R.string.folder_not_found)
- break
- }
- }
-
- val searchTerm = action.search_term
- val searchResults = if (!searchTerm.isNullOrBlank()) {
- notesToSearch.filter {
- it.name.contains(searchTerm, ignoreCase = true) ||
- it.description.contains(searchTerm, ignoreCase = true)
- }
- } else {
- notesToSearch
- }
-
- if (searchResults.isEmpty()) {
- finalResponseMessage = stringProvider.getString(R.string.not_found_note)
- } else {
- val resultsText = searchResults.joinToString("\n") { "- ${it.name}" }
- finalResponseMessage = stringProvider.getString(R.string.found_note, resultsText)
- }
- }
-
- "CHAT" -> {
- if (action.response != null) {
- finalResponseMessage = action.response
- }
- _lastActionContext.value = null
- }
-
- else -> {
- Log.w("HomeViewModel", "Unknown AI action type: ${action.action_type}")
- _lastActionContext.value = null
- }
- }
- }
- aiChatUseCase.updateMessageById(loadingMessageId, finalResponseMessage, false)
- } else {
- _lastActionContext.value = null
- val errorMessage = jsonResult.exceptionOrNull()?.message
- ?: stringProvider.getString(R.string.error_unknown)
- aiChatUseCase.updateMessageById(loadingMessageId, errorMessage, false, true)
- }
-
- } catch (e: JsonSyntaxException) {
- _lastActionContext.value = null
- Log.e("HomeViewModel", "JSON Syntax Error: ${e.message}")
- aiChatUseCase.updateMessageById(loadingMessageId, stringProvider.getString(R.string.ai_error_invalid_json), false, true)
-
- } catch (e: Exception) {
- _lastActionContext.value = null
- Log.e("HomeViewModel", "Error in sendChatOnly: ${e.message}", e)
- val errorMessage = e.message ?: stringProvider.getString(R.string.error_unknown)
- aiChatUseCase.updateMessageById(loadingMessageId, errorMessage, false, true)
- }
- }
-
- private fun sendChatWithAttachment(userMessage: String, uri: Uri, mimeType: String) = viewModelScope.launch {
- val currentState = _chatState.value
- val apiKey = getApiKeyToUse()
-
- val attachmentPart: Part? = try {
- if (mimeType.startsWith("image/")) {
- val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri))
- } else {
- @Suppress("DEPRECATION")
- MediaStore.Images.Media.getBitmap(context.contentResolver, uri)
- }
- ImagePart(bitmap)
- } else if (mimeType == "text/plain") {
- val textContent = readTextFromUri(uri)
- if (textContent.isBlank()) throw Exception(stringProvider.getString(R.string.file_empty_or_not_read))
- TextPart(textContent)
- } else {
- throw IllegalArgumentException("Unsupported file type: $mimeType")
- }
- } catch (e: Exception) {
- onAttachmentRemoved()
- sendChatOnly(userMessage)
- return@launch
- }
-
- if (attachmentPart == null) {
- onAttachmentRemoved()
- sendChatOnly(userMessage)
- return@launch
- }
-
- var sessionId = currentState.currentSessionId
- if (sessionId == null) {
- val newSessionId = aiChatUseCase.startNewSession(
- title = userMessage.take(40),
- aiMode = _aiMode.value,
- serviceName = _selectedAiService.value.name
- )
- _chatState.value = currentState.copy(currentSessionId = newSessionId, hasStartedConversation = true)
- sessionId = newSessionId
- }
-
- val userChatMessage = ChatMessage(userMessage, Participant.USER)
- aiChatUseCase.addMessageToSession(sessionId, userChatMessage)
- onAttachmentRemoved()
-
- val loadingMessage = ChatMessage("", Participant.MODEL, isLoading = true)
- val loadingMessageId = aiChatUseCase.addMessageToSession(sessionId, loadingMessage)
-
- val historyForApi = (currentState.messages + userChatMessage).filter { !it.isLoading }
- val responseBuilder = StringBuilder()
-
- try {
- geminiRepository.generateChatResponse(
- historyForApi,
- apiKey,
- _aiMode.value,
- null,
- attachmentPart
- )
- .collect { chunk ->
- responseBuilder.append(chunk)
- aiChatUseCase.updateMessageById(loadingMessageId, responseBuilder.toString(), true, false)
- }
- } catch (e: Exception) {
- Log.e("HomeViewModel_GeminiChat", "Akış hatası: ${e.message}", e)
- aiChatUseCase.updateMessageById(loadingMessageId, e.message ?: "Bir hata oluştu", false, true)
- } finally {
- val fullResponse = responseBuilder.toString().trim()
- if (fullResponse.isNotBlank()) {
- aiChatUseCase.updateMessageById(loadingMessageId, fullResponse, false, false)
- } else {
- val currentMessages = aiChatUseCase.getMessagesForSession(sessionId).first()
- val msg = currentMessages.find { it.id == loadingMessageId }
- if (msg != null && !msg.text.isNotBlank() && msg.participant != Participant.ERROR) {
- aiChatUseCase.deleteMessageById(loadingMessageId)
- }
- }
- }
- }
-
- fun generateDraft(topic: String) = viewModelScope.launch {
- resetChatState()
- setAiMode(AiMode.NOTE_ASSISTANT)
-
- val sessionId = aiChatUseCase.startNewSession(
- title = topic.take(40),
- aiMode = _aiMode.value,
- serviceName = _selectedAiService.value.name
- )
-
- _chatState.value = _chatState.value.copy(currentSessionId = sessionId, hasStartedConversation = true)
-
- val userMessage = ChatMessage(text = topic, participant = Participant.USER)
- aiChatUseCase.addMessageToSession(sessionId, userMessage)
-
- val loadingMessage = ChatMessage("// Generating Note...", Participant.MODEL, isLoading = true)
- val loadingMessageId = aiChatUseCase.addMessageToSession(sessionId, loadingMessage)
-
- geminiRepository.generateDraft(topic, getApiKeyToUse(), _aiMode.value)
- .onSuccess { result ->
- val (title, content) = parseDraft(result)
- _chatState.value = _chatState.value.copy(
- latestDraft = DraftedNote(
- topic = topic,
- title = title,
- content = content,
- sourceImageUri = null
- )
- )
- val confirmationMessage = "Harika! '$title' üzerine bir not taslağı hazırladım."
- aiChatUseCase.updateMessageById(loadingMessageId, confirmationMessage, false)
- }
- .onFailure { exception ->
- val errorMessage = exception.message ?: stringProvider.getString(R.string.error_message_image_analysis_failed)
- aiChatUseCase.updateMessageById(loadingMessageId, errorMessage, false, isError = true)
- _chatState.value = _chatState.value.copy(latestDraft = null)
- }
- }
-
- private fun parseDraft(result: String): Pair {
- val titleRegex = """(TITLE:|\*\*TITLE:\*\*)\s*(.*)""".toRegex(RegexOption.IGNORE_CASE)
- val contentRegex = """(CONTENT:|\*\*CONTENT:\*\*)\s*(.*)""".toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL))
-
- val titleMatch = titleRegex.find(result)
- val title = titleMatch?.groupValues?.get(2)?.trim()?.replace(Regex("[*#]"), "") ?: "Generated Note"
-
- val contentMatch = contentRegex.find(result)
- val content = contentMatch?.groupValues?.get(2)?.trim()?.replace(Regex("[*#]"), "") ?: ""
-
- return Pair(title, content)
- }
-
- private fun parseAndSetDraft(result: String, topic: String, imageUri: Uri?) {
- val (title, content) = parseDraft(result)
- _chatState.value = _chatState.value.copy(
- messages = emptyList(),
- latestDraft = DraftedNote(
- topic = topic,
- title = title,
- content = content,
- sourceImageUri = imageUri
- ),
- analyzingImageUri = null
- )
- }
-
- private fun handleFailure(exception: Throwable) {
- val finalMessages = listOf(
- ChatMessage(
- text = exception.message ?: stringProvider.getString(R.string.error_message_image_analysis_failed),
- participant = Participant.ERROR
- )
- )
- _chatState.value = _chatState.value.copy(
- messages = finalMessages,
- latestDraft = null,
- analyzingImageUri = null
- )
- }
-
- private suspend fun getOrCreateDreamFolderId(): Long? {
- val dreamFolderName = stringProvider.getString(R.string.dream_journal)
- val foldersFlow = folderUseCase.getAllFolders()
- var folders = foldersFlow.first()
- var dreamFolder = folders.find { it.name.equals(dreamFolderName, ignoreCase = true) }
-
- if (dreamFolder != null) {
- return dreamFolder.id
- }
-
- val newFolder = Folder(name = dreamFolderName, iconName = "Book")
- try {
- folderUseCase.addFolder(newFolder)
- folders = foldersFlow.first()
- dreamFolder = folders.find { it.name.equals(dreamFolderName, ignoreCase = true) }
- return dreamFolder?.id
- } catch (e: Exception) {
- e.printStackTrace()
- folders = foldersFlow.first()
- dreamFolder = folders.find { it.name.equals(dreamFolderName, ignoreCase = true) }
- return dreamFolder?.id
- }
- }
-
- fun onDreamJournalClicked(onNoteClicked: (noteId: Int, isVault: Boolean, folderId: Long?) -> Unit) {
- viewModelScope.launch {
- val folderId = getOrCreateDreamFolderId()
- onNoteClicked(0, _isVaultMode.value, folderId)
- }
- }
-
-
- fun saveDraftedNote() = viewModelScope.launch {
- val draft = _chatState.value.latestDraft ?: return@launch
- noteUseCase.addNote(
- Note(
- name = draft.title,
- description = draft.content,
- folderId = selectedFolderId.value,
- encrypted = isVaultMode.value
- )
- )
- resetChatState()
- toggleAiChatSheet(false)
- }
-
- fun requestImageForAnalysis() {
- viewModelScope.launch {
- _uiActionChannel.send(UiAction.RequestImageForAnalysis)
- }
- }
-
- fun analyzeFileAndCreateDraft(prompt: String, uri: Uri, mimeType: String) = viewModelScope.launch {
- _chatState.value = _chatState.value.copy(
- latestDraft = null,
- messages = listOf(ChatMessage(text = "", participant = Participant.MODEL, isLoading = true)),
- hasStartedConversation = true,
- analyzingImageUri = if (mimeType.startsWith("image/")) uri else null,
- pendingAttachmentUri = null,
- pendingAttachmentMimeType = null
- )
-
- val apiKey = getApiKeyToUse()
-
- val attachmentPart: Part? = try {
- if (mimeType.startsWith("image/")) {
- val bitmap: Bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri))
- } else {
- @Suppress("DEPRECATION")
- MediaStore.Images.Media.getBitmap(context.contentResolver, uri)
- }
- ImagePart(bitmap)
- } else if (mimeType == "text/plain") {
- val textContent = readTextFromUri(uri)
- if (textContent.isBlank()) throw Exception(stringProvider.getString(R.string.file_empty_or_not_read))
- TextPart(textContent)
- } else {
- throw IllegalArgumentException("Desteklenmeyen dosya türü: $mimeType")
- }
- } catch (e: Exception) {
- handleFailure(e)
- null
- }
-
- if (attachmentPart == null) return@launch
-
- geminiRepository.generateDraftFromAttachment(prompt, attachmentPart, apiKey, _aiMode.value)
- .onSuccess { result ->
- parseAndSetDraft(result, prompt, if (mimeType.startsWith("image/")) uri else null)
- }
- .onFailure { exception ->
- handleFailure(exception)
- }
- }
-
-
- fun regenerateDraft() {
- val draft = _chatState.value.latestDraft ?: return
- val sourceImageUri = draft.sourceImageUri
- _chatState.value = _chatState.value.copy(latestDraft = null)
-
- if (sourceImageUri != null) {
- analyzeFileAndCreateDraft(draft.topic, sourceImageUri, "image/*")
- } else {
- generateDraft(draft.topic)
- }
- }
-
- fun resetChatState() {
- _chatState.value = ChatState()
- }
-
- private fun readTextFromUri(uri: Uri): String {
- return try {
- context.contentResolver.openInputStream(uri)?.bufferedReader().use { reader ->
- reader?.readText()
- } ?: ""
- } catch (e: Exception) {
- e.printStackTrace()
- ""
- }
- }
-
- // --- Note and Folder Functions ---
-
- val allFolders: StateFlow> = folderUseCase.getAllFolders()
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
-
- val displayedNotes: StateFlow> =
- combine, Long?, Boolean, String, List>(
- noteUseCase.getAllNotes(),
- selectedFolderId,
- isVaultMode,
- searchQuery
- ) { allNotes, folderId, isVault, query ->
- val notesAfterFolderFilter = if (folderId == null) {
- allNotes
- } else {
- allNotes.filter { it.folderId == folderId }
- }
- val notesAfterVaultFilter = notesAfterFolderFilter.filter { it.encrypted == isVault }
-
- if (query.isBlank()) {
- notesAfterVaultFilter
- } else {
- notesAfterFolderFilter.filter { note ->
- note.name.contains(query, ignoreCase = true) ||
- note.description.contains(query, ignoreCase = true)
- }
- }
- }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
-
- fun onFolderLongPressed(folder: Folder) {
- _folderForAction.value = folder
- }
-
- fun onDismissFolderAction() {
- _folderForAction.value = null
- _showDeleteConfirmDialog.value = false
- }
-
- fun onEditFolderRequest() {
- _folderToEdit.value = _folderForAction.value
- onDismissFolderAction()
- }
-
- fun onDeleteFolderRequest() {
- _showDeleteConfirmDialog.value = true
- }
-
- fun confirmFolderDeletion() {
- viewModelScope.launch {
- _folderForAction.value?.let { folder ->
- folderUseCase.deleteFolderAndUnlinkNotes(folder)
- }
- onDismissFolderAction()
- }
- }
-
- fun onDismissEditFolderDialog() {
- _folderToEdit.value = null
- }
-
- fun setAddFolderDialogVisibility(isVisible: Boolean) {
- _isAddFolderDialogVisible.value = isVisible
- }
-
-
-
- fun toggleIsDeleteMode(enabled: Boolean) {
- _isDeleteMode.value = enabled
- }
-
- fun toggleIsVaultMode(enabled: Boolean) {
- _isVaultMode.value = enabled
- if (!enabled) {
- noteUseCase.decryptionResult = DecryptionResult.LOADING
- }
- noteUseCase.observe()
- }
-
-
- fun toggleIsPasswordPromptVisible(enabled: Boolean) {
- _isPasswordPromptVisible.value = enabled
- }
-
- fun changeSearchQuery(newValue: String) {
- selectNote(null)
- _searchQuery.value = newValue
- }
-
- fun pinOrUnpinNotes() {
- if (selectedNotes.all { it.pinned }) {
- selectedNotes.forEach { note ->
- val updatedNote = note.copy(pinned = false)
- noteUseCase.pinNote(updatedNote)
- }
- } else {
- selectedNotes.forEach { note ->
- val updatedNote = note.copy(pinned = true)
- noteUseCase.pinNote(updatedNote)
- }
- }
- selectedNotes.clear()
- }
-
- fun selectFolder(folderId: Long?) {
- selectNote(null)
- _selectedFolderId.value = folderId
- viewModelScope.launch {
- val settings = settingsRepository.settings.first()
- if (settings.openToLastUsedFolder) {
- settingsRepository.update(settings.copy(lastUsedFolderId = folderId))
- }
- }
- }
-
- fun addFolder(name: String, iconName: String) {
- viewModelScope.launch {
- folderUseCase.addFolder(Folder(name = name, iconName = iconName))
- setAddFolderDialogVisibility(false)
- }
- }
-
- fun setMoveToFolderDialogVisibility(isVisible: Boolean) {
- _isMoveToFolderDialogVisible.value = isVisible
- }
-
- fun moveSelectedNotesToFolder(targetFolderId: Long) {
- viewModelScope.launch {
- val movedNotesCount = selectedNotes.size
- selectedNotes.forEach { note ->
- noteUseCase.addNote(note.copy(folderId = targetFolderId))
- }
- selectedNotes.clear()
- setMoveToFolderDialogVisibility(false)
- _uiEvent.send("$movedNotesCount not taşındı.")
- }
- }
-
- fun onAddNewNoteClicked(onNoteClicked: (noteId: Int, isVault: Boolean, folderId: Long?) -> Unit) {
- val targetFolderId = selectedFolderId.value
- onNoteClicked(0, isVaultMode.value, targetFolderId)
- }
-
- fun updateNote(note: Note) {
- viewModelScope.launch {
- noteUseCase.addNote(note)
- }
- }
-
- fun updateNoteDetails(noteToUpdate: Note, newName: String, newDescription: String) {
- viewModelScope.launch {
- val updatedNote = noteToUpdate.copy(name = newName, description = newDescription)
- noteUseCase.addNote(updatedNote)
- }
- }
-
- fun onNoteLongPressed(note: Note) {
- _noteForAction.value = note
- }
-
- fun onDismissNoteAction() {
- _noteForAction.value = null
- }
-
- fun deleteNoteAction() {
- _noteForAction.value?.let { noteToDelete ->
- deleteNoteById(noteToDelete.id)
- }
- onDismissNoteAction()
- }
-
- fun requestMoveNoteAction() {
- _noteForAction.value?.let { noteToMove ->
- selectedNotes.clear()
- selectedNotes.add(noteToMove)
- setMoveToFolderDialogVisibility(true)
- }
- onDismissNoteAction()
- }
-
- fun updateFolder(name: String, iconName: String) {
- _folderToEdit.value?.let { folder ->
- viewModelScope.launch {
- val updatedFolder = folder.copy(name = name, iconName = iconName)
- folderUseCase.updateFolder(updatedFolder)
- onDismissEditFolderDialog()
- }
- }
- }
-
- fun deleteNoteById(id: Int) {
- viewModelScope.launch {
- noteUseCase.deleteNoteById(id)
- }
- }
-
- fun observeNotes() {
- noteUseCase.observe()
- }
-
- fun createNewNoteForDesktop() {
- viewModelScope.launch {
- val targetFolderId = _selectedFolderId.value
- val newNote = Note(
- name = "Untitled Note",
- description = "",
- folderId = targetFolderId,
- encrypted = isVaultMode.value
- )
- noteUseCase.addNote(newNote)
-
- val latestNote = noteUseCase.getAllNotes().first().maxByOrNull { it.createdAt }
- if (latestNote != null) {
- selectNote(latestNote)
- }
- }
- }
-
- fun executeDesktopAiAction(action: AiAction, tone: AiTone? = null) {
- val currentNote = _selectedNote.value ?: return
- val currentDescription = currentNote.description
-
- viewModelScope.launch {
- geminiRepository.processAiAction(
- action = action,
- text = currentDescription,
- tone = tone,
- apiKey = getApiKeyToUse(),
- aiMode = _aiMode.value
- )
- .onSuccess { result ->
- updateNoteDetails(currentNote, currentNote.name, result)
- }
- .onFailure { exception ->
- _uiEvent.send(exception.message ?: "AI işlemi başarısız oldu.")
- }
- }
- }
-
- fun toggleLockForSelectedNotes() {
- val allLocked = selectedNotes.all { it.isLocked }
- viewModelScope.launch {
- selectedNotes.forEach { note ->
- val updatedNote = note.copy(isLocked = !allLocked)
- noteUseCase.addNote(updatedNote)
- }
- selectedNotes.clear()
- _uiEvent.send(if (!allLocked) stringProvider.getString(R.string.notes_are_locked) else stringProvider.getString(R.string.notes_are_unlocked))
- }
- }
- fun isNoteLocked(noteId: Int): Boolean {
- return displayedNotes.value.find { it.id == noteId }?.isLocked == true
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/AddFolderDialog.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/AddFolderDialog.kt
deleted file mode 100644
index c0d4f45..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/AddFolderDialog.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.presentation.screens.home.widgets
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.domain.model.Folder
-
-@Composable
-fun AddFolderDialog(
- onDismiss: () -> Unit,
- onConfirm: (name: String, colorHex: String) -> Unit,
- folderToEdit: Folder? = null
-) {
- var folderName by remember { mutableStateOf("") }
- val folderColors = listOf(
- "#FF5733", "#33FF57", "#3357FF", "#FF33A1",
- "#A133FF", "#33FFA1", "#FFC300", "#C70039"
- )
- var selectedColor by remember { mutableStateOf(folderColors.first()) }
- val isEditing = folderToEdit != null
-
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text(text = stringResource(if (isEditing) R.string.edit_folder else R.string.add_new_folder)) },
- text = {
- Column {
- // Klasör adı için TextField
- OutlinedTextField(
- value = folderName,
- onValueChange = { folderName = it },
- label = { Text(stringResource(R.string.folder_name)) },
- singleLine = true,
- modifier = Modifier.fillMaxWidth()
- )
- Spacer(modifier = Modifier.height(16.dp))
-
- Text(text = stringResource(R.string.folder_color), style = MaterialTheme.typography.bodyMedium)
- Spacer(modifier = Modifier.height(8.dp))
- LazyRow(
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- modifier = Modifier.fillMaxWidth()
- ) {
- items(folderColors) { colorHex ->
- val color = Color(android.graphics.Color.parseColor(colorHex))
- Box(
- modifier = Modifier
- .size(40.dp)
- .clip(CircleShape)
- .background(color)
- .clickable { selectedColor = colorHex }
- .border(
- width = 2.dp,
- color = if (selectedColor == colorHex) MaterialTheme.colorScheme.primary else Color.Transparent,
- shape = CircleShape
- )
- )
- }
- }
- }
- },
- confirmButton = {
- TextButton(
- onClick = {
- if (folderName.isNotBlank()) {
- onConfirm(folderName, selectedColor)
- }
- },
- enabled = folderName.isNotBlank()
- ) {
- Text(stringResource(if (isEditing) R.string.save else R.string.add))
- }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) {
- Text(stringResource(R.string.cancel))
- }
- }
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/FolderActionBottomSheet.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/FolderActionBottomSheet.kt
deleted file mode 100644
index ada8cb9..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/FolderActionBottomSheet.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.presentation.screens.home.widgets
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Delete
-import androidx.compose.material.icons.rounded.Edit
-import androidx.compose.material3.*
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.domain.model.Folder
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun FolderActionBottomSheet(
- folder: Folder,
- onDismiss: () -> Unit,
- onEditClick: () -> Unit,
- onDeleteClick: () -> Unit
-) {
- ModalBottomSheet(onDismissRequest = onDismiss) {
- Column(modifier = Modifier.padding(bottom = 32.dp)) {
- Text(
- text = folder.name,
- style = MaterialTheme.typography.titleMedium,
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
- )
- Divider()
- ListItem(
- headlineContent = { Text(stringResource(R.string.edit_folder)) },
- leadingContent = { Icon(Icons.Rounded.Edit, contentDescription = null) },
- modifier = Modifier.clickable(onClick = onEditClick)
- )
- ListItem(
- headlineContent = { Text(stringResource(R.string.delete_folder), color = MaterialTheme.colorScheme.error) },
- leadingContent = { Icon(Icons.Rounded.Delete, contentDescription = null, tint = MaterialTheme.colorScheme.error) },
- modifier = Modifier.clickable(onClick = onDeleteClick)
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/MoveToFolderDialog.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/MoveToFolderDialog.kt
deleted file mode 100644
index a40d7af..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/MoveToFolderDialog.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.presentation.screens.home.widgets
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Icon
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.domain.model.Folder
-import com.babelsoftware.airnote.presentation.screens.home.iconNameToVector
-
-@Composable
-fun MoveToFolderDialog(
- folders: List,
- onDismiss: () -> Unit,
- onFolderSelected: (folderId: Long) -> Unit
-) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text(text = stringResource(R.string.move_to_folder)) },
- text = {
- if (folders.isEmpty()) {
- Text(text = stringResource(R.string.no_folders_available))
- } else {
- LazyColumn(
- modifier = Modifier.fillMaxWidth()
- ) {
- items(folders) { folder ->
- ListItem(
- headlineContent = { Text(folder.name) },
- leadingContent = {
- Icon(
- imageVector = iconNameToVector(folder.iconName),
- contentDescription = folder.name
- )
- },
- modifier = Modifier.clickable {
- onFolderSelected(folder.id)
- }
- )
- }
- }
- }
- },
- confirmButton = {},
- dismissButton = {
- TextButton(onClick = onDismiss) {
- Text(stringResource(R.string.cancel))
- }
- }
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/NoteCard.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/NoteCard.kt
deleted file mode 100644
index d090e1c..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/NoteCard.kt
+++ /dev/null
@@ -1,152 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.home.widgets
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.border
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Lock
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.ElevatedCard
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.blur
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.domain.model.Folder
-import com.babelsoftware.airnote.domain.model.Note
-import com.babelsoftware.airnote.presentation.components.FolderIndicator
-import com.babelsoftware.airnote.presentation.components.markdown.MarkdownText
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.theme.FontUtils
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun NoteCard(
- settingsViewModel: SettingsViewModel,
- containerColor: Color,
- note: Note,
- allFolders: List,
- isBorderEnabled: Boolean,
- shape: RoundedCornerShape,
- onShortClick: () -> Unit,
- onLongClick: () -> Unit,
- onNoteUpdate: (Note) -> Unit
-) {
- val borderModifier = if (isBorderEnabled) {
- Modifier.border(
- width = 1.5.dp,
- color = MaterialTheme.colorScheme.primary,
- shape = shape
- )
- } else if (containerColor == Color.Black) {
- Modifier.border(
- width = 1.5.dp,
- color = MaterialTheme.colorScheme.surfaceContainerHighest,
- shape = shape
- )
- } else {
- Modifier
- }
-
- val folder = note.folderId?.let { id ->
- allFolders.find { it.id == id }
- }
-
- val contentModifier = if (note.isLocked) Modifier.blur(15.dp) else Modifier
-
- ElevatedCard(
- modifier = Modifier
- .padding(bottom = 12.dp)
- .clip(shape)
- .combinedClickable(
- onClick = onShortClick,
- onLongClick = onLongClick
- )
- .then(borderModifier),
- colors = CardDefaults.cardColors(
- containerColor = containerColor
- ),
- elevation = CardDefaults.cardElevation(
- defaultElevation = if (containerColor != Color.Black) 6.dp else 0.dp
- ),
- ) {
- Box(modifier = Modifier.fillMaxWidth()) {
- Column(
- modifier = Modifier
- .padding(16.dp, 12.dp, 16.dp, 12.dp)
- .then(contentModifier)
- ) {
- if (note.isLocked) {
- } else {
- if (note.name.isNotBlank()) {
- MarkdownText(
- isPreview = true,
- isEnabled = settingsViewModel.settings.value.isMarkdownEnabled,
- markdown = note.name.replaceFirstChar { it.uppercase() },
- modifier = Modifier
- .heightIn(max = dimensionResource(R.dimen.max_name_height))
- .then(
- if (note.description.isNotBlank() && !settingsViewModel.settings.value.showOnlyTitle) {
- Modifier.padding(bottom = 9.dp)
- } else {
- Modifier
- }
- ),
- weight = FontWeight.Bold,
- spacing = 0.dp,
- onContentChange = { onNoteUpdate(note.copy(name = it)) },
- fontSize = FontUtils.getTitleFontSize(settingsViewModel),
- radius = settingsViewModel.settings.value.cornerRadius,
- settingsViewModel = settingsViewModel
- )
- }
-
- if (note.description.isNotBlank() && !settingsViewModel.settings.value.showOnlyTitle) {
- MarkdownText(
- isPreview = true,
- markdown = note.description,
- isEnabled = settingsViewModel.settings.value.isMarkdownEnabled,
- spacing = 0.dp,
- modifier = Modifier
- .heightIn(max = dimensionResource(R.dimen.max_description_height)),
- onContentChange = { onNoteUpdate(note.copy(description = it)) },
- fontSize = FontUtils.getBodyFontSize(settingsViewModel),
- radius = settingsViewModel.settings.value.cornerRadius,
- settingsViewModel = settingsViewModel
- )
- }
- }
- }
-
- if (note.isLocked) {
- Icon(
- imageVector = Icons.Rounded.Lock,
- contentDescription = "Locked",
- modifier = Modifier.align(Alignment.Center).size(32.dp),
- tint = MaterialTheme.colorScheme.onSurface
- )
- }
-
- if (folder != null && settingsViewModel.settings.value.showFolderIndicator) {
- FolderIndicator(
- folderName = folder.name,
- modifier = Modifier.align(Alignment.TopEnd)
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/NoteFilter.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/NoteFilter.kt
deleted file mode 100644
index b11c3d7..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/NoteFilter.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.home.widgets
-
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
-import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.Notes
-import androidx.compose.material.icons.rounded.Search
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.babelsoftware.airnote.domain.model.Note
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.domain.model.Folder
-import com.babelsoftware.airnote.R
-
-@Composable
-fun NoteFilter(
- modifier: Modifier = Modifier,
- listState: LazyStaggeredGridState = rememberLazyStaggeredGridState(),
- settingsViewModel: SettingsViewModel,
- containerColor : Color,
- onNoteClicked: (Int) -> Unit,
- shape: RoundedCornerShape,
- notes: List,
- allFolders: List,
- searchText: String? = null,
- selectedNotes: MutableList = mutableListOf(),
- viewMode: Boolean = false,
- isDeleteMode: Boolean = false,
- onNoteUpdate: (Note) -> Unit = {},
- onDeleteNote: (Int) -> Unit = {}
-) {
- if (notes.isEmpty()) {
- Placeholder(
- modifier = modifier,
- placeholderIcon = {
- Icon(
- getEmptyIcon(searchText),
- contentDescription = "Placeholder icon",
- tint = MaterialTheme.colorScheme.outline,
- modifier = Modifier.size(64.dp)
- )
- },
- placeholderText = getEmptyText(searchText)
- )
- } else {
- NotesGrid(
- modifier = modifier,
- listState = listState,
- settingsViewModel = settingsViewModel,
- containerColor = containerColor,
- onNoteClicked = onNoteClicked,
- notes = notes,
- allFolders = allFolders,
- shape = shape,
- onNoteUpdate = onNoteUpdate,
- selectedNotes = selectedNotes,
- viewMode = viewMode,
- isDeleteClicked = isDeleteMode,
- animationFinished = onDeleteNote
- )
- }
-}
-
-@Composable
-private fun getEmptyText(searchText: String?): String {
- return when {
- searchText.isNullOrEmpty() -> stringResource(R.string.no_created_notes)
- else -> stringResource(R.string.no_found_notes)
- }
-}
-
-@Composable
-private fun getEmptyIcon(searchText: String?): ImageVector {
- return when {
- searchText.isNullOrEmpty() -> Icons.AutoMirrored.Rounded.Notes
- else -> Icons.Rounded.Search
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/NoteGrid.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/NoteGrid.kt
deleted file mode 100644
index 977c628..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/NoteGrid.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.home.widgets
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.MutableTransitionState
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
-import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
-import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
-import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
-import androidx.compose.foundation.lazy.staggeredgrid.items
-import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.babelsoftware.airnote.domain.model.Note
-import com.babelsoftware.airnote.presentation.components.getNoteEnterAnimation
-import com.babelsoftware.airnote.presentation.components.getNoteExitAnimation
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.domain.model.Folder
-
-@Composable
-fun NotesGrid(
- modifier: Modifier = Modifier,
- listState: LazyStaggeredGridState = rememberLazyStaggeredGridState(),
- settingsViewModel: SettingsViewModel,
- containerColor: Color,
- onNoteClicked: (Int) -> Unit,
- shape : RoundedCornerShape,
- notes: List,
- allFolders: List,
- onNoteUpdate: (Note) -> Unit,
- selectedNotes: MutableList,
- viewMode: Boolean,
- isDeleteClicked: Boolean,
- animationFinished: (Int) -> Unit
-) {
- val (pinnedNotes, otherNotes) = notes.partition { it.pinned }
-
- @Composable
- fun Note(note: Note, notes: List) {
- val isAnimationVisible = rememberTransitionState()
- AnimatedVisibility(
- visibleState = isAnimationVisible,
- enter = getNoteEnterAnimation(),
- exit = getNoteExitAnimation(calculateSlideDirection(notes, note))
- ) {
- NoteCard(
- settingsViewModel = settingsViewModel,
- containerColor = containerColor,
- note = note,
- allFolders = allFolders,
- shape = shape,
- isBorderEnabled = selectedNotes.contains(note),
- onShortClick = { handleShortClick(selectedNotes, note, onNoteClicked) },
- onNoteUpdate = onNoteUpdate,
- onLongClick = { handleLongClick(selectedNotes, note) }
- )
- if (isDeleteClicked && selectedNotes.contains(note)) {
- isAnimationVisible.targetState = false
- }
- }
- handleDeleteAnimation(selectedNotes, note, isAnimationVisible, animationFinished)
- }
-
- LazyVerticalStaggeredGrid(
- modifier = modifier.padding(horizontal = 12.dp),
- state = listState,
- columns = when(viewMode) {
- true -> StaggeredGridCells.Fixed(2)
- false -> StaggeredGridCells.Fixed(1)
- },
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- contentPadding = PaddingValues(bottom = 160.dp),
- content = {
- if (pinnedNotes.isNotEmpty()) {
- item(span = StaggeredGridItemSpan.FullLine) {
- Text(
- modifier = Modifier.padding(bottom = 16.dp),
- text = stringResource(id = R.string.pinned).uppercase(),
- style = TextStyle(fontSize = 10.sp, color = MaterialTheme.colorScheme.secondary)
- )
- }
- items(pinnedNotes) { note ->
- Note(note, pinnedNotes)
- }
- if (otherNotes.isNotEmpty()) {
- item(span = StaggeredGridItemSpan.FullLine) {
- Text(
- modifier = Modifier.padding(vertical = 16.dp),
- text = stringResource(id = R.string.others).uppercase(),
- style = TextStyle(fontSize = 10.sp, color = MaterialTheme.colorScheme.secondary)
- )
- }
- }
- }
- items(otherNotes) { note ->
- Note(note, otherNotes)
- }
- },
- )
-}
-
-
-private fun calculateSlideDirection(notes: List, note: Note): Int {
- return if (notes.indexOf(note) % 2 == 0) -1 else 1
-}
-
-@Composable
-private fun rememberTransitionState(): MutableTransitionState {
- return remember { MutableTransitionState(false).apply { targetState = true } }
-}
-
-private fun handleShortClick(
- selectedNotes: MutableList,
- note: Note,
- onNoteClicked: (Int) -> Unit
-) {
- if (selectedNotes.isNotEmpty()) {
- if (selectedNotes.contains(note)) {
- selectedNotes.remove(note)
- } else {
- selectedNotes.add(note)
- }
- } else {
- onNoteClicked(note.id)
- }
-}
-
-private fun handleLongClick(selectedNotes: MutableList, note: Note) {
- if (!selectedNotes.contains(note)) {
- selectedNotes.add(note)
- }
-}
-
-private fun handleDeleteAnimation(
- selectedNotes: MutableList,
- note: Note,
- isAnimationVisible: MutableTransitionState,
- animationFinished: (Int) -> Unit
-) {
- if (!isAnimationVisible.targetState && !isAnimationVisible.currentState && selectedNotes.contains(note)) {
- selectedNotes.remove(note)
- isAnimationVisible.targetState = true
- animationFinished(note.id)
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/Placeholder.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/Placeholder.kt
deleted file mode 100644
index 518384f..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/home/widgets/Placeholder.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.home.widgets
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-
-@Composable
-fun Placeholder(
- modifier: Modifier = Modifier,
- placeholderIcon: @Composable () -> Unit,
- placeholderText: String
-) {
- Box(
- modifier = Modifier
- .imePadding()
- .fillMaxSize(),
- contentAlignment = Alignment.Center,
- ) {
- Column(
- modifier = modifier
- .fillMaxSize()
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(20.dp)
- ) {
- placeholderIcon()
-
- Text(
- text = placeholderText,
- color = MaterialTheme.colorScheme.outline,
- fontSize = 14.sp
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/SettingsScreen.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/SettingsScreen.kt
deleted file mode 100644
index 5f310e1..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/SettingsScreen.kt
+++ /dev/null
@@ -1,271 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.ArrowForwardIos
-import androidx.compose.material.icons.rounded.Cloud
-import androidx.compose.material.icons.rounded.Coffee
-import androidx.compose.material.icons.rounded.CurrencyBitcoin
-import androidx.compose.material.icons.rounded.Info
-import androidx.compose.material.icons.rounded.Language
-import androidx.compose.material.icons.rounded.Palette
-import androidx.compose.material.icons.rounded.AutoAwesome
-import androidx.compose.material.icons.rounded.DesktopWindows
-import androidx.compose.material.icons.rounded.TextFields
-import androidx.compose.material.icons.rounded.Work
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.key
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.res.vectorResource
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.constant.ConnectionConst
-import com.babelsoftware.airnote.presentation.components.NavigationIcon
-import com.babelsoftware.airnote.presentation.components.NotesScaffold
-import com.babelsoftware.airnote.presentation.components.TitleText
-import com.babelsoftware.airnote.presentation.navigation.NavRoutes
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.settings.shapeManager
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingCategory
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-
-@Composable
-fun SettingsScaffold(
- settingsViewModel: SettingsViewModel,
- title: String,
- onBackNavClicked: () -> Unit,
- snackbarHost: @Composable () -> Unit = {},
- content: @Composable () -> Unit
-) {
- NotesScaffold(
- snackbarHost = snackbarHost,
- topBar = {
- key(settingsViewModel.settings.value) {
- TopBar(title, onBackNavClicked)
- }
- },
- content = {
- Box(Modifier.padding(16.dp, 8.dp, 16.dp)) {
- content()
- }
- }
- )
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun TopBar(
- title: String,
- onBackNavClicked: () -> Unit,
-) {
- TopAppBar(
- colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
- title = {
- TitleText(titleText = title)
- },
- navigationIcon = { NavigationIcon { onBackNavClicked() } }
- )
-}
-
-@Composable
-fun MainSettings(settingsViewModel: SettingsViewModel, navController: NavController) {
- SettingsScaffold(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.screen_settings),
- onBackNavClicked = { navController.navigateUp() }
- ) {
- LazyColumn {
- item {
- SettingCategory(
- smallSetting = true,
- title = stringResource(id = R.string.support),
- subTitle = stringResource(id = R.string.support_description),
- icon = Icons.AutoMirrored.Rounded.ArrowForwardIos,
- shape = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isBoth = true
- ),
- isLast = true,
- composableAction = { onExit ->
- BottomModal(
- navController = navController,
- settingsViewModel = settingsViewModel
- ) { onExit() }
- },
- settingsViewModel = settingsViewModel
- )
- }
- item {
- SettingCategory(
- title = stringResource(id = R.string.color_styles),
- subTitle = stringResource(R.string.description_color_styles),
- icon = Icons.Rounded.Palette,
- shape = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isFirst = true
- ),
- action = { navController.navigate(NavRoutes.ColorStyles.route) },
- settingsViewModel = settingsViewModel
- )
- }
- item {
- SettingCategory(
- title = stringResource(R.string.dex_mode),
- subTitle = stringResource(R.string.dex_mode_description),
- icon = Icons.Rounded.DesktopWindows,
- shape = shapeManager(radius = settingsViewModel.settings.value.cornerRadius),
- action = { navController.navigate(NavRoutes.DesktopModeSettings.route) },
- settingsViewModel = settingsViewModel
- )
- }
- item {
- SettingCategory(
- title = stringResource(id = R.string.Behavior),
- subTitle = stringResource(id = R.string.description_markdown),
- icon = Icons.Rounded.TextFields,
- shape = shapeManager(radius = settingsViewModel.settings.value.cornerRadius),
- action = { navController.navigate(NavRoutes.Markdown.route) },
- settingsViewModel = settingsViewModel
- )
- }
- item {
- SettingCategory(
- title = stringResource(R.string.airnote_ai),
- subTitle = stringResource(R.string.airnote_ai_description),
- icon = Icons.Rounded.AutoAwesome,
- shape = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- action = { navController.navigate(NavRoutes.AirNoteAiSettings.route) },
- settingsViewModel = settingsViewModel
- )
- }
- item {
- SettingCategory(
- title = stringResource(id = R.string.language),
- subTitle = stringResource(R.string.description_language),
- icon = Icons.Rounded.Language,
- isLast = true,
- shape = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = true
- ),
- action = { navController.navigate(NavRoutes.Language.route) },
- settingsViewModel = settingsViewModel
- )
- }
- item {
- SettingCategory(
- title = stringResource(id = R.string.backup),
- subTitle = stringResource(R.string.description_cloud),
- icon = Icons.Rounded.Cloud,
- shape = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isFirst = true
- ),
- action = { navController.navigate(NavRoutes.Cloud.route) },
- settingsViewModel = settingsViewModel
- )
- }
- item {
- SettingCategory(
- title = stringResource(id = R.string.privacy),
- subTitle = stringResource(id = R.string.screen_protection),
- icon = ImageVector.vectorResource(id = R.drawable.incognito_fill),
- shape = shapeManager(radius = settingsViewModel.settings.value.cornerRadius),
- settingsViewModel = settingsViewModel,
- action = { navController.navigate(NavRoutes.Privacy.route) }
- )
- }
- item {
- SettingCategory(
- title = stringResource(id = R.string.tools),
- subTitle = stringResource(R.string.description_tools),
- icon = Icons.Rounded.Work,
- shape = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = true
- ),
- isLast = true,
- action = { navController.navigate(NavRoutes.Tools.route) },
- settingsViewModel = settingsViewModel
- )
- }
- item {
- val aboutSubtitle = if (settingsViewModel.updateAvailable.value) {
- stringResource(R.string.update_available_description)
- } else {
- stringResource(R.string.description_about)
- }
- SettingCategory(
- title = stringResource(id = R.string.about),
- subTitle = aboutSubtitle,
- icon = Icons.Rounded.Info,
- shape = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isBoth = true
- ),
- action = { navController.navigate(NavRoutes.About.route) },
- settingsViewModel = settingsViewModel
- )
- }
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun BottomModal(navController: NavController, settingsViewModel: SettingsViewModel, onExit: () -> Unit) {
- val uriHandler = LocalUriHandler.current
-
- ModalBottomSheet(
- containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
- onDismissRequest = { onExit() }
- ) {
- Column(
- modifier = Modifier.padding(20.dp, 0.dp, 20.dp, 20.dp)
- ) {
- SettingsBox(
- size = 8.dp,
- title = "Buy Me A Coffee",
- icon = IconResource.Vector(Icons.Rounded.Coffee),
- isCentered = true,
- actionType = ActionType.CUSTOM,
- radius = shapeManager(
- isFirst = true,
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- customAction = { uriHandler.openUri(ConnectionConst.SUPPORT_BUYMEACOFFEE) },
- settingsViewModel = settingsViewModel
- )
- SettingsBox(
- title = stringResource(R.string.cryptocurrency),
- size = 8.dp,
- icon = IconResource.Vector(Icons.Rounded.CurrencyBitcoin),
- isCentered = true,
- actionType = ActionType.CUSTOM,
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = true
- ),
- customAction = { LaunchedEffect(true) { navController.navigate(NavRoutes.Support.route) } },
- settingsViewModel = settingsViewModel
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/model/IconResource.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/model/IconResource.kt
deleted file mode 100644
index 7821f87..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/model/IconResource.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- */
-
-package com.babelsoftware.airnote.presentation.screens.settings.model
-
-import androidx.compose.ui.graphics.vector.ImageVector
-
-sealed class IconResource {
- data class Vector(val imageVector: ImageVector) : IconResource()
- data class Url(val url: String) : IconResource()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/model/SettingsModel.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/model/SettingsModel.kt
deleted file mode 100644
index 9933661..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/model/SettingsModel.kt
+++ /dev/null
@@ -1,412 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.model
-
-import android.content.Context
-import android.net.Uri
-import android.util.Log
-import android.widget.Toast
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableStateOf
-import androidx.core.os.LocaleListCompat
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.babelsoftware.airnote.data.repository.BackupResult
-import com.babelsoftware.airnote.data.repository.ImportExportRepository
-import com.babelsoftware.airnote.domain.model.Settings
-import com.babelsoftware.airnote.domain.usecase.ImportExportUseCase
-import com.babelsoftware.airnote.domain.usecase.ImportResult
-import com.babelsoftware.airnote.domain.usecase.NoteUseCase
-import com.babelsoftware.airnote.domain.usecase.SettingsUseCase
-import com.babelsoftware.airnote.presentation.components.GalleryObserver
-import com.babelsoftware.airnote.presentation.navigation.NavRoutes
-import com.babelsoftware.airnote.data.repository.SecureStorageRepository
-import com.babelsoftware.airnote.BuildConfig
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.data.provider.StringProvider
-import com.babelsoftware.airnote.data.repository.GeminiRepository
-import com.babelsoftware.airnote.domain.repository.SettingsRepository
-import com.babelsoftware.airnote.util.checkForUpdates
-import com.babelsoftware.airnote.util.isNewerVersion
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-import java.io.IOException
-import javax.inject.Inject
-
-
-@HiltViewModel
-class SettingsViewModel @Inject constructor(
- val galleryObserver: GalleryObserver,
- val backup: ImportExportRepository,
- private val settingsUseCase: SettingsUseCase,
- val noteUseCase: NoteUseCase,
- private val importExportUseCase: ImportExportUseCase,
- private val secureStorageRepository: SecureStorageRepository,
- private val settingsRepository: SettingsRepository,
- val geminiRepository: GeminiRepository,
- private val stringProvider: StringProvider
-) : ViewModel() {
- var defaultRoute: String? = null
-
- // ---> Update Check States
- private val _updateAvailable = mutableStateOf(false)
- val updateAvailable: State = _updateAvailable
-
- private val _latestVersion = mutableStateOf("")
- val latestVersion: MutableState = _latestVersion
-
- private val _showUpdateDialog = mutableStateOf(false)
- val showUpdateDialog: State = _showUpdateDialog
-
- // --- Gemini API Key States ---
- private val _userApiKey = mutableStateOf("")
- val userApiKey: State = _userApiKey
-
- private val _isVerifyingApiKey = mutableStateOf(false)
- val isVerifyingApiKey: State = _isVerifyingApiKey
-
- private val _isApiKeyVerified = mutableStateOf(false)
- val isApiKeyVerified: State = _isApiKeyVerified
-
- // --- Perplexity API Key States ---
- private val _perplexityApiKey = mutableStateOf("")
- val perplexityApiKey: State = _perplexityApiKey
-
- private val _isVerifyingPerplexityApiKey = mutableStateOf(false)
- val isVerifyingPerplexityApiKey: State = _isVerifyingPerplexityApiKey
-
- private val _isPerplexityApiKeyVerified = mutableStateOf(false)
- val isPerplexityApiKeyVerified: State = _isPerplexityApiKeyVerified
- // ---
-
- private val _uiEvent = Channel()
- val uiEvent = _uiEvent.receiveAsFlow()
- private val _downloadedModels = MutableStateFlow>(emptySet())
- val downloadedModels: StateFlow> = _downloadedModels.asStateFlow()
-
- private val _processingLanguageCode = MutableStateFlow(null)
- val processingLanguageCode: StateFlow = _processingLanguageCode.asStateFlow()
-
- fun fetchDownloadedModels() {
- viewModelScope.launch {
- geminiRepository.getDownloadedModels()
- .onSuccess { _downloadedModels.value = it }
- .onFailure {
- Log.e("SettingsViewModel", "Failed to fetch models", it)
- viewModelScope.launch { _uiEvent.send(stringProvider.getString(R.string.lang_models_fetch_error)) }
- }
- }
- }
-
- fun downloadLanguageModel(languageCode: String, onResult: (Boolean, String) -> Unit) {
- viewModelScope.launch {
- _processingLanguageCode.value = languageCode
- geminiRepository.downloadLanguageModel(languageCode)
- .onSuccess {
- _downloadedModels.value = _downloadedModels.value + languageCode
- val languageName = geminiRepository.supportedLanguages[languageCode] ?: languageCode
- onResult(true, stringProvider.getString(R.string.lang_model_download_success, languageName))
- }
- .onFailure {
- val languageName = geminiRepository.supportedLanguages[languageCode] ?: languageCode
- onResult(false, stringProvider.getString(R.string.lang_model_download_failure, languageName))
- }
- _processingLanguageCode.value = null
- }
- }
-
- fun deleteLanguageModel(languageCode: String, onResult: (Boolean, String) -> Unit) {
- viewModelScope.launch {
- _processingLanguageCode.value = languageCode
- geminiRepository.deleteLanguageModel(languageCode)
- .onSuccess {
- _downloadedModels.value = _downloadedModels.value - languageCode
- val languageName = geminiRepository.supportedLanguages[languageCode] ?: languageCode
- onResult(true, stringProvider.getString(R.string.lang_model_delete_success, languageName))
- }
- .onFailure {
- val languageName = geminiRepository.supportedLanguages[languageCode] ?: languageCode
- onResult(false, stringProvider.getString(R.string.lang_model_delete_failure, languageName))
- }
- _processingLanguageCode.value = null
- }
- }
-
-
- fun checkForNewUpdate(context: Context) {
- viewModelScope.launch {
- val latestVersionFromGitHub = checkForUpdates()
- if (latestVersionFromGitHub != null) {
- val isNewer = isNewerVersion(latestVersionFromGitHub, version)
- _updateAvailable.value = isNewer
- if (isNewer) {
- _latestVersion.value = latestVersionFromGitHub
- val sharedPreferences = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
- val lastNotificationTime = sharedPreferences.getLong("last_update_notification_time", 0L)
- val currentTime = System.currentTimeMillis()
- // --->Show update pop-up every 6 hours
- if (currentTime - lastNotificationTime > 6 * 60 * 60 * 1000) {
- _showUpdateDialog.value = true
- sharedPreferences.edit().putLong("last_update_notification_time", currentTime).apply()
- }
- // <---
- }
- }
- }
- }
-
- fun dismissUpdateDialog() {
- _showUpdateDialog.value = false
- }
- // <---
-
- fun loadDefaultRoute() {
- if (!_settings.value.appLockEnabled) {
- defaultRoute = NavRoutes.Home.route
- return
- }
-
- if (_settings.value.fingerprint == false && _settings.value.passcode == null && _settings.value.pattern == null) {
- defaultRoute = NavRoutes.Home.route
- } else {
- defaultRoute = _settings.value.defaultRouteType
- }
- }
-
- fun updateUserApiKey(newApiKey: String) {
- _userApiKey.value = newApiKey
- if (_isApiKeyVerified.value) {
- _isApiKeyVerified.value = false
- }
- }
-
- fun updatePerplexityApiKey(newApiKey: String) {
- _perplexityApiKey.value = newApiKey
- if (_isPerplexityApiKeyVerified.value) {
- _isPerplexityApiKeyVerified.value = false
- }
- }
-
- private fun checkStoredApiKey() {
- val storedKey = secureStorageRepository.getUserApiKey() ?: ""
- _userApiKey.value = storedKey
- val storedModel = settings.value.selectedModelName
-
- if (storedKey.isNotBlank()) {
- viewModelScope.launch {
- val result = geminiRepository.validateApiKey(
- apiKey = storedKey,
- modelName = storedModel
- )
- _isApiKeyVerified.value = result.isSuccess
- }
- }
- }
-
- private fun checkStoredPerplexityApiKey() {
- val storedKey = secureStorageRepository.getPerplexityApiKey() ?: ""
- _perplexityApiKey.value = storedKey
- if (storedKey.isNotBlank()) {
- viewModelScope.launch {
- val result = geminiRepository.validatePerplexityApiKey(storedKey)
- _isPerplexityApiKeyVerified.value = result.isSuccess
- }
- }
- }
-
- fun verifyUserApiKey() {
- val keyToVerify = _userApiKey.value
- val modelToVerify = settings.value.selectedModelName
-
- if (keyToVerify.isBlank()) {
- viewModelScope.launch { _uiEvent.send(stringProvider.getString(R.string.error_api_key_blank)) }
- return
- }
-
- viewModelScope.launch {
- _isVerifyingApiKey.value = true
- val result = geminiRepository.validateApiKey(
- apiKey = keyToVerify,
- modelName = modelToVerify
- )
- _isApiKeyVerified.value = result.isSuccess
-
- val message: String
- if (result.isSuccess) {
- secureStorageRepository.saveUserApiKey(keyToVerify)
- message = stringProvider.getString(R.string.api_key_validation_success)
- } else {
- message = stringProvider.getString(R.string.api_key_validation_failure)
- }
- _uiEvent.send(message)
- _isVerifyingApiKey.value = false
- }
- }
-
- fun verifyPerplexityApiKey() {
- val keyToVerify = _perplexityApiKey.value
- if (keyToVerify.isBlank()) {
- viewModelScope.launch { _uiEvent.send(stringProvider.getString(R.string.perplexity_api_not_empty)) }
- return
- }
-
- viewModelScope.launch {
- _isVerifyingPerplexityApiKey.value = true
- val result = geminiRepository.validatePerplexityApiKey(keyToVerify)
- _isPerplexityApiKeyVerified.value = result.isSuccess
-
- val message: String
- if (result.isSuccess) {
- secureStorageRepository.savePerplexityApiKey(keyToVerify)
- message = stringProvider.getString(R.string.api_key_validation_success)
- } else {
- message = result.exceptionOrNull()?.message ?: stringProvider.getString(R.string.api_key_validation_failure)
- }
- _uiEvent.send(message)
- _isVerifyingPerplexityApiKey.value = false
- }
- }
-
- fun updateSelectedModel(modelName: String) {
- update(settings.value.copy(selectedModelName = modelName))
- if (_isApiKeyVerified.value) {
- _isApiKeyVerified.value = false
- }
- }
-
- fun updateSelectedPerplexityModel(modelName: String) {
- update(settings.value.copy(selectedPerplexityModelName = modelName))
- if (_isPerplexityApiKeyVerified.value) {
- _isPerplexityApiKeyVerified.value = false
- }
- }
-
- fun updateDefaultRoute(route: String) {
- _settings.value = _settings.value.copy(defaultRouteType = route)
- update(settings.value.copy(defaultRouteType = route))
- }
-
- val databaseUpdate = mutableStateOf(false)
- var password : String? = null
-
- private val _settings = mutableStateOf(Settings())
- var settings: State = _settings
-
- private suspend fun loadSettings() {
- val loadedSettings = runBlocking(Dispatchers.IO) {
- settingsUseCase.loadSettingsFromRepository()
- }
- _settings.value = loadedSettings
- if (_settings.value.fingerprint == false && _settings.value.passcode == null && _settings.value.pattern == null) {
- defaultRoute = NavRoutes.Home.route
- } else {
- defaultRoute = loadedSettings.defaultRouteType
- }
- }
-
- fun update(newSettings: Settings) {
- _settings.value = newSettings.copy()
- viewModelScope.launch {
- settingsUseCase.saveSettingsToRepository(newSettings)
- }
- }
-
-
- fun onExportBackup(uri: Uri, context: Context) {
- viewModelScope.launch {
- val result = backup.exportBackup(uri, password)
- handleBackupResult(result, context)
- databaseUpdate.value = true
- }
- }
-
- fun onImportBackup(uri: Uri, context: Context) {
- viewModelScope.launch {
- val result = backup.importBackup(uri, password)
- handleBackupResult(result, context)
- databaseUpdate.value = true
- }
- }
-
- fun onImportFiles(uris: List, context: Context) {
- viewModelScope.launch {
- importExportUseCase.importNotes(uris) { result ->
- handleImportResult(result, context)
- }
- }
- }
-
- // Taken from: https://stackoverflow.com/questions/74114067/get-list-of-locales-from-locale-config-in-android-13
- private fun getLocaleListFromXml(context: Context): LocaleListCompat {
- val tagsList = mutableListOf()
- try {
- val xpp: XmlPullParser = context.resources.getXml(R.xml.locales_config)
- while (xpp.eventType != XmlPullParser.END_DOCUMENT) {
- if (xpp.eventType == XmlPullParser.START_TAG) {
- if (xpp.name == "locale") {
- tagsList.add(xpp.getAttributeValue(0))
- }
- }
- xpp.next()
- }
- } catch (e: XmlPullParserException) {
- e.printStackTrace()
- } catch (e: IOException) {
- e.printStackTrace()
- }
-
- return LocaleListCompat.forLanguageTags(tagsList.joinToString(","))
- }
-
- fun getSupportedLanguages(context: Context): Map {
- val localeList = getLocaleListFromXml(context)
- val map = mutableMapOf()
-
- for (a in 0 until localeList.size()) {
- localeList[a].let {
- it?.let { it1 -> map.put(it1.getDisplayName(it), it.toLanguageTag()) }
- }
- }
- return map
- }
-
- private fun handleBackupResult(result: BackupResult, context: Context) {
- when (result) {
- is BackupResult.Success -> {}
- is BackupResult.Error -> showToast(stringProvider.getString(R.string.error_generic), context)
- BackupResult.BadPassword -> showToast(stringProvider.getString(R.string.detabase_restore_error), context)
- }
- }
-
- private fun handleImportResult(result: ImportResult, context: Context) {
- when (result.successful) {
- result.total -> {showToast(stringProvider.getString(R.string.file_import_success), context)}
- 0 -> {showToast(stringProvider.getString(R.string.file_import_error), context)}
- else -> {showToast(stringProvider.getString(R.string.file_import_partial_error), context)}
- }
- }
-
- private fun showToast(message: String, context: Context) {
- Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
- }
-
- val version: String = BuildConfig.VERSION_NAME
- val build: String = BuildConfig.BUILD_TYPE
-
- init {
- runBlocking {
- loadSettings()
- }
- checkStoredApiKey()
- checkStoredPerplexityApiKey()
- fetchDownloadedModels()
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/About.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/About.kt
deleted file mode 100644
index 52c1e5d..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/About.kt
+++ /dev/null
@@ -1,512 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings
-
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.os.Build
-import android.provider.Settings
-import android.widget.Toast
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.animation.Crossfade
-import androidx.compose.animation.animateContentSize
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowDownward
-import androidx.compose.material.icons.filled.Cancel
-import androidx.compose.material.icons.filled.CheckCircle
-import androidx.compose.material.icons.filled.CloudDownload
-import androidx.compose.material.icons.filled.ErrorOutline
-import androidx.compose.material.icons.filled.RocketLaunch
-import androidx.compose.material.icons.rounded.AutoAwesome
-import androidx.compose.material.icons.rounded.BugReport
-import androidx.compose.material.icons.rounded.Build
-import androidx.compose.material.icons.rounded.DesktopWindows
-import androidx.compose.material.icons.rounded.Download
-import androidx.compose.material.icons.rounded.Email
-import androidx.compose.material.icons.rounded.Info
-import androidx.compose.material.icons.rounded.Refresh
-import androidx.compose.material.icons.rounded.Translate
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.LinearProgressIndicator
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.constant.ConnectionConst
-import com.babelsoftware.airnote.presentation.screens.settings.SettingsScaffold
-import com.babelsoftware.airnote.presentation.screens.settings.model.AppUpdateViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.model.UpdateState
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-import com.babelsoftware.airnote.util.ReleaseInfo
-import com.babelsoftware.airnote.util.getLatestReleaseInfo
-import com.babelsoftware.airnote.util.translateText
-import kotlinx.coroutines.launch
-import java.util.Locale
-
-
-@Composable
-fun AboutScreen(
- navController: NavController,
- settingsViewModel: SettingsViewModel
-) {
- val uriHandler = LocalUriHandler.current
- val cornerRadius = settingsViewModel.settings.value.cornerRadius.dp
-
- SettingsScaffold(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.about),
- onBackNavClicked = { navController.navigateUp() }
- ) {
- LazyColumn(
- modifier = Modifier.fillMaxWidth()
- ) {
- item {
- UpdateCard(settingsViewModel = settingsViewModel)
- Spacer(modifier = Modifier.height(18.dp))
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.build_type),
- description = settingsViewModel.build,
- icon = IconResource.Vector(Icons.Rounded.Build),
- actionType = ActionType.TEXT,
- radius = RoundedCornerShape(topStart = cornerRadius, topEnd = cornerRadius)
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.version),
- description = settingsViewModel.version,
- icon = IconResource.Vector(Icons.Rounded.Info),
- actionType = ActionType.TEXT,
- radius = RoundedCornerShape(0.dp)
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(R.string.ai_version),
- description = "v1.3 build v0.9.0",
- icon = IconResource.Vector(Icons.Rounded.AutoAwesome),
- actionType = ActionType.TEXT,
- radius = RoundedCornerShape(0.dp)
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(R.string.dex_version),
- description = "v0.2.0-beta",
- icon = IconResource.Vector(Icons.Rounded.DesktopWindows),
- actionType = ActionType.TEXT,
- radius = RoundedCornerShape(0.dp)
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.developer),
- description = stringResource(id = R.string.info_dev),
- icon = IconResource.Url("https://avatars.githubusercontent.com/u/178022701?v=4"),
- actionType = ActionType.TEXT,
- radius = RoundedCornerShape(bottomStart = cornerRadius, bottomEnd = cornerRadius)
- )
- Spacer(modifier = Modifier.height(18.dp))
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.instagram),
- icon = IconResource.Url("https://www.apkmirror.com/wp-content/themes/APKMirror/ap_resize/ap_resize.php?src=https%3A%2F%2Fdownloadr2.apkmirror.com%2Fwp-content%2Fuploads%2F2022%2F08%2F91%2F6309b6f6b92ef.png&w=96&h=96&q=100"),
- linkClicked = { uriHandler.openUri("https://instagram.com/airnoteapp") },
- actionType = ActionType.LINK,
- radius = RoundedCornerShape(topStart = cornerRadius, topEnd = cornerRadius, bottomStart = cornerRadius, bottomEnd = cornerRadius)
- )
- Spacer(modifier = Modifier.height(18.dp))
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.email),
- icon = IconResource.Vector(Icons.Rounded.Email),
- clipboardText = ConnectionConst.SUPPORT_MAIL,
- actionType = ActionType.CLIPBOARD,
- radius = RoundedCornerShape(topStart = cornerRadius, topEnd = cornerRadius)
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.source_code),
- icon = IconResource.Vector(Icons.Rounded.Download),
- actionType = ActionType.LINK,
- radius = RoundedCornerShape(0.dp),
- linkClicked = { uriHandler.openUri("https://github.com/RRechz/AirNote/") }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- size = 8.dp,
- title = stringResource(id = R.string.feature),
- icon = IconResource.Vector(Icons.Rounded.BugReport),
- linkClicked = { uriHandler.openUri(ConnectionConst.FEATURE_REQUEST) },
- actionType = ActionType.LINK,
- radius = RoundedCornerShape(bottomStart = cornerRadius, bottomEnd = cornerRadius)
- )
- }
- }
- }
-}
-
-@Composable
-fun UpdateCard(
- settingsViewModel: SettingsViewModel,
- updateViewModel: AppUpdateViewModel = hiltViewModel()
-) {
- val context = LocalContext.current
- val updateState by updateViewModel.updateState.collectAsState()
-
- var latestReleaseInfo by remember { mutableStateOf(null) }
- var isLoading by remember { mutableStateOf(true) }
- var changelogVisible by remember { mutableStateOf(false) }
-
- val scope = rememberCoroutineScope()
- var displayedChangelog by remember { mutableStateOf("") }
- var isTranslating by remember { mutableStateOf(false) }
- var isOriginalChangelog by remember { mutableStateOf(true) }
-
-
- LaunchedEffect(key1 = true) {
- if (latestReleaseInfo == null) {
- isLoading = true
- latestReleaseInfo = getLatestReleaseInfo()
- isLoading = false
- }
- }
-
- LaunchedEffect(latestReleaseInfo) {
- latestReleaseInfo?.let {
- val cleanOriginalChangelog = it.changelog.substringAfter("## Changelog", it.changelog).trim()
- displayedChangelog = cleanOriginalChangelog
- isOriginalChangelog = true
- isTranslating = false
- }
- }
-
- val installPermissionLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.StartActivityForResult()
- ) {
- if (context.canInstallUnknownApps()) {
- (updateState as? UpdateState.ReadyToInstall)?.let {
- installApk(context, it.apkUri)
- }
- } else {
- Toast.makeText(context, context.getString(R.string.update_card_install_permission_needed), Toast.LENGTH_LONG).show()
- }
- }
-
- if (isLoading) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- CircularProgressIndicator()
- Text(
- text = stringResource(id = R.string.update_card_checking_for_updates),
- style = MaterialTheme.typography.bodyMedium,
- textAlign = TextAlign.Center
- )
- }
- } else {
- val isUpdateAvailable = settingsViewModel.updateAvailable.value && latestReleaseInfo != null
- val isError = updateState is UpdateState.Failed
- val currentCornerRadius = settingsViewModel.settings.value.cornerRadius.dp
-
- val updateBrush = Brush.linearGradient(colors = listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.tertiary))
- val successBrush = Brush.linearGradient(colors = listOf(MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.tertiary.copy(alpha = 0.7f)))
- val errorBrush = Brush.linearGradient(colors = listOf(MaterialTheme.colorScheme.error, MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.8f)))
-
- val backgroundBrush = when {
- isError -> errorBrush
- isUpdateAvailable -> updateBrush
- else -> successBrush
- }
- val contentColor = MaterialTheme.colorScheme.onPrimary
-
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(currentCornerRadius))
- .background(backgroundBrush)
- .border(BorderStroke(1.dp, contentColor.copy(alpha = 0.3f)), RoundedCornerShape(currentCornerRadius))
- .animateContentSize(animationSpec = tween(500))
- .padding(horizontal = 20.dp, vertical = 24.dp)
- ) {
- Column {
- Row(verticalAlignment = Alignment.Top) {
- Box(
- modifier = Modifier
- .padding(top = 4.dp)
- .size(64.dp)
- .clip(CircleShape)
- .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.2f)),
- contentAlignment = Alignment.Center
- ) {
- val infiniteTransition = rememberInfiniteTransition(label = "icon animation")
- val iconOffsetY by infiniteTransition.animateFloat(
- initialValue = 0f,
- targetValue = if (isUpdateAvailable && !changelogVisible) -6f else 0f,
- animationSpec = infiniteRepeatable(animation = tween(1500), repeatMode = RepeatMode.Reverse), label = ""
- )
- val icon = when {
- isError -> Icons.Filled.ErrorOutline
- isUpdateAvailable -> Icons.Filled.CloudDownload
- else -> Icons.Filled.CheckCircle
- }
- Icon(
- imageVector = icon,
- contentDescription = "Update Status Icon",
- modifier = Modifier
- .size(40.dp)
- .offset(y = iconOffsetY.dp),
- tint = contentColor
- )
- }
-
- Spacer(modifier = Modifier.width(16.dp))
-
- Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.Center) {
- val updateStateType = remember(updateState) {
- when (updateState) {
- is UpdateState.Downloading -> "Downloading"
- is UpdateState.Failed -> "Failed"
- is UpdateState.Idle -> "Idle"
- is UpdateState.ReadyToInstall -> "ReadyToInstall"
- }
- }
-
- Crossfade(targetState = updateStateType, animationSpec = tween(300), label = "state crossfade") { stateType ->
- val currentState = updateState
- Column {
- if (isUpdateAvailable) {
- val info = latestReleaseInfo!!
- Text(text = stringResource(id = R.string.update_card_new_version_available), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = contentColor)
- Spacer(modifier = Modifier.height(4.dp))
- Text(text = "${settingsViewModel.version} → ${settingsViewModel.latestVersion.value}", style = MaterialTheme.typography.bodyMedium, color = contentColor.copy(alpha = 0.9f), fontWeight = FontWeight.SemiBold)
- Spacer(modifier = Modifier.height(16.dp))
-
- when (stateType) {
- "Idle" -> {
- Column(verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.Start) {
- Button(
- onClick = { if (info.apkDownloadUrl != null) updateViewModel.downloadAndInstallApk(info.apkDownloadUrl) else Toast.makeText(context, context.getString(R.string.update_card_download_link_not_found), Toast.LENGTH_SHORT).show() },
- colors = ButtonDefaults.buttonColors(containerColor = contentColor, contentColor = MaterialTheme.colorScheme.primary)
- ) {
- Icon(Icons.Default.ArrowDownward, contentDescription = null, modifier = Modifier.size(ButtonDefaults.IconSize))
- Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text(stringResource(id = R.string.update_card_download_now))
- }
- Row(verticalAlignment = Alignment.CenterVertically) {
- TextButton(onClick = { changelogVisible = !changelogVisible }, colors = ButtonDefaults.textButtonColors(contentColor = contentColor)) {
- Text(if (changelogVisible) stringResource(R.string.hide) else stringResource(R.string.whats_new))
- }
- if (changelogVisible) {
- Spacer(Modifier.width(8.dp))
- if (isTranslating) {
- CircularProgressIndicator(modifier = Modifier.size(24.dp), color = contentColor, strokeWidth = 2.dp)
- } else {
- TextButton(
- onClick = {
- scope.launch {
- isTranslating = true
- val originalChangelog = latestReleaseInfo?.changelog?.substringAfter("## Changelog", latestReleaseInfo?.changelog ?: "")?.trim() ?: ""
- if (isOriginalChangelog) {
- val translatedLines = StringBuilder()
- originalChangelog.lines().forEach { line ->
- val trimmedLine = line.trim()
- if (trimmedLine.startsWith("*") || trimmedLine.startsWith("-")) {
- val contentToTranslate = trimmedLine.substring(1).trim()
- val translatedContent = translateText(contentToTranslate, Locale.getDefault().language)
- translatedLines.append("* ${translatedContent ?: contentToTranslate}\n")
- } else {
- translatedLines.append("$line\n")
- }
- }
- displayedChangelog = translatedLines.toString().trim()
- isOriginalChangelog = false
- } else {
- displayedChangelog = originalChangelog
- isOriginalChangelog = true
- }
- isTranslating = false
- }
- },
- colors = ButtonDefaults.textButtonColors(contentColor = contentColor)
- ) {
- Icon(Icons.Rounded.Translate, contentDescription = "Translate", modifier = Modifier.size(ButtonDefaults.IconSize))
- Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text(if (isOriginalChangelog) stringResource(R.string.translate) else stringResource(R.string.original_text))
- }
- }
- }
- }
- }
- }
- "Downloading" -> {
- val progress = (currentState as? UpdateState.Downloading)?.progress ?: 0
- Column {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(text = stringResource(id = R.string.update_card_downloading, progress), style = MaterialTheme.typography.bodyMedium, color = contentColor, modifier = Modifier.weight(1f))
- IconButton(onClick = { updateViewModel.resetState() }) {
- Icon(Icons.Default.Cancel, contentDescription = "Cancel Download", tint = contentColor)
- }
- }
- Spacer(modifier = Modifier.height(4.dp))
- LinearProgressIndicator(progress = { if (progress > 0) progress / 100f else 0f }, modifier = Modifier.fillMaxWidth().height(8.dp).clip(CircleShape), color = contentColor, trackColor = contentColor.copy(alpha = 0.3f))
- }
- }
- "ReadyToInstall" -> {
- val apkUri = (currentState as? UpdateState.ReadyToInstall)?.apkUri
- if (apkUri != null) {
- Column {
- Text(text = stringResource(R.string.ready_to_setup), style = MaterialTheme.typography.bodyMedium, color = contentColor.copy(alpha = 0.9f))
- Spacer(modifier = Modifier.height(12.dp))
- Button(
- onClick = {
- if (context.canInstallUnknownApps()) installApk(context, apkUri)
- else installPermissionLauncher.launch(Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData(Uri.parse("package:${context.packageName}")))
- },
- colors = ButtonDefaults.buttonColors(containerColor = contentColor, contentColor = MaterialTheme.colorScheme.primary)
- ) {
- Icon(Icons.Default.RocketLaunch, contentDescription = null, modifier = Modifier.size(ButtonDefaults.IconSize))
- Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text(stringResource(id = R.string.update_card_install))
- }
- }
- }
- }
- }
- } else if (isError && currentState is UpdateState.Failed) {
- Text(text = stringResource(id = R.string.update_card_update_failed), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = contentColor)
- Spacer(modifier = Modifier.height(4.dp))
- Text(text = currentState.error, style = MaterialTheme.typography.bodyMedium, color = contentColor.copy(alpha = 0.9f))
- Spacer(modifier = Modifier.height(12.dp))
- Button(
- onClick = { updateViewModel.resetState() },
- colors = ButtonDefaults.buttonColors(containerColor = contentColor, contentColor = MaterialTheme.colorScheme.error)
- ) {
- Icon(Icons.Rounded.Refresh, contentDescription = null, modifier = Modifier.size(ButtonDefaults.IconSize))
- Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text(stringResource(id = R.string.update_card_try_again))
- }
- } else {
- Text(text = stringResource(id = R.string.update_card_app_up_to_date), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = contentColor)
- Spacer(modifier = Modifier.height(4.dp))
- Text(text = stringResource(R.string.using_latest_features), style = MaterialTheme.typography.bodyMedium, color = contentColor.copy(alpha = 0.9f))
- }
- }
- }
- }
- }
-
- if (changelogVisible && latestReleaseInfo != null) {
- Spacer(modifier = Modifier.height(16.dp))
- ChangelogContent(changelogText = displayedChangelog, color = contentColor)
- }
- }
- }
- }
-}
-
-@Composable
-private fun ChangelogContent(changelogText: String, color: Color) {
- Column {
- Text(text = stringResource(R.string.whats_new_this_version), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = color)
- Spacer(modifier = Modifier.height(8.dp))
- changelogText.lines().forEach { line ->
- if (line.trim().startsWith("*") || line.trim().startsWith("-")) {
- Row(modifier = Modifier.padding(bottom = 4.dp)) {
- Text("• ", color = color.copy(alpha = 0.9f), style = MaterialTheme.typography.bodyMedium)
- Text(text = line.trim().substring(1).trim(), style = MaterialTheme.typography.bodyMedium, color = color.copy(alpha = 0.9f), lineHeight = 20.sp)
- }
- } else if (line.isNotBlank()){
- Text(text = line, style = MaterialTheme.typography.bodyMedium, color = color.copy(alpha = 0.9f), lineHeight = 20.sp, modifier = Modifier.padding(bottom = 4.dp))
- }
- }
- }
-}
-
-
-private fun installApk(context: Context, uri: Uri) {
- val intent = Intent(Intent.ACTION_VIEW).apply {
- setDataAndType(uri, "application/vnd.android.package-archive")
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- context.startActivity(intent)
-}
-
-private fun Context.canInstallUnknownApps(): Boolean {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- packageManager.canRequestPackageInstalls()
- } else {
- true
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/AirNoteAi.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/AirNoteAi.kt
deleted file mode 100644
index 038535d..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/AirNoteAi.kt
+++ /dev/null
@@ -1,918 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings
-
-import androidx.compose.runtime.getValue
-import com.babelsoftware.airnote.domain.model.Settings
-import android.content.ActivityNotFoundException
-import android.widget.Toast
-import androidx.annotation.StringRes
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.HelpOutline
-import androidx.compose.material.icons.rounded.CheckCircle
-import androidx.compose.material.icons.rounded.Delete
-import androidx.compose.material.icons.rounded.Key
-import androidx.compose.material.icons.rounded.Memory
-import androidx.compose.material.icons.rounded.Search
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Button
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.RadioButton
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.material3.rememberModalBottomSheetState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.data.repository.GeminiModels
-import com.babelsoftware.airnote.presentation.screens.settings.SettingsScaffold
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-import kotlinx.coroutines.launch
-import java.lang.IllegalArgumentException
-
-private data class PerplexityModelInfo(val name: String, @StringRes val displayNameResId: Int)
-private object PerplexityModels {
- val supportedModels = listOf(
- PerplexityModelInfo(
- "pplx-7b-online",
- R.string.perplexity_models_pplx_7b_online
- ),
- PerplexityModelInfo(
- "sonar-medium-online",
- R.string.perplexity_models_sonar_medium_online
- )
- )
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun AirNoteAiSettingsScreen(navController: NavController, settingsViewModel: SettingsViewModel) {
- val snackbarHostState = remember { SnackbarHostState() }
- val scope = rememberCoroutineScope()
- val settings: Settings by settingsViewModel.settings
- val geminiSheetState = rememberModalBottomSheetState()
- var showGeminiBottomSheet by remember { mutableStateOf(false) }
- val perplexitySheetState = rememberModalBottomSheetState()
- var showPerplexityBottomSheet by remember { mutableStateOf(false) }
-
-
- LaunchedEffect(key1 = true) {
- settingsViewModel.uiEvent.collect { message ->
- scope.launch { snackbarHostState.showSnackbar(message) }
- }
- }
-
- if (showGeminiBottomSheet) {
- ModalBottomSheet(onDismissRequest = { showGeminiBottomSheet = false }, sheetState = geminiSheetState) {
- ApiKeyGuide()
- Spacer(modifier = Modifier.height(32.dp))
- }
- }
-
- if (showPerplexityBottomSheet) {
- ModalBottomSheet(onDismissRequest = { showPerplexityBottomSheet = false }, sheetState = perplexitySheetState) {
- PerplexityApiKeyGuide()
- Spacer(modifier = Modifier.height(32.dp))
- }
- }
-
- SettingsScaffold(
- settingsViewModel = settingsViewModel,
- title = stringResource(R.string.airnote_ai),
- snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
- onBackNavClicked = { navController.navigateUp() }
- ) {
- LazyColumn(modifier = Modifier.padding(top = 16.dp, start = 12.dp, end = 12.dp)) {
- item {
- ApiKeySetting(
- settingsViewModel = settingsViewModel,
- onHelpClick = { showGeminiBottomSheet = true },
- shape = shapeManager(
- isFirst = true,
- isLast = false,
- radius = settings.cornerRadius
- )
- )
- }
- item {
- ModelChoiceSetting(
- settingsViewModel = settingsViewModel,
- shape = shapeManager(
- isFirst = false,
- isLast = true,
- radius = settings.cornerRadius
- )
- )
- Spacer(modifier = Modifier.height(24.dp))
- }
- item {
- Column {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = stringResource(R.string.ai_service_perplexity),
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.SemiBold,
- modifier = Modifier
- .weight(1f)
- .padding(start = 4.dp)
- )
- IconButton(onClick = { showPerplexityBottomSheet = true }) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.HelpOutline,
- contentDescription = "Perplexity Help"
- )
- }
- }
- Spacer(modifier = Modifier.height(8.dp))
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(R.string.enable_perplexity_ai),
- description = if (settings.isPerplexityEnabled) stringResource(R.string.enabled) else stringResource(R.string.perplexity_ai_service_description),
- icon = IconResource.Vector(Icons.Rounded.Search),
- actionType = ActionType.SWITCH,
- variable = settings.isPerplexityEnabled,
- switchEnabled = { isEnabled ->
- settingsViewModel.update(settings.copy(isPerplexityEnabled = isEnabled))
- },
- radius = if (settings.isPerplexityEnabled) {
- shapeManager(isFirst = true, isLast = false, radius = settings.cornerRadius)
- } else {
- shapeManager(isBoth = true, radius = settings.cornerRadius)
- },
- containerColor = MaterialTheme.colorScheme.surfaceContainer
- )
- }
- }
- item {
- AnimatedVisibility(
- visible = settings.isPerplexityEnabled,
- enter = slideInVertically() + fadeIn(),
- exit = slideOutVertically() + fadeOut()
- ) {
- Column {
- PerplexityApiKeyBox(
- settingsViewModel = settingsViewModel,
- shape = RoundedCornerShape(0.dp)
- )
- PerplexityModelChoiceSetting(
- settingsViewModel = settingsViewModel,
- shape = shapeManager(
- isFirst = false,
- isLast = true,
- radius = settings.cornerRadius
- )
- )
- Spacer(modifier = Modifier.height(24.dp))
- }
- }
- if (!settings.isPerplexityEnabled) {
- Spacer(modifier = Modifier.height(24.dp))
- }
- }
-
- item {
- Text(
- text = stringResource(id = R.string.offline_translation_models),
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.primary,
- modifier = Modifier.padding(start = 4.dp, bottom = 8.dp)
- )
- }
- item {
- OfflineTranslationSettings(settingsViewModel = settingsViewModel)
- }
- }
- }
-}
-
-@Composable
-private fun ApiKeySetting(
- settingsViewModel: SettingsViewModel,
- onHelpClick: () -> Unit,
- shape: RoundedCornerShape
-) {
- val userApiKey by settingsViewModel.userApiKey
-
- Column {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = stringResource(R.string.ai_service_gemini),
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.SemiBold,
- modifier = Modifier
- .weight(1f)
- .padding(start = 4.dp)
- )
- IconButton(onClick = onHelpClick) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.HelpOutline,
- contentDescription = stringResource(R.string.api_key_guide_title)
- )
- }
- }
- Spacer(modifier = Modifier.height(8.dp))
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.gemini_api_key),
- description = if (userApiKey.isNotBlank()) "••••••••••••••••••••" else stringResource(R.string.not_set),
- icon = IconResource.Vector(Icons.Rounded.Key),
- actionType = ActionType.CUSTOM,
- radius = shape,
- customAction = { onDismiss ->
- ApiKeyPopup(
- settingsViewModel = settingsViewModel,
- onDismiss = onDismiss
- )
- }
- )
- }
-}
-
-@Composable
-private fun ModelChoiceSetting(
- settingsViewModel: SettingsViewModel,
- shape: RoundedCornerShape
-) {
- val settings = settingsViewModel.settings.value
- val selectedModelInfo = GeminiModels.supportedModels.find {
- it.name == settings.selectedModelName
- }
- val selectedModelDisplayName = if (selectedModelInfo != null) {
- stringResource(id = selectedModelInfo.displayNameResId)
- } else {
- settings.selectedModelName
- }
- val descriptionText = stringResource(
- R.string.model_in_use_prefix,
- selectedModelDisplayName
- )
-
- Column {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.model_to_use),
- description = descriptionText,
- icon = IconResource.Vector(Icons.Rounded.Memory),
- actionType = ActionType.CUSTOM,
- radius = shape,
- customAction = { onDismiss ->
- ModelChoicePopup(
- settingsViewModel = settingsViewModel,
- onDismiss = onDismiss
- )
- }
- )
- }
-}
-
-@Composable
-private fun PerplexityModelChoiceSetting(
- settingsViewModel: SettingsViewModel,
- shape: RoundedCornerShape
-) {
- val settings = settingsViewModel.settings.value
- val selectedModelInfo = PerplexityModels.supportedModels.find {
- it.name == settings.selectedPerplexityModelName
- }
- val selectedModelDisplayName = if (selectedModelInfo != null) {
- stringResource(id = selectedModelInfo.displayNameResId)
- } else {
- settings.selectedPerplexityModelName
- }
-
- val descriptionText = stringResource(
- R.string.model_in_use_prefix,
- selectedModelDisplayName
- )
-
- Column {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(R.string.model_to_use),
- description = descriptionText,
- icon = IconResource.Vector(Icons.Rounded.Memory),
- actionType = ActionType.CUSTOM,
- radius = shape,
- customAction = { onDismiss ->
- PerplexityModelChoicePopup(
- settingsViewModel = settingsViewModel,
- onDismiss = onDismiss
- )
- }
- )
- }
-}
-
-@Composable
-private fun PerplexityApiKeyBox(
- settingsViewModel: SettingsViewModel,
- shape: RoundedCornerShape
-) {
- val perplexityApiKey by settingsViewModel.perplexityApiKey
-
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(R.string.perplexity_api_key),
- description = if (perplexityApiKey.isNotBlank()) "••••••••••••••••••••" else stringResource(R.string.not_set),
- icon = IconResource.Vector(Icons.Rounded.Key),
- actionType = ActionType.CUSTOM,
- radius = shape,
- customAction = { onDismiss ->
- PerplexityApiKeyPopup(
- settingsViewModel = settingsViewModel,
- onDismiss = onDismiss
- )
- }
- )
-}
-
-
-@Composable
-private fun ApiKeyPopup(
- settingsViewModel: SettingsViewModel,
- onDismiss: () -> Unit
-) {
- val userApiKey by settingsViewModel.userApiKey
- val isApiKeyVerified by settingsViewModel.isApiKeyVerified
- val isVerifyingApiKey by settingsViewModel.isVerifyingApiKey
- var tempApiKey by remember { mutableStateOf(userApiKey) }
-
- Dialog(
- onDismissRequest = onDismiss,
- properties = DialogProperties(usePlatformDefaultWidth = false)
- ) {
- Surface(
- modifier = Modifier
- .fillMaxWidth(0.9f)
- .padding(16.dp),
- shape = RoundedCornerShape(32.dp),
- color = MaterialTheme.colorScheme.surfaceContainerLow,
- tonalElevation = 4.dp
- ) {
- Column(
- modifier = Modifier.padding(24.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = stringResource(R.string.gemini_api_key),
- style = MaterialTheme.typography.titleLarge,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(bottom = 16.dp)
- )
- OutlinedTextField(
- value = tempApiKey,
- onValueChange = { tempApiKey = it },
- modifier = Modifier.fillMaxWidth(),
- label = { Text(stringResource(R.string.gemini_api_key)) },
- singleLine = true,
- )
- Spacer(modifier = Modifier.height(16.dp))
- Button(
- onClick = {
- settingsViewModel.updateUserApiKey(tempApiKey)
- settingsViewModel.verifyUserApiKey()
- },
- enabled = !isVerifyingApiKey && tempApiKey.isNotBlank(),
- modifier = Modifier.fillMaxWidth()
- ) {
- if (isVerifyingApiKey) {
- CircularProgressIndicator(
- modifier = Modifier.size(24.dp),
- color = MaterialTheme.colorScheme.onPrimary
- )
- } else {
- Text(stringResource(R.string.save_and_check))
- }
- }
- AnimatedVisibility(
- visible = isApiKeyVerified && !isVerifyingApiKey,
- enter = slideInVertically { it } + fadeIn(),
- exit = slideOutVertically { it } + fadeOut()
- ) {
- Row(
- modifier = Modifier.padding(top = 16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- imageVector = Icons.Rounded.CheckCircle,
- contentDescription = stringResource(R.string.successfully_verified_icon_cd),
- tint = MaterialTheme.colorScheme.primary
- )
- Text(
- text = stringResource(R.string.successfully_verified),
- color = MaterialTheme.colorScheme.primary,
- modifier = Modifier.padding(start = 4.dp)
- )
- }
- }
- }
- }
- }
-}
-
-@Composable
-private fun ModelChoicePopup(
- settingsViewModel: SettingsViewModel,
- onDismiss: () -> Unit
-) {
- val settings = settingsViewModel.settings.value
- val models = GeminiModels.supportedModels
-
- Dialog(
- onDismissRequest = onDismiss,
- properties = DialogProperties(usePlatformDefaultWidth = false)
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth(0.8f)
- .background(
- color = MaterialTheme.colorScheme.surfaceContainerLow,
- shape = RoundedCornerShape(32.dp)
- )
- .padding(12.dp)
- ) {
- Text(
- text = stringResource(id = R.string.model_choice),
- textAlign = androidx.compose.ui.text.style.TextAlign.Center,
- fontWeight = FontWeight.Bold,
- modifier = Modifier
- .fillMaxWidth()
- .padding(20.dp),
- fontSize = 20.sp,
- )
- LazyColumn(
- modifier = Modifier
- .padding(bottom = 12.dp)
- .clip(RoundedCornerShape(32.dp))
- ) {
- itemsIndexed(models) { index, model ->
- val isFirst = index == 0
- val isLast = index == models.lastIndex
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .clip(
- shapeManager(
- isFirst = isFirst, isLast = isLast,
- radius = settings.cornerRadius
- )
- )
- .clickable {
- settingsViewModel.updateSelectedModel(model.name)
- onDismiss()
- },
- color = MaterialTheme.colorScheme.surfaceContainer,
- tonalElevation = 1.dp
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = stringResource(id = model.displayNameResId),
- modifier = Modifier.weight(1f),
- style = MaterialTheme.typography.titleSmall,
- fontWeight = FontWeight.Bold
- )
- RadioButton(
- selected = settings.selectedModelName == model.name,
- onClick = {
- settingsViewModel.updateSelectedModel(model.name)
- onDismiss()
- }
- )
- }
- }
- }
- }
- }
- }
-}
-
-@Composable
-private fun PerplexityModelChoicePopup(
- settingsViewModel: SettingsViewModel,
- onDismiss: () -> Unit
-) {
- val settings = settingsViewModel.settings.value
- val models = PerplexityModels.supportedModels
-
- Dialog(
- onDismissRequest = onDismiss,
- properties = DialogProperties(usePlatformDefaultWidth = false)
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth(0.8f)
- .background(
- color = MaterialTheme.colorScheme.surfaceContainerLow,
- shape = RoundedCornerShape(32.dp)
- )
- .padding(12.dp)
- ) {
- Text(
- text = stringResource(R.string.model_choice),
- textAlign = androidx.compose.ui.text.style.TextAlign.Center,
- fontWeight = FontWeight.Bold,
- modifier = Modifier
- .fillMaxWidth()
- .padding(20.dp),
- fontSize = 20.sp,
- )
- LazyColumn(
- modifier = Modifier
- .padding(bottom = 12.dp)
- .clip(RoundedCornerShape(32.dp))
- ) {
- itemsIndexed(models) { index, model ->
- val isFirst = index == 0
- val isLast = index == models.lastIndex
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .clip(
- shapeManager(
- isFirst = isFirst, isLast = isLast,
- radius = settings.cornerRadius
- )
- )
- .clickable {
- settingsViewModel.updateSelectedPerplexityModel(model.name)
- onDismiss()
- },
- color = MaterialTheme.colorScheme.surfaceContainer,
- tonalElevation = 1.dp
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = stringResource(id = model.displayNameResId),
- modifier = Modifier.weight(1f),
- style = MaterialTheme.typography.titleSmall,
- fontWeight = FontWeight.Bold
- )
- RadioButton(
- selected = settings.selectedPerplexityModelName == model.name,
- onClick = {
- settingsViewModel.updateSelectedPerplexityModel(model.name)
- onDismiss()
- }
- )
- }
- }
- }
- }
- }
- }
-}
-
-@Composable
-private fun PerplexityApiKeyPopup(
- settingsViewModel: SettingsViewModel,
- onDismiss: () -> Unit
-) {
- val perplexityApiKey by settingsViewModel.perplexityApiKey
- val isApiKeyVerified by settingsViewModel.isPerplexityApiKeyVerified
- val isVerifyingApiKey by settingsViewModel.isVerifyingPerplexityApiKey
- var tempApiKey by remember { mutableStateOf(perplexityApiKey) }
-
- Dialog(
- onDismissRequest = onDismiss,
- properties = DialogProperties(usePlatformDefaultWidth = false)
- ) {
- Surface(
- modifier = Modifier
- .fillMaxWidth(0.9f)
- .padding(16.dp),
- shape = RoundedCornerShape(32.dp),
- color = MaterialTheme.colorScheme.surfaceContainerLow,
- tonalElevation = 4.dp
- ) {
- Column(
- modifier = Modifier.padding(24.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = stringResource(R.string.perplexity_api_key),
- style = MaterialTheme.typography.titleLarge,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(bottom = 16.dp)
- )
- OutlinedTextField(
- value = tempApiKey,
- onValueChange = { tempApiKey = it },
- modifier = Modifier.fillMaxWidth(),
- label = { Text(stringResource(R.string.perplexity_api_key)) },
- singleLine = true,
- )
- Spacer(modifier = Modifier.height(16.dp))
- Button(
- onClick = {
- settingsViewModel.updatePerplexityApiKey(tempApiKey)
- settingsViewModel.verifyPerplexityApiKey()
- },
- enabled = !isVerifyingApiKey && tempApiKey.isNotBlank(),
- modifier = Modifier.fillMaxWidth()
- ) {
- if (isVerifyingApiKey) {
- CircularProgressIndicator(
- modifier = Modifier.size(24.dp),
- color = MaterialTheme.colorScheme.onPrimary
- )
- } else {
- Text(stringResource(R.string.save_and_check))
- }
- }
- AnimatedVisibility(
- visible = isApiKeyVerified && !isVerifyingApiKey,
- enter = slideInVertically { it } + fadeIn(),
- exit = slideOutVertically { it } + fadeOut()
- ) {
- Row(
- modifier = Modifier.padding(top = 16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- imageVector = Icons.Rounded.CheckCircle,
- contentDescription = stringResource(R.string.successfully_verified_icon_cd),
- tint = MaterialTheme.colorScheme.primary
- )
- Text(
- text = stringResource(R.string.successfully_verified),
- color = MaterialTheme.colorScheme.primary,
- modifier = Modifier.padding(start = 4.dp)
- )
- }
- }
- }
- }
- }
-}
-
-
-@Composable
-private fun OfflineTranslationSettings(settingsViewModel: SettingsViewModel) {
- val downloadedModels by settingsViewModel.downloadedModels.collectAsState()
- val processingLanguageCode by settingsViewModel.processingLanguageCode.collectAsState()
- val context = LocalContext.current
- var showDeleteDialog by remember { mutableStateOf(null) }
-
- if (showDeleteDialog != null) {
- val langCodeToDelete = showDeleteDialog!!
- val langNameToDelete = settingsViewModel.geminiRepository.supportedLanguages[langCodeToDelete] ?: langCodeToDelete
-
- AlertDialog(
- onDismissRequest = { showDeleteDialog = null },
- title = { Text(text = stringResource(R.string.delete_language_model_title, langNameToDelete)) },
- text = { Text(text = stringResource(R.string.delete_language_model_body)) },
- confirmButton = {
- TextButton(
- onClick = {
- settingsViewModel.deleteLanguageModel(langCodeToDelete) { _, message ->
- Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
- }
- showDeleteDialog = null
- }
- ) {
- Text(stringResource(R.string.delete))
- }
- },
- dismissButton = {
- TextButton(onClick = { showDeleteDialog = null }) {
- Text(stringResource(R.string.cancel))
- }
- }
- )
- }
-
- Column(modifier = Modifier.fillMaxWidth()) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- ) {
- Column {
- settingsViewModel.geminiRepository.supportedLanguages.forEach { (code, name) ->
- val isDownloaded = downloadedModels.contains(code)
-
- ListItem(
- headlineContent = { Text(name) },
- supportingContent = {
- Text(if (isDownloaded) stringResource(R.string.downloaded) else stringResource(R.string.not_downloaded))
- },
- trailingContent = {
- if (processingLanguageCode == code) {
- CircularProgressIndicator(modifier = Modifier.size(24.dp))
- } else {
- if (isDownloaded) {
- IconButton(
- onClick = { showDeleteDialog = code },
- enabled = processingLanguageCode == null
- ) {
- Icon(
- imageVector = Icons.Rounded.Delete,
- contentDescription = stringResource(R.string.delete_language_model_cd, name),
- tint = MaterialTheme.colorScheme.error
- )
- }
- } else {
- TextButton(
- onClick = {
- settingsViewModel.downloadLanguageModel(code) { _, message ->
- Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
- }
- },
- enabled = processingLanguageCode == null
- ) {
- Text(stringResource(id = R.string.download))
- }
- }
- }
- }
- )
- }
- }
- }
- }
-}
-
-
-@Composable
-private fun ApiKeyGuide() {
- val uriHandler = LocalUriHandler.current
- val geminiStudioUrl = "https://aistudio.google.com/app/apikey"
- val context = LocalContext.current
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp, vertical = 8.dp)
- ) {
- Text(
- text = stringResource(id = R.string.api_key_guide_title),
- style = MaterialTheme.typography.titleLarge,
- modifier = Modifier.padding(bottom = 16.dp)
- )
- LazyColumn {
- item {
- Text(
- text = stringResource(id = R.string.api_key_guide_intro),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.padding(bottom = 24.dp)
- )
- }
- item { Text(stringResource(id = R.string.api_key_guide_step_1)) }
- item { Spacer(modifier = Modifier.height(12.dp)) }
- item {
- Button(
- onClick = {
- try {
- uriHandler.openUri(geminiStudioUrl)
- } catch (e: ActivityNotFoundException) {
- Toast.makeText(context, "Web tarayıcısı bulunamadı.", Toast.LENGTH_SHORT).show()
- } catch (e: IllegalArgumentException) {
- Toast.makeText(context, "URL açılamadı. Cihazınızda bir web tarayıcı yüklü mü?", Toast.LENGTH_LONG).show()
- }
- },
- modifier = Modifier.fillMaxWidth()
- ) {
- Text(stringResource(id = R.string.api_key_guide_button))
- }
- }
- item { Spacer(modifier = Modifier.height(12.dp)) }
-
- items(
- listOf(
- R.string.api_key_guide_step_2,
- R.string.api_key_guide_step_3,
- R.string.api_key_guide_step_4,
- R.string.api_key_guide_step_5,
- R.string.api_key_guide_step_6
- )
- ) {
- Text(
- stringResource(id = it),
- modifier = Modifier.padding(vertical = 4.dp)
- )
- }
- }
- }
-}
-
-@Composable
-private fun PerplexityApiKeyGuide() {
- val uriHandler = LocalUriHandler.current
- val perplexityApiUrl = "https://perplexity.ai/settings/api"
- val context = LocalContext.current
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp, vertical = 8.dp)
- ) {
- Text(
- text = stringResource(R.string.api_key_guide_title),
- style = MaterialTheme.typography.titleLarge,
- modifier = Modifier.padding(bottom = 16.dp)
- )
- LazyColumn {
- item {
- Text(
- text = stringResource(R.string.perplexity_api_key_guide_intro),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.padding(bottom = 24.dp)
- )
- }
- item { Text(stringResource(R.string.perplexity_api_key_guide_step_1)) }
- item { Spacer(modifier = Modifier.height(12.dp)) }
- item {
- Button(
- onClick = {
- try {
- uriHandler.openUri(perplexityApiUrl)
- } catch (e: ActivityNotFoundException) {
- Toast.makeText(context, "Web tarayıcısı bulunamadı.", Toast.LENGTH_SHORT).show()
- } catch (e: IllegalArgumentException) {
- Toast.makeText(context, "URL açılamadı. Cihazınızda bir web tarayıcı yüklü mü?", Toast.LENGTH_LONG).show()
- }
- },
- modifier = Modifier.fillMaxWidth()
- ) {
- Text(stringResource(R.string.perplexity_api_key_guide_button))
- }
- }
- item { Spacer(modifier = Modifier.height(12.dp)) }
-
- items(
- listOf(
- R.string.perplexity_api_key_guide_step_2,
- R.string.perplexity_api_key_guide_step_3,
- R.string.perplexity_api_key_guide_step_4,
- R.string.perplexity_api_key_guide_step_5,
- R.string.perplexity_api_key_guide_step_6,
- R.string.perplexity_api_key_guide_step_7
- )
- ) {
- Text(
- stringResource(id = it),
- modifier = Modifier.padding(vertical = 4.dp)
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Backup.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Backup.kt
deleted file mode 100644
index 2a00f5e..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Backup.kt
+++ /dev/null
@@ -1,276 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings
-
-import android.content.Context
-import android.widget.Toast
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Backup
-import androidx.compose.material.icons.rounded.EnhancedEncryption
-import androidx.compose.material.icons.rounded.FileOpen
-import androidx.compose.material.icons.rounded.ImportExport
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.AutofillType
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.constant.DatabaseConst
-import com.babelsoftware.airnote.presentation.screens.edit.components.CustomTextField
-import com.babelsoftware.airnote.presentation.screens.settings.SettingsScaffold
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-import java.time.LocalDateTime
-import java.time.format.DateTimeFormatter
-
-@Composable
-fun CloudScreen(navController: NavController, settingsViewModel: SettingsViewModel) {
- val context = LocalContext.current
-
- val exportBackupLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.CreateDocument("*/.zip"),
- onResult = { uri ->
- if (uri != null) settingsViewModel.onExportBackup(uri, context)
- }
- )
- val importBackupLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.OpenDocument(),
- onResult = { uri ->
- if (uri != null) settingsViewModel.onImportBackup(uri, context)
- }
- )
-
- val importFileLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.OpenMultipleDocuments(),
- onResult = { uris ->
- settingsViewModel.onImportFiles(uris, context)
- }
- )
-
- SettingsScaffold(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.backup),
- onBackNavClicked = { navController.navigateUp() }
- ) {
- LazyColumn {
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.encrypt_databse),
- description = stringResource(id = R.string.encrypt_databse_description),
- icon = IconResource.Vector(Icons.Rounded.EnhancedEncryption),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isBoth = true
- ),
- variable = settingsViewModel.settings.value.encryptBackup,
- actionType = ActionType.SWITCH,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- encryptBackup = it
- )
- )
- }
- )
- Spacer(modifier = Modifier.height(18.dp))
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.backup),
- description = stringResource(id = R.string.backup_description),
- icon = IconResource.Vector(Icons.Rounded.Backup),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isFirst = true
- ),
- actionType = ActionType.CUSTOM,
- customAction = { onExit ->
- if (settingsViewModel.settings.value.encryptBackup) {
- PasswordPrompt(
- context = context,
- text = stringResource(id = R.string.backup),
- settingsViewModel = settingsViewModel,
- onExit = { password ->
- if (password != null) {
- settingsViewModel.password = password.text
- }
- onExit()
- },
- onBackup = {
- exportBackupLauncher.launch("${DatabaseConst.NOTES_DATABASE_BACKUP_NAME}-${currentDateTime()}.zip")
- }
- )
- } else {
- LaunchedEffect(true) {
- exportBackupLauncher.launch("${DatabaseConst.NOTES_DATABASE_BACKUP_NAME}-${currentDateTime()}.zip")
- }
- }
- }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.restore),
- description = stringResource(id = R.string.restore_description),
- icon = IconResource.Vector(Icons.Rounded.ImportExport),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = true
- ),
- actionType = ActionType.CUSTOM,
- customAction = { onExit ->
- if (settingsViewModel.settings.value.encryptBackup) {
- PasswordPrompt(
- context = context,
- text = stringResource(id = R.string.restore),
- settingsViewModel = settingsViewModel,
- onExit = { password ->
- if (password != null) {
- settingsViewModel.password = password.text
- }
- onExit()
- },
- onBackup = {
- importBackupLauncher.launch(arrayOf("application/zip"))
- }
- )
- } else {
- LaunchedEffect(true) {
- settingsViewModel.password = null
- importBackupLauncher.launch(arrayOf("application/zip"))
- }
- }
- }
- )
- Spacer(modifier = Modifier.height(18.dp))
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = context.getString(R.string.file_import_title),
- description = context.getString(R.string.file_import_description),
- icon = IconResource.Vector(Icons.Rounded.FileOpen),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isBoth = true
- ),
- actionType = ActionType.CUSTOM,
- customAction = {
- LaunchedEffect(true) {
- importFileLauncher.launch(arrayOf("text/*"))
- }
- }
- )
- }
- }
- }
-}
-
-fun currentDateTime(): String {
- val currentDateTime = LocalDateTime.now()
- val formatter = DateTimeFormatter.ofPattern("MM-dd-HH-mm-ms")
- val formattedDateTime = currentDateTime.format(formatter)
-
- return formattedDateTime
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-@Composable
-fun PasswordPrompt(context: Context, text: String, settingsViewModel: SettingsViewModel, onExit: (TextFieldValue?) -> Unit, onBackup: () -> Unit = {}) {
- var password by remember { mutableStateOf(TextFieldValue("")) }
- val focusManager = LocalFocusManager.current
-
- Dialog(
- onDismissRequest = { onExit(null) },
- properties = DialogProperties(usePlatformDefaultWidth = false),
- ) {
- LazyColumn {
- item {
- Column(
- verticalArrangement = Arrangement.Center,
- modifier = Modifier
- .fillMaxWidth(0.8f)
- .fillMaxHeight(0.2f)
- .background(
- color = MaterialTheme.colorScheme.surfaceContainerLow,
- shape = shapeManager(
- isBoth = true,
- radius = settingsViewModel.settings.value.cornerRadius
- )
- )
- ) {
- Box(
- modifier = Modifier
- .padding(12.dp)
- .background(
- color = MaterialTheme.colorScheme.surfaceContainerHighest,
- shape = shapeManager(
- isBoth = true,
- radius = settingsViewModel.settings.value.cornerRadius
- )
- )
- ) {
- CustomTextField(
- hideContent = true,
- value = password,
- onValueChange = { password = it },
- placeholder = stringResource(id = R.string.password_prompt),
- autofillTypes = listOf(AutofillType.Password)
- )
- }
- Button(
- modifier = Modifier
- .padding(12.dp)
- .wrapContentWidth()
- .align(Alignment.End),
- onClick = {
- if (password.text.isNotBlank()) {
- focusManager.clearFocus() // Clear focus to dismiss autofill
- onExit(password)
- onBackup()
- } else {
- Toast.makeText(context, R.string.invalid_input, Toast.LENGTH_SHORT).show()
- }
- },
- content = {
- Text(text)
- }
- )
- }
- }
- }
- }
-}
-
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Behaviour.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Behaviour.kt
deleted file mode 100644
index 4307711..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Behaviour.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings
-
-import android.provider.MediaStore
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Edit
-import androidx.compose.material.icons.rounded.FolderOpen
-import androidx.compose.material.icons.rounded.Image
-import androidx.compose.material.icons.rounded.Style
-import androidx.compose.material.icons.rounded.Swipe
-import androidx.compose.material.icons.rounded.Title
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.presentation.components.unregisterGalleryObserver
-import com.babelsoftware.airnote.presentation.screens.settings.SettingsScaffold
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-
-@Composable
-fun MarkdownScreen(navController: NavController, settingsViewModel: SettingsViewModel) {
- SettingsScaffold(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.Behavior),
- onBackNavClicked = { navController.navigateUp() }
- ) {
- val context = LocalContext.current
- LazyColumn {
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.markdown),
- description = stringResource(id = R.string.markdown_description),
- icon = IconResource.Vector(Icons.Rounded.Style),
- actionType = ActionType.SWITCH,
- radius = shapeManager(
- isBoth = true,
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- variable = settingsViewModel.settings.value.isMarkdownEnabled,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- isMarkdownEnabled = it
- )
- )
- }
- )
- Spacer(modifier = Modifier.height(18.dp))
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.always_edit),
- description = stringResource(id = R.string.always_edit_description),
- icon = IconResource.Vector(Icons.Rounded.Edit),
- actionType = ActionType.SWITCH,
- radius = shapeManager(
- isFirst = true,
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- variable = settingsViewModel.settings.value.editMode,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- editMode = it
- )
- )
- }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.gallery_sync),
- description = stringResource(id = R.string.gallery_sync_description),
- icon = IconResource.Vector(Icons.Rounded.Image),
- actionType = ActionType.SWITCH,
- radius = shapeManager(
- isLast = true,
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- variable = settingsViewModel.settings.value.gallerySync,
- switchEnabled = {
- if (!it) {
- unregisterGalleryObserver(context, settingsViewModel.galleryObserver)
- } else {
- context.contentResolver.registerContentObserver(
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- true,
- settingsViewModel.galleryObserver
- )
- }
- settingsViewModel.update(settingsViewModel.settings.value.copy(gallerySync = it))
- }
- )
- }
-
- item {
- Spacer(modifier = Modifier.height(18.dp))
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.show_only_title),
- description = stringResource(id = R.string.show_only_title_description),
- icon = IconResource.Vector(Icons.Rounded.Title),
- actionType = ActionType.SWITCH,
- radius = shapeManager(
- isFirst = true,
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- variable = settingsViewModel.settings.value.showOnlyTitle,
- switchEnabled = {
- settingsViewModel.update(settingsViewModel.settings.value.copy(showOnlyTitle = it))
- }
- )
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.disable_swipe_edit),
- description = stringResource(id = R.string.disable_swipe_edit_description),
- icon = IconResource.Vector(Icons.Rounded.Swipe),
- actionType = ActionType.SWITCH,
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- variable = settingsViewModel.settings.value.disableSwipeInEditMode,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- disableSwipeInEditMode = it
- )
- )
- }
- )
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.open_last_folder),
- description = stringResource(id = R.string.open_last_folder_description),
- icon = IconResource.Vector(Icons.Rounded.FolderOpen),
- actionType = ActionType.SWITCH,
- radius = shapeManager(
- isLast = true,
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- variable = settingsViewModel.settings.value.openToLastUsedFolder,
- switchEnabled = { isEnabled ->
- settingsViewModel.update(
- if (isEnabled) {
- // Enable the feature. The next folder selection will be saved.
- settingsViewModel.settings.value.copy(openToLastUsedFolder = true)
- } else {
- // When disabling the feature, clear the last used folder ID
- // to ensure a clean state for the next time it's enabled.
- settingsViewModel.settings.value.copy(
- openToLastUsedFolder = false,
- lastUsedFolderId = null
- )
- }
- )
- }
- )
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Colors.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Colors.kt
deleted file mode 100644
index 30ad2a8..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Colors.kt
+++ /dev/null
@@ -1,472 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.Label
-import androidx.compose.material.icons.automirrored.rounded.Sort
-import androidx.compose.material.icons.rounded.Battery1Bar
-import androidx.compose.material.icons.rounded.Colorize
-import androidx.compose.material.icons.rounded.DarkMode
-import androidx.compose.material.icons.rounded.DynamicFeed
-import androidx.compose.material.icons.rounded.FormatSize
-import androidx.compose.material.icons.rounded.GridView
-import androidx.compose.material.icons.rounded.HdrAuto
-import androidx.compose.material.icons.rounded.Palette
-import androidx.compose.material.icons.rounded.RoundedCorner
-import androidx.compose.material.icons.rounded.Search
-import androidx.compose.material.icons.rounded.ViewAgenda
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Slider
-import androidx.compose.material3.SliderDefaults
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.window.Dialog
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.presentation.screens.settings.SettingsScaffold
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-
-fun shapeManager(isBoth: Boolean = false,isLast: Boolean = false,isFirst: Boolean = false,radius: Int): RoundedCornerShape {
- val smallerRadius: Dp = (radius/5).dp
- val defaultRadius: Dp = radius.dp
-
- return when {
- isBoth -> RoundedCornerShape(defaultRadius)
- isLast -> RoundedCornerShape(smallerRadius,smallerRadius,defaultRadius,defaultRadius)
- isFirst -> RoundedCornerShape(defaultRadius,defaultRadius,smallerRadius,smallerRadius)
- else -> RoundedCornerShape(smallerRadius)
- }
-}
-
-@Composable
-fun ColorStylesScreen(navController: NavController, settingsViewModel: SettingsViewModel) {
- SettingsScaffold(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.color_styles),
- onBackNavClicked = { navController.navigateUp() }
- ) {
- LazyColumn {
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.minimalistic_mode),
- description = stringResource(id = R.string.minimalistic_mode_description),
- icon = IconResource.Vector(Icons.Rounded.DynamicFeed),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isBoth = true
- ),
- actionType = ActionType.SWITCH,
- variable = settingsViewModel.settings.value.minimalisticMode,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- minimalisticMode = it
- )
- )
- }
- )
- Spacer(modifier = Modifier.height(18.dp))
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.system_theme),
- description = stringResource(id = R.string.system_theme_description),
- icon = IconResource.Vector(Icons.Rounded.HdrAuto),
- actionType = ActionType.SWITCH,
- radius = shapeManager(
- isFirst = true,
- isBoth = (!isSystemInDarkTheme() && settingsViewModel.settings.value.automaticTheme),
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- variable = settingsViewModel.settings.value.automaticTheme,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- automaticTheme = it
- )
- )
- }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.dark_theme),
- description = stringResource(id = R.string.dark_theme_description),
- isEnabled = !settingsViewModel.settings.value.automaticTheme,
- icon = IconResource.Vector(Icons.Rounded.Palette),
- radius = shapeManager(radius = settingsViewModel.settings.value.cornerRadius),
- actionType = ActionType.SWITCH,
- variable = settingsViewModel.settings.value.darkTheme,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- automaticTheme = false,
- darkTheme = it
- )
- )
- }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.dynamic_colors),
- description = stringResource(id = R.string.dynamic_colors_description),
- icon = IconResource.Vector(Icons.Rounded.Colorize),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = !(settingsViewModel.settings.value.darkTheme)
- ),
- isEnabled = !settingsViewModel.settings.value.automaticTheme,
- actionType = ActionType.SWITCH,
- variable = settingsViewModel.settings.value.dynamicTheme,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- automaticTheme = false,
- dynamicTheme = it
- )
- )
- }
- )
- }
- item {
- val value = settingsViewModel.settings.value.amoledTheme
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.amoled_colors),
- description = stringResource(id = R.string.amoled_colors_description),
- icon = IconResource.Vector(Icons.Rounded.DarkMode),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = !value
- ),
- actionType = ActionType.SWITCH,
- isEnabled = settingsViewModel.settings.value.darkTheme,
- variable = value,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- amoledTheme = it
- )
- )
- }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.extreme_amoled_mode),
- icon = IconResource.Vector(Icons.Rounded.Battery1Bar),
- description = stringResource(id = R.string.extreme_amoled_mode_description),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = true
- ),
- actionType = ActionType.SWITCH,
- variable = settingsViewModel.settings.value.extremeAmoledMode,
- isEnabled = settingsViewModel.settings.value.amoledTheme && settingsViewModel.settings.value.darkTheme,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- extremeAmoledMode = it
- )
- )
- }
- )
- Spacer(modifier = Modifier.height(18.dp))
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.radius),
- description = stringResource(id = R.string.radius_description),
- icon = IconResource.Vector(Icons.Rounded.RoundedCorner),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isFirst = true
- ),
- actionType = ActionType.CUSTOM,
- customAction = { onExit ->
- SettingsSliderDialog(
- title = stringResource(id = R.string.select_radius),
- valueLabelText = "${settingsViewModel.settings.value.cornerRadius} dp",
- initialValue = settingsViewModel.settings.value.cornerRadius.toFloat(),
- valueRange = 5f..35f,
- cornerRadius = settingsViewModel.settings.value.cornerRadius,
- onValueChange = { newValue ->
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(cornerRadius = newValue.toInt())
- )
- },
- onDismiss = { onExit() }
- ) { currentValue ->
- @Composable
- fun Example(shape: RoundedCornerShape) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(32.dp, 3.dp, 32.dp, 1.dp)
- .height(62.dp)
- .background(
- shape = shape,
- color = MaterialTheme.colorScheme.surfaceContainerHigh
- )
- )
- }
- Example(shapeManager(radius = currentValue.toInt(), isFirst = true))
- Example(shapeManager(radius = currentValue.toInt()))
- Example(shapeManager(radius = currentValue.toInt(), isLast = true))
- }
- }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = if (settingsViewModel.settings.value.viewMode) stringResource(id = R.string.grid_view) else stringResource(
- id = R.string.column_view
- ),
- icon = if (settingsViewModel.settings.value.viewMode) IconResource.Vector(Icons.Rounded.GridView) else IconResource.Vector(Icons.Rounded.ViewAgenda),
- description = stringResource(id = R.string.view_style_description),
- radius = shapeManager(radius = settingsViewModel.settings.value.cornerRadius),
- actionType = ActionType.SWITCH,
- variable = settingsViewModel.settings.value.viewMode,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- viewMode = it
- )
- )
- }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(R.string.folder_tag),
- description = stringResource(R.string.description_folder_tag),
- icon = IconResource.Vector(Icons.AutoMirrored.Rounded.Label),
- radius = shapeManager(radius = settingsViewModel.settings.value.cornerRadius),
- actionType = ActionType.SWITCH,
- variable = settingsViewModel.settings.value.showFolderIndicator,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- showFolderIndicator = it
- )
- )
- }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = if (settingsViewModel.settings.value.sortDescending) stringResource(id = R.string.sort_descending) else stringResource(
- id = R.string.sort_ascending
- ),
- description = stringResource(id = R.string.sort_description),
- icon = IconResource.Vector(Icons.AutoMirrored.Rounded.Sort),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = true
- ),
- actionType = ActionType.SWITCH,
- variable = settingsViewModel.settings.value.sortDescending,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- sortDescending = it
- )
- )
- }
- )
- Spacer(modifier = Modifier.height(18.dp))
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.make_search_bar_longer),
- description = stringResource(id = R.string.make_search_bar_longer_description),
- icon = IconResource.Vector(Icons.Rounded.Search),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isFirst = true
- ),
- actionType = ActionType.SWITCH,
- variable = settingsViewModel.settings.value.makeSearchBarLonger,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- makeSearchBarLonger = it
- )
- )
- }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.font_size),
- description = stringResource(id = R.string.font_size_description),
- icon = IconResource.Vector(Icons.Rounded.FormatSize),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = true
- ),
- actionType = ActionType.CUSTOM,
- customAction = { onExit ->
- SettingsSliderDialog(
- title = stringResource(id = R.string.font_size),
- valueLabelText = stringResource(id = R.string.font_size_value, settingsViewModel.settings.value.fontSize),
- initialValue = settingsViewModel.settings.value.fontSize.toFloat(),
- valueRange = 12f..18f,
- cornerRadius = settingsViewModel.settings.value.cornerRadius,
- onValueChange = { newValue ->
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(fontSize = newValue.toInt())
- )
- },
- onDismiss = { onExit() }
- ) { currentValue ->
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- .background(
- color = MaterialTheme.colorScheme.surfaceContainerHigh,
- shape = RoundedCornerShape(settingsViewModel.settings.value.cornerRadius / 2)
- )
- .padding(16.dp)
- ) {
- Text(
- text = stringResource(id = R.string.font_size_preview_title),
- style = MaterialTheme.typography.titleMedium,
- modifier = Modifier.padding(bottom = 8.dp)
- )
- Text(
- text = stringResource(id = R.string.font_size_preview_text),
- fontSize = currentValue.toInt().sp,
- fontWeight = FontWeight.Normal,
- modifier = Modifier.padding(vertical = 8.dp)
- )
- }
- }
- }
- )
- }
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun SettingsSliderDialog(
- title: String,
- valueLabelText: String,
- initialValue: Float,
- valueRange: ClosedFloatingPointRange,
- cornerRadius: Int,
- onValueChange: (Float) -> Unit,
- onDismiss: () -> Unit,
- previewContent: @Composable (currentValue: Float) -> Unit
-) {
- var sliderPosition by remember { mutableFloatStateOf(initialValue) }
-
- Dialog(onDismissRequest = onDismiss) {
- Column(
- modifier = Modifier
- .background(
- color = MaterialTheme.colorScheme.surfaceContainerLow,
- shape = RoundedCornerShape(cornerRadius / 3)
- )
- .fillMaxWidth()
- ) {
- Text(
- text = title,
- textAlign = TextAlign.Center,
- fontWeight = FontWeight.Bold,
- style = MaterialTheme.typography.titleLarge,
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 24.dp, bottom = 8.dp)
- )
-
- Text(
- text = valueLabelText,
- textAlign = TextAlign.Center,
- style = MaterialTheme.typography.bodyLarge,
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 16.dp)
- )
-
- previewContent(sliderPosition)
-
- Slider(
- value = sliderPosition,
- onValueChange = {
- sliderPosition = it
- onValueChange(it)
- },
- valueRange = valueRange,
- steps = (valueRange.endInclusive - valueRange.start).toInt() - 1,
- modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
- colors = SliderDefaults.colors(inactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHigh)
- )
-
- // Onay Butonu
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 24.dp, top = 8.dp),
- contentAlignment = Alignment.Center
- ) {
- Box(
- modifier = Modifier
- .background(
- color = MaterialTheme.colorScheme.primary,
- shape = RoundedCornerShape(cornerRadius / 2)
- )
- .clickable { onDismiss() }
- .padding(horizontal = 24.dp, vertical = 12.dp)
- ) {
- Text(
- text = stringResource(id = R.string.agree),
- color = MaterialTheme.colorScheme.onPrimary,
- fontWeight = FontWeight.Bold
- )
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Crypto.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Crypto.kt
deleted file mode 100644
index e1c01e5..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Crypto.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings
-
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.AccountBalanceWallet
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.stringResource
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.constant.SupportConst
-import com.babelsoftware.airnote.presentation.screens.settings.SettingsScaffold
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-
-@Composable
-fun SupportScreen(navController: NavController, settingsViewModel: SettingsViewModel) {
- SettingsScaffold(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.cryptocurrency),
- onBackNavClicked = { navController.navigateUp() }
- ) {
- LazyColumn {
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = "Bitcoin (BTC)",
- icon = IconResource.Vector(Icons.Rounded.AccountBalanceWallet),
- actionType = ActionType.CLIPBOARD,
- radius = shapeManager(
- isFirst = true,
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- clipboardText = SupportConst.BITCOIN_ADDRESS
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = "Proton Wallet",
- icon = IconResource.Vector(Icons.Rounded.AccountBalanceWallet),
- actionType = ActionType.CLIPBOARD,
- radius = shapeManager(
- isLast = true,
- radius = settingsViewModel.settings.value.cornerRadius),
- clipboardText = SupportConst.PROTON_WALLET_ADDRESS
- )
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/DesktopModeSettingsScreen.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/DesktopModeSettingsScreen.kt
deleted file mode 100644
index a18b0bd..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/DesktopModeSettingsScreen.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings
-
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.AutoAwesome
-import androidx.compose.material.icons.rounded.DesktopWindows
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.stringResource
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.presentation.screens.settings.SettingsScaffold
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-
-@Composable
-fun DesktopModeSettingsScreen(navController: NavController, settingsViewModel: SettingsViewModel) {
- SettingsScaffold(
- settingsViewModel = settingsViewModel,
- title = stringResource(R.string.dex_mode),
- onBackNavClicked = { navController.navigateUp() }
- ) {
- LazyColumn {
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(R.string.dex_mode_uı),
- description = stringResource(R.string.dex_mode_uı_description),
- icon = IconResource.Vector(Icons.Rounded.DesktopWindows),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isFirst = true
- ),
- actionType = ActionType.SWITCH,
- variable = settingsViewModel.settings.value.desktopModeEnabled,
- switchEnabled = { isEnabled ->
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- desktopModeEnabled = isEnabled
- )
- )
- }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(R.string.dex_ai_features),
- description = stringResource(R.string.dex_ai_features_description),
- icon = IconResource.Vector(Icons.Rounded.AutoAwesome),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isLast = true
- ),
- actionType = ActionType.SWITCH,
- isEnabled = settingsViewModel.settings.value.desktopModeEnabled,
- variable = settingsViewModel.settings.value.desktopModeAiEnabled,
- switchEnabled = { isEnabled ->
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- desktopModeAiEnabled = isEnabled
- )
- )
- }
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Language.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Language.kt
deleted file mode 100644
index 50d697f..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Language.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings
-
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.FontDownload
-import androidx.compose.material.icons.rounded.Translate
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.core.os.LocaleListCompat
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.presentation.screens.settings.SettingsScaffold
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ListDialog
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-
-@Composable
-fun LanguageScreen(navController: NavController, settingsViewModel: SettingsViewModel) {
- SettingsScaffold(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.language),
- onBackNavClicked = { navController.navigateUp() }
- ) {
- LazyColumn {
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.language),
- description = stringResource(id = R.string.language_description),
- icon = IconResource.Vector(Icons.Rounded.Translate),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isBoth = true
- ),
- actionType = ActionType.CUSTOM,
- customAction = { onExit ->
- OnLanguageClicked(settingsViewModel) { onExit() }
- }
- )
- Spacer(modifier = Modifier.height(18.dp))
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.monospace_font),
- icon = IconResource.Vector(Icons.Rounded.FontDownload),
- description = stringResource(id = R.string.monospace_font_description),
- radius = shapeManager(
- radius = settingsViewModel.settings.value.cornerRadius,
- isBoth = true
- ),
- actionType = ActionType.SWITCH,
- variable = settingsViewModel.settings.value.useMonoSpaceFont,
- switchEnabled = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- useMonoSpaceFont = it
- )
- )
- }
- )
- }
- }
- }
-}
-
-@Composable
-private fun OnLanguageClicked(settingsViewModel: SettingsViewModel, onExit: () -> Unit) {
- val context = LocalContext.current
- val languages = settingsViewModel.getSupportedLanguages(context).toList()
- ListDialog(
- text = stringResource(R.string.language),
- list = languages,
- settingsViewModel = settingsViewModel,
- onExit = onExit,
- extractDisplayData = { it },
- initialItem = Pair(context.getString(R.string.system_language), second = ""),
- setting = { isFirstItem, isLastItem, displayData ->
- SettingsBox(
- settingsViewModel = settingsViewModel,
- size = 8.dp,
- title = displayData.first,
- icon = IconResource.Vector(Icons.Rounded.Translate),
- radius = shapeManager(
- isFirst = isFirstItem,
- isLast = isLastItem,
- radius = settingsViewModel.settings.value.cornerRadius
- ),
- actionType = ActionType.RADIOBUTTON,
- variable = if (displayData.second.isNotBlank()) {
- AppCompatDelegate.getApplicationLocales()[0]?.language == displayData.second
- } else {
- AppCompatDelegate.getApplicationLocales().isEmpty
- },
- switchEnabled = {
- if (displayData.second.isNotBlank()) {
- AppCompatDelegate.setApplicationLocales(
- LocaleListCompat.forLanguageTags(
- displayData.second
- )
- )
- } else {
- AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
- }
- }
- )
- }
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Privacy.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Privacy.kt
deleted file mode 100644
index 9eb5352..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Privacy.kt
+++ /dev/null
@@ -1,300 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings
-
-import android.widget.Toast
-import androidx.biometric.BiometricManager
-import androidx.biometric.BiometricPrompt
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.expandVertically
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.shrinkVertically
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Fingerprint
-import androidx.compose.material.icons.filled.Grid3x3
-import androidx.compose.material.icons.filled.Lock
-import androidx.compose.material.icons.filled.Password
-import androidx.compose.material.icons.filled.RemoveRedEye
-import androidx.compose.material.icons.rounded.Lock
-import androidx.compose.material.icons.rounded.Security
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Button
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.RadioButton
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.unit.dp
-import androidx.core.content.ContextCompat
-import androidx.fragment.app.FragmentActivity
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.presentation.navigation.NavRoutes
-import com.babelsoftware.airnote.presentation.screens.settings.SettingsScaffold
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-
-@Composable
-fun PrivacyScreen(navController: NavController, settingsViewModel: SettingsViewModel) {
- val settings = settingsViewModel.settings.value
- val context = LocalContext.current
-
- SettingsScaffold(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.privacy),
- onBackNavClicked = { navController.navigateUp() }
- ) {
- LazyColumn {
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.screen_protection),
- description = stringResource(id = R.string.screen_protection_description),
- icon = IconResource.Vector(Icons.Filled.RemoveRedEye),
- radius = shapeManager(radius = settings.cornerRadius, isBoth = true),
- actionType = ActionType.SWITCH,
- variable = settings.screenProtection,
- switchEnabled = { settingsViewModel.update(settings.copy(screenProtection = it)) }
- )
- Spacer(modifier = Modifier.height(18.dp))
- }
-
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.vault),
- description = stringResource(id = R.string.vault_description),
- icon = IconResource.Vector(Icons.Rounded.Security),
- radius = shapeManager(radius = settings.cornerRadius, isFirst = true),
- actionType = ActionType.CUSTOM,
- customAction = { onDismiss ->
- if (settings.vaultSettingEnabled) {
- settingsViewModel.update(settings.copy(vaultSettingEnabled = false))
- onDismiss()
- } else {
- var password by remember { mutableStateOf("") }
- var encryptionType by remember { mutableStateOf("AES-256") }
-
- AlertDialog(
- onDismissRequest = { onDismiss() },
- title = { Text(stringResource(R.string.vault_setup)) },
- text = {
- Column {
- Text(stringResource(R.string.set_a_password_vault))
- Spacer(modifier = Modifier.height(8.dp))
- OutlinedTextField(
- value = password,
- onValueChange = { password = it },
- label = { Text(stringResource(R.string.vault_password)) },
- visualTransformation = PasswordVisualTransformation(),
- singleLine = true
- )
- Spacer(modifier = Modifier.height(16.dp))
- Text(stringResource(R.string.encryption_type))
- Row(verticalAlignment = Alignment.CenterVertically) {
- RadioButton(selected = encryptionType == "AES-256", onClick = { encryptionType = "AES-256" })
- Text("AES-256")
- }
- Row(verticalAlignment = Alignment.CenterVertically) {
- RadioButton(selected = encryptionType == "ChaCha20", onClick = { encryptionType = "ChaCha20" })
- Text("ChaCha20")
- }
- }
- },
- confirmButton = {
- Button(onClick = {
- if (password.isNotBlank()) {
- settingsViewModel.update(settings.copy(
- vaultSettingEnabled = true,
- vaultEnabled = true,
- vaultEncryptionType = encryptionType
- ))
- Toast.makeText(context, context.getString(R.string.vault_created), Toast.LENGTH_SHORT).show()
- onDismiss()
- } else {
- Toast.makeText(context, context.getString(R.string.password_not_empty), Toast.LENGTH_SHORT).show()
- }
- }) { Text(stringResource(R.string.activate)) }
- },
- dismissButton = {
- TextButton(onClick = { onDismiss() }) { Text(stringResource(R.string.cancel)) }
- }
- )
- }
- }
- )
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(R.string.note_lock_password),
- description = if (settings.noteLockPassword != null) "********" else stringResource(R.string.not_set),
- icon = IconResource.Vector(Icons.Rounded.Lock),
- radius = shapeManager(radius = settings.cornerRadius, isLast = true),
- actionType = ActionType.CUSTOM,
- customAction = { onDismiss ->
- var notePass by remember { mutableStateOf("") }
- AlertDialog(
- onDismissRequest = { onDismiss() },
- title = { Text(stringResource(R.string.note_lock_password)) },
- text = {
- Column {
- Text(stringResource(R.string.set_a_password_individual_notes))
- Spacer(modifier = Modifier.height(8.dp))
- OutlinedTextField(
- value = notePass,
- onValueChange = { notePass = it },
- label = { Text(stringResource(R.string.note_password)) },
- visualTransformation = PasswordVisualTransformation(),
- singleLine = true
- )
- }
- },
- confirmButton = {
- Button(onClick = {
- if (notePass.isNotEmpty()) {
- settingsViewModel.update(settings.copy(noteLockPassword = notePass))
- Toast.makeText(context, context.getString(R.string.passoword_updated), Toast.LENGTH_SHORT).show()
- onDismiss()
- }
- }) { Text(stringResource(R.string.save)) }
- },
- dismissButton = {
- TextButton(onClick = { onDismiss() }) { Text(stringResource(R.string.cancel)) }
- }
- )
- }
- )
- Spacer(modifier = Modifier.height(18.dp))
- }
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- radius = shapeManager(
- radius = settings.cornerRadius,
- isBoth = !settings.appLockEnabled,
- isFirst = settings.appLockEnabled
- ),
- title = stringResource(id = R.string.app_lock),
- description = if (settings.appLockEnabled) stringResource(id = R.string.app_lock_description_active) else stringResource(id = R.string.app_lock_description),
- icon = IconResource.Vector(Icons.Filled.Lock),
- actionType = ActionType.SWITCH,
- variable = settings.appLockEnabled,
- switchEnabled = { enabled ->
- settingsViewModel.update(settings.copy(appLockEnabled = enabled))
- if (!enabled) settingsViewModel.defaultRoute = NavRoutes.Home.route
- }
- )
- }
- item {
- AnimatedVisibility(
- visible = settings.appLockEnabled,
- enter = expandVertically() + fadeIn(),
- exit = shrinkVertically() + fadeOut()
- ) {
- Column {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.passcode),
- description = if (!settings.passcode.isNullOrBlank()) stringResource(id = R.string.active) else stringResource(R.string.tap_to_setup),
- icon = IconResource.Vector(Icons.Filled.Password),
- radius = shapeManager(radius = settings.cornerRadius),
- actionType = ActionType.CUSTOM,
- customAction = { onDismiss ->
- var pass by remember { mutableStateOf("") }
- AlertDialog(
- onDismissRequest = { onDismiss() },
- title = { Text(stringResource(R.string.applock_password)) },
- text = {
- OutlinedTextField(
- value = pass,
- onValueChange = { pass = it },
- label = { Text(stringResource(R.string.new_password)) },
- visualTransformation = PasswordVisualTransformation(),
- singleLine = true
- )
- },
- confirmButton = {
- Button(onClick = {
- if (pass.length >= 4) {
- settingsViewModel.update(settings.copy(passcode = pass))
- settingsViewModel.updateDefaultRoute(NavRoutes.LockScreen.createRoute(null))
- Toast.makeText(context, context.getString(R.string.password_set), Toast.LENGTH_SHORT).show()
- onDismiss()
- } else {
- Toast.makeText(context, context.getString(R.string.enter_least_4), Toast.LENGTH_SHORT).show()
- }
- }) { Text(stringResource(R.string.save)) }
- },
- dismissButton = {
- TextButton(onClick = { onDismiss() }) { Text(stringResource(R.string.cancel)) }
- }
- )
- }
- )
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.fingerprint),
- description = if (settings.fingerprint) stringResource(id = R.string.active) else stringResource(R.string.tap_to_activate),
- icon = IconResource.Vector(Icons.Filled.Fingerprint),
- radius = shapeManager(radius = settings.cornerRadius),
- actionType = ActionType.SWITCH,
- variable = settings.fingerprint,
- switchEnabled = { isChecked ->
- if (isChecked) {
- checkBiometricSupport(context) {
- settingsViewModel.update(settings.copy(fingerprint = true))
- Toast.makeText(context, context.getString(R.string. fingerprint_activate), Toast.LENGTH_SHORT).show()
- }
- } else {
- settingsViewModel.update(settings.copy(fingerprint = false))
- }
- }
- )
- }
- }
- }
- }
- }
-}
-
-private fun checkBiometricSupport(context: android.content.Context, onSuccess: () -> Unit) {
- val biometricManager = BiometricManager.from(context)
- if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS) {
- val activity = context as? FragmentActivity ?: return
- val executor = ContextCompat.getMainExecutor(context)
- val callback = object : BiometricPrompt.AuthenticationCallback() {
- override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
- super.onAuthenticationSucceeded(result)
- onSuccess()
- }
- override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
- super.onAuthenticationError(errorCode, errString)
- Toast.makeText(context, "Error: $errString", Toast.LENGTH_SHORT).show()
- }
- }
- val promptInfo = BiometricPrompt.PromptInfo.Builder()
- .setTitle(context.getString(R.string.fingerprint_verification))
- .setSubtitle(context.getString(R.string.verify_to_enable))
- .setNegativeButtonText(context.getString(R.string.cancel))
- .build()
- BiometricPrompt(activity, executor, callback).authenticate(promptInfo)
- } else {
- Toast.makeText(context, context.getString(R.string.not_support_fingerprint), Toast.LENGTH_LONG).show()
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Tools.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Tools.kt
deleted file mode 100644
index 396a0ac..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/Tools.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings
-
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Build
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.stringResource
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.presentation.screens.settings.SettingsScaffold
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.widgets.SettingsBox
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-
-
-@Composable
-fun ToolsScreen(navController: NavController, settingsViewModel: SettingsViewModel) {
- settingsViewModel.noteUseCase.observe()
- SettingsScaffold(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.tools),
- onBackNavClicked = { navController.navigateUp() }
- ) {
- LazyColumn {
- item {
- SettingsBox(
- settingsViewModel = settingsViewModel,
- title = stringResource(id = R.string.notes),
- description = settingsViewModel.noteUseCase.notes.size.toString(),
- icon = IconResource.Vector(Icons.Rounded.Build),
- actionType = ActionType.TEXT,
- radius = shapeManager(
- isBoth = true,
- radius = settingsViewModel.settings.value.cornerRadius
- )
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/LockScreen.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/LockScreen.kt
deleted file mode 100644
index 88c5d89..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/LockScreen.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2024 Vexzure
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
-*/
-
-package com.babelsoftware.airnote.presentation.screens.settings.settings.lock
-
-import androidx.compose.runtime.*
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.presentation.components.NotesScaffold
-import com.babelsoftware.airnote.presentation.navigation.ActionType
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.settings.lock.components.FingerprintLock
-import com.babelsoftware.airnote.presentation.screens.settings.settings.lock.components.PasscodeLock
-import com.babelsoftware.airnote.presentation.screens.settings.settings.lock.components.PatternLock
-
-@Composable
-fun LockScreen(
- settingsViewModel: SettingsViewModel,
- navController: NavController,
- action: ActionType?,
-) {
- NotesScaffold {
- if (action != null) {
- when (action) {
- ActionType.PASSCODE -> PasscodeLock(settingsViewModel, navController)
- ActionType.FINGERPRINT -> FingerprintLock(settingsViewModel = settingsViewModel, navController = navController)
- ActionType.PATTERN -> PatternLock(settingsViewModel = settingsViewModel, navController = navController)
- }
- } else {
- when {
- settingsViewModel.settings.value.pattern != null -> PatternLock(settingsViewModel = settingsViewModel, navController = navController)
- settingsViewModel.settings.value.fingerprint -> FingerprintLock(settingsViewModel = settingsViewModel, navController = navController)
- settingsViewModel.settings.value.passcode != null -> PasscodeLock(settingsViewModel, navController)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/Fingerprint.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/Fingerprint.kt
deleted file mode 100644
index 292dbcd..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/Fingerprint.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings.lock.components
-
-import android.content.Context
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.BackHandler
-import androidx.appcompat.app.AppCompatActivity
-import androidx.biometric.BiometricPrompt
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Fingerprint
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.platform.LocalContext
-import androidx.core.content.ContextCompat
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.presentation.navigation.NavRoutes
-import com.babelsoftware.airnote.presentation.popUpToTop
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.R
-import java.util.concurrent.Executor
-
-
-
-@Composable
-fun FingerprintLock(
- settingsViewModel: SettingsViewModel,
- navController: NavController
-) {
- val context = LocalContext.current
-
- BackHandler {
- if (settingsViewModel.settings.value.fingerprint) {
- (context as? ComponentActivity)?.finish()
- } else {
- navController.navigateUp()
- }
- }
-
- Column(
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.background.copy(alpha = 0.1f)),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- Icon(
- imageVector = Icons.Rounded.Fingerprint,
- contentDescription = null,
- tint = MaterialTheme.colorScheme.primary,
- modifier = Modifier.scale(2f)
- )
- LaunchedEffect(Unit) {
- customizedPrompt(context, settingsViewModel, navController)
- }
- }
-}
-
-fun customizedPrompt(
- context: Context,
- settingsViewModel: SettingsViewModel,
- navController: NavController
-) {
- showBiometricPrompt(
- context,
- context as AppCompatActivity,
- onAuthError = {
- customizedPrompt(context, settingsViewModel, navController)
- },
- onAuthSuccess = {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- passcode = null,
- fingerprint = true,
- pattern = null,
- defaultRouteType = NavRoutes.LockScreen.route
- )
- )
- settingsViewModel.updateDefaultRoute(NavRoutes.LockScreen.createRoute(null),)
- navController.navigate(NavRoutes.Home.route) { popUpToTop(navController) }
- }
- )
-}
-
-fun showBiometricPrompt(
- context: Context,
- activity: AppCompatActivity,
- onAuthSuccess: () -> Unit,
- onAuthError: (String) -> Unit
-) {
- val executor: Executor = ContextCompat.getMainExecutor(activity)
-
- val biometricPrompt = BiometricPrompt(
- activity,
- executor,
- object : BiometricPrompt.AuthenticationCallback() {
- override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
- super.onAuthenticationSucceeded(result)
- onAuthSuccess()
- }
-
- override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
- super.onAuthenticationError(errorCode, errString)
- onAuthError(errString.toString())
- }
-
- override fun onAuthenticationFailed() {
- super.onAuthenticationFailed()
- onAuthError("Authentication failed. Please try again.")
- }
- })
-
- val promptInfo = BiometricPrompt.PromptInfo.Builder()
- .setTitle(context.getString(R.string.fingerprint_name))
- .setSubtitle(context.getString(R.string.app_name))
- .setNegativeButtonText(context.getString(R.string.cancel))
- .setConfirmationRequired(true)
- .build()
-
- biometricPrompt.authenticate(promptInfo)
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/Passcode.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/Passcode.kt
deleted file mode 100644
index 352ad1c..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/Passcode.kt
+++ /dev/null
@@ -1,277 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings.lock.components
-
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.BackHandler
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Backspace
-import androidx.compose.material.icons.rounded.Fingerprint
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.presentation.screens.settings.settings.lock.viewModel.LockScreenViewModel
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.presentation.navigation.NavRoutes
-import com.babelsoftware.airnote.presentation.popUpToTop
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-
-@Composable
-fun PasscodeLock(
- settingsViewModel: SettingsViewModel,
- navController: NavController,
- passcodeLockViewModel: LockScreenViewModel = viewModel(),
-) {
- val context = LocalContext.current
-
- BackHandler {
- if (settingsViewModel.settings.value.passcode != null) {
- (context as? ComponentActivity)?.finish()
- } else {
- navController.navigateUp()
- }
- }
-
- Column(
- modifier = Modifier
- .padding(horizontal = 32.dp)
- .fillMaxSize(),
- horizontalAlignment = Alignment.Start,
- verticalArrangement = Arrangement.SpaceBetween
- ) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Spacer(modifier = Modifier.fillMaxHeight(0.2f))
-
- val text = if (settingsViewModel.settings.value.passcode.isNullOrBlank()) stringResource(id = R.string.setup_password) else stringResource(
- id = R.string.enter_password
- )
-
- TitleText(text = text)
-
- PinCodeDisplay(
- pinCode = passcodeLockViewModel.pinCode.value,
- isPinIncorrect = passcodeLockViewModel.isPinIncorrect.value,
- animateError = passcodeLockViewModel.animateError.value
- )
- }
-
- Column {
- NumberPad(
- numbers = (1..9).toList(),
- onNumberClick = { number ->
- passcodeLockViewModel.addNumber(settingsViewModel, number) { result ->
- if (result) {
- navController.navigate(NavRoutes.Home.route) { popUpToTop(navController) }
- }
- }
- }
- )
- Spacer(modifier = Modifier.height(24.dp))
-
- ActionRow(
- onFingerprintClick = {
- passcodeLockViewModel.onReset()
- },
- onZeroClick = {
- passcodeLockViewModel.addNumber(settingsViewModel, 0) { result ->
- if (result) {
- navController.navigate(NavRoutes.Home.route) { popUpToTop(navController) }
- }
- }
- },
- onBackspaceClick = {
- passcodeLockViewModel.removeNumber()
- }
- )
-
- Spacer(modifier = Modifier.height(48.dp))
- }
- }
-}
-
-@Composable
-fun PinCodeDisplay(pinCode: List, isPinIncorrect: Boolean, animateError: Boolean) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceEvenly
- ) {
- repeat(6) { index ->
- Box(
- modifier = Modifier
- .size(55.dp)
- .clip(RoundedCornerShape(8.dp))
- .background(MaterialTheme.colorScheme.surfaceContainer)
- .border(
- width = 2.dp,
- color = getBorderColor(index < pinCode.size, isPinIncorrect, animateError),
- shape = RoundedCornerShape(8.dp)
- ),
- contentAlignment = Alignment.Center
- ) {
- if (index < pinCode.size) {
- PinCodeDot()
- }
- }
- }
- }
-}
-
-@Composable
-fun PinCodeDot() {
- Box(
- modifier = Modifier
- .size(15.dp)
- .clip(CircleShape)
- .background(MaterialTheme.colorScheme.primary)
- )
-}
-
-@Composable
-fun getBorderColor(isFilled: Boolean, isPinIncorrect: Boolean, animateError: Boolean): Color {
- val targetColor = if (isPinIncorrect && animateError) MaterialTheme.colorScheme.error else Color.Transparent
- return if (isFilled) {
- animateColorAsState(
- targetValue = targetColor,
- animationSpec = tween(durationMillis = 600), label = ""
- ).value
- } else {
- Color.Transparent
- }
-}
-
-@Composable
-fun ActionRow(
- onFingerprintClick: () -> Unit,
- onZeroClick: () -> Unit,
- onBackspaceClick: () -> Unit
-) {
- Row(
- modifier = Modifier
- .padding(horizontal = 48.dp)
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Icon(
- imageVector = Icons.Rounded.Fingerprint,
- contentDescription = "Fingerprint",
- modifier = Modifier
- .scale(2f)
- .clickable(onClick = onFingerprintClick),
- tint = MaterialTheme.colorScheme.primary
- )
- NumberButton(
- number = 0,
- ) {
- onZeroClick()
- }
- Icon(
- imageVector = Icons.Rounded.Backspace,
- contentDescription = "Backspace",
- modifier = Modifier
- .scale(1.5f)
- .clickable(onClick = onBackspaceClick),
- tint = MaterialTheme.colorScheme.outline
- )
- }
-}
-
-@Composable
-fun NumberPad(
- numbers: List,
- onNumberClick: (Int) -> Unit
-) {
- Column {
- numbers.chunked(3).forEach { rowNumbers ->
- NumberRow(
- rowNumbers = rowNumbers,
- onNumberClick = onNumberClick
- )
- }
- }
-}
-
-@Composable
-fun NumberRow(
- rowNumbers: List,
- onNumberClick: (Int) -> Unit
-) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceEvenly
- ) {
- rowNumbers.forEach { number ->
- NumberButton(
- modifier = Modifier
- .height(84.dp)
- .weight(1f),
- number = number,
- onClick = onNumberClick
- )
- }
- if (rowNumbers.size < 3) {
- repeat(3 - rowNumbers.size) {
- Spacer(modifier = Modifier.weight(1f))
- }
- }
- }
-}
-
-@Composable
-fun NumberButton(
- modifier: Modifier = Modifier,
- number: Int,
- onClick: (Int) -> Unit
-) {
- val interactionSource = remember { MutableInteractionSource() }
-
- Box(
- modifier = modifier
- .clip(RoundedCornerShape(50))
- .clickable(
- interactionSource = interactionSource,
- indication = null
- ) { onClick(number) },
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = number.toString(),
- textAlign = TextAlign.Center,
- fontWeight = FontWeight.Bold,
- fontSize = 28.sp
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/Pattern.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/Pattern.kt
deleted file mode 100644
index 04a05bb..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/Pattern.kt
+++ /dev/null
@@ -1,226 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings.lock.components
-
-import android.view.MotionEvent
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Path
-import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.input.pointer.pointerInteropFilter
-import androidx.compose.ui.unit.dp
-import com.babelsoftware.airnote.presentation.screens.settings.settings.lock.viewModel.LockScreenViewModel
-import kotlin.math.sqrt
-import androidx.compose.ui.graphics.StrokeCap
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavController
-import com.babelsoftware.airnote.presentation.navigation.NavRoutes
-import com.babelsoftware.airnote.presentation.popUpToTop
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.R
-
-@OptIn(ExperimentalComposeUiApi::class)
-@Composable
-fun PatternLock(
- settingsViewModel: SettingsViewModel,
- lockScreenViewModel: LockScreenViewModel = viewModel(),
- navController: NavController
-) {
- val context = LocalContext.current
-
- BackHandler {
- if (settingsViewModel.settings.value.pattern != null) {
- (context as? ComponentActivity)?.finish()
- } else {
- navController.navigateUp()
- }
- }
-
- val rowCount = 3
- val columnCount = 3
-
- val dotColor = MaterialTheme.colorScheme.primary
- val pathColor = MaterialTheme.colorScheme.primary
-
- Column(
- modifier = Modifier
- .padding(vertical = 64.dp)
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.background),
- verticalArrangement = Arrangement.SpaceBetween,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- val text = when {
- settingsViewModel.settings.value.pattern == null -> stringResource(id = R.string.setup_pattern)
- settingsViewModel.settings.value.pattern != null -> stringResource(id = R.string.lock_pattern)
- else -> "Unknown action"
- }
-
- TitleText(text = text)
-
- Canvas(
- modifier = Modifier
- .width(300.dp)
- .height(300.dp)
- .background(MaterialTheme.colorScheme.background)
- .onSizeChanged { size ->
- lockScreenViewModel.canvasSize = Size(size.width.toFloat(), size.height.toFloat())
- }
- .pointerInteropFilter {
- when (it.action) {
- MotionEvent.ACTION_DOWN -> {
- lockScreenViewModel.canvasSize
- .takeIf { it != Size.Zero }
- ?.let { size ->
- val cell = getNearestCell(Offset(it.x, it.y), size.width, size.height)
- if (cell != null) {
- lockScreenViewModel.clearPattern()
- lockScreenViewModel.selectedCellsIndexList.add(cell.index)
- lockScreenViewModel.selectedCellCenterList.add(cell.center)
- lockScreenViewModel.lastCellCenter = cell.center
- lockScreenViewModel.path = Path().apply { moveTo(cell.center.x, cell.center.y) }
- }
- }
- }
-
- MotionEvent.ACTION_MOVE -> {
- val touchOffset = Offset(it.x, it.y)
- lockScreenViewModel.currentTouchOffset = touchOffset
-
- lockScreenViewModel.canvasSize
- .takeIf { it != Size.Zero }
- ?.let { size ->
- val cell = getNearestCell(touchOffset, size.width, size.height)
-
- if (cell != null && cell.index !in lockScreenViewModel.selectedCellsIndexList) {
- lockScreenViewModel.selectedCellsIndexList.add(cell.index)
- lockScreenViewModel.selectedCellCenterList.add(cell.center)
-
- lockScreenViewModel.updatePath(cell.center)
- } else if (cell == null) {
- // Prevent invalid lines from being drawn by skipping path updates
- lockScreenViewModel.currentTouchOffset = null
- }
- }
- }
-
-
- MotionEvent.ACTION_UP -> {
- println(settingsViewModel.settings.value.pattern)
- if (settingsViewModel.settings.value.pattern == null) {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- passcode = null,
- pattern = lockScreenViewModel.selectedCellsIndexList.joinToString(""),
- fingerprint = false
- )
- )
- settingsViewModel.updateDefaultRoute(NavRoutes.LockScreen.createRoute(null))
- } else {
- if (settingsViewModel.settings.value.pattern == lockScreenViewModel.selectedCellsIndexList.joinToString("")) {
- navController.navigate(NavRoutes.Home.route) { popUpToTop(navController) }
- }
- }
- lockScreenViewModel.clearPattern()
- }
- }
- true
- }
- ) {
- val width = size.width
- val height = size.height
-
- val boxSizeInX = width / columnCount
- val boxCenterInX = boxSizeInX / 2
- val boxSizeInY = height / rowCount
- val boxCenterInY = boxSizeInY / 2
-
- for (row in 0 until rowCount) {
- for (column in 0 until columnCount) {
- drawCircle(
- color = dotColor,
- radius = 25f,
- center = Offset(
- (boxCenterInX + boxSizeInX * column),
- (boxCenterInY + boxSizeInY * row)
- )
- )
- }
- }
-
- drawPath(
- path = lockScreenViewModel.path,
- color = pathColor,
- style = Stroke(
- width = 20f,
- cap = StrokeCap.Round
- )
- )
-
- lockScreenViewModel.currentTouchOffset?.let { offset ->
- lockScreenViewModel.lastCellCenter?.let { lastCenter ->
- val connectingPath = Path().apply {
- moveTo(lastCenter.x, lastCenter.y)
- lineTo(offset.x, offset.y)
- }
- drawPath(
- path = connectingPath,
- color = pathColor,
- style = Stroke(
- width = 20f,
- cap = StrokeCap.Round
- )
- )
- }
- }
- }
- }
-}
-
-
-data class CellModel(
- val index: Int,
- val center: Offset
-)
-
-private fun getNearestCell(offset: Offset, width: Float, height: Float): CellModel? {
- val rowCount = 3
- val columnCount = 3
-
- val boxSizeInX = width / columnCount
- val boxCenterInX = boxSizeInX / 2
- val boxSizeInY = height / rowCount
- val boxCenterInY = boxSizeInY / 2
-
- val circleRadius = width / 8
-
- for (row in 0 until rowCount) {
- for (column in 0 until columnCount) {
- val cellCenter = Offset(
- (boxCenterInX + boxSizeInX * column),
- (boxCenterInY + boxSizeInY * row)
- )
- val distanceFromCenter = sqrt(
- (offset.x - cellCenter.x) * (offset.x - cellCenter.x) +
- (offset.y - cellCenter.y) * (offset.y - cellCenter.y)
- )
-
- if (distanceFromCenter < circleRadius) {
- val index = column + 1 + row * columnCount
- return CellModel(index, cellCenter)
- }
- }
- }
- return null
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/TitleText.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/TitleText.kt
deleted file mode 100644
index 5ac075e..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/components/TitleText.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2024 Vexzure
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
-*/
-
-package com.babelsoftware.airnote.presentation.screens.settings.settings.lock.components
-
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-
-
-@Composable
-fun TitleText(text: String) {
- Text(
- text = text,
- modifier = Modifier.padding(16.dp),
- textAlign = TextAlign.Center,
- fontWeight = FontWeight.Bold
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/viewModel/LockScreenViewModel.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/viewModel/LockScreenViewModel.kt
deleted file mode 100644
index e008228..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/settings/lock/viewModel/LockScreenViewModel.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.settings.lock.viewModel
-
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Path
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.babelsoftware.airnote.presentation.navigation.NavRoutes
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-class LockScreenViewModel : ViewModel() {
- val selectedCellsIndexList = mutableStateListOf()
- val selectedCellCenterList = mutableStateListOf()
- var canvasSize by mutableStateOf(Size.Zero)
- var currentTouchOffset by mutableStateOf(null)
- var lastCellCenter by mutableStateOf(null)
- var path by mutableStateOf(Path())
-
- private val _pinCode : MutableState> = mutableStateOf(emptyList())
- val pinCode: State> = _pinCode
-
- private val _isPinIncorrect = mutableStateOf(false)
- val isPinIncorrect: State = _isPinIncorrect
-
- private val _animateError = mutableStateOf(false)
- val animateError: State = _animateError
-
- fun updatePath(cellCenter: Offset) {
- lastCellCenter?.let {
- path.lineTo(it.x, it.y)
- }
- lastCellCenter = cellCenter
- path.lineTo(cellCenter.x, cellCenter.y)
- }
-
-
- fun clearPattern() {
- selectedCellsIndexList.clear()
- selectedCellCenterList.clear()
- path = Path()
- currentTouchOffset = null
- lastCellCenter = null
- }
-
- fun addNumber(settingsViewModel: SettingsViewModel, number: Int, onResult: (Boolean) -> Unit) {
- viewModelScope.launch {
- if (_pinCode.value.size < 6) {
- _pinCode.value += number
- if (pinCode.value.size == 6) {
- onResult(checkPinCode(settingsViewModel))
- }
- }
- }
- }
-
- fun removeNumber() {
- viewModelScope.launch {
- if (_pinCode.value.isNotEmpty()) {
- _pinCode.value = _pinCode.value.dropLast(1)
- }
- }
- }
-
- private fun checkPinCode(settingsViewModel: SettingsViewModel): Boolean {
- if (settingsViewModel.settings.value.passcode == null) {
- settingsViewModel.update(
- settingsViewModel.settings.value.copy(
- passcode = _pinCode.value.joinToString(""),
- fingerprint = false,
- pattern = null
- )
- )
- settingsViewModel.updateDefaultRoute(NavRoutes.LockScreen.createRoute(null),)
-
- return true
- } else {
- if (_pinCode.value.joinToString("") == settingsViewModel.settings.value.passcode) {
- return true
- } else {
- _isPinIncorrect.value = true
- _animateError.value = true
- viewModelScope.launch {
- delay(500)
- _animateError.value = false
- onReset()
- }
- return false
- }
- }
- }
-
- fun onReset() {
- _pinCode.value = emptyList()
- _isPinIncorrect.value = false
- _animateError.value = false
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/CircleWrapper.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/CircleWrapper.kt
deleted file mode 100644
index 70ee659..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/CircleWrapper.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.widgets
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-
-@Composable
-fun CircleWrapper(
- color: Color = MaterialTheme.colorScheme.background,
- size: Dp = 8.dp,
- content: @Composable () -> Unit
-) {
- Box(
- modifier = Modifier
- .background(
- color = color,
- shape = RoundedCornerShape(50)
- )
- .padding(size),
- ) {
- content()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/MaterialText.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/MaterialText.kt
deleted file mode 100644
index 342c2cf..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/MaterialText.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.widgets
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.theme.FontUtils
-
-@Composable
-fun MaterialText(
- title: String,
- description: String? = null,
- modifier: Modifier = Modifier,
- titleSize: TextUnit = 14.sp,
- descriptionSize: TextUnit = 11.sp,
- center: Boolean = false,
- titleColor: Color = MaterialTheme.colorScheme.onSurface,
- descriptionColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
- settingsViewModel: SettingsViewModel? = null
-) {
- Column(
- modifier = modifier,
- horizontalAlignment = if (center) Alignment.CenterHorizontally else Alignment.Start,) {
- Text(
- text = title,
- style = MaterialTheme.typography.titleMedium.copy(
- fontSize = settingsViewModel?.let {
- FontUtils.getFontSize(it, baseSize = titleSize.value.toInt())
- } ?: titleSize
- ),
- color = titleColor,
- modifier = Modifier.padding(bottom = 3.dp),
- textAlign = if (center) TextAlign.Center else TextAlign.Start
- )
- if (description != null) {
- Text(
- text = description,
- style = MaterialTheme.typography.bodySmall.copy(
- fontSize = settingsViewModel?.let {
- FontUtils.getFontSize(it, baseSize = descriptionSize.value.toInt())
- } ?: descriptionSize
- ),
- color = descriptionColor,
- modifier = Modifier.padding(bottom = 3.dp),
- maxLines = 6,
- overflow = TextOverflow.Ellipsis
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingCategory.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingCategory.kt
deleted file mode 100644
index 9d4537c..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingCategory.kt
+++ /dev/null
@@ -1,144 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.widgets
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.ElevatedCard
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.scale
-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 com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.theme.FontUtils
-
-@Composable
-fun SettingCategory(
- title: String,
- subTitle: String = "",
- icon: ImageVector,
- shape: RoundedCornerShape,
- isLast: Boolean = false,
- smallSetting: Boolean = false,
- action: () -> Unit = {},
- composableAction: @Composable (() -> Unit) -> Unit = {},
- settingsViewModel: SettingsViewModel? = null
-) {
- var showCustomAction by remember { mutableStateOf(false) }
- if (showCustomAction) composableAction { showCustomAction = !showCustomAction }
-
- ElevatedCard(
- shape = shape,
- modifier = Modifier
- .clip(shape)
- .clickable {
- showCustomAction = showCustomAction.not()
- action()
- },
- colors = CardDefaults.elevatedCardColors(
- containerColor = if (smallSetting) {
- MaterialTheme.colorScheme.primary
- } else {
- MaterialTheme.colorScheme.surfaceContainer // Varsayılan yerine belirli bir renk
- }
- ),
- elevation = CardDefaults.elevatedCardElevation(defaultElevation = 1.dp), // Gölgeyi 1.dp'ye düşürdük
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .clip(shape)
- .fillMaxSize()
- .padding(
- 24.dp,
- if (smallSetting) 11.dp else 16.dp,
- 14.dp,
- if (smallSetting) 11.dp else 16.dp
- ),
- ) {
- if (smallSetting) {
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- RenderCategoryTitle(title = title, settingsViewModel = settingsViewModel)
- Spacer(modifier = Modifier.weight(1f))
- RenderCategoryDescription(subTitle = subTitle, smallSetting = true, settingsViewModel = settingsViewModel)
- RenderCategoryIcon(icon, true)
- }
- } else {
- Column {
- RenderCategoryTitle(title = title, settingsViewModel = settingsViewModel)
- RenderCategoryDescription(subTitle = subTitle, smallSetting = false, settingsViewModel = settingsViewModel)
- }
- Spacer(modifier = Modifier.weight(1f))
- RenderCategoryIcon(icon, false)
- }
- }
- }
- Spacer(modifier = Modifier.height(if (isLast) 26.dp else 2.dp))
-}
-
-@Composable
-private fun RenderCategoryTitle(title: String, settingsViewModel: SettingsViewModel? = null) {
- Text(
- text = title,
- fontSize = settingsViewModel?.let {
- FontUtils.getFontSize(it, baseSize = 16)
- } ?: 16.sp,
- fontWeight = FontWeight.Bold
- )
-}
-
-@Composable
-private fun RenderCategoryDescription(subTitle: String, smallSetting: Boolean, settingsViewModel: SettingsViewModel? = null) {
- if (subTitle.isNotBlank()) {
- Text(
- color = if (smallSetting) MaterialTheme.colorScheme.surfaceContainerHigh else MaterialTheme.colorScheme.primary,
- text = subTitle,
- fontSize = settingsViewModel?.let {
- FontUtils.getFontSize(it, baseSize = if (smallSetting) 13 else 10)
- } ?: if (smallSetting) 13.sp else 10.sp,
- modifier = Modifier.padding(if (smallSetting) 7.dp else 0.dp)
- )
- }
-}
-
-@Composable
-private fun RenderCategoryIcon(icon: ImageVector, reverseColors: Boolean) {
- Box(
- modifier = Modifier
- .background(
- color = if (reverseColors) MaterialTheme.colorScheme.surfaceContainerHighest else MaterialTheme.colorScheme.primary,
- shape = RoundedCornerShape(50)
- ),
- ) {
- Icon(
- imageVector = icon,
- contentDescription = null,
- tint = if (reverseColors) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.surfaceContainerHigh,
- modifier = Modifier
- .scale(if (reverseColors) 0.8f else 1f)
- .padding(if (reverseColors) 9.dp else 9.dp)
- )
- }
-}
-
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingsBox.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingsBox.kt
deleted file mode 100644
index fb2979d..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingsBox.kt
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * Copyright (C) 2025 Babel Software
- * ...
-*/
-
-package com.babelsoftware.airnote.presentation.screens.settings.widgets
-
-import android.content.ClipData
-import android.content.ClipboardManager
-import android.content.Context
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.ArrowForwardIos
-import androidx.compose.material.icons.automirrored.rounded.OpenInNew
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.ContentCopy
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.RadioButton
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Switch
-import androidx.compose.material3.SwitchDefaults
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import coil.compose.rememberAsyncImagePainter
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.presentation.screens.settings.model.IconResource
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.theme.FontUtils
-
-enum class ActionType {
- RADIOBUTTON,
- SWITCH,
- LINK,
- TEXT,
- CUSTOM,
- CLIPBOARD,
- NAVIGATION
-}
-
-@Composable
-fun SettingsBox(
- radius: RoundedCornerShape? = null,
- title: String,
- description: String? = null,
- icon: IconResource,
- size: Dp = 12.dp,
- isEnabled: Boolean = true,
- isCentered: Boolean = false,
- actionType: ActionType,
- variable: Boolean? = null,
- switchEnabled: (Boolean) -> Unit = {},
- linkClicked: () -> Unit = {},
- customButton: @Composable () -> Unit = { RenderCustomIcon() },
- customAction: @Composable (() -> Unit) -> Unit = {},
- customText: String = "",
- clipboardText: String = "",
- settingsViewModel: SettingsViewModel? = null,
- containerColor: Color = MaterialTheme.colorScheme.surfaceContainer,
-) {
- val context = LocalContext.current
- var showCustomAction by remember { mutableStateOf(false) }
- if (showCustomAction) customAction { showCustomAction = !showCustomAction }
-
- AnimatedVisibility(visible = isEnabled) {
- Surface(
- modifier = Modifier
- .padding(bottom = dimensionResource(id = R.dimen.card_padding_bottom))
- .clip(radius ?: RoundedCornerShape(13.dp))
- .clickable {
- handleAction(
- context,
- actionType,
- variable,
- switchEnabled,
- { showCustomAction = !showCustomAction },
- linkClicked,
- clipboardText
- )
- },
- color = containerColor,
- tonalElevation = 1.dp
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .padding(
- horizontal = dimensionResource(id = R.dimen.card_padding_horizontal),
- vertical = size
- )
- .fillMaxWidth()
- ) {
- Row(
- Modifier.weight(1f),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
- ) {
- CircleWrapper(
- size = 12.dp,
- color = containerColor
- ) {
- when (icon) {
- is IconResource.Vector -> {
- Icon(
- imageVector = icon.imageVector,
- contentDescription = null,
- tint = MaterialTheme.colorScheme.primary
- )
- }
- is IconResource.Url -> {
- Image(
- painter = rememberAsyncImagePainter(model = icon.url),
- contentDescription = title,
- modifier = Modifier
- .size(24.dp)
- .clip(CircleShape),
- contentScale = ContentScale.Crop
- )
- }
- }
- }
- Spacer(modifier = Modifier.width(8.dp))
-
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = title,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.titleSmall
- )
- if (!description.isNullOrBlank()) {
- Text(
- text = description,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- style = MaterialTheme.typography.bodySmall
- )
- }
- }
- }
- RenderActionComponent(
- actionType,
- variable,
- switchEnabled,
- linkClicked,
- customText,
- customButton,
- settingsViewModel
- )
- }
- }
- }
-}
-
-private fun handleAction(
- context: Context,
- actionType: ActionType,
- variable: Boolean?,
- onSwitchEnabled: (Boolean) -> Unit,
- customAction: () -> Unit,
- onLinkClicked: () -> Unit,
- clipboardText: String
-) {
- when (actionType) {
- ActionType.RADIOBUTTON -> onSwitchEnabled(variable == false)
- ActionType.SWITCH -> onSwitchEnabled(variable == false)
- ActionType.LINK -> onLinkClicked()
- ActionType.NAVIGATION -> onLinkClicked()
- ActionType.CUSTOM -> customAction()
- ActionType.CLIPBOARD -> copyToClipboard(context, clipboardText)
- ActionType.TEXT -> { /* No action needed */ }
- }
-}
-
-@Composable
-private fun RenderActionComponent(
- actionType: ActionType,
- variable: Boolean?,
- onSwitchEnabled: (Boolean) -> Unit,
- onLinkClicked: () -> Unit,
- customText: String,
- customButton: @Composable () -> Unit,
- settingsViewModel: SettingsViewModel? = null
-) {
- when (actionType) {
- ActionType.RADIOBUTTON -> RenderRadioButton(variable, onSwitchEnabled)
- ActionType.SWITCH -> RenderSwitch(variable, onSwitchEnabled)
- ActionType.LINK -> RenderLinkIcon(onLinkClicked)
- ActionType.NAVIGATION -> RenderCustomIcon()
- ActionType.TEXT -> RenderText(customText, settingsViewModel)
- ActionType.CLIPBOARD -> RenderClipboardIcon()
- ActionType.CUSTOM -> customButton()
- }
-}
-
-@Composable
-private fun RenderRadioButton(variable: Boolean?, onSwitchEnabled: (Boolean) -> Unit) {
- RadioButton(selected = variable == true, onClick = { onSwitchEnabled(true) })
-}
-
-@Composable
-private fun RenderSwitch(variable: Boolean?, onSwitchEnabled: (Boolean) -> Unit) {
- val isChecked = variable == true
- Switch(
- checked = isChecked,
- onCheckedChange = onSwitchEnabled,
- modifier = Modifier.scale(0.9f),
- thumbContent = {
- Icon(
- imageVector = if (isChecked) Icons.Filled.Check else Icons.Filled.Close,
- contentDescription = null,
- modifier = Modifier.size(SwitchDefaults.IconSize)
- )
- }
- )
-}
-
-@Composable
-private fun RenderLinkIcon(onLinkClicked: () -> Unit) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.OpenInNew,
- contentDescription = null,
- modifier = Modifier.padding(dimensionResource(id = R.dimen.icon_padding)).clickable { onLinkClicked() },
- tint = MaterialTheme.colorScheme.primary
- )
-}
-
-@Composable
-fun RenderCustomIcon() {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos,
- contentDescription = null,
- modifier = Modifier.scale(0.6f).padding(dimensionResource(id = R.dimen.icon_padding))
- )
-}
-
-@Composable
-private fun RenderClipboardIcon() {
- Icon(
- imageVector = Icons.Default.ContentCopy,
- contentDescription = null,
- modifier = Modifier.padding(dimensionResource(id = R.dimen.icon_padding)),
- tint = MaterialTheme.colorScheme.primary
- )
-}
-
-@Composable
-private fun RenderText(customText: String, settingsViewModel: SettingsViewModel? = null) {
- Text(
- text = customText,
- fontSize = settingsViewModel?.let { FontUtils.getFontSize(it, baseSize = 14) } ?: 14.sp,
- modifier = Modifier.padding(dimensionResource(id = R.dimen.icon_padding))
- )
-}
-
-fun copyToClipboard(context: Context, clipboardText: String) {
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
- val clip = ClipData.newPlainText("Copied Text", clipboardText)
- clipboard.setPrimaryClip(clip)
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingsDialog.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingsDialog.kt
deleted file mode 100644
index 5221343..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingsDialog.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.widgets
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.settings.shapeManager
-
-@Composable
-fun ListDialog(
- text: String,
- list: List,
- initialItem: T? = null,
- settingsViewModel: SettingsViewModel,
- onExit: () -> Unit,
- extractDisplayData: (T) -> Pair,
- setting: @Composable (Boolean, Boolean, Pair) -> Unit
-) {
- Dialog(
- onDismissRequest = { onExit() },
- properties = DialogProperties(usePlatformDefaultWidth = false),
- ) {
- Column(
- modifier = Modifier
- .background(
- color = MaterialTheme.colorScheme.surfaceContainerLow,
- shape = shapeManager(
- isBoth = true,
- radius = settingsViewModel.settings.value.cornerRadius
- )
- )
- .padding(22.dp, 0.dp, 22.dp, 0.dp)
- .fillMaxSize(.8f)
- ) {
- Text(
- text = text,
- textAlign = TextAlign.Center,
- fontWeight = FontWeight.Bold,
- modifier = Modifier
- .fillMaxWidth()
- .padding(20.dp),
- fontSize = 20.sp,
- )
- LazyColumn {
- initialItem?.let { initial ->
- item {
- val displayData = extractDisplayData(initial)
- setting(true, list.size == 1, displayData)
- }
- }
- itemsIndexed(list) { index, content ->
- val isFirstItem = (initialItem == null && index == 0)
- val isLastItem = index == list.lastIndex
- val displayData = extractDisplayData(content)
- setting(isFirstItem, isLastItem, displayData)
- if (isLastItem) Spacer(modifier = Modifier.height(20.dp))
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingsList.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingsList.kt
deleted file mode 100644
index 42f8c57..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/settings/widgets/SettingsList.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.settings.widgets
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
-
-
-@Composable
-fun ListDialog(
- text: String,
- list: List,
- initialItem: T? = null,
- onExit: () -> Unit,
- extractDisplayData: (T) -> Pair,
- setting: @Composable (Pair) -> Unit
-) {
- Dialog(
- onDismissRequest = { onExit() },
- properties = DialogProperties(usePlatformDefaultWidth = false),
- ) {
- Column(
- modifier = Modifier
- .background(
- color = MaterialTheme.colorScheme.background,
- shape = RoundedCornerShape(32.dp)
- )
- .padding(12.dp, 0.dp, 12.dp, 0.dp)
- .fillMaxWidth(0.8f)
- ) {
- Text(
- text = text,
- textAlign = TextAlign.Center,
- fontWeight = FontWeight.Bold,
- modifier = Modifier
- .fillMaxWidth()
- .padding(20.dp),
- fontSize = 20.sp,
- )
- LazyColumn(
- modifier = Modifier
- .padding(bottom = 12.dp)
- .clip(RoundedCornerShape(32.dp))
- .heightIn(max = 600.dp)
- ) {
- initialItem?.let { initial ->
- item {
- val displayData = extractDisplayData(initial)
- setting(displayData)
- }
- }
- items(list) { content ->
- val displayData = extractDisplayData(content)
- setting(displayData)
- }
- }
- }
- }
-}
-
-@Composable
-fun CustomListDialog(
- text: String,
- onExit: () -> Unit,
- content: LazyListScope.() -> Unit
-) {
- Dialog(
- onDismissRequest = { onExit() },
- properties = DialogProperties(usePlatformDefaultWidth = false),
- ) {
- Column(
- modifier = Modifier
- .background(
- color = MaterialTheme.colorScheme.surfaceContainerLow,
- shape = RoundedCornerShape(32.dp)
- )
- .padding(12.dp, 0.dp, 12.dp, 0.dp)
- .fillMaxWidth(0.8f)
- ) {
- Text(
- text = text,
- textAlign = TextAlign.Center,
- fontWeight = FontWeight.Bold,
- modifier = Modifier
- .fillMaxWidth()
- .padding(20.dp),
- fontSize = 20.sp,
- )
- LazyColumn(
- modifier = Modifier
- .padding(bottom = 12.dp)
- .clip(RoundedCornerShape(32.dp))
- .heightIn(max = 600.dp)
- ) {
- content()
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/terms/TermsScreen.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/screens/terms/TermsScreen.kt
deleted file mode 100644
index 2683999..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/screens/terms/TermsScreen.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-package com.babelsoftware.airnote.presentation.screens.terms
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.babelsoftware.airnote.constant.ConnectionConst
-import com.babelsoftware.airnote.constant.SupportConst
-import com.babelsoftware.airnote.presentation.components.AgreeButton
-import com.babelsoftware.airnote.presentation.components.NotesScaffold
-import com.babelsoftware.airnote.presentation.components.markdown.MarkdownText
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.settings.shapeManager
-import com.babelsoftware.airnote.R
-
-
-@Composable
-fun TermsScreen(
- settingsViewModel: SettingsViewModel
-) {
- NotesScaffold(
- floatingActionButton = {
- AgreeButton(
- text = stringResource(id = R.string.agree),
- modifier = Modifier.semantics {
- contentDescription = "Agree"
- }
- ) {
- settingsViewModel.update(settingsViewModel.settings.value.copy(termsOfService = true))
- }
- },
- content = {
- Column(
- modifier = Modifier.padding(16.dp)
- ) {
- Text(
- text = stringResource(id = R.string.terms_of_service),
- style = MaterialTheme.typography.headlineLarge,
- modifier = Modifier.padding(0.dp, 16.dp, 16.dp, 16.dp)
- )
- Box(
- modifier = Modifier
- .fillMaxSize()
- .clip(
- shapeManager(
- isBoth = true,
- radius = settingsViewModel.settings.value.cornerRadius
- )
- )
- .background(
- color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.5f)
- )
- .padding(1.dp)
- .semantics {
- contentDescription = "Terms"
- }
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- MarkdownText(
- fontSize = 12.sp,
- radius = settingsViewModel.settings.value.cornerRadius,
- markdown = getTermsOfService(),
- isEnabled = true
- )
- }
- }
- }
- }
- )
-}
-
-
-@Composable
-fun getTermsOfService(): String {
- return buildString {
- append("### ${stringResource(R.string.terms_acceptance_title)}\n")
- append("${stringResource(R.string.terms_acceptance_body)}\n\n")
-
- append("### ${stringResource(R.string.terms_license_title)}\n")
- append("${stringResource(R.string.terms_license_body)}\n\n")
-
- append("### ${stringResource(R.string.terms_use_title)}\n")
- append("${stringResource(R.string.terms_use_body)}\n\n")
-
- append("### ${stringResource(R.string.terms_intellectual_property_title)}\n")
- append("${stringResource(R.string.terms_intellectual_property_body)}\n\n")
-
- append("### ${stringResource(R.string.terms_disclaimer_title)}\n")
- append("${stringResource(R.string.terms_disclaimer_body)}\n\n")
-
- append("### ${stringResource(R.string.terms_liability_title)}\n")
- append("${stringResource(R.string.terms_liability_body)}\n\n")
-
- append("### ${stringResource(R.string.terms_termination_title)}\n")
- append("${stringResource(R.string.terms_termination_body)}\n\n")
-
- append("### ${stringResource(R.string.terms_changes_title)}\n")
- append("${stringResource(R.string.terms_changes_body)}\n\n")
-
- append("### ${stringResource(R.string.terms_contact_title)}\n")
- append("${stringResource(R.string.terms_contact_body)} ${ConnectionConst.SUPPORT_MAIL}.\n\n")
-
- append("### ${stringResource(R.string.terms_privacy_title)}\n")
- append("${stringResource(R.string.terms_privacy_body)} **https://github.com/Kin69/EasyNotes/wiki/Privacy-Policy**.\n\n")
-
- append("*${stringResource(R.string.terms_effective_date)}: ${SupportConst.TERMS_EFFECTIVE_DATE}\n")
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Color.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Color.kt
deleted file mode 100644
index 1596069..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Color.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.babelsoftware.airnote.presentation.theme
-
-import androidx.compose.ui.graphics.Color
-
-val primaryLight = Color(0xFF725855)
-val primaryContainerLight = Color(0xFFFFDFDA)
-val onPrimaryContainerLight = Color(0xFF5C4541)
-val secondaryLight = Color(0xFF685B59)
-val onSecondaryLight = Color(0xFFFFFFFF)
-val secondaryContainerLight = Color(0xFFF5E2DF)
-val onSecondaryContainerLight = Color(0xFF534746)
-val tertiaryLight = Color(0xFF655E48)
-val onTertiaryLight = Color(0xFFFFFFFF)
-val tertiaryContainerLight = Color(0xFFF1E6CA)
-val onTertiaryContainerLight = Color(0xFF514A35)
-val errorLight = Color(0xFFBA1A1A)
-val onErrorLight = Color(0xFFFFFFFF)
-val errorContainerLight = Color(0xFFFFDAD6)
-val onErrorContainerLight = Color(0xFF410002)
-val backgroundLight = Color(0xFFFFF8F7)
-val onBackgroundLight = Color(0xFF1E1B1B)
-val surfaceLight = Color(0xFFFFF8F7)
-val onSurfaceLight = Color(0xFF1E1B1B)
-val surfaceVariantLight = Color(0xFFF0DFDC)
-val onSurfaceVariantLight = Color(0xFF4F4443)
-val outlineLight = Color(0xFF817472)
-val outlineVariantLight = Color(0xFFD3C3C1)
-val scrimLight = Color(0xFF000000)
-val inverseSurfaceLight = Color(0xFF33302F)
-val inverseOnSurfaceLight = Color(0xFFF6EFEE)
-val inversePrimaryLight = Color(0xFFE0BFBA)
-val surfaceDimLight = Color(0xFFDFD8D7)
-val surfaceBrightLight = Color(0xFFFFF8F7)
-val surfaceContainerLowestLight = Color(0xFFFFFFFF)
-val surfaceContainerLowLight = Color(0xFFF9F2F1)
-val surfaceContainerLight = Color(0xFFF4ECEB)
-val surfaceContainerHighLight = Color(0xFFEEE7E5)
-val surfaceContainerHighestLight = Color(0xFFE8E1E0)
-
-val primaryDark = Color(0xFFFFFFFF)
-val onPrimaryDark = Color(0xFF402B28)
-val primaryContainerDark = Color(0xFFEECDC8)
-val onPrimaryContainerDark = Color(0xFF503A37)
-val secondaryDark = Color(0xFFD4C3C0)
-val onSecondaryDark = Color(0xFF392E2C)
-val secondaryContainerDark = Color(0xFF463B39)
-val onSecondaryContainerDark = Color(0xFFDFCDCA)
-val tertiaryDark = Color(0xFFFFFFFF)
-val onTertiaryDark = Color(0xFF36301D)
-val tertiaryContainerDark = Color(0xFFDED4B8)
-val onTertiaryContainerDark = Color(0xFF453F2B)
-val errorDark = Color(0xFFFFB4AB)
-val onErrorDark = Color(0xFF690005)
-val errorContainerDark = Color(0xFF93000A)
-val onErrorContainerDark = Color(0xFFFFDAD6)
-val backgroundDark = Color(0xFF151312)
-val onBackgroundDark = Color(0xFFE8E1E0)
-val surfaceDark = Color(0xFF151312)
-val onSurfaceDark = Color(0xFFE8E1E0)
-val surfaceVariantDark = Color(0xFF4F4443)
-val onSurfaceVariantDark = Color(0xFFD3C3C1)
-val outlineDark = Color(0xFF9C8E8B)
-val outlineVariantDark = Color(0xFF4F4443)
-val scrimDark = Color(0xFF000000)
-val inverseSurfaceDark = Color(0xFFE8E1E0)
-val inverseOnSurfaceDark = Color(0xFF33302F)
-val inversePrimaryDark = Color(0xFF725855)
-val surfaceDimDark = Color(0xFF151312)
-val surfaceBrightDark = Color(0xFF3C3838)
-val surfaceContainerLowestDark = Color(0xFF100E0D)
-val surfaceContainerLowDark = Color(0xFF1E1B1B)
-val surfaceContainerDark = Color(0xFF221F1F)
-val surfaceContainerHighDark = Color(0xFF2C2929)
-val surfaceContainerHighestDark = Color(0xFF373433)
-
-
-val linkColor = Color(0xFF90D5FF)
-
-// Custom color palette for our AI theme buttons
-object AiButtonColors {
- val GeminiContainer = Color(0xFF3B3F8C) // A rich navy blue/purple hue for a dark theme
- val GeminiOnContainer = Color(0xFFE0E0FF) // A readable, vibrant text/icon color on this dark background
-
- // ---> A more standard but harmonious color for the “New Note” button
- val SecondaryContainer = Color(0xFF4A4458)
- val SecondaryOnContainer = Color(0xFFE8DEF8)
- // <---
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/theme/FontUtils.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/theme/FontUtils.kt
deleted file mode 100644
index a2523be..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/theme/FontUtils.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.babelsoftware.airnote.presentation.theme
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.sp
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-
-/**
- * Utility functions for font size management throughout the app
- */
-object FontUtils {
- /**
- * Get the appropriate font size based on the base size and settings
- * @param settingsViewModel The settings view model
- * @param baseSize The base size to scale from (default is 16sp)
- * @param scaleFactor Optional scale factor to apply (e.g., 0.875 for smaller text)
- * @return The calculated text unit
- */
- @Composable
- fun getFontSize(
- settingsViewModel: SettingsViewModel,
- baseSize: Int = 16,
- scaleFactor: Float = 1f
- ): TextUnit {
- val settingsFontSize = settingsViewModel.settings.value.fontSize
- val sizeDifference = settingsFontSize - baseSize
-
- return ((baseSize + sizeDifference) * scaleFactor).sp
- }
-
- /**
- * Get title font size
- */
- @Composable
- fun getTitleFontSize(settingsViewModel: SettingsViewModel): TextUnit {
- return getFontSize(settingsViewModel, baseSize = 18, scaleFactor = 1.1f)
- }
-
-
- /**
- * Get body font size
- */
- @Composable
- fun getBodyFontSize(settingsViewModel: SettingsViewModel): TextUnit {
- return getFontSize(settingsViewModel)
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Schemes.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Schemes.kt
deleted file mode 100644
index afa20e1..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Schemes.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package com.babelsoftware.airnote.presentation.theme
-
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.lightColorScheme
-
-val lightScheme = lightColorScheme(
- primary = primaryLight,
- onPrimary = surfaceDimLight,
- primaryContainer = primaryContainerLight,
- onPrimaryContainer = onPrimaryContainerLight,
- secondary = secondaryLight,
- onSecondary = onSecondaryLight,
- secondaryContainer = secondaryContainerLight,
- onSecondaryContainer = onSecondaryContainerLight,
- tertiary = tertiaryLight,
- onTertiary = onTertiaryLight,
- tertiaryContainer = tertiaryContainerLight,
- onTertiaryContainer = onTertiaryContainerLight,
- error = errorLight,
- onError = onErrorLight,
- errorContainer = errorContainerLight,
- onErrorContainer = onErrorContainerLight,
- background = backgroundLight,
- onBackground = onBackgroundLight,
- surface = surfaceLight,
- onSurface = onSurfaceLight,
- surfaceVariant = surfaceVariantLight,
- onSurfaceVariant = onSurfaceVariantLight,
- outline = outlineLight,
- outlineVariant = outlineVariantLight,
- scrim = scrimLight,
- inverseSurface = inverseSurfaceLight,
- inverseOnSurface = inverseOnSurfaceLight,
- inversePrimary = inversePrimaryLight,
- surfaceDim = surfaceDimLight,
- surfaceBright = surfaceBrightLight,
- surfaceContainerLowest = surfaceContainerLowestLight,
- surfaceContainerLow = surfaceContainerLowLight,
- surfaceContainer = surfaceContainerLight,
- surfaceContainerHigh = surfaceContainerHighLight,
- surfaceContainerHighest = surfaceContainerHighestLight,
-)
-
-val darkScheme = darkColorScheme(
- primary = primaryDark,
- onPrimary = onPrimaryDark,
- primaryContainer = primaryContainerDark,
- onPrimaryContainer = onPrimaryContainerDark,
- secondary = secondaryDark,
- onSecondary = onSecondaryDark,
- secondaryContainer = secondaryContainerDark,
- onSecondaryContainer = onSecondaryContainerDark,
- tertiary = tertiaryDark,
- onTertiary = onTertiaryDark,
- tertiaryContainer = tertiaryContainerDark,
- onTertiaryContainer = onTertiaryContainerDark,
- error = errorDark,
- onError = onErrorDark,
- errorContainer = errorContainerDark,
- onErrorContainer = onErrorContainerDark,
- background = backgroundDark,
- onBackground = onBackgroundDark,
- surface = surfaceDark,
- onSurface = onSurfaceDark,
- surfaceVariant = surfaceVariantDark,
- onSurfaceVariant = onSurfaceVariantDark,
- outline = outlineDark,
- outlineVariant = outlineVariantDark,
- scrim = scrimDark,
- inverseSurface = inverseSurfaceDark,
- inverseOnSurface = inverseOnSurfaceDark,
- inversePrimary = inversePrimaryDark,
- surfaceDim = surfaceDimDark,
- surfaceBright = surfaceBrightDark,
- surfaceContainerLowest = surfaceContainerLowestDark,
- surfaceContainerLow = surfaceContainerLowDark,
- surfaceContainer = surfaceContainerDark,
- surfaceContainerHigh = surfaceContainerHighDark,
- surfaceContainerHighest = surfaceContainerHighestDark,
-)
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Theme.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Theme.kt
deleted file mode 100644
index fc3a767..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Theme.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.babelsoftware.airnote.presentation.theme
-
-import android.app.Activity
-import android.content.Context
-import android.os.Build
-import android.view.WindowManager
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.ColorScheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalView
-import androidx.core.view.WindowCompat
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-
-private fun getColorScheme(context: Context, isDarkTheme: Boolean, isDynamicTheme: Boolean, isAmoledTheme: Boolean): ColorScheme {
- if (isDynamicTheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- if (isDarkTheme) {
- if (isAmoledTheme) {
- return dynamicDarkColorScheme(context).copy(surfaceContainerLow = Color.Black, surface = Color.Black)
- }
-
- return dynamicDarkColorScheme(context)
- } else {
- return dynamicLightColorScheme(context)
- }
- } else if (isDarkTheme) {
- if (isAmoledTheme) {
- return darkScheme.copy(surfaceContainerLow = Color.Black, surface = Color.Black)
- }
-
- return darkScheme
- } else {
- return lightScheme
- }
-}
-
-@Composable
-fun LeafNotesTheme(
- settingsModel: SettingsViewModel,
- content: @Composable () -> Unit
-) {
- if (settingsModel.settings.value.automaticTheme) {
- settingsModel.update(settingsModel.settings.value.copy(dynamicTheme = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S))
- settingsModel.update(settingsModel.settings.value.copy(darkTheme = isSystemInDarkTheme()))
- }
-
- val context = LocalContext.current
- val activity = LocalView.current.context as Activity
- WindowCompat.getInsetsController(activity.window, activity.window.decorView).apply {
- isAppearanceLightStatusBars = !settingsModel.settings.value.darkTheme
- }
- if (settingsModel.settings.value.screenProtection) {
- activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
- } else {
- activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
- }
-
- MaterialTheme(
- colorScheme = getColorScheme(context, settingsModel.settings.value.darkTheme, settingsModel.settings.value.dynamicTheme, settingsModel.settings.value.amoledTheme),
- typography = getTypography(settingsModel),
- content = content
- )
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Type.kt b/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Type.kt
deleted file mode 100644
index 7041bd2..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/presentation/theme/Type.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package com.babelsoftware.airnote.presentation.theme
-
-import androidx.compose.material3.Typography
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-
-/**
- * Get a Typography object with font sizes scaled based on user settings
- */
-@Composable
-fun getTypography(settingsViewModel: SettingsViewModel): Typography {
- val baseFontSize = settingsViewModel.settings.value.fontSize
- val fontSizeFactor = baseFontSize / 16f
-
- fun scaledSize(size: Int): Int = (size * fontSizeFactor).toInt()
-
- return Typography(
- displayLarge = TextStyle(
- fontWeight = FontWeight.Normal,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(57)),
- ),
- displayMedium = TextStyle(
- fontWeight = FontWeight.Normal,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(45)),
- ),
- displaySmall = TextStyle(
- fontWeight = FontWeight.Normal,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(36)),
- ),
- headlineLarge = TextStyle(
- fontWeight = FontWeight.Normal,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(32)),
- ),
- headlineMedium = TextStyle(
- fontWeight = FontWeight.Normal,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(28)),
- ),
- headlineSmall = TextStyle(
- fontWeight = FontWeight.Normal,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(24)),
- ),
- titleLarge = TextStyle(
- fontWeight = FontWeight.Bold,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(22)),
- ),
- titleMedium = TextStyle(
- fontWeight = FontWeight.Medium,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(16)),
- ),
- titleSmall = TextStyle(
- fontWeight = FontWeight.Medium,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(14)),
- ),
- bodyLarge = TextStyle(
- fontWeight = FontWeight.Normal,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(16)),
- ),
- bodyMedium = TextStyle(
- fontWeight = FontWeight.Normal,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(14)),
- ),
- bodySmall = TextStyle(
- fontWeight = FontWeight.Normal,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(12)),
- ),
- labelLarge = TextStyle(
- fontWeight = FontWeight.Medium,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(14)),
- ),
- labelMedium = TextStyle(
- fontWeight = FontWeight.Medium,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(12)),
- ),
- labelSmall = TextStyle(
- fontWeight = FontWeight.Medium,
- fontSize = FontUtils.getFontSize(settingsViewModel, scaledSize(11)),
- ),
- )
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/util/UpdateUtils.kt b/app/src/main/java/com/babelsoftware/airnote/util/UpdateUtils.kt
deleted file mode 100644
index 719f160..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/util/UpdateUtils.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (c) 2025 Babel Software.
- *
- * This file and codes created by RRechz - Babel Software
- */
-
-package com.babelsoftware.airnote.util
-
-import android.util.Log
-import com.google.mlkit.common.model.DownloadConditions
-import com.google.mlkit.nl.translate.TranslateLanguage
-import com.google.mlkit.nl.translate.Translation
-import com.google.mlkit.nl.translate.TranslatorOptions
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.tasks.await
-import kotlinx.coroutines.withContext
-import org.json.JSONObject
-import java.net.URL
-
-// ---> A better Changelog demonstrator
-sealed class ChangelogResult {
- data class Success(val body: String) : ChangelogResult()
- data class Error(val exception: Exception) : ChangelogResult()
-}
-// <---
-
-// Data class that holds version information (changelog, download url, checksum) together
-data class ReleaseInfo(
- val changelog: String,
- val apkDownloadUrl: String?,
- val sha256Checksum: String?
-)
-
-/**
- * Pulls the latest version tag (tag_name) from the GitHub API.
- * @return Latest version tag or null in case of error.
- */
-suspend fun checkForUpdates(): String? = withContext(Dispatchers.IO) {
- try {
- val url = URL("https://api.github.com/repos/RRechz/AirNote/releases/latest")
- val connection = url.openConnection()
- connection.connect()
- val json = connection.getInputStream().bufferedReader().use { it.readText() }
- val jsonObject = JSONObject(json)
- return@withContext jsonObject.getString("tag_name")
- } catch (e: Exception) {
- e.printStackTrace()
- return@withContext null
- }
-}
-
-/**
- * Checks if the remote version is newer than the current version.
- * It removes the “v” prefix and suffixes such as “-default”, “-debug” from the version names for comparison.
- * @param remoteVersion The version from the remote server (e.g. “v1.2”).
- * @param currentVersion The version installed on the device (e.g. “v1.1-default”).
- * @return True if the remote version is newer, false otherwise.
- */
-fun isNewerVersion(remoteVersion: String, currentVersion: String): Boolean {
- val remote = remoteVersion.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 }
- val current = currentVersion.removePrefix("v").split("-")[0].split(".").map { it.toIntOrNull() ?: 0 }
-
- for (i in 0 until maxOf(remote.size, current.size)) {
- val r = remote.getOrNull(i) ?: 0
- val c = current.getOrNull(i) ?: 0
- if (r > c) return true
- if (r < c) return false
- }
- return false
-}
-
-suspend fun getLatestReleaseInfo(): ReleaseInfo? = withContext(Dispatchers.IO) {
- try {
- val url = URL("https://api.github.com/repos/RRechz/AirNote/releases/latest")
- val connection = url.openConnection()
- connection.connect()
- val json = connection.getInputStream().bufferedReader().use { it.readText() }
- val jsonObject = JSONObject(json)
-
- val changelog = jsonObject.optString("body", "Değişiklik günlüğü alınamadı.")
- val checksum = changelog.lines().find { it.startsWith("SHA-256:") }?.substringAfter(":")?.trim() // Find and extract the checksum from changelog
-
- // ---> Find APK download link
- val assets = jsonObject.getJSONArray("assets")
- var apkUrl: String? = null
- if (assets.length() > 0) {
- val asset = assets.getJSONObject(0)
- apkUrl = asset.optString("browser_download_url")
- }
- // <---
-
- ReleaseInfo(changelog, apkUrl, checksum)
- } catch (e: Exception) {
- e.printStackTrace()
- null
- }
-}
-
-/**
- * Pulls the changelog of the latest version from the GitHub API.
- * @return Returns Success(body) if successful or Error(exception) if failed.
- */
-suspend fun getChangelogFromGitHub(): ChangelogResult {
- return withContext(Dispatchers.IO) {
- try {
- val url = URL("https://api.github.com/repos/RRechz/AirNote/releases/latest")
- val connection = url.openConnection()
- connection.connect()
- val json = connection.getInputStream().bufferedReader().use { it.readText() }
- val jsonObject = JSONObject(json)
- val body = jsonObject.optString("body", "")
-
- // ---> Find the section starting with “## Changelog” and get the text after this section. Also, remove the special characters (** and ##).
- val cleanChangelog = body.substringAfter("## Changelog", "")
- .substringAfter("\n") // Skip the title line itself
- .replace("**", "")
- .replace("##", "")
- .trim()
-
- ChangelogResult.Success(cleanChangelog)
- // <---
- } catch (e: Exception) {
- ChangelogResult.Error(e)
- }
- }
-}
-
-/**
- * Translates the given text to the target language using ML Kit.
- * @param textToTranslate The text to be translated (assumed to be in English).
- * @param targetLanguage The language code to translate to (e.g., "tr").
- * @return The translated text, or null if translation fails.
- */
-suspend fun translateText(textToTranslate: String, targetLanguage: String): String? {
- return withContext(Dispatchers.IO) {
- if (targetLanguage == "en" || textToTranslate.isBlank()) {
- return@withContext textToTranslate
- }
- try {
- val options = TranslatorOptions.Builder()
- .setSourceLanguage(TranslateLanguage.ENGLISH)
- .setTargetLanguage(targetLanguage)
- .build()
- val translator = Translation.getClient(options)
-
- val conditions = DownloadConditions.Builder()
- .requireWifi()
- .build()
- translator.downloadModelIfNeeded(conditions).await()
-
- val translatedText = translator.translate(textToTranslate).await()
- translator.close()
- translatedText
- } catch (e: Exception) {
- Log.e("Translation", "Text translation failed.", e)
- null
- }
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/widget/AddNoteReceiver.kt b/app/src/main/java/com/babelsoftware/airnote/widget/AddNoteReceiver.kt
deleted file mode 100644
index 7e5bdd4..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/widget/AddNoteReceiver.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.babelsoftware.airnote.widget
-
-import androidx.glance.appwidget.GlanceAppWidget
-import androidx.glance.appwidget.GlanceAppWidgetReceiver
-
-class AddNoteReceiver : GlanceAppWidgetReceiver() {
- override val glanceAppWidget: GlanceAppWidget = AddNoteWidget()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/widget/AddNoteWidget.kt b/app/src/main/java/com/babelsoftware/airnote/widget/AddNoteWidget.kt
deleted file mode 100644
index 168fad8..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/widget/AddNoteWidget.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.babelsoftware.airnote.widget
-
-import android.content.Context
-import android.content.Intent
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.glance.GlanceId
-import androidx.glance.GlanceModifier
-import androidx.glance.GlanceTheme
-import androidx.glance.action.clickable
-import androidx.glance.appwidget.GlanceAppWidget
-import androidx.glance.appwidget.cornerRadius
-import androidx.glance.appwidget.provideContent
-import androidx.glance.background
-import androidx.glance.layout.Alignment
-import androidx.glance.layout.Row
-import androidx.glance.layout.fillMaxSize
-import androidx.glance.layout.height
-import androidx.glance.layout.width
-import androidx.glance.text.Text
-import androidx.glance.text.TextStyle
-import com.babelsoftware.airnote.presentation.MainActivity
-import com.babelsoftware.airnote.R
-
-class AddNoteWidget : GlanceAppWidget() {
- override suspend fun provideGlance(context: Context, id: GlanceId) {
- provideContent {
- GlanceTheme {
- val intent = Intent(context, MainActivity::class.java).apply {
- putExtra("noteId", 0)
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK // <-- Doğru satır
- }
- Row(
- modifier = GlanceModifier
- .fillMaxSize()
- .height(50.dp)
- .width(100.dp)
- .background(GlanceTheme.colors.primary)
- .clickable { context.startActivity(intent) }
- .cornerRadius(20.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = context.getString(R.string.new_note),
- style = TextStyle(
- fontSize = 16.sp,
- color = GlanceTheme.colors.background
- ),
- )
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/widget/NotesWidget.kt b/app/src/main/java/com/babelsoftware/airnote/widget/NotesWidget.kt
deleted file mode 100644
index 48c04c2..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/widget/NotesWidget.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.babelsoftware.airnote.widget
-
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.glance.GlanceId
-import androidx.glance.GlanceTheme
-import androidx.glance.appwidget.GlanceAppWidget
-import androidx.glance.appwidget.GlanceAppWidgetManager
-import androidx.glance.appwidget.provideContent
-import androidx.glance.currentState
-import androidx.glance.state.GlanceStateDefinition
-import com.babelsoftware.airnote.domain.usecase.NoteUseCase
-import com.babelsoftware.airnote.widget.ui.SelectedNote
-import com.babelsoftware.airnote.widget.ui.ZeroState
-import dagger.hilt.EntryPoint
-import dagger.hilt.EntryPoints
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import java.io.File
-
-@EntryPoint
-@InstallIn(SingletonComponent::class)
-interface WidgetModelRepositoryEntryPoint {
- fun noteUseCase() : NoteUseCase
-}
-
-class NotesWidget : GlanceAppWidget() {
- override val stateDefinition: GlanceStateDefinition>>
- get() = object: GlanceStateDefinition>> {
- override suspend fun getDataStore(
- context: Context,
- fileKey: String
- ): DataStore>> {
- return NotesDataStore(context)
- }
- override fun getLocation(context: Context, fileKey: String): File {
- throw NotImplementedError("Not implemented")
- }
- }
-
- override suspend fun provideGlance(context: Context, id: GlanceId) {
- val noteUseCase = getNoteUseCase(context)
- val widgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)
-
- provideContent {
- GlanceTheme {
- currentState>>().firstOrNull { it.first == widgetId }?.second.let { noteId ->
- noteUseCase.observe()
- val selectedNote = noteUseCase.notes.filter { it.id == noteId }
- when {
- selectedNote.isEmpty() -> ZeroState(widgetId = widgetId)
- else -> SelectedNote(selectedNote.first(), noteUseCase, widgetId = widgetId)
- }
- }
- }
- }
- }
-}
-
-
-fun getNoteUseCase(applicationContext: Context): NoteUseCase {
- var widgetModelRepositoryEntrypoint: WidgetModelRepositoryEntryPoint = EntryPoints.get(
- applicationContext,
- WidgetModelRepositoryEntryPoint::class.java,
- )
- return widgetModelRepositoryEntrypoint.noteUseCase()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/widget/NotesWidgetActivity.kt b/app/src/main/java/com/babelsoftware/airnote/widget/NotesWidgetActivity.kt
deleted file mode 100644
index bf61447..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/widget/NotesWidgetActivity.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.babelsoftware.airnote.widget
-
-import android.appwidget.AppWidgetManager
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.res.stringResource
-import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.glance.appwidget.updateAll
-import androidx.hilt.navigation.compose.hiltViewModel
-import com.babelsoftware.airnote.domain.usecase.NoteUseCase
-import com.babelsoftware.airnote.domain.repository.SettingsRepository
-import com.babelsoftware.airnote.presentation.components.NotesScaffold
-import com.babelsoftware.airnote.presentation.screens.home.getContainerColor
-import com.babelsoftware.airnote.presentation.screens.home.sorter
-import com.babelsoftware.airnote.presentation.screens.home.widgets.NoteFilter
-import com.babelsoftware.airnote.presentation.screens.settings.TopBar
-import com.babelsoftware.airnote.presentation.screens.settings.model.SettingsViewModel
-import com.babelsoftware.airnote.presentation.screens.settings.settings.shapeManager
-import com.babelsoftware.airnote.presentation.theme.LeafNotesTheme
-import com.babelsoftware.airnote.R
-import com.babelsoftware.airnote.domain.usecase.FolderUseCase
-import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.runBlocking
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class NotesWidgetActivity : ComponentActivity() {
- @Inject
- lateinit var noteUseCase: NoteUseCase
-
- @Inject
- lateinit var folderUseCase: FolderUseCase
-
- @Inject
- lateinit var settingsRepository: SettingsRepository
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- installSplashScreen()
- enableEdgeToEdge()
-
- val appWidgetId = intent?.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID,) ?: AppWidgetManager.INVALID_APPWIDGET_ID
- setContent {
- val settings = hiltViewModel()
- noteUseCase.observe()
- val allFolders by folderUseCase.getAllFolders().collectAsState(initial = emptyList())
-
- LeafNotesTheme(settingsModel = settings) {
- NotesScaffold(
- topBar = {
- TopBar(
- title = stringResource(id = R.string.select_note),
- onBackNavClicked = { finish() }
- )
- },
- content = {
- NoteFilter(
- settingsViewModel = settings,
- containerColor = getContainerColor(settings),
- shape = shapeManager(
- radius = settings.settings.value.cornerRadius / 2,
- isBoth = true
- ),
- onNoteClicked = { id ->
- runBlocking {
- settingsRepository.putInt(
- "${NotesWidgetReceiver.WIDGET_PREFERENCE}${appWidgetId}",
- id
- )
- NotesWidget().updateAll(this@NotesWidgetActivity)
- val resultValue = Intent().putExtra(
- AppWidgetManager.EXTRA_APPWIDGET_ID,
- appWidgetId
- )
- setResult(RESULT_OK, resultValue)
- finish()
- }
- },
- notes = noteUseCase.notes.sortedWith(sorter(settings.settings.value.sortDescending)),
- allFolders = allFolders,
- viewMode = false,
- )
- }
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/widget/NotesWidgetReceiver.kt b/app/src/main/java/com/babelsoftware/airnote/widget/NotesWidgetReceiver.kt
deleted file mode 100644
index c8f7d42..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/widget/NotesWidgetReceiver.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.babelsoftware.airnote.widget
-
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.glance.appwidget.GlanceAppWidget
-import androidx.glance.appwidget.GlanceAppWidgetReceiver
-import com.babelsoftware.airnote.data.repository.SettingsRepositoryImpl
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flow
-
-
-class NotesWidgetReceiver : GlanceAppWidgetReceiver() {
- override val glanceAppWidget: GlanceAppWidget = NotesWidget()
-
- companion object {
- const val WIDGET_PREFERENCE = "widgetNote_"
- }
-}
-
-
-class NotesDataStore(private val context: Context): DataStore>> {
- override val data: Flow>>
- get() {
- val settingsRepository = SettingsRepositoryImpl(context)
- return flow { emit(settingsRepository.getEveryNotesWidget()) }
- }
-
- override suspend fun updateData(transform: suspend (t: List>) -> List>): List> {
- throw NotImplementedError("Not implemented")
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/babelsoftware/airnote/widget/ui/Note.kt b/app/src/main/java/com/babelsoftware/airnote/widget/ui/Note.kt
deleted file mode 100644
index 569edd7..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/widget/ui/Note.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.babelsoftware.airnote.widget.ui
-
-import android.content.Intent
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.glance.GlanceModifier
-import androidx.glance.GlanceTheme
-import androidx.glance.LocalContext
-import androidx.glance.action.clickable
-import androidx.glance.background
-import androidx.glance.layout.Column
-import androidx.glance.layout.fillMaxSize
-import androidx.glance.layout.fillMaxWidth
-import androidx.glance.layout.padding
-import androidx.glance.text.FontWeight
-import com.babelsoftware.airnote.domain.model.Note
-import com.babelsoftware.airnote.domain.usecase.NoteUseCase
-import com.babelsoftware.airnote.presentation.MainActivity
-import com.babelsoftware.airnote.presentation.components.markdown.WidgetText
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-
-
-@Composable
-fun SelectedNote(note: Note, noteUseCase: NoteUseCase, widgetId: Int) {
- val context = LocalContext.current
- val glanceModifier = GlanceModifier.clickable {
- val intent = Intent(context, MainActivity::class.java).apply {
- putExtra("noteId", note.id)
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK // <-- Doğru satır
- }
- context.startActivity(intent)
- }
-
-
- Column(
- modifier = glanceModifier
- .background(GlanceTheme.colors.background)
- .fillMaxSize()
- .padding(6.dp)
- ) {
- if(note.name.isNotBlank()) {
- WidgetText(
- modifier = glanceModifier,
- markdown = note.name,
- weight = FontWeight.Bold,
- fontSize = 24.sp,
- color = GlanceTheme.colors.primary,
- onContentChange = {
- CoroutineScope(Dispatchers.IO).launch {
- noteUseCase.addNote(note.copy(name = it))
- noteUseCase.observe()
- }
- }
- )
- }
- if(note.description.isNotBlank()) {
- WidgetText(
- modifier = glanceModifier.fillMaxWidth(),
- markdown = note.description,
- weight = FontWeight.Normal,
- fontSize = 12.sp,
- color = GlanceTheme.colors.primary,
- onContentChange = {
- CoroutineScope(Dispatchers.IO).launch {
- noteUseCase.addNote(note.copy(description = it))
- noteUseCase.observe()
- }
- }
- )
- }
- }
-}
diff --git a/app/src/main/java/com/babelsoftware/airnote/widget/ui/ZeroState.kt b/app/src/main/java/com/babelsoftware/airnote/widget/ui/ZeroState.kt
deleted file mode 100644
index 6a20421..0000000
--- a/app/src/main/java/com/babelsoftware/airnote/widget/ui/ZeroState.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.babelsoftware.airnote.widget.ui
-
-import android.appwidget.AppWidgetManager
-import androidx.compose.runtime.Composable
-import androidx.glance.Button
-import androidx.glance.GlanceModifier
-import androidx.glance.GlanceTheme
-import androidx.glance.LocalContext
-import androidx.glance.action.ActionParameters
-import androidx.glance.action.actionParametersOf
-import androidx.glance.action.actionStartActivity
-import androidx.glance.background
-import androidx.glance.layout.Alignment
-import androidx.glance.layout.Box
-import androidx.glance.layout.fillMaxSize
-import com.babelsoftware.airnote.widget.NotesWidgetActivity
-import com.babelsoftware.airnote.R
-
-@Composable
-fun ZeroState(widgetId: Int) {
- val widgetIdKey = ActionParameters.Key