diff --git a/.gitignore b/.gitignore index 12c6da86..04555e29 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml +/.idea/inspectionProfiles/Project_Default.xml /.idea/deploymentTargetDropDown.xml .DS_Store /build @@ -16,3 +17,5 @@ .externalNativeBuild .cxx local.properties +/.idea/deploymentTargetSelector.xml +/app/release diff --git a/.idea/gradle.xml b/.idea/gradle.xml index ae388c2a..0897082f 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,16 +4,15 @@ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 12731b42..00000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - \ 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 @@ [Get it on F-Droid](https://f-droid.org/packages/com.mhss.app.mybrain) -[Get it on GitHub](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 @@ Password Every hour Every 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 @@ Password Every hour Every 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 @@ Password Every hour Every 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 @@ Password Every hour Every 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 @@ Password Every hour Every 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 @@ Password Every hour Every 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 @@ Password Every hour Every 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 @@ Password Every hour Every 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 exporting Import data - Successfully imported %1$s + Successfully imported data Something went wrong while importing. Make sure the file content are not corrupted Grant permission to export Importing.... @@ -164,4 +164,12 @@ Password Every hour Every 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