-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index fdf8d994..fe63bb67 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 00000000..f8051a6f
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 007b7f70..cb68096c 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,6 @@
# My Brain
-
### Open-source, All-in-one productivity app for Tasks, Notes, Calendar, Diary and Bookmarks.
@@ -21,12 +20,12 @@
[](https://f-droid.org/packages/com.mhss.app.mybrain)
-[](https://github.com/mhss1/MyBrain/releases/latest)
## Features
-- Private with no data collection and no internet permission at all.
+- Private with no data collection at all.
- Create tasks with priority, sub-tasks, description and due date and reminders.
- Create Notes that supports markdown which enables you to use Headers, lists, links etc..
- Record your mood daily and view your mood summary with beautiful graphs.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index dd4f6d33..46680a06 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -4,6 +4,7 @@ plugins {
id("org.jetbrains.kotlin.android")
id ("com.google.dagger.hilt.android")
id ("com.google.devtools.ksp")
+ kotlin("plugin.serialization")
}
android {
@@ -14,8 +15,8 @@ android {
applicationId = "com.mhss.app.mybrain"
minSdk = 26
targetSdk = 34
- versionCode = 7
- versionName = "1.0.6"
+ versionCode = 8
+ versionName = "1.0.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -34,6 +35,7 @@ android {
debug {
isMinifyEnabled = false
applicationIdSuffix = ".debug"
+ isDebuggable = false
resValue("string", "app_name", "MyBrain Debug")
}
}
@@ -50,7 +52,7 @@ android {
buildConfig = true
}
composeOptions {
- kotlinCompilerExtensionVersion = "1.5.2"
+ kotlinCompilerExtensionVersion = "1.5.12"
}
packaging {
resources {
@@ -63,18 +65,20 @@ android {
}
dependencies {
- val roomVersion = "2.6.0"
- val coroutinesVersion = "1.7.3"
- val lifecycleVersion = "2.6.2"
- val workVersion = "2.8.1"
- implementation(platform("androidx.compose:compose-bom:2023.09.01"))
-
- implementation("androidx.core:core-ktx:1.12.0")
+ val roomVersion = "2.6.1"
+ val coroutinesVersion = "1.8.0"
+ val lifecycleVersion = "2.7.0"
+ val workVersion = "2.9.0"
+ implementation(platform("androidx.compose:compose-bom:2024.05.00"))
+
+ implementation("androidx.appcompat:appcompat:1.6.1")
+ implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material:material")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
- implementation("androidx.activity:activity-compose:1.8.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion")
+ implementation("androidx.activity:activity-compose:1.9.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
@@ -82,8 +86,8 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-tooling")
// Compose navigation
- implementation("androidx.navigation:navigation-compose:2.7.4")
- implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
+ implementation("androidx.navigation:navigation-compose:2.7.7")
+ implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
// Room
implementation("androidx.room:room-runtime:$roomVersion")
@@ -91,20 +95,17 @@ dependencies {
implementation("androidx.room:room-ktx:$roomVersion")
//Dagger - Hilt
- implementation("com.google.dagger:hilt-android:2.48")
- ksp("com.google.dagger:hilt-android-compiler:2.48")
- ksp("androidx.hilt:hilt-compiler:1.0.0")
- implementation("androidx.hilt:hilt-work:1.0.0")
+ implementation("com.google.dagger:hilt-android:2.49")
+ ksp("com.google.dagger:hilt-android-compiler:2.49")
+ ksp("androidx.hilt:hilt-compiler:1.2.0")
+ implementation("androidx.hilt:hilt-work:1.2.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
- // Gson
- implementation("com.google.code.gson:gson:2.10")
-
// Preferences DataStore
- implementation("androidx.datastore:datastore-preferences:1.0.0")
+ implementation("androidx.datastore:datastore-preferences:1.1.1")
// Accompanist libraries
implementation("com.google.accompanist:accompanist-flowlayout:0.23.1")
@@ -112,11 +113,11 @@ dependencies {
implementation("com.google.accompanist:accompanist-permissions:0.23.1")
// Compose MarkDown
- implementation("com.github.jeziellago:compose-markdown:0.3.4")
+ implementation("com.github.jeziellago:compose-markdown:0.5.0")
// Compose Glance (Widgets)
- implementation("androidx.glance:glance-appwidget:1.0.0")
- implementation("androidx.glance:glance-material:1.0.0")
+ implementation("androidx.glance:glance-appwidget:1.1.0-beta02")
+ implementation("androidx.glance:glance-material:1.1.0-beta02")
//Moshi
implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
@@ -129,6 +130,12 @@ dependencies {
// DocumentFile
implementation("androidx.documentfile:documentfile:1.0.1")
+
+ // Biometric
+ implementation("androidx.biometric:biometric:1.2.0-alpha05")
+
+ // Kotlinx serialization
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
}
ksp {
diff --git a/app/schemas/com.mhss.app.mybrain.data.local.MyBrainDatabase/5.json b/app/schemas/com.mhss.app.mybrain.data.local.MyBrainDatabase/5.json
new file mode 100644
index 00000000..34769571
--- /dev/null
+++ b/app/schemas/com.mhss.app.mybrain.data.local.MyBrainDatabase/5.json
@@ -0,0 +1,314 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 5,
+ "identityHash": "d2ca0cf03a7c6387ed1006d644284527",
+ "entities": [
+ {
+ "tableName": "notes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `content` TEXT NOT NULL, `created_date` INTEGER NOT NULL, `updated_date` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `folder_id` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`folder_id`) REFERENCES `note_folders`(`name`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdDate",
+ "columnName": "created_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedDate",
+ "columnName": "updated_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pinned",
+ "columnName": "pinned",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "folderId",
+ "columnName": "folder_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "note_folders",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "folder_id"
+ ],
+ "referencedColumns": [
+ "name"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "tasks",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `description` TEXT NOT NULL, `is_completed` INTEGER NOT NULL, `priority` INTEGER NOT NULL, `created_date` INTEGER NOT NULL, `updated_date` INTEGER NOT NULL, `sub_tasks` TEXT NOT NULL, `dueDate` INTEGER NOT NULL, `recurring` INTEGER NOT NULL, `frequency` INTEGER NOT NULL, `frequency_amount` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isCompleted",
+ "columnName": "is_completed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdDate",
+ "columnName": "created_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedDate",
+ "columnName": "updated_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subTasks",
+ "columnName": "sub_tasks",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dueDate",
+ "columnName": "dueDate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recurring",
+ "columnName": "recurring",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "frequency",
+ "columnName": "frequency",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "frequencyAmount",
+ "columnName": "frequency_amount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "diary",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `content` TEXT NOT NULL, `created_date` INTEGER NOT NULL, `updated_date` INTEGER NOT NULL, `mood` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdDate",
+ "columnName": "created_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedDate",
+ "columnName": "updated_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mood",
+ "columnName": "mood",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "bookmarks",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `created_date` INTEGER NOT NULL, `updated_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdDate",
+ "columnName": "created_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedDate",
+ "columnName": "updated_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "alarms",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `time` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "time",
+ "columnName": "time",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "note_folders",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "name"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd2ca0cf03a7c6387ed1006d644284527')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/mhss/app/mybrain/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/mhss/app/mybrain/ExampleInstrumentedTest.kt
deleted file mode 100644
index 6898cbce..00000000
--- a/app/src/androidTest/java/com/mhss/app/mybrain/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.mhss.app.mybrain
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.mhss.app.mybrain", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 122c2c5c..a658ee89 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,9 +6,12 @@
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/mhss/app/mybrain/app/MyBrainApplication.kt b/app/src/main/java/com/mhss/app/mybrain/app/MyBrainApplication.kt
index 4ef8e4e7..0356a6d2 100644
--- a/app/src/main/java/com/mhss/app/mybrain/app/MyBrainApplication.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/app/MyBrainApplication.kt
@@ -27,8 +27,8 @@ class MyBrainApplication : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
- override fun getWorkManagerConfiguration() =
- Configuration.Builder()
+ override val workManagerConfiguration: Configuration
+ get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
diff --git a/app/src/main/java/com/mhss/app/mybrain/data/backup/RoomBackupRepositoryImpl.kt b/app/src/main/java/com/mhss/app/mybrain/data/backup/RoomBackupRepositoryImpl.kt
index 05ba8e51..78165731 100644
--- a/app/src/main/java/com/mhss/app/mybrain/data/backup/RoomBackupRepositoryImpl.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/data/backup/RoomBackupRepositoryImpl.kt
@@ -3,20 +3,30 @@ package com.mhss.app.mybrain.data.backup
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
+import androidx.room.withTransaction
import com.mhss.app.mybrain.data.local.MyBrainDatabase
+import com.mhss.app.mybrain.domain.model.Bookmark
+import com.mhss.app.mybrain.domain.model.DiaryEntry
+import com.mhss.app.mybrain.domain.model.Note
+import com.mhss.app.mybrain.domain.model.NoteFolder
+import com.mhss.app.mybrain.domain.model.Task
import com.mhss.app.mybrain.domain.repository.RoomBackupRepository
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
-import java.io.File
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import kotlinx.serialization.json.encodeToStream
import javax.inject.Inject
class RoomBackupRepositoryImpl @Inject constructor(
- private val database: MyBrainDatabase,
private val context: Context,
+ private val database: MyBrainDatabase
) : RoomBackupRepository {
- private val dbPath = File(context.getDatabasePath(database.openHelper.databaseName).toURI())
-
+ @OptIn(ExperimentalSerializationApi::class)
override suspend fun exportDatabase(
directoryUri: Uri,
encrypted: Boolean, // To be added in a future version
@@ -24,16 +34,33 @@ class RoomBackupRepositoryImpl @Inject constructor(
): Boolean {
return withContext(Dispatchers.IO) {
try {
- val fileName = "MyBrain_Backup_${System.currentTimeMillis()}.sqlite3"
+ val fileName = "MyBrain_Backup_${System.currentTimeMillis()}.json"
val pickedDir = DocumentFile.fromTreeUri(context, directoryUri)
- val destination = pickedDir!!.createFile("application/sqlite3", fileName)
+ val destination = pickedDir!!.createFile("application/json", fileName)
+
+ val notes = database.noteDao().getAllNotes().map {
+ it.copy(id = 0)
+ }
+ val noteFolders = database.noteDao().getAllNoteFolders().first()
+ val tasks = database.taskDao().getAllTasks().first().map {
+ it.copy(id = 0)
+ }
+ val diary = database.diaryDao().getAllEntries().first().map {
+ it.copy(id = 0)
+ }
+ val bookmarks = database.bookmarkDao().getAllBookmarks().first().map {
+ it.copy(id = 0)
+ }
+ val backupData = BackupData(notes, noteFolders, tasks, diary, bookmarks)
val outputStream =
destination?.let { context.contentResolver.openOutputStream(it.uri) }
?: return@withContext false
- dbPath.inputStream().copyTo(outputStream)
+ outputStream.use {
+ Json.encodeToStream(backupData, outputStream)
+ }
true
} catch (e: Exception) {
@@ -43,6 +70,7 @@ class RoomBackupRepositoryImpl @Inject constructor(
}
}
+ @OptIn(ExperimentalSerializationApi::class)
override suspend fun importDatabase(
fileUri: Uri,
encrypted: Boolean, // To be added in a future version
@@ -50,10 +78,28 @@ class RoomBackupRepositoryImpl @Inject constructor(
): Boolean {
return withContext(Dispatchers.IO) {
try {
- database.close()
- context.contentResolver.openInputStream(fileUri)?.use {
- it.copyTo(dbPath.outputStream())
+ val json = Json {
+ ignoreUnknownKeys = true
+ }
+ val backupData = context.contentResolver.openInputStream(fileUri)?.use {
+ json.decodeFromStream(it)
} ?: return@withContext false
+ val oldNoteFolderIds = backupData.noteFolders.map { it.id }
+ database.withTransaction {
+ val newNoteFolderIds = database.noteDao().insertNoteFolders(backupData.noteFolders.map { it.copy(id = 0) })
+ if (newNoteFolderIds.size != oldNoteFolderIds.size) throw Exception("New folder count does not match old folder count.")
+ val notes = backupData.notes.map { note ->
+ if (note.folderId != null) {
+ note.copy(
+ folderId = newNoteFolderIds[oldNoteFolderIds.indexOf(note.folderId)].toInt()
+ )
+ } else note
+ }
+ database.noteDao().insertNotes(notes)
+ database.taskDao().insertTasks(backupData.tasks)
+ database.diaryDao().insertEntries(backupData.diary)
+ database.bookmarkDao().insertBookmarks(backupData.bookmarks)
+ }
true
} catch (e: Exception) {
e.printStackTrace()
@@ -61,4 +107,13 @@ class RoomBackupRepositoryImpl @Inject constructor(
}
}
}
+
+ @Serializable
+ private data class BackupData(
+ val notes: List,
+ val noteFolders: List,
+ val tasks: List,
+ val diary: List,
+ val bookmarks: List
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/data/local/converters/DBConverters.kt b/app/src/main/java/com/mhss/app/mybrain/data/local/converters/DBConverters.kt
index 63f0b16c..afd0bbea 100644
--- a/app/src/main/java/com/mhss/app/mybrain/data/local/converters/DBConverters.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/data/local/converters/DBConverters.kt
@@ -1,25 +1,21 @@
package com.mhss.app.mybrain.data.local.converters
import androidx.room.TypeConverter
-import com.google.gson.Gson
-import com.google.gson.reflect.TypeToken
import com.mhss.app.mybrain.domain.model.SubTask
import com.mhss.app.mybrain.util.diary.Mood
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
class DBConverters {
@TypeConverter
fun fromSubTasksList(value: List): String {
- val gson = Gson()
- val type = TypeToken.getParameterized(List::class.java, SubTask::class.java).type
- return gson.toJson(value, type)
+ return Json.encodeToString(value)
}
@TypeConverter
fun toSubTasksList(value: String): List {
- val gson = Gson()
- val type = TypeToken.getParameterized(List::class.java, SubTask::class.java).type
- return gson.fromJson(value, type)
+ return Json.decodeFromString>(value)
}
@TypeConverter
diff --git a/app/src/main/java/com/mhss/app/mybrain/data/local/dao/BookmarkDao.kt b/app/src/main/java/com/mhss/app/mybrain/data/local/dao/BookmarkDao.kt
index 98d2aa43..edfe6579 100644
--- a/app/src/main/java/com/mhss/app/mybrain/data/local/dao/BookmarkDao.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/data/local/dao/BookmarkDao.kt
@@ -16,7 +16,7 @@ interface BookmarkDao {
@Query("SELECT * FROM bookmarks WHERE title LIKE '%' || :query || '%' OR description LIKE '%' || :query || '%' OR url LIKE '%' || :query || '%'")
suspend fun getBookmark(query: String): List
- @Insert
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertBookmark(bookmark: Bookmark)
@Update
@@ -25,7 +25,7 @@ interface BookmarkDao {
@Delete
suspend fun deleteBookmark(bookmark: Bookmark)
- @Insert
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertBookmarks(bookmarks: List)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/data/local/dao/DiaryDao.kt b/app/src/main/java/com/mhss/app/mybrain/data/local/dao/DiaryDao.kt
index 29780490..e3760e96 100644
--- a/app/src/main/java/com/mhss/app/mybrain/data/local/dao/DiaryDao.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/data/local/dao/DiaryDao.kt
@@ -16,10 +16,10 @@ interface DiaryDao {
@Query("SELECT * FROM diary WHERE title LIKE '%' || :query || '%' OR content LIKE '%' || :query || '%'")
suspend fun getEntriesByTitle(query: String): List
- @Insert
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertEntry(diary: DiaryEntry)
- @Insert
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertEntries(diary: List)
@Update
diff --git a/app/src/main/java/com/mhss/app/mybrain/data/local/dao/NoteDao.kt b/app/src/main/java/com/mhss/app/mybrain/data/local/dao/NoteDao.kt
index 23432216..865f2418 100644
--- a/app/src/main/java/com/mhss/app/mybrain/data/local/dao/NoteDao.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/data/local/dao/NoteDao.kt
@@ -8,8 +8,11 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface NoteDao {
+ @Query("SELECT title, SUBSTR(content, 1, 450) AS content, created_date, updated_date, pinned, folder_id, id FROM notes WHERE folder_id IS NULL")
+ fun getAllFolderlessNotes(): Flow>
+
@Query("SELECT * FROM notes")
- fun getAllNotes(): Flow>
+ suspend fun getAllNotes(): List
@Query("SELECT * FROM notes WHERE id = :id")
suspend fun getNote(id: Int): Note
@@ -17,7 +20,7 @@ interface NoteDao {
@Query("SELECT * FROM notes WHERE title LIKE '%' || :query || '%' OR content LIKE '%' || :query || '%'")
suspend fun getNotesByTitle(query: String): List
- @Query("SELECT * FROM notes WHERE folder_id = :folderId")
+ @Query("SELECT title, SUBSTR(content, 1, 450) AS content, created_date, updated_date, pinned, folder_id, id FROM notes WHERE folder_id = :folderId")
fun getNotesByFolder(folderId: Int): Flow>
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -36,7 +39,7 @@ interface NoteDao {
suspend fun insertNoteFolder(folder: NoteFolder)
@Insert(onConflict = OnConflictStrategy.IGNORE)
- suspend fun insertNoteFolders(folders: List)
+ suspend fun insertNoteFolders(folders: List): List
@Update
suspend fun updateNoteFolder(folder: NoteFolder)
@@ -46,4 +49,7 @@ interface NoteDao {
@Query("SELECT * FROM note_folders")
fun getAllNoteFolders(): Flow>
+
+ @Query("SELECT * FROM note_folders WHERE id = :folderId")
+ fun getNoteFolder(folderId: Int): NoteFolder?
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/data/local/migrations/RoomMigrations.kt b/app/src/main/java/com/mhss/app/mybrain/data/local/migrations/RoomMigrations.kt
index e4cad589..3f0f7468 100644
--- a/app/src/main/java/com/mhss/app/mybrain/data/local/migrations/RoomMigrations.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/data/local/migrations/RoomMigrations.kt
@@ -5,25 +5,25 @@ import androidx.sqlite.db.SupportSQLiteDatabase
// Added note folders
val MIGRATION_1_2 = object : Migration(1, 2) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("CREATE TABLE note_folders (name TEXT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)")
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("CREATE TABLE note_folders (name TEXT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)")
- database.execSQL("CREATE TABLE IF NOT EXISTS `notes_new` (`title` TEXT NOT NULL, `content` TEXT NOT NULL, `created_date` INTEGER NOT NULL, `updated_date` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `folder_id` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY (folder_id) REFERENCES note_folders (id) ON UPDATE NO ACTION ON DELETE CASCADE)")
- database.execSQL("INSERT INTO notes_new (title, content, created_date, updated_date, pinned, id) SELECT title, content, created_date, updated_date, pinned, id FROM notes")
- database.execSQL("DROP TABLE notes")
- database.execSQL("ALTER TABLE notes_new RENAME TO notes")
+ db.execSQL("CREATE TABLE IF NOT EXISTS `notes_new` (`title` TEXT NOT NULL, `content` TEXT NOT NULL, `created_date` INTEGER NOT NULL, `updated_date` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `folder_id` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY (folder_id) REFERENCES note_folders (id) ON UPDATE NO ACTION ON DELETE CASCADE)")
+ db.execSQL("INSERT INTO notes_new (title, content, created_date, updated_date, pinned, id) SELECT title, content, created_date, updated_date, pinned, id FROM notes")
+ db.execSQL("DROP TABLE notes")
+ db.execSQL("ALTER TABLE notes_new RENAME TO notes")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("ALTER TABLE tasks ADD COLUMN recurring INTEGER NOT NULL DEFAULT 0")
- database.execSQL("ALTER TABLE tasks ADD COLUMN frequency INTEGER NOT NULL DEFAULT 0")
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("ALTER TABLE tasks ADD COLUMN recurring INTEGER NOT NULL DEFAULT 0")
+ db.execSQL("ALTER TABLE tasks ADD COLUMN frequency INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATION_3_4 = object : Migration(3, 4) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("ALTER TABLE tasks ADD COLUMN frequency_amount INTEGER NOT NULL DEFAULT 1")
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("ALTER TABLE tasks ADD COLUMN frequency_amount INTEGER NOT NULL DEFAULT 1")
}
}
diff --git a/app/src/main/java/com/mhss/app/mybrain/data/repository/NoteRepositoryImpl.kt b/app/src/main/java/com/mhss/app/mybrain/data/repository/NoteRepositoryImpl.kt
index acffe7ef..bd979b5b 100644
--- a/app/src/main/java/com/mhss/app/mybrain/data/repository/NoteRepositoryImpl.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/data/repository/NoteRepositoryImpl.kt
@@ -14,8 +14,8 @@ class NoteRepositoryImpl(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : NoteRepository {
- override fun getAllNotes(): Flow> {
- return noteDao.getAllNotes()
+ override fun getAllFolderlessNotes(): Flow> {
+ return noteDao.getAllFolderlessNotes()
}
override suspend fun getNote(id: Int): Note {
@@ -73,4 +73,10 @@ class NoteRepositoryImpl(
override fun getAllNoteFolders(): Flow> {
return noteDao.getAllNoteFolders()
}
+
+ override suspend fun getNoteFolder(folderId: Int): NoteFolder? {
+ return withContext(Dispatchers.IO) {
+ noteDao.getNoteFolder(folderId)
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/di/AppModule.kt b/app/src/main/java/com/mhss/app/mybrain/di/AppModule.kt
index b16c8933..34c945f3 100644
--- a/app/src/main/java/com/mhss/app/mybrain/di/AppModule.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/di/AppModule.kt
@@ -91,7 +91,10 @@ object AppModule {
@Singleton
@Provides
fun provideBackupRepository(
- myBrainDatabase: MyBrainDatabase,
- @ApplicationContext context: Context
- ): RoomBackupRepository = RoomBackupRepositoryImpl(myBrainDatabase ,context)
+ @ApplicationContext context: Context,
+ database: MyBrainDatabase
+ ): RoomBackupRepository = RoomBackupRepositoryImpl(
+ context,
+ database
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/model/Bookmark.kt b/app/src/main/java/com/mhss/app/mybrain/domain/model/Bookmark.kt
index cf75db64..ea6560f3 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/model/Bookmark.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/model/Bookmark.kt
@@ -3,8 +3,10 @@ package com.mhss.app.mybrain.domain.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
+import kotlinx.serialization.Serializable
@Entity(tableName = "bookmarks")
+@Serializable
data class Bookmark(
val url: String,
val title: String = "",
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/model/CalendarEvent.kt b/app/src/main/java/com/mhss/app/mybrain/domain/model/CalendarEvent.kt
index 4a33378e..fa4519f9 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/model/CalendarEvent.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/model/CalendarEvent.kt
@@ -1,5 +1,8 @@
package com.mhss.app.mybrain.domain.model
+import kotlinx.serialization.Serializable
+
+@Serializable
data class CalendarEvent(
val id: Long,
val title: String,
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/model/DiaryEntry.kt b/app/src/main/java/com/mhss/app/mybrain/domain/model/DiaryEntry.kt
index fb5794a2..31c9328b 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/model/DiaryEntry.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/model/DiaryEntry.kt
@@ -4,8 +4,10 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.mhss.app.mybrain.util.diary.Mood
+import kotlinx.serialization.Serializable
@Entity(tableName = "diary")
+@Serializable
data class DiaryEntry(
val title: String = "",
val content: String = "",
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/model/Note.kt b/app/src/main/java/com/mhss/app/mybrain/domain/model/Note.kt
index ac65f8ab..393322d2 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/model/Note.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/model/Note.kt
@@ -4,6 +4,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
+import kotlinx.serialization.Serializable
@Entity(
tableName = "notes",
@@ -17,6 +18,7 @@ import androidx.room.PrimaryKey
)
]
)
+@Serializable
data class Note(
val title: String = "",
val content: String = "",
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/model/NoteFolder.kt b/app/src/main/java/com/mhss/app/mybrain/domain/model/NoteFolder.kt
index 68fa48b6..fe96a3fb 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/model/NoteFolder.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/model/NoteFolder.kt
@@ -2,10 +2,12 @@ package com.mhss.app.mybrain.domain.model
import androidx.room.Entity
import androidx.room.PrimaryKey
+import kotlinx.serialization.Serializable
@Entity(
tableName = "note_folders",
)
+@Serializable
data class NoteFolder(
val name: String,
@PrimaryKey(autoGenerate = true)
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/model/SubTask.kt b/app/src/main/java/com/mhss/app/mybrain/domain/model/SubTask.kt
index 434e38db..0ffca5cd 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/model/SubTask.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/model/SubTask.kt
@@ -1,9 +1,29 @@
package com.mhss.app.mybrain.domain.model
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
import java.util.*
+@Serializable
data class SubTask(
val title: String,
val isCompleted: Boolean,
+ @Serializable(with = UUIDSerializer::class)
val id: UUID = UUID.randomUUID()
)
+
+object UUIDSerializer : KSerializer {
+ override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): UUID {
+ return UUID.fromString(decoder.decodeString())
+ }
+
+ override fun serialize(encoder: Encoder, value: UUID) {
+ encoder.encodeString(value.toString())
+ }
+}
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/model/Task.kt b/app/src/main/java/com/mhss/app/mybrain/domain/model/Task.kt
index deb7d309..b6523681 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/model/Task.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/model/Task.kt
@@ -3,8 +3,10 @@ package com.mhss.app.mybrain.domain.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
+import kotlinx.serialization.Serializable
@Entity(tableName = "tasks")
+@Serializable
data class Task(
val title: String,
val description: String = "",
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/repository/NoteRepository.kt b/app/src/main/java/com/mhss/app/mybrain/domain/repository/NoteRepository.kt
index c8cbc7fd..f4612e46 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/repository/NoteRepository.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/repository/NoteRepository.kt
@@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow
interface NoteRepository {
- fun getAllNotes(): Flow>
+ fun getAllFolderlessNotes(): Flow>
suspend fun getNote(id: Int): Note
@@ -28,4 +28,6 @@ interface NoteRepository {
fun getAllNoteFolders(): Flow>
+ suspend fun getNoteFolder(folderId: Int): NoteFolder?
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/alarm/AddAlarmUseCase.kt b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/alarm/AddAlarmUseCase.kt
index 3fe4b616..fe5de0a9 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/alarm/AddAlarmUseCase.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/alarm/AddAlarmUseCase.kt
@@ -2,6 +2,7 @@ package com.mhss.app.mybrain.domain.use_case.alarm
import android.app.AlarmManager
import android.content.Context
+import android.os.Build
import com.mhss.app.mybrain.domain.model.Alarm
import com.mhss.app.mybrain.domain.repository.AlarmRepository
import com.mhss.app.mybrain.util.alarms.scheduleAlarm
@@ -11,9 +12,17 @@ class AddAlarmUseCase @Inject constructor(
private val alarmRepository: AlarmRepository,
private val context: Context
) {
- suspend operator fun invoke(alarm: Alarm) {
+ suspend operator fun invoke(alarm: Alarm): Boolean {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (!alarmManager.canScheduleExactAlarms()) {
+ return false
+ }
+ }
alarmManager.scheduleAlarm(alarm, context)
alarmRepository.insertAlarm(alarm)
+ return true
}
+
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetAllNoteFoldersUseCase.kt b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetAllNoteFoldersUseCase.kt
index 0dda0f0f..83229d68 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetAllNoteFoldersUseCase.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetAllNoteFoldersUseCase.kt
@@ -6,5 +6,5 @@ import javax.inject.Inject
class GetAllNoteFoldersUseCase @Inject constructor(
private val repository: NoteRepository
) {
- suspend operator fun invoke() = repository.getAllNoteFolders()
+ operator fun invoke() = repository.getAllNoteFolders()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetAllNotesUseCase.kt b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetAllNotesUseCase.kt
index 7f47ee1a..9d78f77e 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetAllNotesUseCase.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetAllNotesUseCase.kt
@@ -12,7 +12,7 @@ class GetAllNotesUseCase @Inject constructor(
private val notesRepository: NoteRepository
) {
operator fun invoke(order: Order) : Flow> {
- return notesRepository.getAllNotes().map { list ->
+ return notesRepository.getAllFolderlessNotes().map { list ->
when (order.orderType) {
is OrderType.ASC -> {
when (order) {
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetNoteFolderUseCase.kt b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetNoteFolderUseCase.kt
new file mode 100644
index 00000000..dc1c9363
--- /dev/null
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetNoteFolderUseCase.kt
@@ -0,0 +1,10 @@
+package com.mhss.app.mybrain.domain.use_case.notes
+
+import com.mhss.app.mybrain.domain.repository.NoteRepository
+import javax.inject.Inject
+
+class GetNoteFolderUseCase @Inject constructor(
+ private val repository: NoteRepository
+) {
+ suspend operator fun invoke(folderId: Int) = repository.getNoteFolder(folderId)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetNotesByFolderUseCase.kt b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetNotesByFolderUseCase.kt
index f8f62244..30abcec5 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetNotesByFolderUseCase.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/GetNotesByFolderUseCase.kt
@@ -9,7 +9,7 @@ import javax.inject.Inject
class GetNotesByFolderUseCase @Inject constructor(
private val notesRepository: NoteRepository
) {
- operator fun invoke(folderId: Int, order: Order) = notesRepository.getNotesByFolder(folderId).map { list ->
+ operator fun invoke(id: Int, order: Order) = notesRepository.getNotesByFolder(id).map { list ->
when (order.orderType) {
is OrderType.ASC -> {
when (order) {
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/NoteFolderDetailsScreen.kt b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/NoteFolderDetailsScreen.kt
index d6f65877..b1ba6a02 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/NoteFolderDetailsScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/notes/NoteFolderDetailsScreen.kt
@@ -4,6 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
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.grid.GridCells
@@ -103,7 +104,7 @@ fun NoteFolderDetailsScreen(
)
}
}
- ) { _ ->
+ ) { contentPadding ->
if (uiState.noteView == ItemView.LIST) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -112,7 +113,8 @@ fun NoteFolderDetailsScreen(
bottom = 24.dp,
start = 12.dp,
end = 12.dp
- )
+ ),
+ modifier = Modifier.padding(contentPadding)
) {
items(uiState.folderNotes, key = { it.id }) { note ->
NoteItem(
@@ -199,7 +201,7 @@ fun NoteFolderDetailsScreen(
}
)
if (openEditDialog){
- var name by remember { mutableStateOf(folder?.name ?: "") }
+ var folderName by remember { mutableStateOf(folder?.name ?: "") }
AlertDialog(
onDismissRequest = { openEditDialog = false },
title = {
@@ -210,8 +212,8 @@ fun NoteFolderDetailsScreen(
},
text = {
TextField(
- value = name,
- onValueChange = { name = it },
+ value = folderName,
+ onValueChange = { folderName = it },
label = {
Text(
text = stringResource(id = R.string.name),
@@ -224,7 +226,7 @@ fun NoteFolderDetailsScreen(
Button(
shape = RoundedCornerShape(25.dp),
onClick = {
- viewModel.onEvent(NoteEvent.UpdateFolder(folder?.copy(name = name)!!))
+ viewModel.onEvent(NoteEvent.UpdateFolder(folder?.copy(name = folderName)!!))
openEditDialog = false
},
) {
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/tasks/AddTaskUseCase.kt b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/tasks/AddTaskUseCase.kt
index 2a3bb13c..49467782 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/tasks/AddTaskUseCase.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/tasks/AddTaskUseCase.kt
@@ -1,13 +1,19 @@
package com.mhss.app.mybrain.domain.use_case.tasks
+import android.content.Context
+import androidx.glance.appwidget.updateAll
import com.mhss.app.mybrain.domain.model.Task
import com.mhss.app.mybrain.domain.repository.TaskRepository
+import com.mhss.app.mybrain.presentation.glance_widgets.TasksHomeWidget
import javax.inject.Inject
class AddTaskUseCase @Inject constructor(
private val tasksRepository: TaskRepository,
+ private val context: Context
) {
suspend operator fun invoke(task: Task): Long {
- return tasksRepository.insertTask(task)
+ val id = tasksRepository.insertTask(task)
+ TasksHomeWidget().updateAll(context)
+ return id
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/tasks/UpdateTaskUseCase.kt b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/tasks/UpdateTaskUseCase.kt
index 05ade138..a0471442 100644
--- a/app/src/main/java/com/mhss/app/mybrain/domain/use_case/tasks/UpdateTaskUseCase.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/domain/use_case/tasks/UpdateTaskUseCase.kt
@@ -1,13 +1,18 @@
package com.mhss.app.mybrain.domain.use_case.tasks
+import android.content.Context
+import androidx.glance.appwidget.updateAll
import com.mhss.app.mybrain.domain.model.Task
import com.mhss.app.mybrain.domain.repository.TaskRepository
+import com.mhss.app.mybrain.presentation.glance_widgets.TasksHomeWidget
import javax.inject.Inject
class UpdateTaskUseCase @Inject constructor(
- private val tasksRepository: TaskRepository
+ private val tasksRepository: TaskRepository,
+ private val context: Context
) {
suspend operator fun invoke(task: Task) {
tasksRepository.updateTask(task)
+ TasksHomeWidget().updateAll(context)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/auth/AuthManager.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/auth/AuthManager.kt
new file mode 100644
index 00000000..40521068
--- /dev/null
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/auth/AuthManager.kt
@@ -0,0 +1,83 @@
+package com.mhss.app.mybrain.presentation.auth
+
+import android.os.Build
+import androidx.appcompat.app.AppCompatActivity
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+import androidx.biometric.BiometricPrompt
+import com.mhss.app.mybrain.R
+import com.mhss.app.mybrain.app.getString
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.receiveAsFlow
+
+class AuthManager(
+ private val activity: AppCompatActivity
+) {
+
+ private val biometricManager = BiometricManager.from(activity)
+
+ private val resultChannel = Channel()
+ val resultFlow = resultChannel.receiveAsFlow()
+
+ private val authenticators = if (Build.VERSION.SDK_INT >= 30) {
+ BIOMETRIC_WEAK or DEVICE_CREDENTIAL
+ } else BIOMETRIC_WEAK
+
+ fun showAuthPrompt() {
+ val info = BiometricPrompt.PromptInfo.Builder()
+ .setTitle(getString(R.string.auth_title))
+ .setConfirmationRequired(false)
+ .setAllowedAuthenticators(authenticators)
+
+ if (Build.VERSION.SDK_INT < 30) info.setNegativeButtonText(activity.getString(R.string.cancel))
+
+ when (biometricManager.canAuthenticate(authenticators)) {
+ BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
+ resultChannel.trySend(AuthResult.NoHardware)
+ }
+ BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
+ resultChannel.trySend(AuthResult.HardwareUnavailable)
+ }
+ BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
+ resultChannel.trySend(AuthResult.NoneEnrolled)
+ }
+ else -> Unit
+ }
+ val prompt = BiometricPrompt(
+ activity,
+ object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ if (errorCode != BiometricPrompt.ERROR_USER_CANCELED) {
+ resultChannel.trySend(AuthResult.Error(errString.toString()))
+ }
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ resultChannel.trySend(AuthResult.Success)
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ resultChannel.trySend(AuthResult.Failed)
+ }
+ }
+ )
+ prompt.authenticate(info.build())
+ }
+
+ fun canUseFeature(): Boolean {
+ return biometricManager.canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
+ }
+
+ sealed interface AuthResult {
+ data object NoneEnrolled: AuthResult
+ data object HardwareUnavailable: AuthResult
+ data object NoHardware: AuthResult
+ data class Error(val message: String): AuthResult
+ data object Success: AuthResult
+ data object Failed: AuthResult
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/auth/AuthScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/auth/AuthScreen.kt
new file mode 100644
index 00000000..924374e6
--- /dev/null
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/auth/AuthScreen.kt
@@ -0,0 +1,53 @@
+package com.mhss.app.mybrain.presentation.auth
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Button
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.mhss.app.mybrain.R
+
+@Composable
+fun AuthScreen(
+ onAuthClick: () -> Unit
+) {
+ Column(
+ modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_lock),
+ contentDescription = null,
+ modifier = Modifier.size(48.dp)
+ )
+ Spacer(Modifier.height(12.dp))
+ Text(
+ text = stringResource(R.string.auth_title),
+ style = MaterialTheme.typography.h5
+ )
+ Spacer(Modifier.height(24.dp))
+ Button(
+ onClick = onAuthClick,
+ shape = RoundedCornerShape(99.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.auth_button),
+ style = MaterialTheme.typography.body1,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarEventDetailsScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarEventDetailsScreen.kt
index c75094f1..7a2fd747 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarEventDetailsScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarEventDetailsScreen.kt
@@ -30,7 +30,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
-import com.google.gson.Gson
import com.mhss.app.mybrain.R
import com.mhss.app.mybrain.domain.model.Calendar
import com.mhss.app.mybrain.domain.model.CalendarEvent
@@ -38,6 +37,7 @@ import com.mhss.app.mybrain.util.calendar.*
import com.mhss.app.mybrain.util.date.HOUR_IN_MILLIS
import com.mhss.app.mybrain.util.date.formatDate
import com.mhss.app.mybrain.util.date.formatTime
+import kotlinx.serialization.json.Json
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
@@ -56,9 +56,9 @@ fun CalendarEventDetailsScreen(
val context = LocalContext.current
val event by remember {
mutableStateOf(
- if (eventJson.isNotEmpty()) {
+ if (eventJson.isNotBlank()) {
val decodedJson = URLDecoder.decode(eventJson, StandardCharsets.UTF_8.toString())
- Gson().fromJson(decodedJson, CalendarEvent::class.java)
+ Json.decodeFromString(decodedJson)
} else
null
)
@@ -66,12 +66,12 @@ fun CalendarEventDetailsScreen(
var title by rememberSaveable { mutableStateOf(event?.title ?: "") }
var description by rememberSaveable { mutableStateOf(event?.description ?: "") }
var startDate by rememberSaveable {
- mutableStateOf(
+ mutableLongStateOf(
event?.start ?: (System.currentTimeMillis() + HOUR_IN_MILLIS)
)
}
var endDate by rememberSaveable {
- mutableStateOf(
+ mutableLongStateOf(
event?.end ?: (System.currentTimeMillis() + 2 * HOUR_IN_MILLIS)
)
}
@@ -158,7 +158,7 @@ fun CalendarEventDetailsScreen(
)
}
}
- ) {
+ ) { paddingValues ->
DeleteEventDialog(
openDeleteDialog,
onDelete = { viewModel.onEvent(CalendarViewModelEvent.DeleteEvent(event!!)) },
@@ -168,6 +168,7 @@ fun CalendarEventDetailsScreen(
modifier = Modifier
.fillMaxSize()
.padding(12.dp)
+ .padding(paddingValues)
.verticalScroll(rememberScrollState()),
) {
OutlinedTextField(
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarEventWidgetItem.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarEventWidgetItem.kt
index b9a0f711..e0b41424 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarEventWidgetItem.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarEventWidgetItem.kt
@@ -16,12 +16,13 @@ import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
-import com.google.gson.Gson
import com.mhss.app.mybrain.R
import com.mhss.app.mybrain.domain.model.CalendarEvent
import com.mhss.app.mybrain.presentation.glance_widgets.CalendarWidgetItemClick
import com.mhss.app.mybrain.presentation.glance_widgets.eventJson
import com.mhss.app.mybrain.util.date.formatEventStartEnd
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
@Composable
fun CalendarEventWidgetItem(
@@ -77,7 +78,7 @@ fun CalendarEventWidgetItem(
Box(GlanceModifier.fillMaxSize().clickable(
actionRunCallback(
parameters = actionParametersOf(
- eventJson to Gson().toJson(event, CalendarEvent::class.java)
+ eventJson to Json.encodeToString(event)
)
)
)) {}
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarScreen.kt
index 94bff016..7eec1fbb 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/calendar/CalendarScreen.kt
@@ -30,14 +30,14 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
-import com.google.gson.Gson
import com.mhss.app.mybrain.R
import com.mhss.app.mybrain.domain.model.Calendar
-import com.mhss.app.mybrain.domain.model.CalendarEvent
import com.mhss.app.mybrain.presentation.util.Screen
import com.mhss.app.mybrain.util.Constants
import com.mhss.app.mybrain.util.date.*
import kotlinx.coroutines.launch
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
@@ -107,11 +107,11 @@ fun CalendarScreen(
)
}
},
- ) {
+ ) { paddingValues ->
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier.fillMaxSize()
+ modifier = Modifier.fillMaxSize().padding(paddingValues)
) {
if (readCalendarPermissionState.hasPermission) {
LaunchedEffect(true) {
@@ -157,7 +157,7 @@ fun CalendarScreen(
)
events.forEach { event ->
CalendarEventItem(event = event, onClick = {
- val eventJson = Gson().toJson(event, CalendarEvent::class.java)
+ val eventJson = Json.encodeToString(event)
// encoding the string to avoid crashes when the event contains fields that equals a URL
val encodedJson = URLEncoder.encode(eventJson, StandardCharsets.UTF_8.toString())
navController.navigate(
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryEntryDetailsScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryEntryDetailsScreen.kt
index 5589c749..1f3c5312 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryEntryDetailsScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryEntryDetailsScreen.kt
@@ -4,10 +4,11 @@ import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Context
import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -166,6 +167,7 @@ fun DiaryEntryDetailsScreen(
.fillMaxSize()
.padding(12.dp)
.padding(paddingValues)
+ .verticalScroll(rememberScrollState())
) {
Text(
text = stringResource(R.string.mood),
@@ -187,13 +189,15 @@ fun DiaryEntryDetailsScreen(
Spacer(Modifier.height(8.dp))
if (readingMode) {
MarkdownText(
- markdown = content.ifBlank { stringResource(R.string.content) },
+ markdown = content,
modifier = Modifier
.fillMaxWidth()
- .fillMaxHeight()
.padding(vertical = 6.dp)
- .border(1.dp, Color.Gray, RoundedCornerShape(20.dp))
- .padding(10.dp)
+ .padding(10.dp),
+ linkColor = Color.Blue,
+ style = MaterialTheme.typography.body1.copy(
+ color = MaterialTheme.colors.onBackground
+ )
)
} else {
OutlinedTextField(
@@ -201,7 +205,10 @@ fun DiaryEntryDetailsScreen(
onValueChange = { content = it },
label = { Text(text = stringResource(R.string.content)) },
shape = RoundedCornerShape(15.dp),
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(bottom = 8.dp)
)
}
}
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryEntryItem.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryEntryItem.kt
index 2796f7c4..92740180 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryEntryItem.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryEntryItem.kt
@@ -59,9 +59,13 @@ fun LazyItemScope.DiaryEntryItem(
if (entry.content.isNotBlank()){
MarkdownText(
markdown = entry.content,
- maxLines = 10,
+ maxLines = 14,
+ style = MaterialTheme.typography.body2.copy(
+ fontSize = 14.sp,
+ color = MaterialTheme.colors.onBackground
+ ),
onClick = {onClick(entry)},
- fontSize = 12.sp
+ onLinkClicked = {onClick(entry)},
)
Spacer(Modifier.height(8.dp))
}
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryScreen.kt
index 3fa71e85..6baee9a2 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryScreen.kt
@@ -76,10 +76,9 @@ fun DiaryScreen(
)
}
}
- ) {
- if (uiState.entries.isEmpty())
- NoEntriesMessage()
- Column {
+ ) { paddingValues ->
+ if (uiState.entries.isEmpty()) { NoEntriesMessage() }
+ Column(Modifier.padding(paddingValues)) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryViewModel.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryViewModel.kt
index b0ea62ab..4c412e77 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryViewModel.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/diary/DiaryViewModel.kt
@@ -65,7 +65,8 @@ class DiaryViewModel @Inject constructor(
is DiaryEvent.GetEntry -> viewModelScope.launch {
val entry = getEntry(event.entryId)
uiState = uiState.copy(
- entry = entry
+ entry = entry,
+ readingMode = true
)
}
is DiaryEvent.SearchEntries -> viewModelScope.launch {
@@ -102,7 +103,7 @@ class DiaryViewModel @Inject constructor(
val searchEntries: List = emptyList(),
val navigateUp: Boolean = false,
val chartEntries : List = emptyList(),
- val readingMode: Boolean = true
+ val readingMode: Boolean = false
)
private fun getEntries(order: Order) {
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/main/DashboardScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/main/DashboardScreen.kt
index e7ac0003..bd9eeabd 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/main/DashboardScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/main/DashboardScreen.kt
@@ -11,14 +11,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
-import com.google.gson.Gson
import com.mhss.app.mybrain.R
-import com.mhss.app.mybrain.domain.model.CalendarEvent
import com.mhss.app.mybrain.presentation.calendar.CalendarDashboardWidget
import com.mhss.app.mybrain.presentation.diary.MoodCircularBar
import com.mhss.app.mybrain.presentation.tasks.TasksDashboardWidget
import com.mhss.app.mybrain.presentation.util.Screen
import com.mhss.app.mybrain.util.Constants
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
@@ -66,7 +66,7 @@ fun DashboardScreen(
)
},
onEventClicked = {
- val eventJson = Gson().toJson(it, CalendarEvent::class.java)
+ val eventJson = Json.encodeToString(it)
// encoding the string to avoid crashes when the event contains fields that equals a URL
val encodedJson = URLEncoder.encode(eventJson, StandardCharsets.UTF_8.toString())
navController.navigate(
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/main/MainActivity.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/main/MainActivity.kt
index 00532cba..8cfcf0bd 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/main/MainActivity.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/main/MainActivity.kt
@@ -5,9 +5,10 @@ import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.WindowManager.LayoutParams
-import androidx.activity.ComponentActivity
+import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
@@ -17,6 +18,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@@ -24,7 +28,10 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import com.google.accompanist.systemuicontroller.rememberSystemUiController
+import com.mhss.app.mybrain.R
import com.mhss.app.mybrain.domain.use_case.notes.NoteFolderDetailsScreen
+import com.mhss.app.mybrain.presentation.auth.AuthManager
+import com.mhss.app.mybrain.presentation.auth.AuthScreen
import com.mhss.app.mybrain.presentation.bookmarks.BookmarkDetailsScreen
import com.mhss.app.mybrain.presentation.bookmarks.BookmarkSearchScreen
import com.mhss.app.mybrain.presentation.bookmarks.BookmarksScreen
@@ -50,27 +57,31 @@ import com.mhss.app.mybrain.util.settings.ThemeSettings
import com.mhss.app.mybrain.util.settings.toFontFamily
import com.mhss.app.mybrain.util.settings.toInt
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.launch
-@Suppress("BlockingMethodInNonBlockingContext")
@AndroidEntryPoint
-class MainActivity : ComponentActivity() {
+class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
+ private val authManager by lazy {
+ AuthManager(this)
+ }
+ private var appUnlocked by mutableStateOf(true)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
setContent {
val themeMode = viewModel.themeMode.collectAsState(initial = ThemeSettings.AUTO.value)
val font = viewModel.font.collectAsState(initial = Rubik.toInt())
- val blockScreenshots = viewModel.blockScreenshots.collectAsState(initial = false)
- var startUpScreenSettings by remember { mutableStateOf(StartUpScreenSettings.SPACES.value) }
+ val blockScreenshots by viewModel.blockScreenshots.collectAsState(initial = false)
val systemUiController = rememberSystemUiController()
- LaunchedEffect(true) {
- runBlocking {
- startUpScreenSettings = viewModel.defaultStartUpScreen.first()
+ var startDestination by remember { mutableStateOf(Screen.SpacesScreen.route) }
+
+ LaunchedEffect(Unit) {
+ if (viewModel.defaultStartUpScreen.first() == StartUpScreenSettings.DASHBOARD.value) {
+ startDestination = Screen.DashboardScreen.route
}
if (!isNotificationPermissionGranted())
ActivityCompat.requestPermissions(
@@ -79,8 +90,9 @@ class MainActivity : ComponentActivity() {
0
)
}
- LaunchedEffect(blockScreenshots.value) {
- if (blockScreenshots.value) {
+
+ LaunchedEffect(blockScreenshots) {
+ if (blockScreenshots) {
window.setFlags(
LayoutParams.FLAG_SECURE,
LayoutParams.FLAG_SECURE
@@ -88,9 +100,6 @@ class MainActivity : ComponentActivity() {
} else
window.clearFlags(LayoutParams.FLAG_SECURE)
}
- val startUpScreen =
- if (startUpScreenSettings == StartUpScreenSettings.SPACES.value)
- Screen.SpacesScreen.route else Screen.DashboardScreen.route
val isDarkMode = when (themeMode.value) {
ThemeSettings.DARK.value -> true
ThemeSettings.LIGHT.value -> false
@@ -114,7 +123,7 @@ class MainActivity : ComponentActivity() {
) {
composable(Screen.Main.route) {
MainScreen(
- startUpScreen = startUpScreen,
+ startUpScreen = startDestination,
mainNavController = navController
)
}
@@ -263,15 +272,68 @@ class MainActivity : ComponentActivity() {
ImportExportScreen()
}
}
+ if (!appUnlocked) {
+ AuthScreen {
+ authManager.showAuthPrompt()
+ }
+ }
}
}
}
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ if (viewModel.lockApp.first()) {
+ appUnlocked = false
+ }
+ authManager.resultFlow.collectLatest { authResult ->
+ when (authResult) {
+ is AuthManager.AuthResult.Error -> {
+ toast(authResult.message)
+ }
+
+ AuthManager.AuthResult.Failed -> {
+ toast(
+ this@MainActivity.getString(R.string.auth_failed)
+ )
+ }
+
+ AuthManager.AuthResult.NoHardware, AuthManager.AuthResult.HardwareUnavailable -> {
+ toast(
+ this@MainActivity.getString(R.string.auth_no_hardware)
+ )
+ }
+
+ AuthManager.AuthResult.Success -> {
+ appUnlocked = true
+ }
+
+ AuthManager.AuthResult.NoneEnrolled -> {
+ // User disabled biometric authentication
+ viewModel.disableAppLock()
+ appUnlocked = true
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun toast(message: String) {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
private fun isNotificationPermissionGranted(): Boolean {
- return ContextCompat.checkSelfPermission(
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
+ || ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
- ) == PackageManager.PERMISSION_GRANTED || Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ if (hasFocus && !appUnlocked) {
+ authManager.showAuthPrompt()
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/main/MainViewModel.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/main/MainViewModel.kt
index 86521f73..66459184 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/main/MainViewModel.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/main/MainViewModel.kt
@@ -14,6 +14,7 @@ import com.mhss.app.mybrain.domain.model.Task
import com.mhss.app.mybrain.domain.use_case.calendar.GetAllEventsUseCase
import com.mhss.app.mybrain.domain.use_case.diary.GetAllEntriesUseCase
import com.mhss.app.mybrain.domain.use_case.settings.GetSettingsUseCase
+import com.mhss.app.mybrain.domain.use_case.settings.SaveSettingsUseCase
import com.mhss.app.mybrain.domain.use_case.tasks.GetAllTasksUseCase
import com.mhss.app.mybrain.domain.use_case.tasks.UpdateTaskUseCase
import com.mhss.app.mybrain.ui.theme.Rubik
@@ -29,6 +30,7 @@ import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val getSettings: GetSettingsUseCase,
+ private val saveSettings: SaveSettingsUseCase,
private val getAllTasks: GetAllTasksUseCase,
private val getAllEntriesUseCase: GetAllEntriesUseCase,
private val updateTask: UpdateTaskUseCase,
@@ -40,6 +42,7 @@ class MainViewModel @Inject constructor(
private var refreshTasksJob : Job? = null
+ val lockApp = getSettings(booleanPreferencesKey(Constants.LOCK_APP_KEY), false)
val themeMode = getSettings(intPreferencesKey(Constants.SETTINGS_THEME_KEY), ThemeSettings.AUTO.value)
val defaultStartUpScreen = getSettings(intPreferencesKey(Constants.DEFAULT_START_UP_SCREEN_KEY), StartUpScreenSettings.SPACES.value)
val font = getSettings(intPreferencesKey(Constants.APP_FONT_KEY), Rubik.toInt())
@@ -105,4 +108,8 @@ class MainViewModel @Inject constructor(
}.launchIn(viewModelScope)
}
+ fun disableAppLock() = viewModelScope.launch {
+ saveSettings(booleanPreferencesKey(Constants.LOCK_APP_KEY), false)
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/main/SettingsScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/main/SettingsScreen.kt
index 2fa9e1fb..dff09c43 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/main/SettingsScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/main/SettingsScreen.kt
@@ -1,5 +1,9 @@
package com.mhss.app.mybrain.presentation.main
+import android.content.Context
+import android.content.ContextWrapper
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.*
@@ -8,6 +12,7 @@ import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
@@ -19,8 +24,11 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.mhss.app.mybrain.BuildConfig
import com.mhss.app.mybrain.R
+import com.mhss.app.mybrain.app.getString
+import com.mhss.app.mybrain.presentation.auth.AuthManager
import com.mhss.app.mybrain.presentation.settings.SettingsBasicLinkItem
import com.mhss.app.mybrain.presentation.settings.SettingsItemCard
+import com.mhss.app.mybrain.presentation.settings.SettingsSwitchCard
import com.mhss.app.mybrain.presentation.settings.SettingsViewModel
import com.mhss.app.mybrain.presentation.util.Screen
import com.mhss.app.mybrain.ui.theme.Rubik
@@ -46,6 +54,12 @@ fun SettingsScreen(
)
}
) { paddingValues ->
+ val context = LocalContext.current
+ val authManager = remember {
+ context.getActivity()?.let {
+ AuthManager(it)
+ }
+ }
LazyColumn(modifier = Modifier.fillMaxWidth(), contentPadding = paddingValues) {
item {
val theme = viewModel
@@ -120,7 +134,8 @@ fun SettingsScreen(
).collectAsState(
initial = false
)
- BlockScreenshotsSettingsItem(
+ SettingsSwitchCard(
+ stringResource(R.string.block_screenshots),
block.value
){
viewModel.saveSettings(
@@ -130,6 +145,31 @@ fun SettingsScreen(
}
}
+ item {
+ val block = viewModel
+ .getSettings(
+ booleanPreferencesKey(Constants.LOCK_APP_KEY),
+ false
+ ).collectAsState(
+ initial = false
+ )
+ SettingsSwitchCard(
+ stringResource(R.string.lock_app),
+ block.value
+ ){
+ if (authManager?.canUseFeature() == true) {
+ viewModel.saveSettings(
+ booleanPreferencesKey(Constants.LOCK_APP_KEY),
+ it
+ )
+ } else {
+ Toast.makeText(context, getString(
+ R.string.no_auth_method
+ ), Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
item {
SettingsItemCard(
cornerRadius = 16.dp,
@@ -362,24 +402,8 @@ fun AppFontSettingsItem(
}
}
-@Composable
-fun BlockScreenshotsSettingsItem(
- block: Boolean,
- onBlockClick: (Boolean) -> Unit = {}
-) {
- SettingsItemCard(
- cornerRadius = 16.dp,
- onClick = {
- onBlockClick(!block)
- },
- vPadding = 10.dp
- ) {
- Text(
- text = stringResource(R.string.block_screenshots),
- style = MaterialTheme.typography.h6
- )
- Switch(checked = block, onCheckedChange = {
- onBlockClick(it)
- })
- }
+fun Context.getActivity(): AppCompatActivity? = when (this) {
+ is AppCompatActivity -> this
+ is ContextWrapper -> baseContext.getActivity()
+ else -> null
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/notes/AddNoteFromShareActivity.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/AddNoteFromShareActivity.kt
new file mode 100644
index 00000000..ecfd0f81
--- /dev/null
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/AddNoteFromShareActivity.kt
@@ -0,0 +1,43 @@
+package com.mhss.app.mybrain.presentation.notes
+
+import android.content.Intent
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.viewModels
+import com.mhss.app.mybrain.R
+import com.mhss.app.mybrain.domain.model.Note
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class AddNoteFromShareActivity : ComponentActivity() {
+
+ private val viewModel: NotesViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (intent != null) {
+ if (intent.action == Intent.ACTION_SEND && intent.type == "text/plain") {
+ val content = intent.getStringExtra(Intent.EXTRA_TEXT)
+ val title = intent.getStringExtra(Intent.EXTRA_SUBJECT)
+ if (!content.isNullOrBlank()) {
+ viewModel.onEvent(
+ NoteEvent.AddNote(
+ Note(
+ title = title ?: "",
+ content = content,
+ createdDate = System.currentTimeMillis(),
+ updatedDate = System.currentTimeMillis()
+ )
+ )
+ )
+ Toast.makeText(this, getString(R.string.added_note), Toast.LENGTH_SHORT)
+ .show()
+ } else
+ Toast.makeText(this, getString(R.string.error_empty_title), Toast.LENGTH_SHORT)
+ .show()
+ }
+ }
+ finish()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NoteDetailsScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NoteDetailsScreen.kt
index a1c04bab..b6e0eef5 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NoteDetailsScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NoteDetailsScreen.kt
@@ -5,7 +5,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -196,6 +198,7 @@ fun NoteDetailsScreen(
.fillMaxSize()
.padding(12.dp)
.padding(paddingValues)
+ .verticalScroll(rememberScrollState())
) {
OutlinedTextField(
value = title,
@@ -206,13 +209,15 @@ fun NoteDetailsScreen(
)
if (readingMode)
MarkdownText(
- markdown = content.ifBlank { stringResource(R.string.note_content) },
+ markdown = content,
modifier = Modifier
.fillMaxWidth()
- .weight(1f)
.padding(vertical = 6.dp)
- .border(1.dp, Color.Gray, RoundedCornerShape(20.dp))
- .padding(10.dp)
+ .padding(8.dp),
+ linkColor = Color.Blue,
+ style = MaterialTheme.typography.body1.copy(
+ color = MaterialTheme.colors.onBackground
+ )
)
else
OutlinedTextField(
@@ -225,7 +230,7 @@ fun NoteDetailsScreen(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
- .padding(bottom = 8.dp),
+ .padding(bottom = 8.dp)
)
Row(
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NoteItem.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NoteItem.kt
index a2feaeb3..794cfd6e 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NoteItem.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NoteItem.kt
@@ -64,8 +64,12 @@ fun NoteItem(
MarkdownText(
markdown = note.content,
maxLines = 14,
+ style = MaterialTheme.typography.body2.copy(
+ fontSize = 14.sp,
+ color = MaterialTheme.colors.onBackground
+ ),
onClick = {onClick(note)},
- fontSize = 12.sp
+ onLinkClicked = {onClick(note)},
)
Spacer(Modifier.height(8.dp))
Text(
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesScreen.kt
index 83c58177..d5384a86 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesScreen.kt
@@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -43,7 +44,7 @@ fun NotesScreen(
) {
val uiState = viewModel.notesUiState
var orderSettingsVisible by remember { mutableStateOf(false) }
- var selectedTab by remember { mutableStateOf(0) }
+ var selectedTab by rememberSaveable { mutableIntStateOf(0) }
var openCreateFolderDialog by remember { mutableStateOf(false) }
val scaffoldState = rememberScaffoldState()
LaunchedEffect(uiState.error) {
@@ -77,7 +78,7 @@ fun NotesScreen(
navController.navigate(
Screen.NoteDetailsScreen.route.replace(
"{${Constants.NOTE_ID_ARG}}",
- "${-1}"
+ "-1"
).replace(
"{${Constants.FOLDER_ID}}",
"-1"
@@ -202,7 +203,7 @@ fun NotesScreen(
"${note.id}"
).replace(
"{${Constants.FOLDER_ID}}",
- "-1"
+ ""
)
)
},
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesSearchScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesSearchScreen.kt
index 4ddcb3ab..b2bd3313 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesSearchScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesSearchScreen.kt
@@ -1,6 +1,5 @@
package com.mhss.app.mybrain.presentation.notes
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -24,7 +23,6 @@ import com.mhss.app.mybrain.presentation.util.Screen
import com.mhss.app.mybrain.util.Constants
import com.mhss.app.mybrain.util.settings.ItemView
-@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NotesSearchScreen(
navController: NavHostController,
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesViewModel.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesViewModel.kt
index 8c79d7a6..1400f837 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesViewModel.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/notes/NotesViewModel.kt
@@ -36,6 +36,7 @@ class NotesViewModel @Inject constructor(
private val deleteFolder: DeleteNoteFolderUseCass,
private val updateFolder: UpdateNoteFolderUseCass,
private val getFolderNotes: GetNotesByFolderUseCase,
+ private val getNoteFolder: GetNoteFolderUseCase
) : ViewModel() {
var notesUiState by mutableStateOf((UiState()))
@@ -88,7 +89,7 @@ class NotesViewModel @Inject constructor(
is NoteEvent.GetNote -> viewModelScope.launch {
val note = getNote(event.noteId)
val folder = getAllFolders().first().firstOrNull { it.id == note.folderId }
- notesUiState = notesUiState.copy(note = note, folder = folder)
+ notesUiState = notesUiState.copy(note = note, folder = folder, readingMode = true)
}
is NoteEvent.SearchNotes -> viewModelScope.launch {
val notes = searchNotes(event.query)
@@ -136,14 +137,14 @@ class NotesViewModel @Inject constructor(
notesUiState = notesUiState.copy(navigateUp = true)
}
is NoteEvent.UpdateFolder -> viewModelScope.launch {
- if (event.folder.name.isBlank()) {
- notesUiState = notesUiState.copy(error = getString(R.string.error_empty_title))
+ notesUiState = if (event.folder.name.isBlank()) {
+ notesUiState.copy(error = getString(R.string.error_empty_title))
} else {
if (!notesUiState.folders.contains(event.folder)) {
updateFolder(event.folder)
- notesUiState = notesUiState.copy(folder = event.folder)
+ notesUiState.copy(folder = event.folder)
} else {
- notesUiState = notesUiState.copy(error = getString(R.string.error_folder_exists))
+ notesUiState.copy(error = getString(R.string.error_folder_exists))
}
}
}
@@ -151,7 +152,7 @@ class NotesViewModel @Inject constructor(
getNotesFromFolder(event.id, notesUiState.notesOrder)
}
is NoteEvent.GetFolder -> viewModelScope.launch {
- val folder = getAllFolders().first().firstOrNull { it.id == event.id }
+ val folder = getNoteFolder(event.id)
notesUiState = notesUiState.copy(folder = folder)
}
}
@@ -164,7 +165,7 @@ class NotesViewModel @Inject constructor(
val error: String? = null,
val noteView: ItemView = ItemView.LIST,
val navigateUp: Boolean = false,
- val readingMode: Boolean = true,
+ val readingMode: Boolean = false,
val searchNotes: List = emptyList(),
val folders: List = emptyList(),
val folderNotes: List = emptyList(),
@@ -186,7 +187,7 @@ class NotesViewModel @Inject constructor(
getFolderNotesJob?.cancel()
getFolderNotesJob = getFolderNotes(id, order)
.onEach { notes ->
- val noteFolder = getAllFolders().first().firstOrNull() { it.id == id }
+ val noteFolder = getNoteFolder(id)
notesUiState = notesUiState.copy(
folderNotes = notes,
folder = noteFolder
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/settings/ImportExportScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/settings/ImportExportScreen.kt
index d4f4360c..3c6e5fc6 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/settings/ImportExportScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/settings/ImportExportScreen.kt
@@ -1,9 +1,5 @@
package com.mhss.app.mybrain.presentation.settings
-import android.app.Activity
-import android.content.Context
-import android.content.ContextWrapper
-import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
@@ -15,7 +11,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -23,7 +18,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.mhss.app.mybrain.R
-import com.mhss.app.mybrain.presentation.main.MainActivity
@Composable
fun ImportExportScreen(
@@ -35,10 +29,6 @@ fun ImportExportScreen(
val password by remember {
mutableStateOf("")
}
- val context = LocalContext.current
- val activity = remember {
- context.findActivity()
- }
val pickFileLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
@@ -54,19 +44,6 @@ fun ImportExportScreen(
}
}
val backupResult by viewModel.backupResult.collectAsState()
-
- LaunchedEffect(backupResult) {
- if (backupResult == SettingsViewModel.BackupResult.ImportSuccess) {
- activity?.let {
- val intent = Intent(it, MainActivity::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
- it.startActivity(intent)
- it.finish()
- Runtime.getRuntime().exit(0)
- }
- }
- }
Scaffold(
topBar = {
TopAppBar(
@@ -178,6 +155,16 @@ fun ImportExportScreen(
color = MaterialTheme.colors.error
)
}
+ if (backupResult == SettingsViewModel.BackupResult.ImportSuccess) {
+ Text(
+ text = stringResource(R.string.import_success),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ style = MaterialTheme.typography.body2.copy(fontWeight = FontWeight.Bold),
+ textAlign = TextAlign.Center
+ )
+ }
if (backupResult == SettingsViewModel.BackupResult.Loading) {
CircularProgressIndicator(
Modifier
@@ -188,13 +175,4 @@ fun ImportExportScreen(
}
}
-}
-
-fun Context.findActivity(): Activity? {
- var context = this
- while (context is ContextWrapper) {
- if (context is Activity) return context
- context = context.baseContext
- }
- return null
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/settings/SettingsSwitchCard.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/settings/SettingsSwitchCard.kt
new file mode 100644
index 00000000..21b689d7
--- /dev/null
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/settings/SettingsSwitchCard.kt
@@ -0,0 +1,30 @@
+package com.mhss.app.mybrain.presentation.settings
+
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Switch
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun SettingsSwitchCard(
+ text: String,
+ checked: Boolean,
+ onCheck: (Boolean) -> Unit = {}
+) {
+ SettingsItemCard(
+ cornerRadius = 16.dp,
+ onClick = {
+ onCheck(!checked)
+ },
+ vPadding = 10.dp
+ ) {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.h6
+ )
+ Switch(checked = checked, onCheckedChange = {
+ onCheck(it)
+ })
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/AddTaskBottomSheetContent.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/AddTaskBottomSheetContent.kt
index ec8f8d99..cc4fcf0b 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/AddTaskBottomSheetContent.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/AddTaskBottomSheetContent.kt
@@ -1,27 +1,18 @@
package com.mhss.app.mybrain.presentation.tasks
-import android.app.DatePickerDialog
-import android.app.TimePickerDialog
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
-import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.mhss.app.mybrain.R
@@ -31,8 +22,6 @@ import com.mhss.app.mybrain.util.date.formatDateDependingOnDay
import com.mhss.app.mybrain.util.settings.TaskFrequency
import com.mhss.app.mybrain.util.settings.Priority
import com.mhss.app.mybrain.util.settings.toInt
-import com.mhss.app.mybrain.util.settings.toPriority
-import com.mhss.app.mybrain.util.settings.toTaskFrequency
import java.util.*
@Composable
@@ -40,28 +29,28 @@ fun AddTaskBottomSheetContent(
onAddTask: (Task) -> Unit,
focusRequester: FocusRequester
) {
+ var completed by rememberSaveable { mutableStateOf(false) }
var title by rememberSaveable { mutableStateOf("") }
var description by rememberSaveable { mutableStateOf("") }
var priority by rememberSaveable { mutableStateOf(Priority.LOW) }
var dueDate by rememberSaveable { mutableStateOf(Calendar.getInstance()) }
var dueDateExists by rememberSaveable { mutableStateOf(false) }
var recurring by rememberSaveable { mutableStateOf(false) }
- var frequency by rememberSaveable { mutableIntStateOf(0) }
+ var frequency by rememberSaveable { mutableStateOf(TaskFrequency.DAILY) }
var frequencyAmount by rememberSaveable { mutableIntStateOf(1) }
val subTasks = remember { mutableStateListOf() }
val priorities = listOf(Priority.LOW, Priority.MEDIUM, Priority.HIGH)
- val context = LocalContext.current
val formattedDate by remember {
derivedStateOf {
dueDate.timeInMillis.formatDateDependingOnDay()
}
}
+ val keyboardController = LocalSoftwareKeyboardController.current
Column(
modifier = Modifier
.defaultMinSize(minHeight = 1.dp)
- .padding(horizontal = 16.dp, vertical = 24.dp)
- .verticalScroll(rememberScrollState())
+ .padding(horizontal = 16.dp)
) {
SheetHandle(Modifier.align(Alignment.CenterHorizontally))
Text(
@@ -69,189 +58,30 @@ fun AddTaskBottomSheetContent(
style = MaterialTheme.typography.h5
)
Spacer(Modifier.height(16.dp))
- OutlinedTextField(
- value = title,
- onValueChange = { title = it },
- label = { Text(text = stringResource(R.string.title)) },
- shape = RoundedCornerShape(15.dp),
- modifier = Modifier
- .fillMaxWidth()
- .focusRequester(focusRequester),
- )
- Spacer(Modifier.height(12.dp))
- Column {
- subTasks.forEachIndexed { index, item ->
- SubTaskItem(
- subTask = item,
- onChange = { subTasks[index] = it },
- onDelete = { subTasks.removeAt(index) }
- )
- }
- }
- Row(
- Modifier
- .fillMaxWidth()
- .clickable {
- subTasks.add(
- SubTask(
- title = "",
- isCompleted = false,
- )
- )
- },
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = stringResource(R.string.add_sub_task),
- modifier = Modifier.padding(vertical = 8.dp)
- )
- Icon(
- modifier = Modifier.size(10.dp),
- painter = painterResource(id = R.drawable.ic_add),
- contentDescription = stringResource(
- id = R.string.add_sub_task
- )
- )
- }
- Spacer(Modifier.height(12.dp))
- Text(
- text = stringResource(R.string.priority),
- style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Bold)
- )
- Spacer(Modifier.height(12.dp))
- PriorityTabRow(
+ TaskDetailsContent(
+ modifier = Modifier.weight(1f),
+ completed = completed,
+ title = title,
+ description = description,
+ priority = priority,
+ dueDate = dueDate.timeInMillis,
+ dueDateExists = dueDateExists,
+ recurring = recurring,
+ frequency = frequency,
+ frequencyAmount = frequencyAmount,
+ subTasks = subTasks,
priorities = priorities,
- priority,
- onChange = { priority = it }
- )
- Spacer(Modifier.height(12.dp))
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- Checkbox(checked = dueDateExists, onCheckedChange = { dueDateExists = it })
- Spacer(Modifier.width(4.dp))
- Text(
- text = stringResource(R.string.due_date),
- style = MaterialTheme.typography.body2
- )
- }
- AnimatedVisibility(dueDateExists) {
- Column {
- Row(
- Modifier
- .fillMaxWidth()
- .clickable {
- val date =
- if (dueDate.timeInMillis == 0L) Calendar.getInstance() else dueDate
- val tempDate = Calendar.getInstance()
- val timePicker = TimePickerDialog(
- context,
- { _, hour, minute ->
- tempDate[Calendar.HOUR_OF_DAY] = hour
- tempDate[Calendar.MINUTE] = minute
- dueDate = tempDate
- }, date[Calendar.HOUR_OF_DAY], date[Calendar.MINUTE], false
- )
- val datePicker = DatePickerDialog(
- context,
- { _, year, month, day ->
- tempDate[Calendar.YEAR] = year
- tempDate[Calendar.MONTH] = month
- tempDate[Calendar.DAY_OF_MONTH] = day
- timePicker.show()
- },
- date[Calendar.YEAR],
- date[Calendar.MONTH],
- date[Calendar.DAY_OF_MONTH]
- )
- datePicker.show()
- }
- .padding(8.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(
- painter = painterResource(R.drawable.ic_alarm),
- stringResource(R.string.due_date)
- )
- Spacer(Modifier.width(8.dp))
- Text(
- text = stringResource(R.string.due_date),
- style = MaterialTheme.typography.body1
- )
- }
- Text(
- text = formattedDate,
- style = MaterialTheme.typography.body2
- )
- }
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- Checkbox(checked = recurring, onCheckedChange = {
- recurring = it
- if (!it) frequency = 0
- })
- Spacer(Modifier.width(4.dp))
- Text(
- text = stringResource(R.string.recurring),
- style = MaterialTheme.typography.body2
- )
- }
- AnimatedVisibility(recurring) {
- var expanded by remember { mutableStateOf(false) }
- Column {
- Box {
- DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
- TaskFrequency.values().forEach { f ->
- DropdownMenuItem(
- onClick = {
- expanded = false
- frequency = f.ordinal
- }
- ) {
- Text(text = stringResource(f.title))
- }
- }
- }
- Row(
- Modifier
- .clickable { expanded = true }
- .padding(8.dp)
- ,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = stringResource(
- frequency.toTaskFrequency().title
- )
- )
- Icon(
- imageVector = Icons.Default.ArrowDropDown,
- contentDescription = stringResource(R.string.recurring),
- modifier = Modifier.size(22.dp)
- )
- }
- }
- Spacer(Modifier.height(8.dp))
- NumberPicker(
- stringResource(R.string.repeats_every),
- frequencyAmount
- ) {
- if (it > 0) frequencyAmount = it
- }
- }
- }
- }
- }
- Spacer(Modifier.height(12.dp))
- OutlinedTextField(
- value = description,
- onValueChange = { description = it },
- label = { Text(text = stringResource(R.string.description)) },
- shape = RoundedCornerShape(15.dp),
- modifier = Modifier.fillMaxWidth()
+ formattedDate = formattedDate,
+ focusRequester = focusRequester,
+ onTitleChange = { title = it },
+ onDescriptionChange = { description = it },
+ onPriorityChange = { priority = it },
+ onDueDateExist = { dueDateExists = it },
+ onDueDateChange = { dueDate.timeInMillis = it },
+ onRecurringChange = { recurring = it },
+ onFrequencyChange = { frequency = it },
+ onFrequencyAmountChange = { frequencyAmount = it },
+ onComplete = { completed = it }
)
Button(
onClick = {
@@ -259,10 +89,11 @@ fun AddTaskBottomSheetContent(
Task(
title = title,
description = description,
+ isCompleted = completed,
priority = priority.toInt(),
dueDate = if (dueDateExists) dueDate.timeInMillis else 0L,
recurring = recurring,
- frequency = frequency,
+ frequency = frequency.value,
frequencyAmount = frequencyAmount,
createdDate = System.currentTimeMillis(),
updatedDate = System.currentTimeMillis(),
@@ -275,6 +106,7 @@ fun AddTaskBottomSheetContent(
dueDate = Calendar.getInstance()
dueDateExists = false
subTasks.clear()
+ keyboardController?.hide()
},
modifier = Modifier
.fillMaxWidth()
@@ -286,45 +118,7 @@ fun AddTaskBottomSheetContent(
style = MaterialTheme.typography.h6.copy(Color.White)
)
}
- Spacer(modifier = Modifier.height(54.dp))
- }
-}
-
-@Composable
-fun PriorityTabRow(
- priorities: List,
- selectedPriority: Priority,
- onChange: (Priority) -> Unit
-) {
- val indicator = @Composable { tabPositions: List ->
- AnimatedTabIndicator(Modifier.tabIndicatorOffset(tabPositions[selectedPriority.toInt()]))
}
- TabRow(
- selectedTabIndex = selectedPriority.toInt(),
- indicator = indicator,
- modifier = Modifier.clip(RoundedCornerShape(14.dp))
- ) {
- priorities.forEachIndexed { index, it ->
- Tab(
- text = { Text(stringResource(it.title)) },
- selected = selectedPriority.toInt() == index,
- onClick = {
- onChange(index.toPriority())
- },
- modifier = Modifier.background(it.color)
- )
- }
- }
-}
-
-@Composable
-fun AnimatedTabIndicator(modifier: Modifier = Modifier) {
- Box(
- modifier = modifier
- .padding(5.dp)
- .fillMaxSize()
- .border(BorderStroke(2.dp, Color.White), RoundedCornerShape(8.dp))
- )
}
@Composable
@@ -338,7 +132,7 @@ fun SheetHandle(modifier: Modifier = Modifier) {
)
}
-@Preview
+@Preview(showBackground = true)
@Composable
fun AddTaskSheetPreview() {
AddTaskBottomSheetContent(onAddTask = {}, FocusRequester())
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/AddTaskTileService.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/AddTaskTileService.kt
index c130943d..4f3973aa 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/AddTaskTileService.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/AddTaskTileService.kt
@@ -1,6 +1,9 @@
package com.mhss.app.mybrain.presentation.tasks
+import android.annotation.SuppressLint
+import android.app.PendingIntent
import android.content.Intent
+import android.os.Build
import android.service.quicksettings.TileService
import androidx.core.net.toUri
import com.mhss.app.mybrain.presentation.main.MainActivity
@@ -8,6 +11,7 @@ import com.mhss.app.mybrain.util.Constants
class AddTaskTileService: TileService() {
+ @SuppressLint("StartActivityAndCollapseDeprecated")
override fun onClick() {
super.onClick()
val intent = Intent(
@@ -16,6 +20,13 @@ class AddTaskTileService: TileService() {
this,
MainActivity::class.java
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivityAndCollapse(intent)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ startActivityAndCollapse(pendingIntent)
+ } else {
+ startActivityAndCollapse(intent)
+ }
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/SubTaskItem.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/SubTaskItem.kt
index 5c372fef..7c8c9fa1 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/SubTaskItem.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/SubTaskItem.kt
@@ -1,26 +1,35 @@
package com.mhss.app.mybrain.presentation.tasks
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.*
+import androidx.compose.material.TextFieldDefaults.indicatorLine
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.mhss.app.mybrain.R
import com.mhss.app.mybrain.domain.model.SubTask
+@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SubTaskItem(
subTask: SubTask,
onChange: (SubTask) -> Unit,
onDelete: () -> Unit
) {
+ val interactionSource = remember { MutableInteractionSource() }
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
@@ -30,25 +39,59 @@ fun SubTaskItem(
contentDescription = stringResource(R.string.delete_sub_task),
modifier = Modifier.clickable { onDelete() }
)
- Checkbox(
- checked = subTask.isCompleted,
- onCheckedChange = { onChange(subTask.copy(isCompleted = it)) },
- )
+ CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
+ Checkbox(
+ checked = subTask.isCompleted,
+ onCheckedChange = { onChange(subTask.copy(isCompleted = it)) },
+ )
+ }
Spacer(Modifier.width(8.dp))
BasicTextField(
value = subTask.title,
onValueChange = {
onChange(subTask.copy(title = it))
},
- textStyle = if (subTask.isCompleted)
- TextStyle(
- textDecoration = TextDecoration.LineThrough,
- color = MaterialTheme.colors.onBackground
+ textStyle =
+ MaterialTheme.typography.body1.copy(
+ textDecoration = if (subTask.isCompleted) TextDecoration.LineThrough else null,
+ color = MaterialTheme.colors.onBackground
+ ),
+ modifier = Modifier
+ .padding(top = 4.dp)
+ .weight(1f)
+ .background(
+ MaterialTheme.colors.onBackground.copy(alpha = 0.1f),
+ shape = RoundedCornerShape(8.dp)
)
- else
- MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground),
- modifier = Modifier.fillMaxWidth()
- )
+ .indicatorLine(
+ enabled = true,
+ isError = false,
+ interactionSource,
+ colors = TextFieldDefaults.textFieldColors(
+ backgroundColor = MaterialTheme.colors.onBackground.copy(alpha = 0.1f),
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ disabledIndicatorColor = Color.Transparent
+ )
+ ),
+ cursorBrush = SolidColor(MaterialTheme.colors.primary)
+ ) {
+ TextFieldDefaults.TextFieldDecorationBox(
+ contentPadding = PaddingValues(8.dp),
+ visualTransformation = VisualTransformation.None,
+ enabled = true,
+ singleLine = false,
+ value = subTask.title,
+ interactionSource = interactionSource,
+ innerTextField = it
+ )
+ }
}
}
+
+@Preview(showBackground = true)
+@Composable
+private fun SubTaskItemPreview() {
+ SubTaskItem(subTask = SubTask("Title", true), {}, {})
+}
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskDetailScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskDetailScreen.kt
index dc587dfc..a20be85f 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskDetailScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskDetailScreen.kt
@@ -1,21 +1,32 @@
package com.mhss.app.mybrain.presentation.tasks
+import android.annotation.SuppressLint
import android.app.DatePickerDialog
import android.app.TimePickerDialog
+import android.content.Intent
+import android.net.Uri
+import android.provider.Settings
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
+import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@@ -36,6 +47,7 @@ import com.mhss.app.mybrain.util.settings.toInt
import com.mhss.app.mybrain.util.settings.toPriority
import java.util.*
+@SuppressLint("InlinedApi")
@Composable
fun TaskDetailScreen(
navController: NavHostController,
@@ -48,19 +60,19 @@ fun TaskDetailScreen(
val uiState = viewModel.taskDetailsUiState
val scaffoldState = rememberScaffoldState()
var openDialog by rememberSaveable { mutableStateOf(false) }
+ val context = LocalContext.current
var title by rememberSaveable { mutableStateOf("") }
var description by rememberSaveable { mutableStateOf("") }
var priority by rememberSaveable { mutableStateOf(Priority.LOW) }
var dueDate by rememberSaveable { mutableLongStateOf(0L) }
var recurring by rememberSaveable { mutableStateOf(false) }
- var frequency by rememberSaveable { mutableIntStateOf(0) }
+ var frequency by rememberSaveable { mutableStateOf(TaskFrequency.DAILY) }
var frequencyAmount by rememberSaveable { mutableIntStateOf(1) }
var dueDateExists by rememberSaveable { mutableStateOf(false) }
var completed by rememberSaveable { mutableStateOf(false) }
val subTasks = remember { mutableStateListOf() }
val priorities = listOf(Priority.LOW, Priority.MEDIUM, Priority.HIGH)
- val context = LocalContext.current
val formattedDate by remember {
derivedStateOf {
dueDate.formatDateDependingOnDay()
@@ -75,7 +87,7 @@ fun TaskDetailScreen(
dueDateExists = uiState.task.dueDate != 0L
completed = uiState.task.isCompleted
recurring = uiState.task.recurring
- frequency = uiState.task.frequency
+ frequency = uiState.task.frequency.toTaskFrequency()
frequencyAmount = uiState.task.frequencyAmount
subTasks.addAll(uiState.task.subTasks)
}
@@ -86,9 +98,17 @@ fun TaskDetailScreen(
navController.navigateUp()
}
if (uiState.error != null) {
- scaffoldState.snackbarHostState.showSnackbar(
- uiState.error
+ val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
+ uiState.error,
+ if (uiState.errorAlarm) context.getString(R.string.grant_permission) else null
)
+ if (snackbarResult == SnackbarResult.ActionPerformed) {
+ Intent().also { intent ->
+ intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
+ intent.data = Uri.parse("package:" + context.applicationContext.packageName)
+ context.startActivity(intent)
+ }
+ }
viewModel.onEvent(TaskEvent.ErrorDisplayed)
}
}
@@ -102,7 +122,7 @@ fun TaskDetailScreen(
priority = priority.toInt(),
subTasks = subTasks,
recurring = recurring,
- frequency = frequency,
+ frequency = frequency.value,
frequencyAmount = frequencyAmount
),
{
@@ -131,222 +151,41 @@ fun TaskDetailScreen(
)
}
) { paddingValues ->
- Column(
- Modifier
- .fillMaxSize()
- .verticalScroll(rememberScrollState())
- .padding(12.dp)
- .padding(paddingValues)
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
- ) {
- TaskCheckBox(
- isComplete = completed,
- borderColor = priority.color
- ) {
- completed = !completed
- viewModel.onEvent(
- TaskEvent.CompleteTask(
- uiState.task,
- completed
- )
- )
- }
- Spacer(Modifier.width(8.dp))
- OutlinedTextField(
- value = title,
- onValueChange = { title = it },
- label = { Text(text = stringResource(R.string.title)) },
- shape = RoundedCornerShape(15.dp),
- modifier = Modifier.fillMaxWidth()
- )
- }
- Spacer(Modifier.height(12.dp))
- Column {
- subTasks.forEachIndexed { index, item ->
- SubTaskItem(
- subTask = item,
- onChange = { subTasks[index] = it },
- onDelete = { subTasks.removeAt(index) }
- )
- }
- }
- Row(
- Modifier
- .fillMaxWidth()
- .clickable {
- subTasks.add(
- SubTask(
- title = "",
- isCompleted = false,
- )
- )
- },
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = stringResource(R.string.add_sub_task),
- modifier = Modifier.padding(vertical = 8.dp)
- )
- Icon(
- modifier = Modifier.size(10.dp),
- painter = painterResource(id = R.drawable.ic_add),
- contentDescription = stringResource(
- id = R.string.add_sub_task
+ TaskDetailsContent(
+ modifier = Modifier.padding(paddingValues),
+ completed = completed,
+ title = title,
+ description = description,
+ priority = priority,
+ dueDate = dueDate,
+ dueDateExists = dueDateExists,
+ recurring = recurring,
+ frequency = frequency,
+ frequencyAmount = frequencyAmount,
+ subTasks = subTasks,
+ priorities = priorities,
+ formattedDate = formattedDate,
+ onTitleChange = { title = it },
+ onDescriptionChange = { description = it },
+ onPriorityChange = { priority = it },
+ onDueDateExist = {
+ dueDateExists = it
+ if (it) dueDate = Calendar.getInstance().timeInMillis
+ },
+ onDueDateChange = { dueDate = it },
+ onRecurringChange = { recurring = it },
+ onFrequencyChange = { frequency = it },
+ onFrequencyAmountChange = { frequencyAmount = it },
+ onComplete = {
+ completed = it
+ viewModel.onEvent(
+ TaskEvent.CompleteTask(
+ uiState.task,
+ it
)
)
}
- Spacer(Modifier.height(12.dp))
- Text(
- text = stringResource(R.string.priority),
- style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Bold)
- )
- Spacer(Modifier.height(12.dp))
- PriorityTabRow(
- priorities = priorities,
- priority,
- onChange = { priority = it }
- )
- Spacer(Modifier.height(12.dp))
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- Checkbox(checked = dueDateExists, onCheckedChange = {
- dueDateExists = it
- if (it)
- dueDate = Calendar.getInstance().timeInMillis
- })
- Spacer(Modifier.width(4.dp))
- Text(
- text = stringResource(R.string.due_date),
- style = MaterialTheme.typography.body2
- )
- }
- AnimatedVisibility(dueDateExists) {
- Column {
- Row(
- Modifier
- .fillMaxWidth()
- .clickable {
- val date =
- if (dueDate == 0L) Calendar.getInstance() else Calendar
- .getInstance()
- .apply { timeInMillis = dueDate }
- val tempDate = Calendar.getInstance()
- val timePicker = TimePickerDialog(
- context,
- { _, hour, minute ->
- tempDate[Calendar.HOUR_OF_DAY] = hour
- tempDate[Calendar.MINUTE] = minute
- dueDate = tempDate.timeInMillis
- }, date[Calendar.HOUR_OF_DAY], date[Calendar.MINUTE], false
- )
- val datePicker = DatePickerDialog(
- context,
- { _, year, month, day ->
- tempDate[Calendar.YEAR] = year
- tempDate[Calendar.MONTH] = month
- tempDate[Calendar.DAY_OF_MONTH] = day
- timePicker.show()
- },
- date[Calendar.YEAR],
- date[Calendar.MONTH],
- date[Calendar.DAY_OF_MONTH]
- )
- datePicker.show()
- }
- .padding(vertical = 8.dp, horizontal = 16.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(
- painter = painterResource(R.drawable.ic_alarm),
- stringResource(R.string.due_date),
- modifier = Modifier.size(22.dp)
- )
- Spacer(Modifier.width(8.dp))
- Text(
- text = stringResource(R.string.due_date),
- style = MaterialTheme.typography.body1
- )
- }
- Text(
- text = formattedDate,
- style = MaterialTheme.typography.body2
- )
- }
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- Checkbox(checked = recurring, onCheckedChange = {
- recurring = it
- if (!it) frequency = 0
- })
- Spacer(Modifier.width(4.dp))
- Text(
- text = stringResource(R.string.recurring),
- style = MaterialTheme.typography.body2
- )
- }
- AnimatedVisibility(recurring) {
- var frequencyMenuVisible by remember { mutableStateOf(false) }
- Column {
- Box {
- DropdownMenu(
- expanded = frequencyMenuVisible,
- onDismissRequest = { frequencyMenuVisible = false }) {
- TaskFrequency.values().forEach { f ->
- DropdownMenuItem(
- onClick = {
- frequencyMenuVisible = false
- frequency = f.value
- }
- ) {
- Text(text = stringResource(f.title))
- }
- }
- }
- Row(
- Modifier
- .clickable { frequencyMenuVisible = true }
- .padding(vertical = 8.dp, horizontal = 16.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = stringResource(
- frequency.toTaskFrequency().title
- )
- )
- Icon(
- imageVector = Icons.Default.ArrowDropDown,
- contentDescription = stringResource(R.string.recurring),
- modifier = Modifier.size(22.dp)
- )
- }
- }
- Spacer(Modifier.height(8.dp))
- NumberPicker(
- stringResource(R.string.repeats_every),
- frequencyAmount
- ) {
- if (it > 0) frequencyAmount = it
- }
- }
- }
- }
- }
- Spacer(Modifier.height(12.dp))
- OutlinedTextField(
- value = description,
- onValueChange = { description = it },
- label = { Text(text = stringResource(R.string.description)) },
- shape = RoundedCornerShape(15.dp),
- modifier = Modifier.fillMaxWidth()
- )
- }
+ )
}
if (openDialog)
AlertDialog(
@@ -384,6 +223,276 @@ fun TaskDetailScreen(
)
}
+@Composable
+fun TaskDetailsContent(
+ modifier: Modifier = Modifier,
+ completed: Boolean,
+ title: String,
+ description: String,
+ priority: Priority,
+ dueDate: Long,
+ dueDateExists: Boolean,
+ recurring: Boolean,
+ frequency: TaskFrequency,
+ frequencyAmount: Int,
+ subTasks: MutableList,
+ priorities: List,
+ formattedDate: String,
+ focusRequester: FocusRequester? = null,
+ onTitleChange: (String) -> Unit,
+ onDescriptionChange: (String) -> Unit,
+ onPriorityChange: (Priority) -> Unit,
+ onDueDateExist: (Boolean) -> Unit,
+ onDueDateChange: (Long) -> Unit,
+ onRecurringChange: (Boolean) -> Unit,
+ onFrequencyChange: (TaskFrequency) -> Unit,
+ onFrequencyAmountChange: (Int) -> Unit,
+ onComplete: (Boolean) -> Unit,
+) {
+ val context = LocalContext.current
+ Column(
+ modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ .padding(12.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ TaskCheckBox(
+ isComplete = completed,
+ borderColor = priority.color
+ ) {
+ onComplete(!completed)
+ }
+ Spacer(Modifier.width(8.dp))
+ OutlinedTextField(
+ value = title,
+ onValueChange = onTitleChange,
+ label = { Text(text = stringResource(R.string.title)) },
+ shape = RoundedCornerShape(15.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .then(
+ if (focusRequester != null) Modifier.focusRequester(focusRequester)
+ else Modifier
+ )
+ )
+ }
+ Spacer(Modifier.height(12.dp))
+ Column {
+ subTasks.forEachIndexed { index, item ->
+ SubTaskItem(
+ subTask = item,
+ onChange = { subTasks[index] = it },
+ onDelete = { subTasks.removeAt(index) }
+ )
+ }
+ }
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .clickable {
+ subTasks.add(
+ SubTask(
+ title = "",
+ isCompleted = false,
+ )
+ )
+ },
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.add_sub_task),
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ Icon(
+ modifier = Modifier.size(10.dp),
+ painter = painterResource(id = R.drawable.ic_add),
+ contentDescription = stringResource(
+ id = R.string.add_sub_task
+ )
+ )
+ }
+ Spacer(Modifier.height(12.dp))
+ Text(
+ text = stringResource(R.string.priority),
+ style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Bold)
+ )
+ Spacer(Modifier.height(12.dp))
+ PriorityTabRow(
+ priorities = priorities,
+ priority,
+ onChange = onPriorityChange
+ )
+ Spacer(Modifier.height(12.dp))
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Checkbox(
+ checked = dueDateExists,
+ onCheckedChange = onDueDateExist
+ )
+ Spacer(Modifier.width(4.dp))
+ Text(
+ text = stringResource(R.string.due_date),
+ style = MaterialTheme.typography.body2
+ )
+ }
+ AnimatedVisibility(dueDateExists) {
+ Column {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .clickable {
+ val date =
+ if (dueDate == 0L) Calendar.getInstance() else Calendar
+ .getInstance()
+ .apply { timeInMillis = dueDate }
+ val tempDate = Calendar.getInstance()
+ val timePicker = TimePickerDialog(
+ context,
+ { _, hour, minute ->
+ tempDate[Calendar.HOUR_OF_DAY] = hour
+ tempDate[Calendar.MINUTE] = minute
+ onDueDateChange(tempDate.timeInMillis)
+ }, date[Calendar.HOUR_OF_DAY], date[Calendar.MINUTE], false
+ )
+ val datePicker = DatePickerDialog(
+ context,
+ { _, year, month, day ->
+ tempDate[Calendar.YEAR] = year
+ tempDate[Calendar.MONTH] = month
+ tempDate[Calendar.DAY_OF_MONTH] = day
+ timePicker.show()
+ },
+ date[Calendar.YEAR],
+ date[Calendar.MONTH],
+ date[Calendar.DAY_OF_MONTH]
+ )
+ datePicker.show()
+ }
+ .padding(vertical = 8.dp, horizontal = 16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ painter = painterResource(R.drawable.ic_alarm),
+ stringResource(R.string.due_date),
+ modifier = Modifier.size(22.dp)
+ )
+ Spacer(Modifier.width(8.dp))
+ Text(
+ text = stringResource(R.string.due_date),
+ style = MaterialTheme.typography.body1
+ )
+ }
+ Text(
+ text = formattedDate,
+ style = MaterialTheme.typography.body2
+ )
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Checkbox(
+ checked = recurring,
+ onCheckedChange = onRecurringChange
+ )
+ Spacer(Modifier.width(4.dp))
+ Text(
+ text = stringResource(R.string.recurring),
+ style = MaterialTheme.typography.body2
+ )
+ }
+ AnimatedVisibility(recurring) {
+ var frequencyMenuVisible by remember { mutableStateOf(false) }
+ Column {
+ DropDownItem(
+ title = stringResource(R.string.recurring),
+ expanded = frequencyMenuVisible,
+ items = TaskFrequency.entries,
+ selectedItem = frequency,
+ getText = {
+ stringResource(it.title)
+ },
+ onItemSelected = {
+ frequencyMenuVisible = false
+ onFrequencyChange(it)
+ },
+ onDismissRequest = {
+ frequencyMenuVisible = false
+ },
+ onClick = {
+ frequencyMenuVisible = true
+ })
+ Spacer(Modifier.height(8.dp))
+ Row(
+ Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ NumberPicker(
+ stringResource(R.string.repeats_every),
+ frequencyAmount
+ ) {
+ if (it > 0) onFrequencyAmountChange(it)
+ }
+ }
+ }
+ }
+ }
+ }
+ Spacer(Modifier.height(12.dp))
+ OutlinedTextField(
+ value = description,
+ onValueChange = onDescriptionChange,
+ label = { Text(text = stringResource(R.string.description)) },
+ shape = RoundedCornerShape(15.dp),
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+}
+
+@Composable
+fun PriorityTabRow(
+ priorities: List,
+ selectedPriority: Priority,
+ onChange: (Priority) -> Unit
+) {
+ val indicator = @Composable { tabPositions: List ->
+ AnimatedTabIndicator(Modifier.tabIndicatorOffset(tabPositions[selectedPriority.toInt()]))
+ }
+ TabRow(
+ selectedTabIndex = selectedPriority.toInt(),
+ indicator = indicator,
+ modifier = Modifier.clip(RoundedCornerShape(14.dp))
+ ) {
+ priorities.forEachIndexed { index, it ->
+ Tab(
+ text = { Text(stringResource(it.title)) },
+ selected = selectedPriority.toInt() == index,
+ onClick = {
+ onChange(index.toPriority())
+ },
+ modifier = Modifier.background(it.color)
+ )
+ }
+ }
+}
+
+@Composable
+fun AnimatedTabIndicator(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .padding(5.dp)
+ .fillMaxSize()
+ .border(BorderStroke(2.dp, Color.White), RoundedCornerShape(8.dp))
+ )
+}
+
private fun updateTaskIfChanged(
task: Task,
newTask: Task,
@@ -405,4 +514,50 @@ private fun taskChanged(
task.recurring != newTask.recurring ||
task.frequency != newTask.frequency ||
task.frequencyAmount != newTask.frequencyAmount
+}
+
+@Composable
+fun DropDownItem(
+ modifier: Modifier = Modifier,
+ title: String,
+ expanded: Boolean,
+ items: Iterable,
+ selectedItem: T,
+ getText: @Composable (T) -> String,
+ onItemSelected: (T) -> Unit,
+ onDismissRequest: () -> Unit,
+ onClick: () -> Unit,
+) {
+ Box(modifier) {
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismissRequest
+ ) {
+ items.forEach { item ->
+ DropdownMenuItem(
+ onClick = {
+ onDismissRequest()
+ onItemSelected(item)
+ }
+ ) {
+ Text(text = getText(item))
+ }
+ }
+ }
+ Row(
+ Modifier
+ .clickable { onClick() }
+ .padding(vertical = 8.dp, horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = getText(selectedItem)
+ )
+ Icon(
+ imageVector = Icons.Default.ArrowDropDown,
+ contentDescription = title,
+ modifier = Modifier.size(22.dp)
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskItem.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskItem.kt
index 7d604b03..8cf95282 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskItem.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskItem.kt
@@ -1,6 +1,7 @@
package com.mhss.app.mybrain.presentation.tasks
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -13,10 +14,15 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+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.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextDecoration
@@ -24,6 +30,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.mhss.app.mybrain.R
+import com.mhss.app.mybrain.domain.model.SubTask
import com.mhss.app.mybrain.domain.model.Task
import com.mhss.app.mybrain.util.date.formatDateDependingOnDay
import com.mhss.app.mybrain.util.date.isDueDateOverdue
@@ -66,22 +73,37 @@ fun LazyItemScope.TaskItem(
textDecoration = if (task.isCompleted) TextDecoration.LineThrough else TextDecoration.None
)
}
- if (task.dueDate != 0L) {
- Spacer(Modifier.height(8.dp))
- Row(verticalAlignment = Alignment.CenterVertically) {
- Icon(
- modifier = Modifier.size(13.dp),
- painter = painterResource(R.drawable.ic_alarm),
- contentDescription = stringResource(R.string.due_date),
- tint = if (task.dueDate.isDueDateOverdue()) Color.Red else MaterialTheme.colors.onSurface
- )
- Spacer(Modifier.width(4.dp))
- Text(
- text = task.dueDate.formatDateDependingOnDay(),
- style = MaterialTheme.typography.body2,
- color = if (task.dueDate.isDueDateOverdue()) Color.Red else MaterialTheme.colors.onSurface
+ Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ if (task.subTasks.isNotEmpty()) {
+ SubTasksProgressBar(
+ modifier = Modifier.padding(top = 8.dp),
+ subTasks = task.subTasks
)
}
+ Spacer(Modifier.width(8.dp))
+ if (task.dueDate != 0L) {
+ Row(
+ modifier = Modifier.padding(top = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ modifier = Modifier.size(13.dp),
+ painter = painterResource(R.drawable.ic_alarm),
+ contentDescription = stringResource(R.string.due_date),
+ tint = if (task.dueDate.isDueDateOverdue()) Color.Red else MaterialTheme.colors.onBackground.copy(
+ alpha = 0.8f
+ )
+ )
+ Spacer(Modifier.width(4.dp))
+ Text(
+ text = task.dueDate.formatDateDependingOnDay(),
+ style = MaterialTheme.typography.body2,
+ color = if (task.dueDate.isDueDateOverdue()) Color.Red else MaterialTheme.colors.onBackground.copy(
+ alpha = 0.8f
+ )
+ )
+ }
+ }
}
}
}
@@ -93,13 +115,14 @@ fun TaskCheckBox(
borderColor: Color,
onComplete: () -> Unit
) {
- Box(modifier = Modifier
- .size(30.dp)
- .clip(CircleShape)
- .border(2.dp, borderColor, CircleShape)
- .clickable {
- onComplete()
- }, contentAlignment = Alignment.Center
+ Box(
+ modifier = Modifier
+ .size(30.dp)
+ .clip(CircleShape)
+ .border(2.dp, borderColor, CircleShape)
+ .clickable {
+ onComplete()
+ }, contentAlignment = Alignment.Center
) {
AnimatedVisibility(visible = isComplete) {
Icon(
@@ -111,6 +134,48 @@ fun TaskCheckBox(
}
}
+@Composable
+fun SubTasksProgressBar(modifier: Modifier = Modifier, subTasks: List) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier
+ ) {
+ val completed = remember {
+ subTasks.count { it.isCompleted }
+ }
+ val total = subTasks.size
+ val progress by remember {
+ derivedStateOf {
+ completed.toFloat() / total.toFloat()
+ }
+ }
+ val circleColor = MaterialTheme.colors.onBackground.copy(alpha = 0.2f)
+ val progressColor = MaterialTheme.colors.onBackground.copy(alpha = 0.8f)
+ Canvas(
+ modifier = Modifier.size(16.dp)
+ ) {
+ drawCircle(
+ color = circleColor,
+ radius = size.width / 2,
+ style = Stroke(width = 8f)
+ )
+ drawArc(
+ color = progressColor,
+ startAngle = -90f,
+ sweepAngle = 360 * progress,
+ style = Stroke(width = 8f, cap = StrokeCap.Round),
+ useCenter = false
+ )
+ }
+ Spacer(Modifier.width(8.dp))
+ Text(
+ text = "$completed/$total",
+ style = MaterialTheme.typography.body2,
+ color = progressColor,
+ )
+ }
+}
+
@Preview
@Composable
fun LazyItemScope.TaskItemPreview() {
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskWidgetItem.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskWidgetItem.kt
index ca6d09c0..5dbac99d 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskWidgetItem.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TaskWidgetItem.kt
@@ -1,6 +1,7 @@
package com.mhss.app.mybrain.presentation.tasks
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -82,28 +83,60 @@ fun TaskWidgetItem(
)
))
)
+
}
- if (task.dueDate != 0L) {
- Spacer(GlanceModifier.height(4.dp))
- Row(verticalAlignment = Alignment.CenterVertically) {
- Image(
- modifier = GlanceModifier.size(10.dp),
- provider = if (task.dueDate.isDueDateOverdue()) ImageProvider(R.drawable.ic_alarm_red) else ImageProvider(
- R.drawable.ic_alarm
- ),
- contentDescription = "",
- )
- Spacer(GlanceModifier.width(3.dp))
- Text(
- text = task.dueDate.formatDateDependingOnDay(),
- style = TextStyle(
- color = if (task.dueDate.isDueDateOverdue()) ColorProvider(Color.Red) else ColorProvider(
- Color.White
+ Row(GlanceModifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ if (task.subTasks.isNotEmpty()){
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = GlanceModifier.padding(top = 4.dp)
+ ) {
+ val completed = remember {
+ task.subTasks.count { it.isCompleted }
+ }
+ val total = task.subTasks.size
+ Image(
+ provider = ImageProvider(R.drawable.ic_bullet_list),
+ modifier = GlanceModifier
+ .size(12.dp),
+ contentDescription = null
+ )
+ Spacer(GlanceModifier.width(3.dp))
+ Text(
+ text = "$completed/$total",
+ style = TextStyle(
+ color = ColorProvider(Color.White.copy(alpha = 0.8f)),
+ fontSize = 12.sp,
+ textDecoration = if (task.isCompleted) TextDecoration.LineThrough else TextDecoration.None
+ )
+ )
+ }
+ Spacer(GlanceModifier.width(4.dp))
+ }
+ if (task.dueDate != 0L) {
+ Row(
+ modifier = GlanceModifier.padding(top = 4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ modifier = GlanceModifier.size(12.dp),
+ provider = if (task.dueDate.isDueDateOverdue()) ImageProvider(R.drawable.ic_alarm_red) else ImageProvider(
+ R.drawable.ic_alarm
),
- fontWeight = FontWeight.Bold,
- fontSize = 10.sp,
+ contentDescription = "",
)
- )
+ Spacer(GlanceModifier.width(3.dp))
+ Text(
+ text = task.dueDate.formatDateDependingOnDay(),
+ style = TextStyle(
+ color = if (task.dueDate.isDueDateOverdue()) ColorProvider(Color.Red) else ColorProvider(
+ Color.White.copy(0.8f)
+ ),
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ )
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TasksScreen.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TasksScreen.kt
index 953e1a59..caf1e120 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TasksScreen.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TasksScreen.kt
@@ -1,5 +1,9 @@
package com.mhss.app.mybrain.presentation.tasks
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.net.Uri
+import android.provider.Settings
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
@@ -14,6 +18,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -29,6 +34,7 @@ import com.mhss.app.mybrain.util.settings.Order
import com.mhss.app.mybrain.util.settings.OrderType
import kotlinx.coroutines.launch
+@SuppressLint("InlinedApi")
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun TasksScreen(
@@ -36,11 +42,12 @@ fun TasksScreen(
addTask: Boolean = false,
viewModel: TasksViewModel = hiltViewModel()
) {
+ val context = LocalContext.current
var orderSettingsVisible by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
val uiState = viewModel.tasksUiState
val scaffoldState = rememberScaffoldState()
- val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
+ val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden, skipHalfExpanded = true)
val scope = rememberCoroutineScope()
BackHandler {
if (sheetState.isVisible)
@@ -66,21 +73,23 @@ fun TasksScreen(
)
},
floatingActionButton = {
- FloatingActionButton(
- onClick = {
- scope.launch {
- sheetState.show()
- focusRequester.requestFocus()
- }
- },
- backgroundColor = MaterialTheme.colors.primary,
- ) {
- Icon(
- modifier = Modifier.size(25.dp),
- painter = painterResource(R.drawable.ic_add),
- contentDescription = stringResource(R.string.add_task),
- tint = Color.White
- )
+ AnimatedVisibility(!sheetState.isVisible){
+ FloatingActionButton(
+ onClick = {
+ scope.launch {
+ sheetState.show()
+ focusRequester.requestFocus()
+ }
+ },
+ backgroundColor = MaterialTheme.colors.primary,
+ ) {
+ Icon(
+ modifier = Modifier.size(25.dp),
+ painter = painterResource(R.drawable.ic_add),
+ contentDescription = stringResource(R.string.add_task),
+ tint = Color.White
+ )
+ }
}
},
) {paddingValues ->
@@ -100,9 +109,17 @@ fun TasksScreen(
}) {
LaunchedEffect(uiState.error) {
uiState.error?.let {
- scaffoldState.snackbarHostState.showSnackbar(
- uiState.error
+ val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
+ it,
+ if (uiState.errorAlarm) context.getString(R.string.grant_permission) else null
)
+ if (snackbarResult == SnackbarResult.ActionPerformed) {
+ Intent().also { intent ->
+ intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
+ intent.data = Uri.parse("package:" + context.applicationContext.packageName)
+ context.startActivity(intent)
+ }
+ }
viewModel.onEvent(TaskEvent.ErrorDisplayed)
}
}
diff --git a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TasksViewModel.kt b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TasksViewModel.kt
index f008485a..6e644f9d 100644
--- a/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TasksViewModel.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/presentation/tasks/TasksViewModel.kt
@@ -73,13 +73,21 @@ class TasksViewModel @Inject constructor(
if (event.task.title.isNotBlank()) {
viewModelScope.launch {
val taskId = addTask(event.task)
- if (event.task.dueDate != 0L)
- addAlarm(
+ if (event.task.dueDate != 0L){
+ val scheduleSuccess = addAlarm(
Alarm(
taskId.toInt(),
event.task.dueDate,
)
)
+ if (!scheduleSuccess) {
+ tasksUiState = tasksUiState.copy(
+ error = getString(R.string.no_alarm_permission),
+ errorAlarm = true
+ )
+ updateTask(event.task.copy(id = taskId.toInt(), dueDate = 0L))
+ }
+ }
}
}else
@@ -89,8 +97,8 @@ class TasksViewModel @Inject constructor(
completeTask(event.task.id, event.complete)
}
TaskEvent.ErrorDisplayed -> {
- tasksUiState = tasksUiState.copy(error = null)
- taskDetailsUiState = taskDetailsUiState.copy(error = null)
+ tasksUiState = tasksUiState.copy(error = null, errorAlarm = false)
+ taskDetailsUiState = taskDetailsUiState.copy(error = null, errorAlarm = false)
}
is TaskEvent.UpdateOrder -> viewModelScope.launch {
saveSettings(
@@ -115,17 +123,29 @@ class TasksViewModel @Inject constructor(
else {
updateTask(event.task.copy(updatedDate = System.currentTimeMillis()))
if (event.task.dueDate != taskDetailsUiState.task.dueDate){
- if (event.task.dueDate != 0L)
- addAlarm(
+ if (event.task.dueDate != 0L) {
+ val scheduleSuccess = addAlarm(
Alarm(
event.task.id,
event.task.dueDate
)
)
- else
+ taskDetailsUiState = if (!scheduleSuccess) {
+ taskDetailsUiState.copy(
+ error = getString(R.string.no_alarm_permission),
+ errorAlarm = true
+ )
+ } else {
+ taskDetailsUiState.copy(navigateUp = true)
+ }
+ }
+ else {
deleteAlarm(event.task.id)
+ taskDetailsUiState = taskDetailsUiState.copy(navigateUp = true)
+ }
+ } else {
+ taskDetailsUiState = taskDetailsUiState.copy(navigateUp = true)
}
- taskDetailsUiState = taskDetailsUiState.copy(navigateUp = true)
}
}
is TaskEvent.DeleteTask -> viewModelScope.launch {
@@ -147,13 +167,15 @@ class TasksViewModel @Inject constructor(
val taskOrder: Order = Order.DateModified(OrderType.ASC()),
val showCompletedTasks: Boolean = false,
val error: String? = null,
+ val errorAlarm: Boolean = false,
val searchTasks: List = emptyList()
)
data class TaskUiState(
val task: Task = Task(""),
val navigateUp: Boolean = false,
- val error: String? = null
+ val error: String? = null,
+ val errorAlarm: Boolean = false
)
private fun getTasks(order: Order, showCompleted: Boolean) {
diff --git a/app/src/main/java/com/mhss/app/mybrain/util/Constants.kt b/app/src/main/java/com/mhss/app/mybrain/util/Constants.kt
index e768f134..e5ba20c1 100644
--- a/app/src/main/java/com/mhss/app/mybrain/util/Constants.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/util/Constants.kt
@@ -21,6 +21,7 @@ object Constants {
const val EXCLUDED_CALENDARS_KEY = "excluded_calendars"
const val APP_FONT_KEY = "app_font"
const val BLOCK_SCREENSHOTS_KEY = "block_screen_shots"
+ const val LOCK_APP_KEY = "lock_app"
// Navigation
const val TASK_ID_ARG = "task_id"
diff --git a/app/src/main/java/com/mhss/app/mybrain/util/settings/SettingsUtil.kt b/app/src/main/java/com/mhss/app/mybrain/util/settings/SettingsUtil.kt
index c29c9758..9601e1de 100644
--- a/app/src/main/java/com/mhss/app/mybrain/util/settings/SettingsUtil.kt
+++ b/app/src/main/java/com/mhss/app/mybrain/util/settings/SettingsUtil.kt
@@ -78,7 +78,7 @@ enum class ItemView(@StringRes val title: Int, val value: Int) {
}
fun Int.toNotesView(): ItemView {
- return ItemView.values().first { it.value == this }
+ return ItemView.entries.first { it.value == this }
}
@@ -100,7 +100,7 @@ fun Priority.toInt(): Int {
}
fun Int.toTaskFrequency(): TaskFrequency {
- return TaskFrequency.values().firstOrNull { it.value == this } ?: TaskFrequency.DAILY
+ return TaskFrequency.entries.firstOrNull { it.value == this } ?: TaskFrequency.DAILY
}
fun Int.toOrder(): Order {
diff --git a/app/src/main/res/drawable/ic_alarm.xml b/app/src/main/res/drawable/ic_alarm.xml
index 37fdf859..3c219eb0 100644
--- a/app/src/main/res/drawable/ic_alarm.xml
+++ b/app/src/main/res/drawable/ic_alarm.xml
@@ -4,15 +4,15 @@
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/app/src/main/res/drawable/ic_bullet_list.xml b/app/src/main/res/drawable/ic_bullet_list.xml
new file mode 100644
index 00000000..252a6cd8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bullet_list.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml
new file mode 100644
index 00000000..0f1c5c1c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_lock.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index ce9405d1..ef802b50 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -101,7 +101,7 @@
الإنتقال الى الاعداداتإضافة حدثالتقويمات المتضمنة :
- تمت الإضافة اللى المهام
+ تمت الإضافة الى المهامملخص اليومياتمزاجك على مدار الشهرمزاجك على مدار السنة
@@ -155,7 +155,7 @@
تم تصدير البياناتحدث خطأ أثناء التصديراستيراد البيانات
- تم تصدير %1$s
+ تم تصدير البياناتحدث خطأ أثناء الاستيراد. تأكد من عدم تخريب محتوى الملفمنح اذن التخزين للتصديريتم الاستيراد....
@@ -165,4 +165,12 @@
كلمة السركل ساعةكل دقيقة
+ قفل التطبيق
+ فتح القفل لاستخدام التطبيق
+ فتح القفل
+ لا توجد أجهزة للقياسات الحيوية في الجهاز
+ فشل المصادقة
+ لم يتم تعيين قفل شاشة مدعوم على جهازك
+ تمت الإضافة الى الملاحظات
+ لرجاء منح اذن المنبات لإضافة وقت
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index e407de3f..b9880b36 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -164,4 +164,12 @@
PasswordEvery hourEvery minute
+ Lock app
+ Unlock to use the App
+ Unlock
+ No biometric Hardware available
+ Authentication failed
+ No supported authentication method is set on your device
+ Added to notes
+ Please grant alarms permission to set a due date.
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 0ea6fe03..3855816d 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -164,4 +164,12 @@
PasswordEvery hourEvery minute
+ Lock app
+ Unlock to use the App
+ Unlock
+ No biometric Hardware available
+ Authentication failed
+ No supported authentication method is set on your device
+ Added to notes
+ Please grant alarms permission to set a due date.
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 968fd80b..97502a42 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -165,4 +165,12 @@
PasswordEvery hourEvery minute
+ Lock app
+ Unlock to use the App
+ Unlock
+ No biometric Hardware available
+ Authentication failed
+ No supported authentication method is set on your device
+ Added to notes
+ Please grant alarms permission to set a due date.
\ No newline at end of file
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index e4c3190f..1cd0929d 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -164,4 +164,12 @@
PasswordEvery hourEvery minute
+ Lock app
+ Unlock to use the App
+ Unlock
+ No biometric Hardware available
+ Authentication failed
+ No supported authentication method is set on your device
+ Added to notes
+ Please grant alarms permission to set a due date.
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index c3d2528f..4a1b63e8 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -164,4 +164,12 @@
PasswordEvery hourEvery minute
+ Lock app
+ Unlock to use the App
+ Unlock
+ No biometric Hardware available
+ Authentication failed
+ No supported authentication method is set on your device
+ Added to notes
+ Please grant alarms permission to set a due date.
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 8d5d6a79..0bcc456f 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -164,4 +164,12 @@
PasswordEvery hourEvery minute
+ Lock app
+ Unlock to use the App
+ Unlock
+ No biometric Hardware available
+ Authentication failed
+ No supported authentication method is set on your device
+ Added to notes
+ Please grant alarms permission to set a due date.
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 6c6be249..d3a3be17 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -164,4 +164,12 @@
PasswordEvery hourEvery minute
+ Lock app
+ Unlock to use the App
+ Unlock
+ No biometric Hardware available
+ Authentication failed
+ No supported authentication method is set on your device
+ Added to notes
+ Please grant alarms permission to set a due date.
\ No newline at end of file
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
new file mode 100644
index 00000000..23022670
--- /dev/null
+++ b/app/src/main/res/values-vi/strings.xml
@@ -0,0 +1,174 @@
+
+ My Brain
+ Kênh thông báo để gửi thông báo nhắc nhở nhiệm vụ
+ Lời nhắc
+ Hoàn thành
+ Bảng điều khiển
+ Thiết đặt
+ Không gian
+ Chủ đề ứng dụng
+ Chủ đề sáng
+ Chủ đề tối
+ Tự động
+ Màn hình khởi động
+ Giới thiệu
+ Phiên bản ứng dụng
+ Dự án trên GitHub
+ Chính sách bảo mật
+ Sản phẩm
+ Ghi chú
+ Nhiệm vụ
+ Nhật ký
+ Dấu trang
+ Lịch
+ Thêm nhiệm vụ
+ Thêm nhiệm vụ vào My Brain
+ Tìm kiếm
+ Tiêu đề
+ Xóa nhiệm vụ phụ
+ Thêm nhiệm vụ phụ
+ Mô tả
+ Yêu cầu một tính năng / Báo cáo lỗi
+ Thấp
+ Vừa
+ Cao
+ Ngày đáo hạn
+ Tiêu đề không được để trống
+ Ưu tiên
+ Sắp xếp theo
+ Bảng chữ cái
+ Ngày tạo
+ Ngày sửa đổi
+ Tăng dần
+ Giảm dần
+ Hiển thị các nhiệm vụ đã hoàn thành
+ Tìm kiếm nhiệm vụ
+ Lưu nhiệm vụ
+ Xóa nhiệm vụ
+ Xóa nhiệm vụ?
+ Xóa ghi chú?
+ Xóa dấu trang?
+ Xóa mục nhập?
+ Bạn có chắc chắn muốn xóa nhiệm vụ: \n \"%1$s\""?
+ Bạn có chắc chắn muốn xóa ghi chú: \n \"%1$s\""?
+ Bạn có chắc chắn muốn xóa dấu trang này""?
+ Bạn có chắc chắn muốn xóa mục này""?
+ Hủy
+ Bạn không có nhiệm vụ nào\n Nhấp vào nút + để thêm Nhiệm vụ mới\n hoặc \nbằng cách sử dụng lối tắt trong menu cài đặt nhanh
+ Bạn không có ghi chú nào\n Nhấp vào nút + để thêm Ghi chú mới
+ Bạn không có mục nào\n Nhấp vào nút + để thêm mục mới
+ Bạn không có dấu trang nào\n Nhấp vào nút + để thêm dấu trang mới\n hoặc \nbằng cách sử dụng tùy chọn chia sẻ từ bất kỳ trình duyệt nào
+ Lộ trình dự án
+ Danh sách
+ Lưới
+ Nội dung ghi chú (hỗ trợ markdown)
+ Xóa ghi chú
+ Ghi chú không được để trống
+ Ghim ghi chú
+ Chế độ đọc
+ Xem dưới dạng
+ Tìm kiếm theo tiêu đề hoặc nội dung
+ Thêm ghi chú
+ Liên kết dấu trang
+ Thêm vào nhiệm vụ
+ Đã lưu dấu trang thành công
+ URL không hợp lệ
+ Thêm dấu trang
+ Mở liên kết
+ Tìm kiếm dấu trang
+ Xóa dấu trang
+ URL
+ Hủy thay đổi
+ Thêm mục
+ Tuyệt vời
+ Tốt
+ Đồng ý
+ Tệ
+ Kinh khủng
+ Xóa mục nhập
+ Nội dung
+ Lưu mục nhập
+ Tìm kiếm nhật ký
+ Tâm trạng
+ %1$s - %2$s lúc %3$s
+ %1$s - %2$s
+ Cả ngày
+ Cần có quyền đọc Lịch để sử dụng được tính năng này.\nVui lòng cấp quyền
+ Cần có quyền ghi Lịch để tính năng này khả dụng.\nVui lòng cấp quyền
+ Cấp quyền
+ Đi tới thiết đặt
+ Thêm sự kiện
+ Bao gồm các lịch:
+ Đã thêm vào nhiệm vụ
+ Biểu đồ nhật ký
+ Tâm trạng của bạn trong tháng
+ Tâm trạng của bạn trong năm
+ %1$d%%
+ Dòng tâm trạng
+ Tóm tắt tâm trạng
+ "Tâm trạng của bạn lúc đó là"
+ " hầu hết thời gian "
+ 30 ngày qua
+ Năm ngoái
+ Chưa có dữ liệu
+ Chưa có sự kiện nào
+ Tóm tắt nhiệm vụ
+ Bạn đã hoàn thành
+ nhiệm vụ trong tuần qua
+ Chưa có nhiệm vụ nào
+ Đang tải…
+ Nhấp vào nút làm mới nếu bạn đã cấp quyền
+ Thứ năm ngày 30
+ Chủ nhật ngày 5
+ Ăn tối với Ali
+ Bài giảng CS
+ Thăm nha sĩ
+ Mua đồ tạp hóa
+ Gọi mẹ
+ Phông chữ ứng dụng
+ Mặc định hệ thống
+ Xóa sự kiện
+ Vị trí
+ Đừng lặp lại
+ Hằng ngày
+ Hàng tuần
+ Hàng tháng
+ Hàng năm
+ Sự kiện không nên ở trong quá khứ
+ Xóa sự kiện?
+ Bạn có chắc chắn muốn xóa sự kiện này không?
+ Thư mục
+ Tạo thư mục
+ Thư mục đã tồn tại
+ Tên
+ Thay đổi thư mục
+ Không có
+ Xóa thư mục
+ Bạn có chắc chắn muốn xóa thư mục này và tất cả nội dung của nó không?
+ Lưu
+ Chỉnh sửa thư mục
+ Chặn ảnh chụp màn hình
+ Xuất / Nhập
+ Xuất dữ liệu
+ Đã xuất dữ liệu thành công"
+ Đã xảy ra lỗi khi xuất
+ Nhập dữ liệu
+ Đã nhập thành công %1$s
+ Đã xảy ra lỗi khi nhập. Đảm bảo nội dung tập tin không bị hỏng
+ Cấp quyền xuất
+ Đang nhập....
+ Định kỳ
+ Lặp lại mỗi
+ Đã mã hóa
+ Mật khẩu
+ Mỗi giờ
+ Mỗi phút
+ Lock app
+ Unlock to use the App
+ Unlock
+ No biometric Hardware available
+ Authentication failed
+ No supported authentication method is set on your device
+ Added to notes
+ Please grant alarms permission to set a due date.
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 348e1b24..4b5c6804 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -164,4 +164,12 @@
PasswordEvery hourEvery minute
+ Lock app
+ Unlock to use the App
+ Unlock
+ No biometric Hardware available
+ Authentication failed
+ No supported authentication method is set on your device
+ Added to notes
+ Please grant alarms permission to set a due date.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index be33dae9..cb5524cf 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -154,7 +154,7 @@
Successfully exported data"Something went wrong while exportingImport data
- Successfully imported %1$s
+ Successfully imported dataSomething went wrong while importing. Make sure the file content are not corruptedGrant permission to exportImporting....
@@ -164,4 +164,12 @@
PasswordEvery hourEvery minute
+ Lock app
+ Unlock to use the App
+ Unlock
+ No biometric Hardware available
+ Authentication failed
+ No supported authentication method is set on your device
+ Added to notes
+ Please grant alarms permission to set a due date.
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 0dd82e07..5e79a483 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,7 +1,7 @@
-
\ No newline at end of file
diff --git a/app/src/test/java/com/mhss/app/mybrain/ExampleUnitTest.kt b/app/src/test/java/com/mhss/app/mybrain/ExampleUnitTest.kt
deleted file mode 100644
index cf15a475..00000000
--- a/app/src/test/java/com/mhss/app/mybrain/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.mhss.app.mybrain
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
-}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index a1dda366..7ba7b915 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
- id("com.android.application") version "8.1.1" apply false
- id("org.jetbrains.kotlin.android") version "1.9.0" apply false
- id ("com.google.dagger.hilt.android") version "2.48" apply false
- id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
+ id("com.android.application") version "8.4.0" apply false
+ id("org.jetbrains.kotlin.android") version "1.9.23" apply false
+ id ("com.google.dagger.hilt.android") version "2.49" apply false
+ id("com.google.devtools.ksp") version "1.9.23-1.0.20" apply false
+ kotlin("plugin.serialization") version "1.9.23"
}
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/8.txt b/fastlane/metadata/android/en-US/changelogs/8.txt
new file mode 100644
index 00000000..b3de0b0c
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/8.txt
@@ -0,0 +1,11 @@
+- Added Vietnamese language by @ngocanhtve
+- Added lock app option
+- Added the ability to save text to note from share menu
+- Added subtasks progress in task card
+- Fixed backup problems causing missing data
+- Performance Improvements
+- UX improvements
+- Bug fixes
+### Notes:
+- The backup file format has been changed. You won't be able to import old backups in the new version so please create a new backup as soon as you install the new update to be able to restore it later.
+- Added internet permission for markdown features that require it and other future app features
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index ba7c1803..73376d44 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -1,7 +1,7 @@
My Brain is an Open source, All in one productivity app for Tasks, Notes, Calendar, Diary and Bookmarks.
Features :
-- Private with no data collection and no internet permission at all.
+- Private with no data collection at all.
- Create tasks with priority, sub-tasks, description and due date and reminders.
- Create Notes that supports markdown which enables you to use Headers, lists, links etc..
- Record your mood daily and view your mood summary with beautiful graphs.
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e9acb89c..2bdd90df 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Thu Sep 21 12:40:08 EET 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists