diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index decde6c..fecda5a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,13 +1,17 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
+ alias(libs.plugins.aboutlibraries)
}
+
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
-val baseVersionName = "1.0.6"
+// 配置版本信息
+val baseVersionName = "compose-refactor"
val commitHash by lazy { "git rev-parse --short HEAD".exec() }
val verCode by lazy { "git rev-list --count HEAD".exec().toInt() }
@@ -53,50 +57,70 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_18
- targetCompatibility = JavaVersion.VERSION_18
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
}
kotlinOptions {
- jvmTarget = "18"
+ jvmTarget = "21"
+ }
+ aboutLibraries {
+ configPath = "$projectDir/licences"
}
buildFeatures {
- viewBinding = true
+ compose = true
}
}
dependencies {
// Android X
- implementation(libs.androidx.core)
implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.core.core.splashscreen)
- implementation(libs.androidx.appcompat)
- implementation(libs.androidx.activity)
- implementation(libs.androidx.activity.ktx)
- implementation(libs.androidx.constraintlayout)
- implementation(libs.androidx.fragment)
- implementation(libs.androidx.fragment.ktx)
- implementation(libs.androidx.recyclerview)
- implementation(libs.androidx.lifecycle.viewmodel)
- implementation(libs.androidx.lifecycle.viewmodel.ktx)
- implementation(libs.androidx.preference)
- implementation(libs.androidx.preference.ktx)
- implementation(libs.androidx.security)
- // Material Design
- implementation(libs.material)
+ implementation(libs.androidx.core.splashscreen)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.androidx.compose.runtime.livedata)
+ // implementation(libs.androidx.security)
+ // Compose
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.animation)
+ implementation(libs.androidx.navigation)
+ implementation(libs.androidx.material3.adaptive)
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.android)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.material.icon.core)
+ implementation(libs.androidx.material.icon.extended)
+ // About Libraries
+ implementation(libs.aboutlibraries.core)
+ implementation(libs.aboutlibraries.compose)
+ // M3 Color
+ implementation(libs.com.kyant0.m3color)
+ // Konfetti
+ implementation(libs.nl.dionsegijn.konfetti.compose)
+ // Lazy Column Scrollbar
+ implementation(libs.lazycolumnscrollbar)
+ // Kotlin Coroutines
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.kotlinx.coroutines.android)
// Room
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
annotationProcessor(libs.androidx.room.compiler)
ksp(libs.androidx.room.compiler)
- // FastScroll
- implementation(project(":fastscroll"))
// Test
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
}
+// 命令执行工具类
fun String.exec(): String = exec(this)
fun Project.exec(command: String): String = providers.exec {
diff --git a/app/licences/libraries/eastereggs.json b/app/licences/libraries/eastereggs.json
new file mode 100644
index 0000000..249317c
--- /dev/null
+++ b/app/licences/libraries/eastereggs.json
@@ -0,0 +1,16 @@
+{
+ "uniqueId": "com.dede.android_eggs:3.4.0",
+ "artifactVersion": "3.4.0",
+ "name": "AndroidEasterEggs",
+ "developers": [
+ {
+ "name": "hushenghao"
+ }
+ ],
+ "description": "Collections the Android release Easter Egg",
+ "website": "https://github.com/hushenghao/AndroidEasterEggs",
+ "tag": "custom",
+ "licenses": [
+ "Apache-2.0"
+ ]
+}
\ No newline at end of file
diff --git a/app/licences/libraries/libchecker.json b/app/licences/libraries/libchecker.json
new file mode 100644
index 0000000..4c6d340
--- /dev/null
+++ b/app/licences/libraries/libchecker.json
@@ -0,0 +1,16 @@
+{
+ "uniqueId": "com.absinthe.libchecker:2.5.0",
+ "artifactVersion": "2.5.0",
+ "name": "LibChecker",
+ "developers": [
+ {
+ "name": "LibChecker"
+ }
+ ],
+ "description": "An app to view libraries used in apps in your device.",
+ "website": "https://github.com/LibChecker/LibChecker",
+ "tag": "custom",
+ "licenses": [
+ "Apache-2.0"
+ ]
+}
\ No newline at end of file
diff --git a/app/licences/libraries/mauth.json b/app/licences/libraries/mauth.json
new file mode 100644
index 0000000..3a58df7
--- /dev/null
+++ b/app/licences/libraries/mauth.json
@@ -0,0 +1,16 @@
+{
+ "uniqueId": "com.xinto.mauth:0.9.0",
+ "artifactVersion": "0.9.0",
+ "name": "Mauth",
+ "developers": [
+ {
+ "name": "X1nto"
+ }
+ ],
+ "description": "A Material You Two-factor Authentication app",
+ "website": "https://github.com/X1nto/Mauth/",
+ "tag": "custom",
+ "licenses": [
+ "GPL-3.0-or-later"
+ ]
+}
\ No newline at end of file
diff --git a/app/licences/libraries/readyou.json b/app/licences/libraries/readyou.json
new file mode 100644
index 0000000..3ef049f
--- /dev/null
+++ b/app/licences/libraries/readyou.json
@@ -0,0 +1,16 @@
+{
+ "uniqueId": "me.ash.reader:0.11.1",
+ "artifactVersion": "0.11.1",
+ "name": "Read You",
+ "developers": [
+ {
+ "name": "Ashinch"
+ }
+ ],
+ "description": "An Android RSS reader presented in Material You style.",
+ "website": "https://github.com/Ashinch/ReadYou/",
+ "tag": "custom",
+ "licenses": [
+ "GPL-3.0-or-later"
+ ]
+}
\ No newline at end of file
diff --git a/app/licences/libraries/skipad.json b/app/licences/libraries/skipad.json
new file mode 100644
index 0000000..4374c79
--- /dev/null
+++ b/app/licences/libraries/skipad.json
@@ -0,0 +1,13 @@
+{
+ "uniqueId": "com.github.crayonxiaoxin.abc:1.0.1",
+ "artifactVersion": "1.0.1",
+ "name": "SkipAD",
+ "developers": [
+ {
+ "name": "crayonxiaoxin"
+ }
+ ],
+ "description": "Android无障碍跳过开屏广告",
+ "website": "https://github.com/crayonxiaoxin/SkipAD",
+ "tag": "custom"
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index f4ec64c..481bb43 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,6 +18,4 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
--dontwarn javax.annotation.Nullable
--dontwarn javax.annotation.concurrent.GuardedBy
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/schemas/cn.super12138.todo.logic.dao.ToDoRoomDB/1.json b/app/schemas/cn.super12138.todo.logic.dao.ToDoRoomDB/1.json
deleted file mode 100644
index 0bd7a91..0000000
--- a/app/schemas/cn.super12138.todo.logic.dao.ToDoRoomDB/1.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
- "formatVersion": 1,
- "database": {
- "version": 1,
- "identityHash": "bf23f9a3f857e7cfe28dce9eb1657534",
- "entities": [
- {
- "tableName": "todo",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `state` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`uuid`))",
- "fields": [
- {
- "fieldPath": "uuid",
- "columnName": "uuid",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "state",
- "columnName": "state",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "subject",
- "columnName": "subject",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "content",
- "columnName": "content",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "uuid"
- ]
- },
- "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, 'bf23f9a3f857e7cfe28dce9eb1657534')"
- ]
- }
-}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0e3e6b4..6ca0ddb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,11 +3,9 @@
xmlns:tools="http://schemas.android.com/tools">
-
+ android:label="@string/app_name">
-
-
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..3786179
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/kotlin/cn/super12138/todo/ToDoApp.kt b/app/src/main/kotlin/cn/super12138/todo/TodoApp.kt
similarity index 51%
rename from app/src/main/kotlin/cn/super12138/todo/ToDoApp.kt
rename to app/src/main/kotlin/cn/super12138/todo/TodoApp.kt
index 8308a25..b2397d5 100644
--- a/app/src/main/kotlin/cn/super12138/todo/ToDoApp.kt
+++ b/app/src/main/kotlin/cn/super12138/todo/TodoApp.kt
@@ -3,27 +3,25 @@ package cn.super12138.todo
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
-import cn.super12138.todo.logic.dao.ToDoRoomDB
-import cn.super12138.todo.views.activities.CrashHandler
-import com.google.android.material.color.DynamicColors
+import cn.super12138.todo.logic.database.TodoDatabase
+import cn.super12138.todo.ui.pages.crash.CrashHandler
-class ToDoApp : Application() {
- private val database by lazy { ToDoRoomDB.getDatabase(this) }
+class TodoApp : Application() {
+ private val database by lazy { TodoDatabase.getDatabase(this) }
companion object {
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
- lateinit var db: ToDoRoomDB
+ lateinit var db: TodoDatabase
}
override fun onCreate() {
super.onCreate()
- DynamicColors.applyToActivitiesIfAvailable(this)
+
+ db = database
context = applicationContext
- val crashHandler = CrashHandler(this)
+ val crashHandler = CrashHandler(applicationContext)
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
-
- db = database
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/constant/Constants.kt b/app/src/main/kotlin/cn/super12138/todo/constant/Constants.kt
deleted file mode 100644
index 8a54be8..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/constant/Constants.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package cn.super12138.todo.constant
-
-object Constants {
- const val WELCOME_PAGE= "welcome_page"
-
- const val AUTHOR_GITHUB_URL = "https://github.com/Super12138/"
- const val REPO_GITHUB_URL = "https://github.com/Super12138/ToDo"
- const val UPDATE_URL = "https://github.com/Super12138/ToDo/releases"
-
- const val SP_NAME = "cn.super12138.todo_preferences"
-
- const val PREF_DARK_MODE = "dark_mode"
- const val PREF_SECURE_MODE = "secure_mode"
- const val PREF_HAPTIC_FEEDBACK = "haptic_feedback"
- const val PREF_ALL_TASKS = "all_tasks"
- const val PREF_REENTER_WELCOME_ACTIVITY = "reenter_welcome_activity"
- const val PREF_ABOUT = "about"
- const val PREF_DEV_MODE = "dev_mode"
- // const val PREF_SPRING_FESTIVAL_THEME = "spring_festival_theme"
- const val PREF_BACKUP_DB = "backup_db"
- const val PREF_RESTORE_DB = "restore_db"
-
-
- const val STRING_DEV_MODE = "/DEV_MODE"
-
- const val TAG_INFO_BOTTOM_SHEET = "InfoBottomSheet"
- const val TAG_TODO_BOTTOM_SHEET = "ToDoBottomSheet"
-
- const val BUNDLE_EDIT_MODE = "editMode"
- const val BUNDLE_POSITION = "todoPosition"
- const val BUNDLE_TODO_CONTENT = "todoContent"
- const val BUNDLE_TODO_SUBJECT = "todoSubject"
- const val BUNDLE_TODO_STATE = "todoState"
- const val BUNDLE_TODO_UUID = "todoUUID"
-
- const val EMPTY_VIEW_TYPE = 0
- const val DEFAULT_VIEW_TYPE = 1
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/constant/GlobalValues.kt b/app/src/main/kotlin/cn/super12138/todo/constant/GlobalValues.kt
deleted file mode 100644
index 430ed93..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/constant/GlobalValues.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package cn.super12138.todo.constant
-
-import cn.super12138.todo.utils.sp.SPDelegates
-
-object GlobalValues {
- var welcomePage: Boolean by SPDelegates(Constants.WELCOME_PAGE, false)
- var darkMode: String by SPDelegates(Constants.PREF_DARK_MODE, "0")
- var devMode: Boolean by SPDelegates(Constants.PREF_DEV_MODE, false)
- // var springFestivalTheme: Boolean by SPDelegates(Constants.PREF_SPRING_FESTIVAL_THEME, false)
- var secureMode: Boolean by SPDelegates(Constants.PREF_SECURE_MODE, false)
- var hapticFeedback: Boolean by SPDelegates(Constants.PREF_HAPTIC_FEEDBACK, true)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt b/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt
new file mode 100644
index 0000000..79d453e
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt
@@ -0,0 +1,32 @@
+package cn.super12138.todo.constants
+
+object Constants {
+ const val DEVELOPER_GITHUB = "https://github.com/Super12138/"
+ const val GITHUB_REPO = "https://github.com/Super12138/ToDo/"
+
+ const val KEY_TODO_FAB_TRANSITION = "todo_fab"
+ const val KEY_TODO_CONTENT_TRANSITION = "todo_content"
+
+ const val SP_NAME = "cn.super12138.todo_preferences"
+
+ const val PREF_DYNAMIC_COLOR = "dynamic_color"
+ const val PREF_DYNAMIC_COLOR_DEFAULT = true
+
+ const val PREF_PALETTE_STYLE = "palette_style"
+ const val PREF_PALETTE_STYLE_DEFAULT = 1 // TonalSpot
+
+ const val PREF_DARK_MODE = "dark_mode"
+ const val PREF_DARK_MODE_DEFAULT = -1 // Follow System
+
+ const val PREF_CONTRAST_LEVEL = "contrast_level"
+ const val PREF_CONTRAST_LEVEL_DEFAULT = 0f // Normal
+
+ const val PREF_SHOW_COMPLETED = "show_completed"
+ const val PREF_SHOW_COMPLETED_DEFAULT = true
+
+ const val PREF_SORTING_METHOD = "sorting_method"
+ const val PREF_SORTING_METHOD_DEFAULT = 1
+
+ const val PREF_HAPTIC_FEEDBACK = "haptic_feedback"
+ const val PREF_HAPTIC_FEEDBACK_DEFAULT = true
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/constants/GlobalValues.kt b/app/src/main/kotlin/cn/super12138/todo/constants/GlobalValues.kt
new file mode 100644
index 0000000..aa4630c
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/constants/GlobalValues.kt
@@ -0,0 +1,34 @@
+package cn.super12138.todo.constants
+
+import cn.super12138.todo.utils.SPDelegates
+
+object GlobalValues {
+ var dynamicColor: Boolean by SPDelegates(
+ key = Constants.PREF_DYNAMIC_COLOR,
+ default = Constants.PREF_DYNAMIC_COLOR_DEFAULT
+ )
+ var paletteStyle: Int by SPDelegates(
+ key = Constants.PREF_PALETTE_STYLE,
+ default = Constants.PREF_PALETTE_STYLE_DEFAULT
+ )
+ var darkMode: Int by SPDelegates(
+ key = Constants.PREF_DARK_MODE,
+ default = Constants.PREF_DARK_MODE_DEFAULT
+ )
+ var contrastLevel: Float by SPDelegates(
+ key = Constants.PREF_CONTRAST_LEVEL,
+ default = Constants.PREF_CONTRAST_LEVEL_DEFAULT
+ )
+ var showCompleted: Boolean by SPDelegates(
+ key = Constants.PREF_SHOW_COMPLETED,
+ default = Constants.PREF_SHOW_COMPLETED_DEFAULT
+ )
+ var sortingMethod: Int by SPDelegates(
+ key = Constants.PREF_SORTING_METHOD,
+ default = Constants.PREF_SORTING_METHOD_DEFAULT
+ )
+ var hapticFeedback: Boolean by SPDelegates(
+ key = Constants.PREF_HAPTIC_FEEDBACK,
+ default = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt b/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt
index 077682f..034a5c2 100644
--- a/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt
+++ b/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt
@@ -1,87 +1,32 @@
package cn.super12138.todo.logic
-import cn.super12138.todo.ToDoApp
-import cn.super12138.todo.logic.dao.ToDoRoom
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
+import cn.super12138.todo.TodoApp
+import cn.super12138.todo.logic.database.TodoEntity
+import kotlinx.coroutines.flow.Flow
object Repository {
+ private val db get() = TodoApp.db
+ private val toDoDao = db.toDoDao()
- // Room
- private val db get() = ToDoApp.db
- private val todoDao = db.toDoRoomDao()
-
- /**
- * @param toDoRoom 要插入的数据
- */
- suspend fun insert(toDoRoom: ToDoRoom) {
- withContext(Dispatchers.IO) {
- todoDao.insert(toDoRoom)
- }
- }
-
- /**
- * 获取全部未完成的待办
- * @return List
- */
- suspend fun getAllIncomplete(): List {
- return withContext(Dispatchers.IO) {
- todoDao.getAllUnfinished()
- }
+ suspend fun insertTodo(toDo: TodoEntity) {
+ toDoDao.insert(toDo)
}
- /**
- * 获取全部已完成的待办
- * @return List
- */
- suspend fun getAllComplete(): List {
- return withContext(Dispatchers.IO) {
- todoDao.getAllComplete()
- }
- }
+ fun getAllTodos(): Flow> = toDoDao.getAll()
- /**
- * 获取全部待办
- * @return List
- */
- suspend fun getAll(): List {
- return withContext(Dispatchers.IO) {
- todoDao.getAll()
- }
+ suspend fun updateTodo(toDo: TodoEntity) {
+ toDoDao.update(toDo)
}
- /**
- * 根据待办的UUID删除指定待办
- * @param uuid 待办的UUID
- */
- suspend fun deleteByUUID(uuid: String) {
- withContext(Dispatchers.IO) {
- todoDao.deleteByUUID(uuid)
- }
+ suspend fun deleteTodo(toDo: TodoEntity) {
+ toDoDao.delete(toDo)
}
- /**
- * 删除全部代办
- */
- suspend fun deleteAll() {
- withContext(Dispatchers.IO) {
- todoDao.deleteAll()
- }
+ suspend fun deleteTodoFromIds(toDoItems: List) {
+ toDoDao.deleteFromIds(toDoItems.toSet())
}
- /**
- * 根据代办的UUID来把待办状态更新为“已完成”
- * @param uuid 待办的UUID
- */
- suspend fun updateStateByUUID(uuid: String) {
- withContext(Dispatchers.IO) {
- todoDao.updateStateByUUID(uuid)
- }
- }
-
- suspend fun update(toDoRoom: ToDoRoom) {
- withContext(Dispatchers.IO) {
- todoDao.update(toDoRoom)
- }
- }
+ /*suspend fun deleteAllTodo() {
+ toDoDao.deleteAllTodo()
+ }*/
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoom.kt b/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoom.kt
deleted file mode 100644
index 69cf07c..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoom.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package cn.super12138.todo.logic.dao
-
-import androidx.room.ColumnInfo
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-
-/**
- * @param uuid String 待办的uuid
- * @param state Int 待办的完成状态,0表示未完成,1表示完成
- * @param subject String 待办的学科
- * @param content String 待办的内容
- */
-@Entity(tableName = "todo")
-data class ToDoRoom(
- @PrimaryKey @ColumnInfo(name = "uuid") val uuid: String,
- @ColumnInfo(name = "state") val state: Int,
- @ColumnInfo(name = "subject") val subject: String,
- @ColumnInfo(name = "content") val content: String
-)
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDao.kt b/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDao.kt
deleted file mode 100644
index c05fbd3..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDao.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package cn.super12138.todo.logic.dao
-
-import androidx.room.Dao
-import androidx.room.Insert
-import androidx.room.Query
-import androidx.room.Update
-
-@Dao
-interface ToDoRoomDao {
- @Insert
- suspend fun insert(toDoRoom: ToDoRoom)
-
- @Query("SELECT * FROM todo")
- suspend fun getAll(): List
-
- @Query("SELECT * FROM todo WHERE state = 0")
- suspend fun getAllUnfinished(): List
-
- @Query("SELECT * FROM todo WHERE state = 1")
- suspend fun getAllComplete(): List
-
- @Query("DELETE FROM todo")
- suspend fun deleteAll()
-
- @Query("DELETE FROM todo WHERE uuid = :uuid")
- suspend fun deleteByUUID(uuid: String)
-
- @Query("UPDATE todo SET state = 1 WHERE uuid = :uuid")
- suspend fun updateStateByUUID(uuid: String)
-
- @Update
- suspend fun update(toDoRoom: ToDoRoom)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDao.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDao.kt
new file mode 100644
index 0000000..239a664
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDao.kt
@@ -0,0 +1,30 @@
+package cn.super12138.todo.logic.database
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Update
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface TodoDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(toDo: TodoEntity)
+
+ @Query("SELECT * FROM todo")
+ fun getAll(): Flow>
+
+ @Update
+ suspend fun update(toDo: TodoEntity)
+
+ @Delete
+ suspend fun delete(toDo: TodoEntity)
+
+ @Query("DELETE FROM todo WHERE id in (:toDoIds)")
+ suspend fun deleteFromIds(toDoIds: Set)
+
+ /*@Query("DELETE FROM todo")
+ suspend fun deleteAllTodo()*/
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDB.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt
similarity index 50%
rename from app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDB.kt
rename to app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt
index bcc1f2f..8f72c65 100644
--- a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDB.kt
+++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt
@@ -1,24 +1,26 @@
-package cn.super12138.todo.logic.dao
+package cn.super12138.todo.logic.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
-@Database(entities = [ToDoRoom::class], version = 1)
-abstract class ToDoRoomDB : RoomDatabase() {
- abstract fun toDoRoomDao(): ToDoRoomDao
+@Database(entities = [TodoEntity::class], version = 2)
+abstract class TodoDatabase : RoomDatabase() {
+ abstract fun toDoDao(): TodoDao
companion object {
@Volatile
- private var INSTANCE: ToDoRoomDB? = null
- fun getDatabase(context: Context): ToDoRoomDB {
+ private var INSTANCE: TodoDatabase? = null
+ fun getDatabase(context: Context): TodoDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
- ToDoRoomDB::class.java,
+ TodoDatabase::class.java,
"todo"
- ).build()
+ )
+ .fallbackToDestructiveMigration()
+ .build()
INSTANCE = instance
return instance
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoEntity.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoEntity.kt
new file mode 100644
index 0000000..90957b1
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoEntity.kt
@@ -0,0 +1,14 @@
+package cn.super12138.todo.logic.database
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "todo")
+data class TodoEntity(
+ @ColumnInfo(name = "content") val content: String,
+ @ColumnInfo(name = "subject") val subject: Int,
+ @ColumnInfo(name = "completed") val isCompleted: Boolean = false,
+ @ColumnInfo(name = "priority") val priority: Float,
+ @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Int = 0,
+)
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/ContrastLevel.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/ContrastLevel.kt
new file mode 100644
index 0000000..09fd77d
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/ContrastLevel.kt
@@ -0,0 +1,29 @@
+package cn.super12138.todo.logic.model
+
+import android.content.Context
+import cn.super12138.todo.R
+
+enum class ContrastLevel(val value: Float) {
+ VeryLow(-1f),
+ Low(-0.5f),
+ Default(0f),
+ Medium(0.5f),
+ High(1f);
+
+ fun getDisplayName(context: Context): String {
+ val resId = when (this) {
+ VeryLow -> R.string.contrast_very_low
+ Low -> R.string.contrast_low
+ Default -> R.string.contrast_default
+ Medium -> R.string.contrast_high
+ High -> R.string.contrast_very_high
+ }
+ return context.getString(resId)
+ }
+
+ companion object {
+ fun fromFloat(float: Float): ContrastLevel {
+ return ContrastLevel.entries.find { it.value == float } ?: Default
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/DarkMode.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/DarkMode.kt
new file mode 100644
index 0000000..7bd0cc7
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/DarkMode.kt
@@ -0,0 +1,33 @@
+package cn.super12138.todo.logic.model
+
+import android.content.Context
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.DarkMode
+import androidx.compose.material.icons.outlined.LightMode
+import androidx.compose.material.icons.outlined.SettingsSuggest
+import androidx.compose.ui.graphics.vector.ImageVector
+import cn.super12138.todo.R
+
+enum class DarkMode(
+ val id: Int,
+ val icon: ImageVector
+) {
+ FollowSystem(-1, Icons.Outlined.SettingsSuggest),
+ Light(1, Icons.Outlined.LightMode),
+ Dark(2, Icons.Outlined.DarkMode);
+
+ fun getDisplayName(context: Context): String {
+ val resId = when (this) {
+ FollowSystem -> R.string.dark_mode_system
+ Light -> R.string.dark_mode_light
+ Dark -> R.string.dark_mode_dark
+ }
+ return context.getString(resId)
+ }
+
+ companion object {
+ fun fromId(id: Int): DarkMode {
+ return DarkMode.entries.find { it.id == id } ?: FollowSystem
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/Priority.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/Priority.kt
new file mode 100644
index 0000000..859c3f7
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/Priority.kt
@@ -0,0 +1,29 @@
+package cn.super12138.todo.logic.model
+
+import android.content.Context
+import cn.super12138.todo.R
+
+enum class Priority(val value: Float) {
+ Urgent(10f),
+ Important(5f),
+ Default(0f),
+ NotImportant(-5f),
+ NotUrgent(-10f);
+
+ fun getDisplayName(context: Context): String {
+ val resId = when (this) {
+ Urgent -> R.string.priority_urgent
+ Important -> R.string.priority_important
+ Default -> R.string.priority_default
+ NotImportant -> R.string.priority_not_important
+ NotUrgent -> R.string.priority_not_urgent
+ }
+ return context.getString(resId)
+ }
+
+ companion object {
+ fun fromFloat(float: Float): Priority {
+ return Priority.entries.find { it.value == float } ?: Default
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/SortingMethod.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/SortingMethod.kt
new file mode 100644
index 0000000..036a2b9
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/SortingMethod.kt
@@ -0,0 +1,31 @@
+package cn.super12138.todo.logic.model
+
+import android.content.Context
+import cn.super12138.todo.R
+
+enum class SortingMethod(val id: Int) {
+ Date(1),
+ Subject(2),
+ Priority(3),
+ Completion(4),
+ AlphabeticalAscending(5),
+ AlphabeticalDescending(6);
+
+ fun getDisplayName(context: Context): String {
+ val resId = when (this) {
+ Date -> R.string.sorting_date
+ Subject -> R.string.sorting_subject
+ Priority -> R.string.sorting_priority
+ Completion -> R.string.sorting_completion
+ AlphabeticalAscending -> R.string.sorting_alphabetical_ascending
+ AlphabeticalDescending -> R.string.sorting_alphabetical_descending
+ }
+ return context.getString(resId)
+ }
+
+ companion object {
+ fun fromId(id: Int): SortingMethod {
+ return SortingMethod.entries.find { it.id == id } ?: Date
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/Subjects.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/Subjects.kt
new file mode 100644
index 0000000..5fde68e
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/Subjects.kt
@@ -0,0 +1,40 @@
+package cn.super12138.todo.logic.model
+
+import android.content.Context
+import cn.super12138.todo.R
+
+enum class Subjects(val id: Int) {
+ Chinese(0),
+ Math(1),
+ English(2),
+ Biology(3),
+ Geography(4),
+ Physics(5),
+ Moral(6),
+ Chemistry(7),
+ History(8),
+ Others(99);
+
+ fun getDisplayName(context: Context): String {
+ val resId = when (this) {
+ Chinese -> R.string.subject_chinese
+ Math -> R.string.subject_math
+ English -> R.string.subject_english
+ Biology -> R.string.subject_biology
+ Geography -> R.string.subject_geography
+ Physics -> R.string.subject_physics
+ Moral -> R.string.subject_moral
+ Chemistry -> R.string.subject_chemistry
+ History -> R.string.subject_history
+ Others -> R.string.subject_others
+ }
+ return context.getString(resId) // 返回资源中的文本
+ }
+
+ companion object {
+ // 根据 ID 获取 Subjects
+ fun fromId(id: Int): Subjects {
+ return entries.find { it.id == id } ?: Others
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDo.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDo.kt
deleted file mode 100644
index 86a7caa..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDo.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package cn.super12138.todo.logic.model
-
-data class ToDo(val uuid: String, val state: Int, val content: String, val subject: String) {
- // var isAnimated = false
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt b/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt
new file mode 100644
index 0000000..7b962d4
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt
@@ -0,0 +1,14 @@
+package cn.super12138.todo.ui
+
+import androidx.compose.ui.unit.dp
+
+object TodoDefaults {
+ /**
+ * 屏幕左右两边预留边距(防止内容全部贴边显示过丑)
+ */
+ val screenPadding = 16.dp
+ /**
+ * 待办卡片默认高度
+ */
+ val toDoCardHeight = 80.dp
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt b/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt
new file mode 100644
index 0000000..e69a0b5
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt
@@ -0,0 +1,69 @@
+package cn.super12138.todo.ui.activities
+
+import android.os.Build
+import android.os.Bundle
+import android.view.HapticFeedbackConstants
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import cn.super12138.todo.R
+import cn.super12138.todo.ui.pages.crash.CrashPage
+import cn.super12138.todo.ui.theme.ToDoTheme
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+
+class CrashActivity : ComponentActivity() {
+ companion object {
+ const val BRAND_PREFIX = "Brand: "
+ const val MODEL_PREFIX = "Model: "
+ const val DEVICE_SDK_PREFIX = "Device SDK: "
+ const val CRASH_TIME_PREFIX = "Crash time: "
+ const val BEGINNING_CRASH = "======beginning of crash======"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+
+ val crashLogs = intent.getStringExtra("crash_logs")
+
+ val deviceBrand = Build.BRAND
+ val deviceModel = Build.MODEL
+ val sdkLevel = Build.VERSION.SDK_INT
+ val currentDateTime = Calendar.getInstance().time
+ val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
+ val formattedDateTime = formatter.format(currentDateTime)
+
+ val deviceInfo = StringBuilder().apply {
+ append(BRAND_PREFIX).append("").append(deviceBrand).append('\n')
+ append(MODEL_PREFIX).append(deviceModel).append('\n')
+ append(DEVICE_SDK_PREFIX).append(sdkLevel).append('\n').append('\n')
+ append(CRASH_TIME_PREFIX).append(formattedDateTime).append('\n').append('\n')
+ append(BEGINNING_CRASH).append('\n')
+ }
+
+ val crashLogFormatted = StringBuilder().apply {
+ append(deviceInfo)
+ append(crashLogs)
+ }.toString()
+
+ setContent {
+ ToDoTheme {
+ val view = LocalView.current
+ CrashPage(
+ crashLog = if (crashLogs == null) stringResource(R.string.tip_no_crash_logs) else crashLogFormatted,
+ exitApp = {
+ view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
+ finishAffinity()
+ },
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/activities/MainActivity.kt b/app/src/main/kotlin/cn/super12138/todo/ui/activities/MainActivity.kt
new file mode 100644
index 0000000..86bb871
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/activities/MainActivity.kt
@@ -0,0 +1,61 @@
+package cn.super12138.todo.ui.activities
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.view.WindowCompat
+import androidx.lifecycle.viewmodel.compose.viewModel
+import cn.super12138.todo.logic.model.DarkMode.Dark
+import cn.super12138.todo.logic.model.DarkMode.FollowSystem
+import cn.super12138.todo.logic.model.DarkMode.Light
+import cn.super12138.todo.ui.components.Konfetti
+import cn.super12138.todo.ui.navigation.TodoNavigation
+import cn.super12138.todo.ui.theme.ToDoTheme
+import cn.super12138.todo.ui.viewmodels.MainViewModel
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ installSplashScreen()
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+ setContent {
+ val mainViewModel: MainViewModel = viewModel()
+ val showConfetti = mainViewModel.showConfetti
+ // 深色模式
+ val darkTheme = when (mainViewModel.appDarkMode) {
+ FollowSystem -> isSystemInDarkTheme()
+ Light -> false
+ Dark -> true
+ }
+ // 配置状态栏和底部导航栏的颜色(在用户切换深色模式时)
+ // https://github.com/dn0ne/lotus/blob/master/app/src/main/java/com/dn0ne/player/MainActivity.kt#L266
+ LaunchedEffect(mainViewModel.appDarkMode) {
+ WindowCompat.getInsetsController(window, window.decorView).apply {
+ isAppearanceLightStatusBars = !darkTheme
+ isAppearanceLightNavigationBars = !darkTheme
+ }
+ }
+
+ ToDoTheme(
+ darkTheme = darkTheme,
+ contrastLevel = mainViewModel.appContrastLevel.value.toDouble()
+ ) {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ TodoNavigation(
+ viewModel = mainViewModel,
+ modifier = Modifier.fillMaxSize()
+ )
+ Konfetti(state = showConfetti)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/ChipGroup.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/ChipGroup.kt
new file mode 100644
index 0000000..4776d3e
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/ChipGroup.kt
@@ -0,0 +1,67 @@
+package cn.super12138.todo.ui.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Check
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.FilterChipDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.R
+import cn.super12138.todo.utils.VibrationUtils
+
+/**
+ * 部分参考:https://github.com/Rhythamtech/FilterChipGroup-Compose-Android/blob/main/FilterChipGroup.kt
+ */
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun FilterChipGroup(
+ items: List,
+ defaultSelectedItemIndex: Int = 0,
+ onSelectedChanged: (Int) -> Unit = {},
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ var selectedItemIndex by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) }
+
+ FlowRow(modifier = modifier) {
+ items.forEachIndexed { index, item ->
+ FilterChip(
+ selected = items[selectedItemIndex] == items[index],
+ onClick = {
+ selectedItemIndex = index
+ VibrationUtils.performHapticFeedback(view)
+ onSelectedChanged(index)
+ },
+ leadingIcon = {
+ AnimatedVisibility(
+ visible = items[selectedItemIndex] == items[index]
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Check,
+ contentDescription = stringResource(R.string.tip_select_this),
+ modifier = Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
+ },
+ label = {
+ Text(item)
+ },
+ modifier = Modifier.padding(end = 10.dp)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt
new file mode 100644
index 0000000..25e9966
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt
@@ -0,0 +1,120 @@
+package cn.super12138.todo.ui.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ErrorOutline
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import cn.super12138.todo.R
+import cn.super12138.todo.utils.VibrationUtils
+
+@Composable
+fun WarningDialog(
+ visible: Boolean,
+ icon: ImageVector = Icons.Outlined.ErrorOutline,
+ description: String,
+ onConfirm: () -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ BasicDialog(
+ visible = visible,
+ icon = icon,
+ title = stringResource(R.string.title_warning),
+ text = { Text(description) },
+ confirmButton = stringResource(R.string.action_confirm),
+ dismissButton = stringResource(R.string.action_cancel),
+ onConfirm = {
+ onConfirm()
+ onDismiss()
+ },
+ onDismiss = onDismiss,
+ modifier = modifier
+ )
+}
+
+@Composable
+fun BasicDialog(
+ visible: Boolean,
+ icon: ImageVector,
+ title: String,
+ text: @Composable (() -> Unit)? = null,
+ confirmButton: String,
+ dismissButton: String,
+ onConfirm: () -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ BasicDialog(
+ visible = visible,
+ icon = {
+ Icon(
+ imageVector = icon,
+ contentDescription = null // 会跟下面的文本重复,所以设置为 null
+ )
+ },
+ title = { Text(title) },
+ text = text,
+ confirmButton = {
+ FilledTonalButton(
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ onConfirm()
+ }
+ ) {
+ Text(confirmButton)
+ }
+ },
+ dismissButton = {
+ TextButton(
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ onDismiss()
+ }
+ ) {
+ Text(dismissButton)
+ }
+ },
+ onDismissRequest = onDismiss,
+ modifier = modifier
+ )
+}
+
+@Composable
+fun BasicDialog(
+ visible: Boolean,
+ icon: @Composable (() -> Unit)? = null,
+ title: @Composable () -> Unit,
+ text: @Composable (() -> Unit)? = null,
+ confirmButton: (@Composable () -> Unit),
+ dismissButton: (@Composable () -> Unit)? = null,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ if (visible) {
+ AlertDialog(
+ icon = icon,
+ title = title,
+ text = {
+ Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
+ text?.let { it() }
+ }
+ },
+ confirmButton = confirmButton,
+ dismissButton = dismissButton,
+ onDismissRequest = onDismissRequest,
+ modifier = modifier
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/FloatingActionButton.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/FloatingActionButton.kt
new file mode 100644
index 0000000..895b615
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/FloatingActionButton.kt
@@ -0,0 +1,83 @@
+package cn.super12138.todo.ui.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.FloatingActionButtonElevation
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.utils.VibrationUtils
+
+/**
+ * 带动画且比 Material 3 内置组件动画好看的的可扩展 FAB
+ * * (内置组件的动画总是卡一下。。。)
+ *
+ * 将缩放部分转为最简单的`AnimatedVisibility`实现
+ * @param icon FAB 的前置图标
+ * @param text FAB 的文本
+ * @param textOverflow FAB 文本溢出显示方案
+ * @param expanded 是否为展开状态
+ * @param containerColor FAB 容器的颜色
+ * @param contentColor FAB 文本和图标颜色,通常不需要自己指定,会自动依据容器颜色设置
+ * @param elevation FAB 的高度(阴影大小)
+ * @param onClick 点击 FAB 后的回调
+ * @param modifier `Modifier` 修改器
+ */
+@Composable
+fun AnimatedExtendedFloatingActionButton(
+ icon: ImageVector,
+ text: String,
+ textOverflow: TextOverflow = TextOverflow.Clip,
+ expanded: Boolean,
+ containerColor: Color = FloatingActionButtonDefaults.containerColor,
+ contentColor: Color = contentColorFor(containerColor),
+ elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ FloatingActionButton(
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ onClick()
+ },
+ elevation = elevation,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ modifier = modifier
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null
+ )
+ // Spacer(Modifier.width(if (expanded) 8.dp else 0.dp))
+ AnimatedVisibility(expanded) {
+ Row {
+ Spacer(Modifier.width(8.dp))
+ Text(
+ text = text,
+ maxLines = 1,
+ overflow = textOverflow
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/Konfetti.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/Konfetti.kt
new file mode 100644
index 0000000..107a518
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/Konfetti.kt
@@ -0,0 +1,106 @@
+package cn.super12138.todo.ui.components
+
+import androidx.annotation.FloatRange
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.core.graphics.ColorUtils
+import nl.dionsegijn.konfetti.compose.KonfettiView
+import nl.dionsegijn.konfetti.compose.OnParticleSystemUpdateListener
+import nl.dionsegijn.konfetti.core.Angle
+import nl.dionsegijn.konfetti.core.Party
+import nl.dionsegijn.konfetti.core.PartySystem
+import nl.dionsegijn.konfetti.core.Position
+import nl.dionsegijn.konfetti.core.Spread
+import nl.dionsegijn.konfetti.core.emitter.Emitter
+import java.util.concurrent.TimeUnit
+
+/**
+ * 参照于:https://github.com/hushenghao/AndroidEasterEggs/blob/main/app/src/main/java/com/dede/android_eggs/views/main/compose/Konfetti.kt
+ */
+
+@Composable
+fun Konfetti(
+ state: MutableState,
+ primary: Color = MaterialTheme.colorScheme.primary,
+ modifier: Modifier = Modifier
+) {
+ var visible by state
+ if (!visible) {
+ return
+ }
+ val listener = remember(state) {
+ object : OnParticleSystemUpdateListener {
+ override fun onParticleSystemEnded(system: PartySystem, activeSystems: Int) {
+ if (activeSystems == 0)
+ visible = false
+ }
+ }
+ }
+ KonfettiView(
+ modifier = Modifier
+ .fillMaxSize()
+ .then(modifier),
+ parties = remember { particles(primary.toArgb()) },
+ updateListener = listener
+ )
+}
+
+private val defaultColors = listOf(
+ 0xFCE18A,
+ 0x009688,
+ 0xFF726D,
+ 0xF4306D,
+ 0xB48DEF,
+ 0x95FF82,
+ 0x82ECFF,
+ 0xFF9800,
+ 0x0E008A,
+)
+
+private const val colorBlendFraction = 0.3f
+
+private fun particles(primary: Int) = listOf(
+ Party(
+ speed = 0f,
+ maxSpeed = 12f,
+ damping = 0.9f,
+ angle = Angle.BOTTOM,
+ spread = Spread.ROUND,
+ colors = defaultColors.map { it.blend(primary, colorBlendFraction) },
+ emitter = Emitter(duration = 2, TimeUnit.SECONDS).perSecond(100),
+ position = Position.Relative(0.0, 0.0).between(Position.Relative(1.0, 0.0)),
+ ),
+ Party(
+ speed = 10f,
+ maxSpeed = 30f,
+ damping = 0.9f,
+ angle = Angle.RIGHT - 55,
+ spread = 60,
+ colors = defaultColors.map { it.blend(primary, colorBlendFraction) },
+ emitter = Emitter(duration = 2, TimeUnit.SECONDS).perSecond(100),
+ position = Position.Relative(0.0, 1.0)
+ ),
+ Party(
+ speed = 10f,
+ maxSpeed = 30f,
+ damping = 0.9f,
+ angle = Angle.RIGHT - 125,
+ spread = 60,
+ colors = defaultColors.map { it.blend(primary, colorBlendFraction) },
+ emitter = Emitter(duration = 2, TimeUnit.SECONDS).perSecond(100),
+ position = Position.Relative(1.0, 1.0)
+ )
+)
+
+fun Int.blend(
+ color: Int,
+ @FloatRange(from = 0.0, to = 1.0) fraction: Float = 0.5f,
+): Int = ColorUtils.blendARGB(this, color, fraction)
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/Scaffolds.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/Scaffolds.kt
new file mode 100644
index 0000000..12ece54
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/Scaffolds.kt
@@ -0,0 +1,115 @@
+package cn.super12138.todo.ui.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.exclude
+import androidx.compose.foundation.layout.ime
+import androidx.compose.foundation.layout.safeContent
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FabPosition
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.ScaffoldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import cn.super12138.todo.R
+import cn.super12138.todo.utils.VibrationUtils
+
+/**
+ * 带有顶部大标题栏的通用脚手架
+ * * 内容默认由 Box 容器包裹;实际使用时推荐配合 Column 或 Row
+ *
+ * @param title 标题文本
+ * @param scrollBehavior 滚动行为,用于支持页面内容滚动时标题栏的压缩效果
+ * @param onBack 当返回按钮被按下时的操作
+ * @param contentWindowInsets 内容边距,通常用于将内容和系统状态栏等隔开;可以使用 `WindowInsets.safeContent`
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun LargeTopAppBarScaffold(
+ title: String,
+ scrollBehavior: TopAppBarScrollBehavior,
+ onBack: () -> Unit,
+ snackbarHost: @Composable () -> Unit = {},
+ floatingActionButton: @Composable () -> Unit = {},
+ floatingActionButtonPosition: FabPosition = FabPosition.End,
+ contentWindowInsets: WindowInsets = WindowInsets.safeContent.exclude(WindowInsets.ime),
+ modifier: Modifier = Modifier,
+ content: @Composable (PaddingValues) -> Unit
+) {
+ val view = LocalView.current
+ LargeTopAppBarScaffold(
+ title = {
+ Text(
+ text = title,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ navigationIcon = {
+ IconButton(
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ onBack()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
+ contentDescription = stringResource(R.string.action_back)
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior,
+ floatingActionButton = floatingActionButton,
+ floatingActionButtonPosition = floatingActionButtonPosition,
+ contentWindowInsets = contentWindowInsets
+ ) { innerPadding ->
+ Box { content(innerPadding) }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun LargeTopAppBarScaffold(
+ title: @Composable () -> Unit = {},
+ navigationIcon: @Composable () -> Unit = {},
+ actions: @Composable RowScope.() -> Unit = {},
+ scrollBehavior: TopAppBarScrollBehavior,
+ snackbarHost: @Composable () -> Unit = {},
+ floatingActionButton: @Composable () -> Unit = {},
+ floatingActionButtonPosition: FabPosition = FabPosition.End,
+ contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
+ modifier: Modifier = Modifier,
+ content: @Composable (PaddingValues) -> Unit
+) {
+ Scaffold(
+ modifier = modifier
+ .nestedScroll(scrollBehavior.nestedScrollConnection),
+ topBar = {
+ LargeTopAppBar(
+ title = title,
+ navigationIcon = navigationIcon,
+ actions = actions,
+ scrollBehavior = scrollBehavior
+ )
+ },
+ snackbarHost = snackbarHost,
+ floatingActionButton = floatingActionButton,
+ floatingActionButtonPosition = floatingActionButtonPosition,
+ contentWindowInsets = contentWindowInsets
+ ) { innerPadding ->
+ Box { content(innerPadding) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/icons/GithubIcons.kt b/app/src/main/kotlin/cn/super12138/todo/ui/icons/GithubIcons.kt
new file mode 100644
index 0000000..e1066de
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/icons/GithubIcons.kt
@@ -0,0 +1,56 @@
+package cn.super12138.todo.ui.icons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+val GithubIcon: ImageVector
+ get() {
+ if (_GithubIcon != null) {
+ return _GithubIcon!!
+ }
+ _GithubIcon = ImageVector.Builder(
+ name = "GithubIcon",
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 24f,
+ viewportHeight = 24f
+ ).apply {
+ path(fill = SolidColor(Color(0xFF000000))) {
+ moveTo(12.5f, 0.75f)
+ curveTo(6.146f, 0.75f, 1f, 5.896f, 1f, 12.25f)
+ curveToRelative(0f, 5.089f, 3.292f, 9.387f, 7.863f, 10.91f)
+ curveToRelative(0.575f, 0.101f, 0.79f, -0.244f, 0.79f, -0.546f)
+ curveToRelative(0f, -0.273f, -0.014f, -1.178f, -0.014f, -2.142f)
+ curveToRelative(-2.889f, 0.532f, -3.636f, -0.704f, -3.866f, -1.35f)
+ curveToRelative(-0.13f, -0.331f, -0.69f, -1.352f, -1.18f, -1.625f)
+ curveToRelative(-0.402f, -0.216f, -0.977f, -0.748f, -0.014f, -0.762f)
+ curveToRelative(0.906f, -0.014f, 1.553f, 0.834f, 1.769f, 1.179f)
+ curveToRelative(1.035f, 1.74f, 2.688f, 1.25f, 3.349f, 0.948f)
+ curveToRelative(0.1f, -0.747f, 0.402f, -1.25f, 0.733f, -1.538f)
+ curveToRelative(-2.559f, -0.287f, -5.232f, -1.279f, -5.232f, -5.678f)
+ curveToRelative(0f, -1.25f, 0.445f, -2.285f, 1.178f, -3.09f)
+ curveToRelative(-0.115f, -0.288f, -0.517f, -1.467f, 0.115f, -3.048f)
+ curveToRelative(0f, 0f, 0.963f, -0.302f, 3.163f, 1.179f)
+ curveToRelative(0.92f, -0.259f, 1.897f, -0.388f, 2.875f, -0.388f)
+ curveToRelative(0.977f, 0f, 1.955f, 0.13f, 2.875f, 0.388f)
+ curveToRelative(2.2f, -1.495f, 3.162f, -1.179f, 3.162f, -1.179f)
+ curveToRelative(0.633f, 1.581f, 0.23f, 2.76f, 0.115f, 3.048f)
+ curveToRelative(0.733f, 0.805f, 1.179f, 1.825f, 1.179f, 3.09f)
+ curveToRelative(0f, 4.413f, -2.688f, 5.39f, -5.247f, 5.678f)
+ curveToRelative(0.417f, 0.36f, 0.776f, 1.05f, 0.776f, 2.128f)
+ curveToRelative(0f, 1.538f, -0.014f, 2.774f, -0.014f, 3.162f)
+ curveToRelative(0f, 0.302f, 0.216f, 0.662f, 0.79f, 0.547f)
+ curveTo(20.709f, 21.637f, 24f, 17.324f, 24f, 12.25f)
+ curveTo(24f, 5.896f, 18.854f, 0.75f, 12.5f, 0.75f)
+ close()
+ }
+ }.build()
+
+ return _GithubIcon!!
+ }
+
+@Suppress("ObjectPropertyName")
+private var _GithubIcon: ImageVector? = null
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt
new file mode 100644
index 0000000..a4fda68
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt
@@ -0,0 +1,129 @@
+package cn.super12138.todo.ui.navigation
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import cn.super12138.todo.ui.pages.editor.TodoEditorPage
+import cn.super12138.todo.ui.pages.main.MainPage
+import cn.super12138.todo.ui.pages.settings.SettingsAbout
+import cn.super12138.todo.ui.pages.settings.SettingsAboutLicence
+import cn.super12138.todo.ui.pages.settings.SettingsAppearance
+import cn.super12138.todo.ui.pages.settings.SettingsInterface
+import cn.super12138.todo.ui.pages.settings.SettingsMain
+import cn.super12138.todo.ui.theme.materialSharedAxisXIn
+import cn.super12138.todo.ui.theme.materialSharedAxisXOut
+import cn.super12138.todo.ui.viewmodels.MainViewModel
+
+private const val INITIAL_OFFSET_FACTOR = 0.10f
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Composable
+fun TodoNavigation(
+ navController: NavHostController = rememberNavController(),
+ startDestination: String = TodoScreen.Main.name,
+ viewModel: MainViewModel,
+ modifier: Modifier = Modifier
+) {
+ SharedTransitionLayout {
+ NavHost(
+ navController = navController,
+ startDestination = startDestination,
+ enterTransition = {
+ materialSharedAxisXIn(
+ initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }
+ )
+ },
+ exitTransition = {
+ materialSharedAxisXOut(
+ targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }
+ )
+ },
+ popEnterTransition = {
+ materialSharedAxisXIn(
+ initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }
+ )
+ },
+ popExitTransition = {
+ materialSharedAxisXOut(
+ targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }
+ )
+ },
+ modifier = modifier
+ ) {
+ composable(TodoScreen.Main.name) {
+ MainPage(
+ viewModel = viewModel,
+ toTodoEditPage = { navController.navigate(TodoScreen.TodoEditor.name) },
+ toSettingsPage = { navController.navigate(TodoScreen.SettingsMain.name) },
+ sharedTransitionScope = this@SharedTransitionLayout,
+ animatedVisibilityScope = this@composable
+ )
+ }
+
+ composable(TodoScreen.TodoEditor.name) {
+ TodoEditorPage(
+ toDo = viewModel.selectedEditTodo,
+ onSave = {
+ viewModel.addTodo(it)
+ // viewModel.setEditTodoItem(null)
+ navController.navigateUp()
+ },
+ onDelete = {
+ if (viewModel.selectedEditTodo !== null) {
+ viewModel.deleteTodo(viewModel.selectedEditTodo!!)
+ viewModel.setEditTodoItem(null)
+ }
+ navController.navigateUp()
+ },
+ onNavigateUp = {
+ navController.navigateUp()
+ // viewModel.setEditTodoItem(null)
+ },
+ sharedTransitionScope = this@SharedTransitionLayout,
+ animatedVisibilityScope = this@composable
+ )
+ }
+
+ composable(TodoScreen.SettingsMain.name) {
+ SettingsMain(
+ toAppearancePage = { navController.navigate(TodoScreen.SettingsAppearance.name) },
+ toAboutPage = { navController.navigate(TodoScreen.SettingsAbout.name) },
+ toInterfacePage = { navController.navigate(TodoScreen.SettingsInterface.name) },
+ onNavigateUp = { navController.navigateUp() },
+ )
+ }
+
+ composable(TodoScreen.SettingsAppearance.name) {
+ SettingsAppearance(
+ viewModel = viewModel,
+ onNavigateUp = { navController.navigateUp() }
+ )
+ }
+
+ composable(TodoScreen.SettingsInterface.name) {
+ SettingsInterface(
+ viewModel = viewModel,
+ onNavigateUp = { navController.navigateUp() }
+ )
+ }
+
+ composable(TodoScreen.SettingsAbout.name) {
+ SettingsAbout(
+ onNavigateUp = { navController.navigateUp() },
+ toLicencePage = { navController.navigate(TodoScreen.SettingsAboutLicence.name) }
+ )
+ }
+
+ composable(TodoScreen.SettingsAboutLicence.name) {
+ SettingsAboutLicence(
+ onNavigateUp = { navController.navigateUp() }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoScreen.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoScreen.kt
new file mode 100644
index 0000000..68a5837
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoScreen.kt
@@ -0,0 +1,11 @@
+package cn.super12138.todo.ui.navigation
+
+enum class TodoScreen {
+ Main,
+ TodoEditor,
+ SettingsMain,
+ SettingsAppearance,
+ SettingsInterface,
+ SettingsAbout,
+ SettingsAboutLicence
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashHandler.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashHandler.kt
similarity index 90%
rename from app/src/main/kotlin/cn/super12138/todo/views/activities/CrashHandler.kt
rename to app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashHandler.kt
index 07325db..7ecaecf 100644
--- a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashHandler.kt
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashHandler.kt
@@ -1,12 +1,12 @@
-package cn.super12138.todo.views.activities
+package cn.super12138.todo.ui.pages.crash
import android.content.Context
import android.content.Intent
import android.os.Process
+import cn.super12138.todo.ui.activities.CrashActivity
import kotlin.system.exitProcess
class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
-
private val defaultUEH = Thread.getDefaultUncaughtExceptionHandler()
override fun uncaughtException(thread: Thread, ex: Throwable) {
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashPage.kt
new file mode 100644
index 0000000..be089fa
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashPage.kt
@@ -0,0 +1,94 @@
+package cn.super12138.todo.ui.pages.crash
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeContent
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.ExitToApp
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.R
+import cn.super12138.todo.ui.TodoDefaults
+import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CrashPage(
+ crashLog: String,
+ exitApp: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ val scrollState = rememberScrollState()
+ val isExpanded by remember {
+ derivedStateOf {
+ scrollState.value == 0
+ }
+ }
+
+ Scaffold(
+ modifier = modifier
+ .fillMaxSize()
+ .nestedScroll(scrollBehavior.nestedScrollConnection),
+ topBar = {
+ LargeTopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.page_crash),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ scrollBehavior = scrollBehavior
+ )
+ },
+ floatingActionButton = {
+ AnimatedExtendedFloatingActionButton(
+ onClick = exitApp,
+ icon = Icons.AutoMirrored.Outlined.ExitToApp,
+ text = stringResource(R.string.action_exit_app),
+ expanded = isExpanded
+ )
+ },
+ contentWindowInsets = WindowInsets.safeContent
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .padding(horizontal = TodoDefaults.screenPadding)
+ .fillMaxSize()
+ .verticalScroll(scrollState)
+ ) {
+ Spacer(Modifier.height(5.dp))
+ SelectionContainer {
+ Text(
+ text = crashLog,
+ fontFamily = FontFamily.Monospace,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt
new file mode 100644
index 0000000..f9ea722
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt
@@ -0,0 +1,264 @@
+package cn.super12138.todo.ui.pages.editor
+
+import android.view.HapticFeedbackConstants
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.Undo
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.Save
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Label
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.PlainTooltip
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.R
+import cn.super12138.todo.constants.Constants
+import cn.super12138.todo.logic.database.TodoEntity
+import cn.super12138.todo.logic.model.Priority
+import cn.super12138.todo.logic.model.Subjects
+import cn.super12138.todo.ui.TodoDefaults
+import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
+import cn.super12138.todo.ui.components.FilterChipGroup
+import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
+import cn.super12138.todo.ui.components.WarningDialog
+import cn.super12138.todo.utils.VibrationUtils
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
+@Composable
+fun TodoEditorPage(
+ toDo: TodoEntity? = null,
+ onSave: (TodoEntity) -> Unit,
+ onDelete: () -> Unit,
+ onNavigateUp: () -> Unit,
+ sharedTransitionScope: SharedTransitionScope,
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ modifier: Modifier = Modifier
+) {
+ var showExitConfirmDialog by rememberSaveable { mutableStateOf(false) }
+ var showDeleteConfirmDialog by rememberSaveable { mutableStateOf(false) }
+
+ val view = LocalView.current
+ val context = LocalContext.current
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+
+ var toDoContent by rememberSaveable { mutableStateOf(toDo?.content ?: "") }
+ var isError by rememberSaveable { mutableStateOf(false) }
+ var selectedSubjectIndex by rememberSaveable { mutableIntStateOf(toDo?.subject ?: 0) }
+ var priorityState by rememberSaveable { mutableFloatStateOf(toDo?.priority ?: 0f) }
+
+ fun checkModifiedBeforeBack() {
+ var isModified = false
+ if ((toDo?.content ?: "") != toDoContent) isModified = true
+ if ((toDo?.subject ?: 0) != selectedSubjectIndex) isModified = true
+ if ((toDo?.priority ?: 0f) != priorityState) isModified = true
+ if (isModified) {
+ showExitConfirmDialog = true
+ } else {
+ onNavigateUp()
+ }
+ }
+
+ BackHandler {
+ checkModifiedBeforeBack()
+ }
+
+ LargeTopAppBarScaffold(
+ title = stringResource(if (toDo != null) R.string.title_edit_task else R.string.action_add_task),
+ scrollBehavior = scrollBehavior,
+ floatingActionButton = {
+ with(sharedTransitionScope) {
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ if (toDo !== null) {
+ AnimatedExtendedFloatingActionButton(
+ icon = Icons.Outlined.Delete,
+ text = stringResource(R.string.action_delete),
+ expanded = true,
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ onClick = { showDeleteConfirmDialog = true }
+ )
+ }
+ AnimatedExtendedFloatingActionButton(
+ icon = Icons.Outlined.Save,
+ text = stringResource(R.string.action_save),
+ expanded = true,
+ onClick = {
+ if (toDoContent.trim().isEmpty()) {
+ isError = true
+ return@AnimatedExtendedFloatingActionButton
+ }
+
+ isError = false
+ onSave(
+ TodoEntity(
+ content = toDoContent,
+ subject = selectedSubjectIndex,
+ isCompleted = toDo?.isCompleted ?: false,
+ priority = priorityState,
+ id = toDo?.id ?: 0
+ )
+ )
+ },
+ modifier = Modifier.sharedElement(
+ state = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
+ animatedVisibilityScope = animatedVisibilityScope
+ )
+ )
+ }
+ }
+ },
+ onBack = {
+ checkModifiedBeforeBack()
+ },
+ modifier = modifier
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .padding(horizontal = TodoDefaults.screenPadding)
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ ) {
+ with(sharedTransitionScope) {
+ TextField(
+ value = toDoContent,
+ onValueChange = { toDoContent = it },
+ label = { Text(stringResource(R.string.placeholder_add_todo)) },
+ isError = isError,
+ supportingText = {
+ AnimatedVisibility(isError) {
+ Text(stringResource(R.string.error_no_task_content))
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .sharedBounds(
+ sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_${toDo?.id}"),
+ animatedVisibilityScope = animatedVisibilityScope
+ )
+ )
+ }
+
+ Spacer(Modifier.size(5.dp))
+
+ val subjects = remember {
+ Subjects.entries.map {
+ it.getDisplayName(context)
+ }
+ }
+ Text(
+ text = stringResource(R.string.label_subject),
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ Spacer(Modifier.size(5.dp))
+
+ FilterChipGroup(
+ items = subjects,
+ defaultSelectedItemIndex = toDo?.subject ?: 0,
+ onSelectedChanged = {
+ selectedSubjectIndex = it
+ },
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(Modifier.size(10.dp))
+
+ Text(
+ text = stringResource(R.string.label_priority),
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ Spacer(Modifier.size(5.dp))
+
+ val interactionSource = remember { MutableInteractionSource() }
+
+ Slider(
+ modifier = Modifier.semantics {
+ contentDescription = context.getString(R.string.label_priority)
+ },
+ value = priorityState,
+ onValueChange = {
+ VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS)
+ priorityState = it
+ },
+ valueRange = -10f..10f,
+ steps = 3,
+ interactionSource = interactionSource,
+ thumb = {
+ Label(
+ label = {
+ PlainTooltip(
+ modifier = Modifier
+ .sizeIn(45.dp, 25.dp)
+ .wrapContentWidth()
+ ) {
+ Text(Priority.fromFloat(priorityState).getDisplayName(context))
+ }
+ },
+ interactionSource = interactionSource
+ ) {
+ SliderDefaults.Thumb(interactionSource)
+ }
+ }
+ )
+
+ Spacer(Modifier.size(40.dp))
+ }
+ }
+
+ WarningDialog(
+ visible = showExitConfirmDialog,
+ icon = Icons.AutoMirrored.Outlined.Undo,
+ description = stringResource(R.string.tip_discard_changes),
+ onConfirm = {
+ showExitConfirmDialog = false
+ onNavigateUp()
+ },
+ onDismiss = { showExitConfirmDialog = false }
+ )
+
+ WarningDialog(
+ visible = showDeleteConfirmDialog,
+ icon = Icons.Outlined.Delete,
+ description = stringResource(R.string.tip_delete_task, 1),
+ onConfirm = onDelete,
+ onDismiss = { showDeleteConfirmDialog = false }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt
new file mode 100644
index 0000000..7ec9e91
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt
@@ -0,0 +1,219 @@
+package cn.super12138.todo.ui.pages.main
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.animation.expandIn
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkOut
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.exclude
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.ime
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeContent
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.window.core.layout.WindowWidthSizeClass
+import cn.super12138.todo.R
+import cn.super12138.todo.constants.Constants
+import cn.super12138.todo.logic.database.TodoEntity
+import cn.super12138.todo.ui.components.WarningDialog
+import cn.super12138.todo.ui.pages.main.components.TodoFAB
+import cn.super12138.todo.ui.pages.main.components.TodoTopAppBar
+import cn.super12138.todo.ui.viewmodels.MainViewModel
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Composable
+fun MainPage(
+ viewModel: MainViewModel,
+ toTodoEditPage: () -> Unit,
+ toSettingsPage: () -> Unit,
+ sharedTransitionScope: SharedTransitionScope,
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ modifier: Modifier = Modifier
+) {
+ val toDoList = viewModel.sortedTodos.collectAsState(initial = emptyList())
+ val listState = rememberLazyListState()
+ /*val isExpanded by remember {
+ derivedStateOf {
+ listState.firstVisibleItemIndex == 0
+ }
+ }*/
+
+ val selectedTodoIds = viewModel.selectedTodoIds.collectAsState()
+ var showDeleteConfirmDialog by rememberSaveable { mutableStateOf(false) }
+
+ val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
+
+ val isSelectedIdsEmpty by remember {
+ derivedStateOf {
+ selectedTodoIds.value.isEmpty()
+ }
+ }
+
+ val showCompleted = viewModel.showCompletedTodos
+ val filteredTodoList =
+ if (showCompleted) toDoList.value else toDoList.value.filter { item -> !item.isCompleted }
+
+ BackHandler(enabled = !isSelectedIdsEmpty) {
+ // 当按下返回键(或进行返回操作)时清空选择
+ viewModel.clearAllTodoSelection()
+ }
+
+ Scaffold(
+ topBar = {
+ TodoTopAppBar(
+ selectedTodoIds = selectedTodoIds.value,
+ selectedMode = !isSelectedIdsEmpty,
+ onCancelSelect = { viewModel.clearAllTodoSelection() },
+ onSelectAll = { viewModel.toggleAllSelected() },
+ onDeleteSelectedTodo = { showDeleteConfirmDialog = true },
+ toSettingsPage = toSettingsPage
+ )
+ },
+ floatingActionButton = {
+ with(sharedTransitionScope) {
+ AnimatedVisibility(
+ visible = isSelectedIdsEmpty,
+ enter = fadeIn() + expandIn(),
+ exit = shrinkOut() + fadeOut()
+ ) {
+ // TODO: 修复在滑动列表时FAB位移导致的动画不连贯(临时方案为底部加padding)
+ TodoFAB(
+ expanded = true,
+ onClick = {
+ viewModel.setEditTodoItem(null) // 每次添加待办前清除上一次已选待办
+ toTodoEditPage()
+ },
+ modifier = Modifier.sharedElement(
+ state = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
+ animatedVisibilityScope = animatedVisibilityScope
+ )
+ )
+ }
+ }
+ },
+ contentWindowInsets = WindowInsets.safeContent.exclude(WindowInsets.ime),
+ modifier = modifier
+ ) { innerPadding ->
+ if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT) {
+ Column(modifier = Modifier.padding(innerPadding)) {
+ ProgressFragment(
+ totalTasks = toDoList.value.size,
+ completedTasks = toDoList.value.count { it.isCompleted },
+ modifier = Modifier
+ .weight(2f)
+ .fillMaxSize()
+ )
+
+ ManagerFragment(
+ state = listState,
+ list = filteredTodoList,
+ onItemClick = { item ->
+ if (isSelectedIdsEmpty) {
+ viewModel.setEditTodoItem(item)
+ toTodoEditPage()
+ } else {
+ viewModel.toggleTodoSelection(item)
+ }
+ },
+ onItemLongClick = { item ->
+ viewModel.toggleTodoSelection(item)
+ },
+ onItemChecked = { item ->
+ item.apply {
+ viewModel.updateTodo(
+ TodoEntity(
+ content = content,
+ subject = subject,
+ isCompleted = true,
+ priority = priority,
+ id = id
+ )
+ )
+ viewModel.playConfetti()
+ }
+ },
+ selectedTodoIds = selectedTodoIds.value,
+ sharedTransitionScope = sharedTransitionScope,
+ animatedVisibilityScope = animatedVisibilityScope,
+ modifier = Modifier
+ .weight(3f)
+ .fillMaxSize()
+ )
+ }
+ } else {
+ Row(modifier = Modifier.padding(innerPadding)) {
+ ProgressFragment(
+ totalTasks = toDoList.value.size,
+ completedTasks = toDoList.value.count { it.isCompleted },
+ modifier = Modifier
+ .weight(2f)
+ .fillMaxSize()
+ )
+ ManagerFragment(
+ state = listState,
+ list = filteredTodoList,
+ onItemClick = { item ->
+ if (isSelectedIdsEmpty) {
+ viewModel.setEditTodoItem(item)
+ toTodoEditPage()
+ } else {
+ viewModel.toggleTodoSelection(item)
+ }
+ },
+ onItemLongClick = { item ->
+ viewModel.toggleTodoSelection(item)
+ },
+ onItemChecked = { item ->
+ item.apply {
+ viewModel.updateTodo(
+ TodoEntity(
+ content = content,
+ subject = subject,
+ isCompleted = true,
+ priority = priority,
+ id = id
+ )
+ )
+ viewModel.playConfetti()
+ }
+ },
+ selectedTodoIds = selectedTodoIds.value,
+ sharedTransitionScope = sharedTransitionScope,
+ animatedVisibilityScope = animatedVisibilityScope,
+ modifier = Modifier
+ .weight(3f)
+ .fillMaxSize()
+ )
+ }
+ }
+ }
+
+ WarningDialog(
+ visible = showDeleteConfirmDialog,
+ icon = Icons.Outlined.Delete,
+ description = stringResource(R.string.tip_delete_task, selectedTodoIds.value.size),
+ onConfirm = { viewModel.deleteSelectedTodo() },
+ onDismiss = { showDeleteConfirmDialog = false }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt
new file mode 100644
index 0000000..038afe2
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt
@@ -0,0 +1,89 @@
+package cn.super12138.todo.ui.pages.main
+
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.R
+import cn.super12138.todo.logic.database.TodoEntity
+import cn.super12138.todo.logic.model.Subjects
+import cn.super12138.todo.ui.TodoDefaults
+import cn.super12138.todo.ui.pages.main.components.TodoCard
+import my.nanihadesuka.compose.LazyColumnScrollbar
+import my.nanihadesuka.compose.ScrollbarSettings
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Composable
+fun ManagerFragment(
+ state: LazyListState,
+ list: List,
+ onItemClick: (TodoEntity) -> Unit = {},
+ onItemLongClick: (TodoEntity) -> Unit = {},
+ onItemChecked: (TodoEntity) -> Unit = {},
+ selectedTodoIds: List,
+ sharedTransitionScope: SharedTransitionScope,
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+ LazyColumnScrollbar(
+ state = state,
+ settings = ScrollbarSettings(
+ thumbUnselectedColor = MaterialTheme.colorScheme.secondary,
+ thumbSelectedColor = MaterialTheme.colorScheme.primary
+ ),
+ modifier = modifier
+ ) {
+ LazyColumn(
+ state = state,
+ contentPadding = PaddingValues(start = TodoDefaults.screenPadding, bottom = TodoDefaults.toDoCardHeight / 2, end = TodoDefaults.screenPadding)
+ ) {
+ if (list.isEmpty()) {
+ item {
+ Text(
+ text = stringResource(R.string.tip_no_task),
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ }
+ } else {
+ items(
+ items = list,
+ key = { it.id }
+ ) { item ->
+ TodoCard(
+ id = item.id,
+ content = item.content,
+ subject = Subjects.fromId(item.subject).getDisplayName(context),
+ completed = item.isCompleted,
+ priority = item.priority,
+ selected = selectedTodoIds.contains(item.id),
+ onCardClick = { onItemClick(item) },
+ onCardLongClick = { onItemLongClick(item) },
+ onChecked = { onItemChecked(item) },
+ sharedTransitionScope = sharedTransitionScope,
+ animatedVisibilityScope = animatedVisibilityScope,
+ modifier = Modifier
+ .padding(vertical = 5.dp)
+ .animateItem() // TODO: 设置动画时间
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ProgressFragment.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ProgressFragment.kt
new file mode 100644
index 0000000..08b6e6c
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ProgressFragment.kt
@@ -0,0 +1,79 @@
+package cn.super12138.todo.ui.pages.main
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ProgressIndicatorDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.R
+
+@Composable
+fun ProgressFragment(
+ totalTasks: Int,
+ completedTasks: Int,
+ modifier: Modifier = Modifier
+) {
+ val remainTasks = totalTasks - completedTasks
+ val progress = if (totalTasks != 0) {
+ completedTasks / totalTasks.toFloat()
+ } else {
+ // 没有任务时
+ 0f
+ }
+ val animatedProgress by animateFloatAsState(
+ targetValue = progress,
+ animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
+ label = "To Do Progress"
+ )
+ Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(
+ progress = { animatedProgress },
+ strokeWidth = 10.dp,
+ gapSize = 10.dp,
+ modifier = Modifier.size(170.dp)
+ )
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = completedTasks.toString(),
+ style = MaterialTheme.typography.displaySmall.copy(
+ fontWeight = FontWeight.Bold
+ )
+ )
+ Text(
+ text = stringResource(R.string.placeholder_divider),
+ style = MaterialTheme.typography.displayMedium.copy(
+ fontWeight = FontWeight.Bold
+ )
+ )
+ Text(
+ text = totalTasks.toString(),
+ style = MaterialTheme.typography.displayMedium.copy(
+ fontWeight = FontWeight.Bold
+ )
+ )
+ }
+ AnimatedVisibility(remainTasks != 0) {
+ Text(
+ text = stringResource(R.string.tip_remain_tasks, remainTasks),
+ style = MaterialTheme.typography.labelMedium
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoCard.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoCard.kt
new file mode 100644
index 0000000..c50d641
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoCard.kt
@@ -0,0 +1,204 @@
+package cn.super12138.todo.ui.pages.main.components
+
+import android.view.HapticFeedbackConstants
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Check
+import androidx.compose.material3.Badge
+import androidx.compose.material3.BadgedBox
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.R
+import cn.super12138.todo.constants.Constants
+import cn.super12138.todo.logic.model.Priority
+import cn.super12138.todo.ui.TodoDefaults
+import cn.super12138.todo.utils.VibrationUtils
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class)
+@Composable
+fun TodoCard(
+ id: Int,
+ content: String,
+ subject: String,
+ completed: Boolean,
+ priority: Float,
+ selected: Boolean,
+ onCardClick: () -> Unit = {},
+ onCardLongClick: () -> Unit = {},
+ onChecked: () -> Unit = {},
+ sharedTransitionScope: SharedTransitionScope,
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ val context = LocalContext.current
+ ElevatedCard(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(TodoDefaults.toDoCardHeight)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxSize()
+ .combinedClickable(
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ onCardClick()
+ },
+ onLongClick = {
+ VibrationUtils.performHapticFeedback(
+ view,
+ HapticFeedbackConstants.LONG_PRESS
+ )
+ onCardLongClick()
+ }
+ )
+ .padding(horizontal = 15.dp)
+ ) {
+ AnimatedVisibility(selected) {
+ Box(
+ Modifier
+ .padding(end = 15.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.secondary)
+ .padding(5.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Check,
+ tint = contentColorFor(MaterialTheme.colorScheme.secondary),
+ contentDescription = stringResource(R.string.tip_select_this)
+ )
+ }
+ }
+
+ Column(
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxSize()
+ ) {
+ BadgedBox(
+ badge = {
+ Badge(
+ containerColor = when (priority) {
+ -10f -> MaterialTheme.colorScheme.surfaceContainerHighest
+ -5f -> MaterialTheme.colorScheme.surfaceContainerHighest
+ 0f -> MaterialTheme.colorScheme.secondary
+ 5f -> MaterialTheme.colorScheme.tertiary
+ 10f -> MaterialTheme.colorScheme.error
+ else -> MaterialTheme.colorScheme.secondary
+ },
+ modifier = Modifier.padding(start = 5.dp)
+ ) {
+ Text(Priority.fromFloat(priority).getDisplayName(context))
+ }
+ }
+ ) {
+ with(sharedTransitionScope) {
+ Text(
+ text = content,
+ style = MaterialTheme.typography.titleLarge,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
+ modifier = Modifier
+ .sharedBounds(
+ sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_$id"),
+ animatedVisibilityScope = animatedVisibilityScope
+ )
+ .basicMarquee() // TODO: 后续评估性能影响
+ )
+ }
+ }
+
+ Text(
+ text = subject,
+ style = MaterialTheme.typography.labelMedium,
+ textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
+ maxLines = 1
+ )
+ }
+
+ AnimatedVisibility(!selected && !completed) {
+ IconButton(
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ onChecked()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Check,
+ tint = MaterialTheme.colorScheme.primary,
+ contentDescription = stringResource(R.string.tip_mark_completed)
+ )
+ }
+ }
+
+ /*Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .width(50.dp)
+ .fillMaxHeight()
+ .background(MaterialTheme.colorScheme.tertiaryContainer)
+ .clickable {
+ onChecked()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Check,
+ tint = MaterialTheme.colorScheme.primary,
+ contentDescription = ""
+ )
+ }*/
+ }
+ }
+}
+
+/*
+@Preview(locale = "zh-rCN", showBackground = true)
+@Composable
+private fun TodoCardPreview() {
+ TodoCard(
+ content = "背《岳阳楼记》《出师表》《琵琶行》",
+ subject = "语文",
+ completed = false,
+ priority = Priority.Important.value,
+ selected = false,
+ onCardClick = {},
+ onCardLongClick = {},
+ onChecked = {},
+ sharedTransitionScope = ,
+ animatedVisibilityScope =
+ )
+}*/
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoFAB.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoFAB.kt
new file mode 100644
index 0000000..4d6ca5f
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoFAB.kt
@@ -0,0 +1,37 @@
+package cn.super12138.todo.ui.pages.main.components
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Add
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.FloatingActionButtonElevation
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import cn.super12138.todo.R
+import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
+
+/**
+ * 待办页面底部 FAB
+ *
+ * @param expanded 是否为展开状态(展开状态显示内容和图标,收起状态只显示图标)
+ * @param onClick 当点击 FAB 时的回调
+ */
+@Composable
+fun TodoFAB(
+ expanded: Boolean,
+ onClick: () -> Unit,
+ elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
+ modifier: Modifier = Modifier
+) {
+ AnimatedExtendedFloatingActionButton(
+ icon = Icons.Outlined.Add,
+ text = stringResource(R.string.action_add_task),
+ expanded = expanded,
+ elevation = elevation,
+ onClick = onClick,
+ modifier = modifier
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoTopAppBar.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoTopAppBar.kt
new file mode 100644
index 0000000..32559da
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoTopAppBar.kt
@@ -0,0 +1,149 @@
+package cn.super12138.todo.ui.pages.main.components
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.exclude
+import androidx.compose.foundation.layout.ime
+import androidx.compose.foundation.layout.safeContent
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Close
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.SelectAll
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import cn.super12138.todo.R
+import cn.super12138.todo.utils.VibrationUtils
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TodoTopAppBar(
+ selectedTodoIds: List,
+ selectedMode: Boolean,
+ onCancelSelect: () -> Unit,
+ onSelectAll: () -> Unit,
+ onDeleteSelectedTodo: () -> Unit,
+ toSettingsPage: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ val animatedTopAppBarColors by animateColorAsState(
+ targetValue = if (selectedMode) MaterialTheme.colorScheme.surfaceContainerHighest else MaterialTheme.colorScheme.surface
+ )
+
+ TopAppBar(
+ navigationIcon = {
+ AnimatedVisibility(selectedMode) {
+ IconButton(
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ onCancelSelect()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Close,
+ contentDescription = stringResource(R.string.tip_clear_selected_items)
+ )
+ }
+ }
+ },
+ title = {
+ Text(
+ text = if (!selectedMode) {
+ stringResource(R.string.app_name)
+ } else {
+ stringResource(
+ R.string.title_selected_count,
+ selectedTodoIds.size
+ )
+ },
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ actions = {
+ AnimatedContent(
+ targetState = selectedMode,
+ modifier = Modifier.windowInsetsPadding(
+ WindowInsets.safeContent.exclude(WindowInsets.ime)
+ )
+ ) { inSelectedMode ->
+ if (!inSelectedMode) {
+ IconButton(
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ toSettingsPage()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Settings,
+ contentDescription = stringResource(R.string.page_settings)
+ )
+ }
+ } else {
+ Row {
+ IconButton(
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ onSelectAll()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.SelectAll,
+ contentDescription = stringResource(R.string.tip_select_all)
+ )
+ }
+ IconButton(
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ onDeleteSelectedTodo()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Delete,
+ contentDescription = stringResource(R.string.action_delete)
+ )
+ }
+ }
+ }
+ }
+
+ },
+ colors = TopAppBarDefaults.topAppBarColors().copy(
+ containerColor = animatedTopAppBarColors
+ )
+ )
+}
+
+@Preview(locale = "zh-rCN", showBackground = true)
+@Composable
+private fun TodoTopAppBarPreview() {
+ var selectedMode by remember { mutableStateOf(false) }
+ TodoTopAppBar(
+ selectedTodoIds = (1..10).toList(),
+ selectedMode = selectedMode,
+ onCancelSelect = { selectedMode = !selectedMode },
+ onSelectAll = { },
+ onDeleteSelectedTodo = { },
+ toSettingsPage = { selectedMode = !selectedMode }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAbout.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAbout.kt
new file mode 100644
index 0000000..7cc6ed9
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAbout.kt
@@ -0,0 +1,80 @@
+package cn.super12138.todo.ui.pages.settings
+
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Balance
+import androidx.compose.material.icons.outlined.Numbers
+import androidx.compose.material.icons.outlined.Person4
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import cn.super12138.todo.R
+import cn.super12138.todo.constants.Constants
+import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
+import cn.super12138.todo.ui.icons.GithubIcon
+import cn.super12138.todo.ui.pages.settings.components.SettingsItem
+import cn.super12138.todo.utils.SystemUtils
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsAbout(
+ onNavigateUp: () -> Unit,
+ toLicencePage: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ LargeTopAppBarScaffold(
+ title = stringResource(R.string.pref_about),
+ onBack = onNavigateUp,
+ scrollBehavior = scrollBehavior,
+ modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ ) { innerPadding ->
+ val context = LocalContext.current
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .verticalScroll(rememberScrollState())
+ ) {
+ SettingsItem(
+ leadingIcon = Icons.Outlined.Numbers,
+ title = stringResource(R.string.pref_app_version),
+ description = SystemUtils.getAppVersion(context)
+ )
+ SettingsItem(
+ leadingIcon = Icons.Outlined.Person4,
+ title = stringResource(R.string.pref_developer),
+ description = stringResource(R.string.developer_name),
+ onClick = {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(Constants.DEVELOPER_GITHUB))
+ context.startActivity(intent)
+ }
+ )
+ SettingsItem(
+ leadingIcon = GithubIcon,
+ title = stringResource(R.string.pref_view_on_github),
+ description = stringResource(R.string.pref_view_on_github_desc),
+ onClick = {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(Constants.GITHUB_REPO))
+ context.startActivity(intent)
+ }
+ )
+ SettingsItem(
+ leadingIcon = Icons.Outlined.Balance,
+ title = stringResource(R.string.pref_licence),
+ description = stringResource(R.string.pref_licence_desc),
+ onClick = toLicencePage
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAboutLicence.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAboutLicence.kt
new file mode 100644
index 0000000..2c67406
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAboutLicence.kt
@@ -0,0 +1,36 @@
+package cn.super12138.todo.ui.pages.settings
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import cn.super12138.todo.R
+import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
+import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsAboutLicence(
+ onNavigateUp: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ LargeTopAppBarScaffold(
+ title = stringResource(R.string.pref_licence),
+ scrollBehavior = scrollBehavior,
+ onBack = onNavigateUp,
+ modifier = modifier
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ ) {
+ LibrariesContainer()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAppearance.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAppearance.kt
new file mode 100644
index 0000000..414df85
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAppearance.kt
@@ -0,0 +1,67 @@
+package cn.super12138.todo.ui.pages.settings
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ColorLens
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
+import cn.super12138.todo.R
+import cn.super12138.todo.constants.Constants
+import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
+import cn.super12138.todo.ui.pages.settings.components.SwitchSettingsItem
+import cn.super12138.todo.ui.pages.settings.components.contrast.ContrastPicker
+import cn.super12138.todo.ui.pages.settings.components.darkmode.DarkModePicker
+import cn.super12138.todo.ui.pages.settings.components.palette.PalettePicker
+import cn.super12138.todo.ui.theme.appPaletteStyle
+import cn.super12138.todo.ui.theme.isDynamicColorEnable
+import cn.super12138.todo.ui.viewmodels.MainViewModel
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsAppearance(
+ viewModel: MainViewModel,
+ onNavigateUp: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ LargeTopAppBarScaffold(
+ title = stringResource(R.string.pref_appearance),
+ onBack = onNavigateUp,
+ scrollBehavior = scrollBehavior,
+ modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .verticalScroll(rememberScrollState())
+ ) {
+ DarkModePicker(onDarkModeChange = { viewModel.setDarkMode(it) })
+
+ PalettePicker(
+ isDarkMode = viewModel.appDarkMode,
+ contrastLevel = viewModel.appContrastLevel,
+ onPaletteChange = { appPaletteStyle = it }
+ )
+
+ ContrastPicker(onContrastChange = { viewModel.setContrastLevel(it) })
+
+ SwitchSettingsItem(
+ key = Constants.PREF_DYNAMIC_COLOR,
+ default = Constants.PREF_DYNAMIC_COLOR_DEFAULT,
+ leadingIcon = Icons.Outlined.ColorLens,
+ title = stringResource(R.string.pref_appearance_dynamic_color),
+ description = stringResource(R.string.pref_appearance_dynamic_color_desc),
+ onCheckedChange = { isDynamicColorEnable = it },
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsInterfaceInteraction.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsInterfaceInteraction.kt
new file mode 100644
index 0000000..fbd6bb4
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsInterfaceInteraction.kt
@@ -0,0 +1,109 @@
+package cn.super12138.todo.ui.pages.settings
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.Sort
+import androidx.compose.material.icons.outlined.Checklist
+import androidx.compose.material.icons.outlined.Vibration
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import cn.super12138.todo.R
+import cn.super12138.todo.constants.Constants
+import cn.super12138.todo.logic.model.SortingMethod
+import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
+import cn.super12138.todo.ui.pages.settings.components.SettingsCategory
+import cn.super12138.todo.ui.pages.settings.components.SettingsItem
+import cn.super12138.todo.ui.pages.settings.components.SettingsPlainBox
+import cn.super12138.todo.ui.pages.settings.components.SettingsRadioDialog
+import cn.super12138.todo.ui.pages.settings.components.SettingsRadioOptions
+import cn.super12138.todo.ui.pages.settings.components.SwitchSettingsItem
+import cn.super12138.todo.ui.viewmodels.MainViewModel
+import cn.super12138.todo.utils.VibrationUtils
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsInterface(
+ viewModel: MainViewModel,
+ onNavigateUp: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ var showSortingMethodDialog by rememberSaveable { mutableStateOf(false) }
+ LargeTopAppBarScaffold(
+ title = stringResource(R.string.pref_interface_interaction),
+ onBack = onNavigateUp,
+ scrollBehavior = scrollBehavior,
+ modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ ) { innerPadding ->
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .verticalScroll(rememberScrollState())
+ ) {
+ SettingsCategory(stringResource(R.string.pref_category_todo_list))
+
+ SwitchSettingsItem(
+ key = Constants.PREF_SHOW_COMPLETED,
+ default = Constants.PREF_SHOW_COMPLETED_DEFAULT,
+ leadingIcon = Icons.Outlined.Checklist,
+ title = stringResource(R.string.pref_show_completed),
+ description = stringResource(R.string.pref_show_completed_desc),
+ onCheckedChange = { viewModel.setShowCompleted(it) },
+ )
+ SettingsItem(
+ leadingIcon = Icons.AutoMirrored.Outlined.Sort,
+ title = stringResource(R.string.pref_sorting_method),
+ description = viewModel.appSortingMethod.getDisplayName(context),
+ onClick = { showSortingMethodDialog = true }
+ )
+
+ SettingsCategory(stringResource(R.string.pref_category_global_interaction))
+ SwitchSettingsItem(
+ key = Constants.PREF_HAPTIC_FEEDBACK,
+ default = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT,
+ leadingIcon = Icons.Outlined.Vibration,
+ title = stringResource(R.string.pref_haptic_feedback),
+ description = stringResource(R.string.pref_haptic_feedback_desc),
+ onCheckedChange = { VibrationUtils.setEnabled(it) }
+ )
+ SettingsPlainBox(stringResource(R.string.pref_haptic_feedback_more_info))
+ }
+ }
+
+ val sortingList = remember {
+ SortingMethod.entries.map {
+ SettingsRadioOptions(
+ id = it.id,
+ text = it.getDisplayName(context)
+ )
+ }
+ }
+ SettingsRadioDialog(
+ key = Constants.PREF_SORTING_METHOD,
+ defaultIndex = Constants.PREF_SORTING_METHOD_DEFAULT,
+ visible = showSortingMethodDialog,
+ title = stringResource(R.string.pref_sorting_method),
+ options = sortingList,
+ onSelect = { id ->
+ viewModel.setSortingMethod(SortingMethod.fromId(id))
+ },
+ onDismiss = { showSortingMethodDialog = false }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsMain.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsMain.kt
new file mode 100644
index 0000000..25b349e
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsMain.kt
@@ -0,0 +1,62 @@
+package cn.super12138.todo.ui.pages.settings
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ColorLens
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material.icons.outlined.ViewComfy
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import cn.super12138.todo.R
+import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
+import cn.super12138.todo.ui.pages.settings.components.SettingsItem
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsMain(
+ toAppearancePage: () -> Unit,
+ toInterfacePage: () -> Unit,
+ toAboutPage: () -> Unit,
+ onNavigateUp: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ LargeTopAppBarScaffold(
+ title = stringResource(R.string.page_settings),
+ scrollBehavior = scrollBehavior,
+ onBack = onNavigateUp
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .verticalScroll(rememberScrollState())
+ ) {
+ SettingsItem(
+ leadingIcon = Icons.Outlined.ColorLens,
+ title = stringResource(R.string.pref_appearance),
+ description = stringResource(R.string.pref_appearance_desc),
+ onClick = toAppearancePage
+ )
+ SettingsItem(
+ leadingIcon = Icons.Outlined.ViewComfy,
+ title = stringResource(R.string.pref_interface_interaction),
+ description = stringResource(R.string.pref_interface_interaction_desc),
+ onClick = toInterfacePage
+ )
+ SettingsItem(
+ leadingIcon = Icons.Outlined.Info,
+ title = stringResource(R.string.pref_about),
+ description = stringResource(R.string.pref_about_desc),
+ onClick = toAboutPage
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/RowSettingsItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/RowSettingsItem.kt
new file mode 100644
index 0000000..a4948be
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/RowSettingsItem.kt
@@ -0,0 +1,108 @@
+package cn.super12138.todo.ui.pages.settings.components
+
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun RowSettingsItem(
+ title: String,
+ description: String? = null,
+ shape: Shape = MaterialTheme.shapes.large,
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ horizontalArrangement: Arrangement.Horizontal =
+ if (!reverseLayout) Arrangement.Start else Arrangement.End,
+ verticalAlignment: Alignment.Vertical = Alignment.Top,
+ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ userScrollEnabled: Boolean = true,
+ content: LazyListScope.() -> Unit
+) {
+ MoreContentSettingsItem(
+ title = title,
+ description = description,
+ shape = shape
+ ) {
+ LazyRow(
+ modifier = Modifier.fillMaxWidth(),
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ horizontalArrangement = horizontalArrangement,
+ verticalAlignment = verticalAlignment,
+ flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
+ content = content
+ )
+ }
+}
+
+@Composable
+fun MoreContentSettingsItem(
+ title: String,
+ description: String? = null,
+ shape: Shape = MaterialTheme.shapes.large,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .clip(shape)
+ .padding(horizontal = 24.dp, vertical = 20.dp),
+ ) {
+ Text(
+ text = title,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.titleLarge.copy(
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = 20.sp
+ )
+ )
+ description?.let {
+ Text(
+ text = it,
+ // maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ )
+ }
+
+ Spacer(Modifier.size(8.dp))
+
+ /*Box(
+ Modifier
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.large)
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ ) {*/
+ content()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsCategory.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsCategory.kt
new file mode 100644
index 0000000..4a51636
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsCategory.kt
@@ -0,0 +1,29 @@
+package cn.super12138.todo.ui.pages.settings.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun SettingsCategory(
+ title: String,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp, start = 24.dp, end = 24.dp)
+ ) {
+ Text(
+ text = title,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Medium
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsDialogs.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsDialogs.kt
new file mode 100644
index 0000000..9392098
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsDialogs.kt
@@ -0,0 +1,119 @@
+package cn.super12138.todo.ui.pages.settings.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.ui.TodoDefaults
+import cn.super12138.todo.ui.components.BasicDialog
+import cn.super12138.todo.ui.pages.settings.state.rememberPrefIntState
+import cn.super12138.todo.utils.VibrationUtils
+
+@Composable
+fun SettingsRadioDialog(
+ key: String,
+ defaultIndex: Int,
+ visible: Boolean,
+ title: String,
+ options: List,
+ onSelect: (id: Int) -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ SettingsDialog(
+ visible = visible,
+ title = title,
+ text = {
+ var selectedItemIndex by rememberPrefIntState(key, defaultIndex)
+ // Modifier.selectableGroup() 用来确保无障碍功能运行正确
+ Column(Modifier.selectableGroup()) {
+ options.forEach { option ->
+ RadioItem(
+ selected = option.id == selectedItemIndex,
+ text = option.text,
+ onClick = {
+ selectedItemIndex = option.id
+ onSelect(option.id)
+ onDismiss()
+ }
+ )
+ }
+ }
+ },
+ onDismissRequest = onDismiss,
+ modifier = modifier
+ )
+}
+
+@Composable
+fun RadioItem(
+ selected: Boolean,
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .height(56.dp)
+ .selectable(
+ selected = selected,
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ onClick()
+ },
+ role = Role.RadioButton
+ )
+ .padding(horizontal = TodoDefaults.screenPadding),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = selected,
+ onClick = null // 设置为 null 有利于屏幕阅读器
+ )
+ Text(
+ text = text,
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.padding(start = 16.dp)
+ )
+ }
+}
+
+data class SettingsRadioOptions(
+ val id: Int,
+ val text: String,
+)
+
+@Composable
+fun SettingsDialog(
+ visible: Boolean,
+ title: String,
+ text: @Composable (() -> Unit)? = null,
+ onDismissRequest: () -> Unit = {},
+ modifier: Modifier = Modifier
+) {
+ BasicDialog(
+ visible = visible,
+ title = { Text(title) },
+ text = text,
+ confirmButton = {},
+ dismissButton = {},
+ onDismissRequest = onDismissRequest,
+ modifier = modifier
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt
new file mode 100644
index 0000000..8707a16
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt
@@ -0,0 +1,134 @@
+package cn.super12138.todo.ui.pages.settings.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import cn.super12138.todo.utils.VibrationUtils
+
+@Composable
+fun SettingsItem(
+ leadingIcon: ImageVector? = null,
+ title: String,
+ description: String? = null,
+ enableClick: Boolean = true,
+ onClick: () -> Unit = {},
+ modifier: Modifier = Modifier
+) {
+ SettingsItem(
+ leadingIcon = leadingIcon,
+ title = title,
+ description = description,
+ trailingContent = null,
+ enableClick = enableClick,
+ onClick = onClick,
+ modifier = modifier
+ )
+}
+
+@Composable
+fun SettingsItem(
+ leadingIcon: ImageVector? = null,
+ title: String,
+ description: String? = null,
+ trailingContent: (@Composable () -> Unit)? = null,
+ enableClick: Boolean = true,
+ onClick: () -> Unit = {},
+ modifier: Modifier = Modifier
+) {
+ SettingsItem(
+ leadingIcon = {
+ leadingIcon?.let {
+ Icon(
+ imageVector = it,
+ contentDescription = title,
+ tint = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.padding(end = 24.dp),
+ )
+ }
+ },
+ title = title,
+ description = description,
+ trailingContent = trailingContent,
+ enableClick = enableClick,
+ onClick = onClick,
+ modifier = modifier
+ )
+}
+
+@Composable
+fun SettingsItem(
+ leadingIcon: (@Composable () -> Unit)? = null,
+ title: String,
+ description: String? = null,
+ trailingContent: (@Composable () -> Unit)? = null,
+ shape: Shape = MaterialTheme.shapes.large,
+ enableClick: Boolean = true,
+ onClick: () -> Unit = {},
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .clip(shape)
+ .clickable(
+ enabled = enableClick,
+ onClick = {
+ VibrationUtils.performHapticFeedback(view)
+ onClick()
+ }
+ )
+ .padding(horizontal = 24.dp, vertical = 20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ leadingIcon?.let {
+ it()
+ }
+
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ ) {
+ Text(
+ text = title,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.titleLarge.copy(
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = 20.sp
+ )
+ )
+ description?.let {
+ Text(
+ text = it,
+ // maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ )
+ }
+ }
+
+ trailingContent?.let {
+ it()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsPlainBox.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsPlainBox.kt
new file mode 100644
index 0000000..50b9cf6
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsPlainBox.kt
@@ -0,0 +1,43 @@
+package cn.super12138.todo.ui.pages.settings.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.R
+
+@Composable
+fun SettingsPlainBox(
+ text: String,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(start = 24.dp, end = 24.dp, bottom = 20.dp),
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Info,
+ contentDescription = stringResource(R.string.tip_tips)
+ )
+ Spacer(Modifier.size(20.dp))
+ Text(
+ text = text,
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SwitchSettingsItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SwitchSettingsItem.kt
new file mode 100644
index 0000000..3a31e33
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SwitchSettingsItem.kt
@@ -0,0 +1,48 @@
+package cn.super12138.todo.ui.pages.settings.components
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Switch
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.ui.pages.settings.state.rememberPrefBooleanState
+import cn.super12138.todo.utils.VibrationUtils
+
+@Composable
+fun SwitchSettingsItem(
+ key: String,
+ default: Boolean,
+ leadingIcon: ImageVector? = null,
+ title: String,
+ description: String? = null,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ var switchState by rememberPrefBooleanState(key, default)
+ SettingsItem(
+ leadingIcon = leadingIcon,
+ title = title,
+ description = description,
+ trailingContent = {
+ Switch(
+ checked = switchState,
+ onCheckedChange = {
+ switchState = it
+ VibrationUtils.performHapticFeedback(view)
+ onCheckedChange(it)
+ },
+ modifier = Modifier.padding(start = 14.dp)
+ )
+ },
+ onClick = {
+ switchState = !switchState
+ onCheckedChange(switchState)
+ },
+ modifier = modifier
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/contrast/ContrastPicker.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/contrast/ContrastPicker.kt
new file mode 100644
index 0000000..ad31ab9
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/contrast/ContrastPicker.kt
@@ -0,0 +1,76 @@
+package cn.super12138.todo.ui.pages.settings.components.contrast
+
+import android.view.HapticFeedbackConstants
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.R
+import cn.super12138.todo.constants.Constants
+import cn.super12138.todo.logic.model.ContrastLevel
+import cn.super12138.todo.ui.pages.settings.components.MoreContentSettingsItem
+import cn.super12138.todo.ui.pages.settings.state.rememberPrefFloatState
+import cn.super12138.todo.utils.VibrationUtils
+
+@Composable
+fun ContrastPicker(
+ onContrastChange: (ContrastLevel) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ val context = LocalContext.current
+ MoreContentSettingsItem(
+ title = stringResource(R.string.pref_contrast_level),
+ description = stringResource(R.string.pref_contrast_level_desc)
+ ) {
+ var contrastState by rememberPrefFloatState(
+ Constants.PREF_CONTRAST_LEVEL,
+ Constants.PREF_CONTRAST_LEVEL_DEFAULT
+ )
+
+ Slider(
+ modifier = Modifier.semantics {
+ contentDescription = context.getString(R.string.tip_change_contrast_level)
+ },
+ value = contrastState,
+ onValueChange = {
+ VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS)
+ contrastState = it
+ onContrastChange(ContrastLevel.fromFloat(it))
+ },
+ valueRange = -1f..1f,
+ steps = 3,
+ )
+
+ Spacer(Modifier.size(5.dp))
+
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyMedium) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(stringResource(R.string.contrast_very_low))
+ Text(stringResource(R.string.contrast_low))
+ Text(stringResource(R.string.contrast_default))
+ Text(stringResource(R.string.contrast_high))
+ Text(stringResource(R.string.contrast_very_high))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/darkmode/DarkModeItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/darkmode/DarkModeItem.kt
new file mode 100644
index 0000000..fa76169
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/darkmode/DarkModeItem.kt
@@ -0,0 +1,76 @@
+package cn.super12138.todo.ui.pages.settings.components.darkmode
+
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.utils.VibrationUtils
+
+@Composable
+fun DarkModeItem(
+ icon: ImageVector,
+ contentDescription: String,
+ contentColor: Color,
+ containerColor: Color,
+ selected: Boolean,
+ onSelect: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ val borderWidth by animateDpAsState(if (selected) 3.dp else (-1).dp)
+ Column(
+ modifier = modifier
+ .clip(MaterialTheme.shapes.large)
+ .clickable {
+ VibrationUtils.performHapticFeedback(view)
+ onSelect()
+ }
+ .padding(8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .size(90.dp)
+ .clip(MaterialTheme.shapes.large)
+ .background(containerColor)
+ .border(
+ width = borderWidth,
+ color = MaterialTheme.colorScheme.primary,
+ shape = MaterialTheme.shapes.large
+ ),
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = contentDescription,
+ tint = contentColor,
+ modifier = Modifier
+ .size(30.dp)
+ .align(Alignment.Center)
+ )
+ }
+
+ Spacer(Modifier.size(8.dp))
+
+ Text(
+ text = contentDescription,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/darkmode/DarkModePicker.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/darkmode/DarkModePicker.kt
new file mode 100644
index 0000000..a78de22
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/darkmode/DarkModePicker.kt
@@ -0,0 +1,80 @@
+package cn.super12138.todo.ui.pages.settings.components.darkmode
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.R
+import cn.super12138.todo.constants.Constants
+import cn.super12138.todo.logic.model.DarkMode
+import cn.super12138.todo.ui.pages.settings.components.RowSettingsItem
+import cn.super12138.todo.ui.pages.settings.state.rememberPrefIntState
+
+@Composable
+fun DarkModePicker(
+ onDarkModeChange: (darkMode: DarkMode) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+
+ val isInDarkTheme = isSystemInDarkTheme()
+
+ var darkModeState by rememberPrefIntState(
+ Constants.PREF_DARK_MODE,
+ Constants.PREF_DARK_MODE_DEFAULT
+ )
+
+ RowSettingsItem(
+ title = stringResource(R.string.pref_dark_mode),
+ description = stringResource(R.string.pref_dark_mode_desc),
+ horizontalArrangement = Arrangement.spacedBy(5.dp)
+ ) {
+ item {
+ DarkModeItem(
+ icon = DarkMode.FollowSystem.icon,
+ contentDescription = DarkMode.FollowSystem.getDisplayName(context),
+ contentColor = if (isInDarkTheme) Color.White else Color.Black,
+ containerColor = if (isInDarkTheme) Color.Black else Color.White,
+ selected = DarkMode.fromId(darkModeState) == DarkMode.FollowSystem,
+ onSelect = {
+ darkModeState = DarkMode.FollowSystem.id
+ onDarkModeChange(DarkMode.FollowSystem)
+ }
+ )
+ }
+
+ item {
+ DarkModeItem(
+ icon = DarkMode.Light.icon,
+ contentDescription = DarkMode.Light.getDisplayName(context),
+ contentColor = Color.Black,
+ containerColor = Color.White,
+ selected = DarkMode.fromId(darkModeState) == DarkMode.Light,
+ onSelect = {
+ darkModeState = DarkMode.Light.id
+ onDarkModeChange(DarkMode.Light)
+ }
+ )
+ }
+
+ item {
+ DarkModeItem(
+ icon = DarkMode.Dark.icon,
+ contentDescription = DarkMode.Dark.getDisplayName(context),
+ contentColor = Color.White,
+ containerColor = Color.Black,
+ selected = DarkMode.fromId(darkModeState) == DarkMode.Dark,
+ onSelect = {
+ darkModeState = DarkMode.Dark.id
+ onDarkModeChange(DarkMode.Dark)
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/palette/PaletteItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/palette/PaletteItem.kt
new file mode 100644
index 0000000..9e4bea9
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/palette/PaletteItem.kt
@@ -0,0 +1,109 @@
+package cn.super12138.todo.ui.pages.settings.components.palette
+
+import android.os.Build
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastForEach
+import cn.super12138.todo.constants.GlobalValues
+import cn.super12138.todo.logic.model.ContrastLevel
+import cn.super12138.todo.ui.theme.PaletteStyle
+import cn.super12138.todo.ui.theme.dynamicColorScheme
+import cn.super12138.todo.utils.VibrationUtils
+
+@Composable
+fun PaletteItem(
+ isDark: Boolean,
+ paletteStyle: PaletteStyle,
+ contrastLevel: ContrastLevel,
+ selected: Boolean,
+ onSelect: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val view = LocalView.current
+ val context = LocalContext.current
+ Column(
+ modifier = Modifier
+ .width(90.dp)
+ .clip(MaterialTheme.shapes.large)
+ .clickable {
+ VibrationUtils.performHapticFeedback(view)
+ onSelect()
+ }
+ .padding(8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ // 为不同主题样式设置不同色板
+ MaterialTheme(
+ colorScheme = dynamicColorScheme(
+ keyColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && GlobalValues.dynamicColor) {
+ colorResource(id = android.R.color.system_accent1_500)
+ } else {
+ Color(0xFF0061A4)
+ },
+ isDark = isDark,
+ contrastLevel = contrastLevel.value.toDouble(),
+ style = paletteStyle
+ )
+ ) {
+ val borderWidth by animateDpAsState(if (selected) 3.dp else (-1).dp)
+ // 颜色预览区域
+ Column(
+ modifier = Modifier
+ .width(70.dp)
+ .clip(MaterialTheme.shapes.large)
+ .border(
+ width = borderWidth,
+ color = MaterialTheme.colorScheme.primary,
+ shape = MaterialTheme.shapes.large
+ ),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ listOf(
+ MaterialTheme.colorScheme.primary,
+ MaterialTheme.colorScheme.secondary,
+ MaterialTheme.colorScheme.tertiary,
+ MaterialTheme.colorScheme.tertiaryContainer,
+ MaterialTheme.colorScheme.secondaryContainer,
+ MaterialTheme.colorScheme.primaryContainer,
+ ).fastForEach {
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(24.dp)
+ .background(it)
+ )
+ }
+ }
+ }
+
+ Spacer(Modifier.size(8.dp))
+
+ Text(
+ text = paletteStyle.getDisplayName(context),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/palette/PalettePicker.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/palette/PalettePicker.kt
new file mode 100644
index 0000000..424b381
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/palette/PalettePicker.kt
@@ -0,0 +1,59 @@
+package cn.super12138.todo.ui.pages.settings.components.palette
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import cn.super12138.todo.R
+import cn.super12138.todo.constants.Constants
+import cn.super12138.todo.logic.model.ContrastLevel
+import cn.super12138.todo.logic.model.DarkMode
+import cn.super12138.todo.ui.pages.settings.components.RowSettingsItem
+import cn.super12138.todo.ui.pages.settings.state.rememberPrefIntState
+import cn.super12138.todo.ui.theme.PaletteStyle
+
+@Composable
+fun PalettePicker(
+ isDarkMode: DarkMode,
+ contrastLevel: ContrastLevel,
+ onPaletteChange: (paletteStyle: PaletteStyle) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var paletteState by rememberPrefIntState(
+ Constants.PREF_PALETTE_STYLE,
+ Constants.PREF_PALETTE_STYLE_DEFAULT
+ )
+
+ val paletteOptions = remember {
+ PaletteStyle.entries.toList()
+ }
+
+ RowSettingsItem(
+ title = stringResource(R.string.pref_palette_style),
+ description = stringResource(R.string.pref_palette_style_desc),
+ horizontalArrangement = Arrangement.spacedBy(5.dp)
+ ) {
+ items(items = paletteOptions, key = { it.id }) { paletteStyle ->
+ PaletteItem(
+ isDark = when (isDarkMode) {
+ DarkMode.FollowSystem -> isSystemInDarkTheme()
+ DarkMode.Light -> false
+ DarkMode.Dark -> true
+ },
+ paletteStyle = paletteStyle,
+ selected = PaletteStyle.fromId(paletteState) == paletteStyle,
+ contrastLevel = contrastLevel,
+ onSelect = {
+ paletteState = paletteStyle.id
+ onPaletteChange(paletteStyle)
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/state/PrefMutableState.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/state/PrefMutableState.kt
new file mode 100644
index 0000000..d5604b0
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/state/PrefMutableState.kt
@@ -0,0 +1,118 @@
+package cn.super12138.todo.ui.pages.settings.state
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableFloatState
+import androidx.compose.runtime.MutableIntState
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+
+/**
+ * 来自:https://github.com/hushenghao/AndroidEasterEggs/blob/main/app/src/main/java/com/dede/android_eggs/views/settings/compose/basic/PrefMutableState.kt
+ * 在命名上有改动
+ * @author hushenghao
+ */
+
+@Composable
+fun rememberPrefBooleanState(key: String, default: Boolean): MutableState {
+ val context = LocalContext.current
+ return remember { PrefMutableBooleanState(context, key, default) }
+}
+
+@Composable
+fun rememberPrefIntState(key: String, default: Int): MutableIntState {
+ val context = LocalContext.current
+ return remember { PrefMutableIntState(context, key, default) }
+}
+
+@Composable
+fun rememberPrefFloatState(key: String, default: Float): MutableFloatState {
+ val context = LocalContext.current
+ return remember { PrefMutableFloatState(context, key, default) }
+}
+
+private class PrefMutableBooleanState(
+ val context: Context,
+ val key: String,
+ default: Boolean,
+) : MutableState {
+ private val delegate = mutableStateOf(context.pref.getBoolean(key, default))
+
+ override var value: Boolean
+ get() = delegate.value
+ set(value) {
+ delegate.value = value
+ context.pref.edit().putBoolean(key, value).apply()
+ }
+
+ override fun component1(): Boolean {
+ return delegate.component1()
+ }
+
+ override fun component2(): (Boolean) -> Unit {
+ return delegate.component2()
+ }
+}
+
+private class PrefMutableIntState(
+ val context: Context,
+ val key: String,
+ default: Int,
+) : MutableIntState {
+ private val delegate = mutableIntStateOf(context.pref.getInt(key, default))
+
+ override var intValue: Int
+ get() = delegate.intValue
+ set(value) {
+ delegate.intValue = value
+ context.pref.edit().putInt(key, value).apply()
+ }
+
+ override fun component1(): Int {
+ return delegate.component1()
+ }
+
+ override fun component2(): (Int) -> Unit {
+ return delegate.component2()
+ }
+}
+
+private class PrefMutableFloatState(
+ val context: Context,
+ val key: String,
+ default: Float,
+) : MutableFloatState {
+ private val delegate = mutableFloatStateOf(context.pref.getFloat(key, default))
+
+ override var floatValue: Float
+ get() = delegate.floatValue
+ set(value) {
+ delegate.floatValue = value
+ context.pref.edit().putFloat(key, value).apply()
+ }
+
+ override fun component1(): Float {
+ return delegate.component1()
+ }
+
+ override fun component2(): (Float) -> Unit {
+ return delegate.component2()
+ }
+}
+
+/**
+ * 来自:https://github.com/hushenghao/AndroidEasterEggs/blob/main/basic/src/main/java/com/dede/android_eggs/util/Pref.kt
+ * @author hushenghao
+ */
+val Context.pref: SharedPreferences
+ get() {
+ return applicationContext.getSharedPreferences(
+ applicationContext.packageName + "_preferences",
+ Context.MODE_PRIVATE
+ )
+ }
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt
new file mode 100644
index 0000000..cdaf9c4
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt
@@ -0,0 +1,87 @@
+package cn.super12138.todo.ui.theme
+
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.FastOutLinearInEasing
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+
+/**
+ * 来自:https://github.com/Ashinch/ReadYou/blob/main/app/src/main/java/me/ash/reader/ui/motion/MaterialSharedAxis.kt
+ */
+
+private const val ProgressThreshold = 0.35f
+
+private val Int.ForOutgoing: Int
+ get() = (this * ProgressThreshold).toInt()
+
+private val Int.ForIncoming: Int
+ get() = this - this.ForOutgoing
+
+
+private const val DefaultMotionDuration: Int = 300
+
+/**
+ * [materialSharedAxisX] allows to switch a layout with shared X-axis transition.
+ *
+ */
+fun materialSharedAxisX(
+ initialOffsetX: (fullWidth: Int) -> Int,
+ targetOffsetX: (fullWidth: Int) -> Int,
+ durationMillis: Int = DefaultMotionDuration,
+): ContentTransform = ContentTransform(
+ materialSharedAxisXIn(
+ initialOffsetX = initialOffsetX,
+ durationMillis = durationMillis
+ ), materialSharedAxisXOut(
+ targetOffsetX = targetOffsetX,
+ durationMillis = durationMillis
+ )
+)
+
+/**
+ * [materialSharedAxisXIn] allows to switch a layout with shared X-axis enter transition.
+ */
+fun materialSharedAxisXIn(
+ initialOffsetX: (fullWidth: Int) -> Int,
+ durationMillis: Int = DefaultMotionDuration,
+): EnterTransition = slideInHorizontally(
+ animationSpec = tween(
+ durationMillis = durationMillis,
+ easing = FastOutSlowInEasing
+ ),
+ initialOffsetX = initialOffsetX
+) + fadeIn(
+ animationSpec = tween(
+ durationMillis = durationMillis.ForIncoming,
+ delayMillis = durationMillis.ForOutgoing,
+ easing = LinearOutSlowInEasing
+ )
+)
+
+/**
+ * [materialSharedAxisXOut] allows to switch a layout with shared X-axis exit transition.
+ *
+ */
+fun materialSharedAxisXOut(
+ targetOffsetX: (fullWidth: Int) -> Int,
+ durationMillis: Int = DefaultMotionDuration,
+): ExitTransition = slideOutHorizontally(
+ animationSpec = tween(
+ durationMillis = durationMillis,
+ easing = FastOutSlowInEasing
+ ),
+ targetOffsetX = targetOffsetX
+) + fadeOut(
+ animationSpec = tween(
+ durationMillis = durationMillis.ForOutgoing,
+ delayMillis = 0,
+ easing = FastOutLinearInEasing
+ )
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/PaletteStyle.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/PaletteStyle.kt
new file mode 100644
index 0000000..1205ff2
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/PaletteStyle.kt
@@ -0,0 +1,37 @@
+package cn.super12138.todo.ui.theme
+
+import android.content.Context
+import cn.super12138.todo.R
+
+enum class PaletteStyle(val id: Int) {
+ TonalSpot(1),
+ Neutral(2),
+ Vibrant(3),
+ Expressive(4),
+ Rainbow(5),
+ FruitSalad(6),
+ Monochrome(7),
+ Fidelity(8),
+ Content(9);
+
+ fun getDisplayName(context: Context): String {
+ val resId = when (this) {
+ TonalSpot -> R.string.palette_tonal_spot
+ Neutral -> R.string.palette_neutral
+ Vibrant -> R.string.palette_vibrant
+ Expressive -> R.string.palette_expressive
+ Rainbow -> R.string.palette_rainbow
+ FruitSalad -> R.string.palette_fruit_salad
+ Monochrome -> R.string.palette_monochrome
+ Fidelity -> R.string.palette_fidelity
+ Content -> R.string.palette_content
+ }
+ return context.getString(resId)
+ }
+
+ companion object {
+ fun fromId(id: Int): PaletteStyle {
+ return PaletteStyle.entries.find { it.id == id } ?: TonalSpot
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt
new file mode 100644
index 0000000..e97059c
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt
@@ -0,0 +1,48 @@
+package cn.super12138.todo.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import cn.super12138.todo.constants.GlobalValues
+
+var isDynamicColorEnable by mutableStateOf(GlobalValues.dynamicColor)
+var appPaletteStyle by mutableStateOf(PaletteStyle.fromId(GlobalValues.paletteStyle))
+
+@Composable
+fun ToDoTheme(
+ color: Color? = null,
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ style: PaletteStyle = appPaletteStyle,
+ contrastLevel: Double = 0.0,
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = isDynamicColorEnable,
+ content: @Composable () -> Unit
+) {
+ val baseColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && dynamicColor) {
+ colorResource(id = android.R.color.system_accent1_500)
+ } else {
+ Color(0xFF0061A4)
+ }
+
+ // 关键色,如果指定就使用
+ val keyColor = color ?: baseColor
+
+ val colorScheme = dynamicColorScheme(
+ keyColor = keyColor,
+ isDark = darkTheme,
+ style = style,
+ contrastLevel = contrastLevel
+ )
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/ThemeExt.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/ThemeExt.kt
new file mode 100644
index 0000000..6389357
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/ThemeExt.kt
@@ -0,0 +1,91 @@
+package cn.super12138.todo.ui.theme
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.spring
+import androidx.compose.material3.ColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import com.kyant.m3color.hct.Hct
+import com.kyant.m3color.scheme.SchemeContent
+import com.kyant.m3color.scheme.SchemeExpressive
+import com.kyant.m3color.scheme.SchemeFidelity
+import com.kyant.m3color.scheme.SchemeFruitSalad
+import com.kyant.m3color.scheme.SchemeMonochrome
+import com.kyant.m3color.scheme.SchemeNeutral
+import com.kyant.m3color.scheme.SchemeRainbow
+import com.kyant.m3color.scheme.SchemeTonalSpot
+import com.kyant.m3color.scheme.SchemeVibrant
+
+@Composable
+@Stable
+fun dynamicColorScheme(
+ keyColor: Color,
+ isDark: Boolean,
+ style: PaletteStyle = PaletteStyle.TonalSpot,
+ contrastLevel: Double = 0.0,
+ animationSpec: AnimationSpec = spring()
+): ColorScheme {
+ val hct = Hct.fromInt(keyColor.toArgb())
+ val scheme = when (style) {
+ PaletteStyle.TonalSpot -> SchemeTonalSpot(hct, isDark, contrastLevel)
+ PaletteStyle.Neutral -> SchemeNeutral(hct, isDark, contrastLevel)
+ PaletteStyle.Vibrant -> SchemeVibrant(hct, isDark, contrastLevel)
+ PaletteStyle.Expressive -> SchemeExpressive(hct, isDark, contrastLevel)
+ PaletteStyle.Rainbow -> SchemeRainbow(hct, isDark, contrastLevel)
+ PaletteStyle.FruitSalad -> SchemeFruitSalad(hct, isDark, contrastLevel)
+ PaletteStyle.Monochrome -> SchemeMonochrome(hct, isDark, contrastLevel)
+ PaletteStyle.Fidelity -> SchemeFidelity(hct, isDark, contrastLevel)
+ PaletteStyle.Content -> SchemeContent(hct, isDark, contrastLevel)
+ }
+
+ return ColorScheme(
+ background = scheme.background.toColor().animate(animationSpec),
+ error = scheme.error.toColor().animate(animationSpec),
+ errorContainer = scheme.errorContainer.toColor().animate(animationSpec),
+ inverseOnSurface = scheme.inverseOnSurface.toColor().animate(animationSpec),
+ inversePrimary = scheme.inversePrimary.toColor().animate(animationSpec),
+ inverseSurface = scheme.inverseSurface.toColor().animate(animationSpec),
+ onBackground = scheme.onBackground.toColor().animate(animationSpec),
+ onError = scheme.onError.toColor().animate(animationSpec),
+ onErrorContainer = scheme.onErrorContainer.toColor().animate(animationSpec),
+ onPrimary = scheme.onPrimary.toColor().animate(animationSpec),
+ onPrimaryContainer = scheme.onPrimaryContainer.toColor().animate(animationSpec),
+ onSecondary = scheme.onSecondary.toColor().animate(animationSpec),
+ onSecondaryContainer = scheme.onSecondaryContainer.toColor().animate(animationSpec),
+ onSurface = scheme.onSurface.toColor().animate(animationSpec),
+ onSurfaceVariant = scheme.onSurfaceVariant.toColor().animate(animationSpec),
+ onTertiary = scheme.onTertiary.toColor().animate(animationSpec),
+ onTertiaryContainer = scheme.onTertiaryContainer.toColor().animate(animationSpec),
+ outline = scheme.outline.toColor().animate(animationSpec),
+ outlineVariant = scheme.outlineVariant.toColor().animate(animationSpec),
+ primary = scheme.primary.toColor().animate(animationSpec),
+ primaryContainer = scheme.primaryContainer.toColor().animate(animationSpec),
+ scrim = scheme.scrim.toColor().animate(animationSpec),
+ secondary = scheme.secondary.toColor().animate(animationSpec),
+ secondaryContainer = scheme.secondaryContainer.toColor().animate(animationSpec),
+ surface = scheme.surface.toColor().animate(animationSpec),
+ surfaceBright = scheme.surfaceBright.toColor().animate(animationSpec),
+ surfaceContainer = scheme.surfaceContainer.toColor().animate(animationSpec),
+ surfaceContainerLow = scheme.surfaceContainerLow.toColor().animate(animationSpec),
+ surfaceContainerLowest = scheme.surfaceContainerLowest.toColor().animate(animationSpec),
+ surfaceContainerHigh = scheme.surfaceContainerHigh.toColor().animate(animationSpec),
+ surfaceContainerHighest = scheme.surfaceContainerHighest.toColor().animate(animationSpec),
+ surfaceDim = scheme.surfaceDim.toColor().animate(animationSpec),
+ surfaceTint = scheme.surfaceTint.toColor().animate(animationSpec),
+ surfaceVariant = scheme.surfaceVariant.toColor().animate(animationSpec),
+ tertiary = scheme.tertiary.toColor().animate(animationSpec),
+ tertiaryContainer = scheme.tertiaryContainer.toColor().animate(animationSpec),
+ )
+}
+
+@Suppress("NOTHING_TO_INLINE")
+private inline fun Int.toColor(): Color = Color(this)
+
+// https://github.com/jordond/MaterialKolor/blob/main/material-kolor/src/commonMain/kotlin/com/materialkolor/DynamicMaterialTheme.kt
+@Composable
+private fun Color.animate(animationSpec: AnimationSpec = spring()): Color {
+ return animateColorAsState(this, animationSpec).value
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt
new file mode 100644
index 0000000..265850d
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt
@@ -0,0 +1,5 @@
+package cn.super12138.todo.ui.theme
+
+import androidx.compose.material3.Typography
+
+val Typography = Typography()
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt
new file mode 100644
index 0000000..6001e8a
--- /dev/null
+++ b/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt
@@ -0,0 +1,153 @@
+package cn.super12138.todo.ui.viewmodels
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import cn.super12138.todo.constants.GlobalValues
+import cn.super12138.todo.logic.Repository
+import cn.super12138.todo.logic.database.TodoEntity
+import cn.super12138.todo.logic.model.ContrastLevel
+import cn.super12138.todo.logic.model.DarkMode
+import cn.super12138.todo.logic.model.SortingMethod
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+class MainViewModel : ViewModel() {
+ // 待办
+ private val toDos: Flow> = Repository.getAllTodos()
+ var appSortingMethod by mutableStateOf(SortingMethod.fromId(GlobalValues.sortingMethod))
+ val sortedTodos: Flow> = toDos.map { list ->
+ when (appSortingMethod) {
+ SortingMethod.Date -> list.sortedBy { it.id }
+ SortingMethod.Subject -> list.sortedBy { it.subject }
+ SortingMethod.Priority -> list.sortedByDescending { it.priority } // 优先级高的在前
+ SortingMethod.Completion -> list.sortedBy { it.isCompleted } // 未完成的在前
+ SortingMethod.AlphabeticalAscending -> list.sortedBy { it.content }
+ SortingMethod.AlphabeticalDescending -> list.sortedByDescending { it.content }
+ }
+ }
+ val showConfetti = mutableStateOf(false)
+ var selectedEditTodo by mutableStateOf(null)
+ private set
+ var showCompletedTodos by mutableStateOf(GlobalValues.showCompleted)
+
+ // 主题颜色
+ var appDarkMode by mutableStateOf(DarkMode.fromId(GlobalValues.darkMode))
+ private set
+ var appContrastLevel by mutableStateOf(ContrastLevel.fromFloat(GlobalValues.contrastLevel))
+ private set
+
+ // 多选逻辑参考:https://github.com/X1nto/Mauth
+ private val _selectedTodoIds = MutableStateFlow(listOf())
+ val selectedTodoIds = _selectedTodoIds.asStateFlow()
+
+ fun addTodo(toDo: TodoEntity) {
+ viewModelScope.launch {
+ Repository.insertTodo(toDo)
+ }
+ }
+
+ fun updateTodo(toDo: TodoEntity) {
+ viewModelScope.launch {
+ Repository.updateTodo(toDo)
+ }
+ }
+
+ fun deleteTodo(toDo: TodoEntity) {
+ viewModelScope.launch {
+ Repository.deleteTodo(toDo)
+ }
+ }
+
+ /*fun deleteAllTodo() {
+ viewModelScope.launch {
+ Repository.deleteAllTodo()
+ }
+ }*/
+
+ fun setEditTodoItem(toDo: TodoEntity?) {
+ selectedEditTodo = toDo
+ }
+
+ /**
+ * 切换待办的选择状态
+ */
+ fun toggleTodoSelection(toDo: TodoEntity) {
+ _selectedTodoIds.update { idList ->
+ if (idList.contains(toDo.id)) {
+ // 若已经选择取消选择
+ idList - toDo.id
+ } else {
+ // 若未选择添加到列表中,立即选中
+ idList + toDo.id
+ }
+ }
+ }
+
+ /**
+ * 切换是否全选
+ */
+ fun toggleAllSelected() {
+ viewModelScope.launch {
+ toDos.firstOrNull()?.let { todos ->
+ val allIds = todos.map { it.id }
+ _selectedTodoIds.update { currentSelectedIds ->
+ if (currentSelectedIds.containsAll(allIds)) {
+ // 如果当前是全选状态,取消所有选择(切换为全不选)
+ emptyList()
+ } else {
+ // 如果当前不是全选状态,选中所有项
+ allIds
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 清除全部已选择的待办
+ */
+ fun clearAllTodoSelection() {
+ _selectedTodoIds.update { emptyList() }
+ }
+
+ /**
+ * 删除选择的待办
+ */
+ fun deleteSelectedTodo() {
+ viewModelScope.launch {
+ Repository.deleteTodoFromIds(selectedTodoIds.value)
+ clearAllTodoSelection()
+ }
+ }
+
+ fun playConfetti() {
+ showConfetti.value = true
+ }
+
+ /**
+ * 应用设置
+ */
+ fun setDarkMode(darkMode: DarkMode) {
+ appDarkMode = darkMode
+ }
+
+ fun setContrastLevel(contrastLevel: ContrastLevel) {
+ appContrastLevel = contrastLevel
+ }
+
+ fun setShowCompleted(show: Boolean) {
+ showCompletedTodos = show
+ }
+
+ fun setSortingMethod(sortingMethod: SortingMethod) {
+ appSortingMethod = sortingMethod
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPDelegates.kt b/app/src/main/kotlin/cn/super12138/todo/utils/SPDelegates.kt
similarity index 92%
rename from app/src/main/kotlin/cn/super12138/todo/utils/sp/SPDelegates.kt
rename to app/src/main/kotlin/cn/super12138/todo/utils/SPDelegates.kt
index b00405a..5af97cd 100644
--- a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPDelegates.kt
+++ b/app/src/main/kotlin/cn/super12138/todo/utils/SPDelegates.kt
@@ -1,4 +1,4 @@
-package cn.super12138.todo.utils.sp
+package cn.super12138.todo.utils
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/SPUtils.kt
similarity index 85%
rename from app/src/main/kotlin/cn/super12138/todo/utils/sp/SPUtils.kt
rename to app/src/main/kotlin/cn/super12138/todo/utils/SPUtils.kt
index dfbf247..453627f 100644
--- a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPUtils.kt
+++ b/app/src/main/kotlin/cn/super12138/todo/utils/SPUtils.kt
@@ -1,13 +1,13 @@
-package cn.super12138.todo.utils.sp
+package cn.super12138.todo.utils
import android.content.Context
import android.content.SharedPreferences
-import cn.super12138.todo.ToDoApp
-import cn.super12138.todo.constant.Constants
+import cn.super12138.todo.TodoApp
+import cn.super12138.todo.constants.Constants
object SPUtils {
private val sp: SharedPreferences by lazy {
- ToDoApp.context.getSharedPreferences(Constants.SP_NAME, Context.MODE_PRIVATE)
+ TodoApp.context.getSharedPreferences(Constants.SP_NAME, Context.MODE_PRIVATE)
}
fun getValue(name: String, default: T): T = with(sp) {
@@ -33,4 +33,5 @@ object SPUtils {
else -> throw IllegalArgumentException("This type can't be saved into Preferences")
}.apply()
}
-}
\ No newline at end of file
+}
+
diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/VersionUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt
similarity index 96%
rename from app/src/main/kotlin/cn/super12138/todo/utils/VersionUtils.kt
rename to app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt
index 56c6dc9..63401a4 100644
--- a/app/src/main/kotlin/cn/super12138/todo/utils/VersionUtils.kt
+++ b/app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt
@@ -3,7 +3,7 @@ package cn.super12138.todo.utils
import android.content.Context
import android.os.Build
-object VersionUtils {
+object SystemUtils {
/**
* 获取应用版本号
* @return 版本名称(版本代码)
diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/TextUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/TextUtils.kt
deleted file mode 100644
index 93adf32..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/utils/TextUtils.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package cn.super12138.todo.utils
-
-import android.text.Editable
-import cn.super12138.todo.R
-import cn.super12138.todo.ToDoApp
-
-object TextUtils {
- // 更换为id匹配(数据库)
- private val subjectMap = mapOf(
- globalGetString(R.string.subject_chinese) to R.id.subject_chinese,
- globalGetString(R.string.subject_math) to R.id.subject_math,
- globalGetString(R.string.subject_english) to R.id.subject_english,
- globalGetString(R.string.subject_biology) to R.id.subject_biology,
- globalGetString(R.string.subject_geography) to R.id.subject_geography,
- globalGetString(R.string.subject_history) to R.id.subject_history,
- globalGetString(R.string.subject_physics) to R.id.subject_physics,
- globalGetString(R.string.subject_chemistry) to R.id.subject_chemistry,
- globalGetString(R.string.subject_law) to R.id.subject_law,
- globalGetString(R.string.subject_other) to R.id.subject_other
- )
-
- fun getSubjectName(id: Int): String {
- return when (id) {
- R.id.subject_chinese -> globalGetString(R.string.subject_chinese)
- R.id.subject_math -> globalGetString(R.string.subject_math)
- R.id.subject_english -> globalGetString(R.string.subject_english)
- R.id.subject_biology -> globalGetString(R.string.subject_biology)
- R.id.subject_geography -> globalGetString(R.string.subject_geography)
- R.id.subject_history -> globalGetString(R.string.subject_history)
- R.id.subject_physics -> globalGetString(R.string.subject_physics)
- R.id.subject_chemistry -> globalGetString(R.string.subject_chemistry)
- R.id.subject_law -> globalGetString(R.string.subject_law)
- R.id.subject_other -> globalGetString(R.string.subject_other)
- else -> globalGetString(R.string.subject_unknown)
- }
- }
-
- fun getSubjectID(name: String): Int? {
- return subjectMap[name]
- }
-}
-
-fun globalGetString(resID: Int): String {
- return ToDoApp.context.resources.getString(resID)
-}
-
-fun String.toEditable(): Editable? {
- return Editable.Factory.getInstance().newEditable(this)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/Utils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/Utils.kt
deleted file mode 100644
index 6d8d67f..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/utils/Utils.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package cn.super12138.todo.utils
-
-import android.content.Context
-import android.os.SystemClock
-import android.view.View
-import android.widget.Toast
-import cn.super12138.todo.ToDoApp
-
-private var clickInterval = 380L
-private var lastTime = 0L
-
-/**
- * 延迟点击
- */
-fun View.setOnIntervalClickListener(onIntervalClickListener: (View) -> Unit) {
- this.setOnClickListener {
- if (SystemClock.elapsedRealtime() - lastTime > clickInterval) {
- lastTime = SystemClock.elapsedRealtime()
- onIntervalClickListener.invoke(it)
- }
- }
-}
-
-/**
- * Toast
- */
-fun String.showToast(
- context: Context = ToDoApp.context,
- duration: Int = Toast.LENGTH_SHORT
-) {
- Toast.makeText(context, this, duration).show()
-}
-
-fun Int.showToast(context: Context = ToDoApp.context, duration: Int = Toast.LENGTH_SHORT) {
- Toast.makeText(context, this, duration).show()
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt
index 6d4d4bb..6c8e0a5 100644
--- a/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt
+++ b/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt
@@ -2,15 +2,21 @@ package cn.super12138.todo.utils
import android.view.HapticFeedbackConstants
import android.view.View
-import cn.super12138.todo.constant.GlobalValues
+import cn.super12138.todo.constants.GlobalValues
object VibrationUtils {
+ private var isEnabled: Boolean = GlobalValues.hapticFeedback
+
+ fun setEnabled(enabled: Boolean) {
+ isEnabled = enabled
+ }
+
fun performHapticFeedback(
- view: View?,
- hapticFeedbackConstants: Int = HapticFeedbackConstants.KEYBOARD_TAP
+ view: View,
+ feedbackConstants: Int = HapticFeedbackConstants.CONTEXT_CLICK
) {
- if (GlobalValues.hapticFeedback) {
- view?.performHapticFeedback(hapticFeedbackConstants)
+ if (isEnabled) {
+ view.performHapticFeedback(feedbackConstants)
}
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/BaseActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/BaseActivity.kt
deleted file mode 100644
index 384583d..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/BaseActivity.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package cn.super12138.todo.views
-
-import android.os.Build
-import android.os.Bundle
-import android.view.WindowManager
-import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.viewbinding.ViewBinding
-import cn.super12138.todo.constant.GlobalValues
-
-abstract class BaseActivity : AppCompatActivity() {
- lateinit var binding: T
- override fun onCreate(savedInstanceState: Bundle?) {
- /*if (GlobalValues.springFestivalTheme) {
- setTheme(R.style.Theme_SpringFestival)
- }*/
- // enableEdgeToEdge(navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT))
- super.onCreate(savedInstanceState)
-
- // 确保 Navigation Bar 区域会被显示
- // WindowCompat.setDecorFitsSystemWindows(window, false)
-
- binding = getViewBinding()
- setContentView(binding.root)
-
- // 深色模式
- when (GlobalValues.darkMode) {
- "0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
-
- "1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
-
- "2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
- }
-
- // 适配刘海屏
- val lp = window.attributes
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- lp.layoutInDisplayCutoutMode =
- WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
- }
- window.attributes = lp
- }
-
- abstract fun getViewBinding(): T
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/BaseFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/BaseFragment.kt
deleted file mode 100644
index 2e58b76..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/BaseFragment.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package cn.super12138.todo.views
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.viewbinding.ViewBinding
-import com.google.android.material.color.MaterialColors
-import com.google.android.material.transition.MaterialSharedAxis
-
-abstract class BaseFragment : Fragment() {
- private var _binding: T? = null
- protected val binding get() = _binding!!
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true)
- returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false)
- exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true)
- reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false)
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- _binding = getViewBinding(inflater, container, false)
- return binding.root
- }
-
- // https://github.com/material-components/material-components-android/issues/1984#issuecomment-1089710991
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- // Overlap colors.
- view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.colorBackground))
- }
-
- abstract fun getViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- attachToRoot: Boolean
- ): T
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashActivity.kt
deleted file mode 100644
index b8cfce2..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashActivity.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package cn.super12138.todo.views.activities
-
-import android.os.Build
-import android.os.Bundle
-import cn.super12138.todo.databinding.ActivityCrashBinding
-import cn.super12138.todo.utils.VersionUtils
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.views.BaseActivity
-import java.text.SimpleDateFormat
-import java.util.Calendar
-import java.util.Locale
-
-class CrashActivity : BaseActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- val crashLogs = intent.getStringExtra("crash_logs")
-
- val deviceBrand = Build.BRAND
- val deviceModel = Build.MODEL
- val sdkLevel = Build.VERSION.SDK_INT
- val currentDateTime = Calendar.getInstance().time
- val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
- val formattedDateTime = formatter.format(currentDateTime)
-
- val deviceInfo = StringBuilder().apply {
- append("ToDo version: ").append(VersionUtils.getAppVersion(this@CrashActivity))
- .append('\n')
- append("Brand: ").append("").append(deviceBrand).append('\n')
- append("Model: ").append(deviceModel).append('\n')
- append("Device SDK: ").append(sdkLevel).append('\n').append('\n')
- append("Crash time: ").append(formattedDateTime).append('\n').append('\n')
- append("======beginning of crash======").append('\n')
- }
-
- binding.crashLog.text = StringBuilder().apply {
- append(deviceInfo)
- append(crashLogs)
- }
-
- binding.exitApp.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- this.finishAffinity()
- }
- }
-
- override fun getViewBinding(): ActivityCrashBinding {
- return ActivityCrashBinding.inflate(layoutInflater)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/activities/MainActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/activities/MainActivity.kt
deleted file mode 100644
index e718af2..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/activities/MainActivity.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package cn.super12138.todo.views.activities
-// 2023.11.18立项
-import android.app.ComponentCaller
-import android.content.Intent
-import android.os.Bundle
-import android.view.WindowManager
-import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.commit
-import cn.super12138.todo.R
-import cn.super12138.todo.constant.GlobalValues
-import cn.super12138.todo.databinding.ActivityMainBinding
-import cn.super12138.todo.views.BaseActivity
-import cn.super12138.todo.views.fragments.SettingsParentFragment
-import cn.super12138.todo.views.fragments.welcome.WelcomeFragment
-import de.raphaelebner.roomdatabasebackup.core.RoomBackup
-
-class MainActivity : BaseActivity() {
- lateinit var roomBackup: RoomBackup
- override fun onCreate(savedInstanceState: Bundle?) {
- installSplashScreen()
- super.onCreate(savedInstanceState)
-
- if (!GlobalValues.welcomePage) {
- startFragment(WelcomeFragment())
- }
-
- when (GlobalValues.secureMode) {
- true -> window.setFlags(
- WindowManager.LayoutParams.FLAG_SECURE,
- WindowManager.LayoutParams.FLAG_SECURE
- )
-
- false -> window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
- }
- handleIntent(intent)
-
- roomBackup = RoomBackup(this)
- }
-
- override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
- super.onNewIntent(intent, caller)
- handleIntent(intent)
- }
-
- override fun getViewBinding(): ActivityMainBinding {
- return ActivityMainBinding.inflate(layoutInflater)
- }
-
- fun startFragment(fragment: Fragment, args: (Bundle.() -> Unit)? = null) {
- supportFragmentManager.commit {
- addToBackStack(System.currentTimeMillis().toString())
- hide(supportFragmentManager.fragments.last())
- add(
- R.id.app_container,
- fragment.apply { args?.let { arguments = Bundle().apply(it) } }
- )
- }
- }
-
- private fun handleIntent(intent: Intent) {
- when (intent.action) {
- Intent.ACTION_APPLICATION_PREFERENCES -> startFragment(SettingsParentFragment())
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/adapters/AllTasksAdapter.kt b/app/src/main/kotlin/cn/super12138/todo/views/adapters/AllTasksAdapter.kt
deleted file mode 100644
index 260d503..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/adapters/AllTasksAdapter.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-package cn.super12138.todo.views.adapters
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Button
-import android.widget.LinearLayout
-import android.widget.TextView
-import androidx.core.content.ContextCompat
-import androidx.fragment.app.FragmentManager
-import androidx.recyclerview.widget.RecyclerView
-import cn.super12138.todo.R
-import cn.super12138.todo.ToDoApp
-import cn.super12138.todo.constant.Constants.DEFAULT_VIEW_TYPE
-import cn.super12138.todo.constant.Constants.EMPTY_VIEW_TYPE
-import cn.super12138.todo.logic.model.ToDo
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.views.fragments.InfoBottomSheet
-
-class AllTasksAdapter(
- private val todoList: MutableList,
- private val fragmentManager: FragmentManager
-) : RecyclerView.Adapter() {
- // 默认待办项
- inner class DefaultViewHolder(view: View) : RecyclerView.ViewHolder(view) {
- val todoContext: TextView = view.findViewById(R.id.todo_content)
- val todoSubject: TextView = view.findViewById(R.id.todo_subject)
- val itemBackground: LinearLayout = view.findViewById(R.id.item_background)
- val checkBtn: Button = view.findViewById(R.id.check_item_btn)
- }
-
- // 空项目提示
- inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
- return if (viewType == EMPTY_VIEW_TYPE) { // 如果列表是空的
- val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.item_empty, parent, false)
- EmptyViewHolder(view)
- } else {
- val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.item_todo, parent, false)
- DefaultViewHolder(view)
- }
- }
-
- override fun getItemViewType(position: Int): Int {
- return if (todoList.isEmpty()) EMPTY_VIEW_TYPE else DEFAULT_VIEW_TYPE
- }
-
- override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
- // 判断当前的holder是不是待办项目的holder
- if (holder is DefaultViewHolder) {
- val todo = todoList[position]
- holder.apply {
- checkBtn.visibility = View.GONE
- todoContext.text = todo.content
- todoSubject.text = todo.subject
- }
-
- if (todo.state == 1) {
- holder.itemBackground.background =
- ContextCompat.getDrawable(ToDoApp.context, R.drawable.bg_item_complete)
- } else {
- holder.itemBackground.background = null
- }
-
- holder.itemView.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- val infoBottomSheet = InfoBottomSheet.newInstance(
- todo.content,
- todo.subject,
- todo.state,
- todo.uuid
- )
- infoBottomSheet.show(fragmentManager, InfoBottomSheet.TAG)
- }
- }
- }
-
- override fun getItemCount(): Int {
- return if (todoList.isEmpty()) 1 else todoList.size
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/adapters/ToDoAdapter.kt b/app/src/main/kotlin/cn/super12138/todo/views/adapters/ToDoAdapter.kt
deleted file mode 100644
index 99c7537..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/adapters/ToDoAdapter.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-package cn.super12138.todo.views.adapters
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Button
-import android.widget.TextView
-import androidx.fragment.app.FragmentManager
-import androidx.lifecycle.ViewModelProvider
-import androidx.lifecycle.ViewModelStoreOwner
-import androidx.recyclerview.widget.RecyclerView
-import cn.super12138.todo.R
-import cn.super12138.todo.constant.Constants.DEFAULT_VIEW_TYPE
-import cn.super12138.todo.constant.Constants.EMPTY_VIEW_TYPE
-import cn.super12138.todo.constant.GlobalValues
-import cn.super12138.todo.logic.model.ToDo
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.utils.showToast
-import cn.super12138.todo.views.fragments.ToDoBottomSheet
-import cn.super12138.todo.views.viewmodels.ProgressViewModel
-import cn.super12138.todo.views.viewmodels.ToDoViewModel
-
-class ToDoAdapter(
- private val todoList: MutableList,
- private val viewModelStoreOwner: ViewModelStoreOwner,
- private val fragmentManager: FragmentManager
-) : RecyclerView.Adapter() {
- // 默认待办项
- inner class DefaultViewHolder(view: View) : RecyclerView.ViewHolder(view) {
- val todoContext: TextView = view.findViewById(R.id.todo_content)
- val todoSubject: TextView = view.findViewById(R.id.todo_subject)
- val checkToDoBtn: Button = view.findViewById(R.id.check_item_btn)
- }
-
- // 空项目提示
- inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
-
- // 判断列表是否为空,为空显示空项目提示
- override fun getItemViewType(position: Int): Int {
- return if (todoList.isEmpty()) EMPTY_VIEW_TYPE else DEFAULT_VIEW_TYPE
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
- return if (viewType == EMPTY_VIEW_TYPE) { // 如果列表是空的
- val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.item_empty, parent, false)
- EmptyViewHolder(view)
- } else {
- val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.item_todo, parent, false)
- DefaultViewHolder(view)
- }
- }
-
- override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
- // 判断当前的holder是不是待办项目的holder
- if (holder is DefaultViewHolder) {
- val todo = todoList[position]
- holder.todoContext.text = todo.content
- holder.todoSubject.text = todo.subject
-
- val progressViewModel =
- ViewModelProvider(viewModelStoreOwner)[ProgressViewModel::class.java]
- val todoViewModel =
- ViewModelProvider(viewModelStoreOwner)[ToDoViewModel::class.java]
-
- holder.checkToDoBtn.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- if (position >= todoList.size) {
- return@setOnClickListener
- }
-
- todoList.removeAt(position)
- notifyItemRemoved(position)
- notifyItemRangeChanged(position, todoList.size)
-
- todoViewModel.updateTaskState(todo.uuid)
-
- progressViewModel.updateProgress()
- }
-
- holder.itemView.setOnClickListener {
- if (GlobalValues.devMode) {
- VibrationUtils.performHapticFeedback(it)
- "Current position: $position".showToast()
- }
- }
-
- holder.itemView.setOnLongClickListener {
- val toDoBottomSheet = ToDoBottomSheet.newInstance(
- true,
- position,
- todo.uuid,
- todo.state,
- todo.subject,
- todo.content
- )
- toDoBottomSheet.show(fragmentManager, ToDoBottomSheet.TAG)
- true
- }
- }
- }
-
- override fun getItemCount(): Int {
- return if (todoList.isEmpty()) 1 else todoList.size
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AboutFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/AboutFragment.kt
deleted file mode 100644
index f5fb487..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AboutFragment.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-package cn.super12138.todo.views.fragments
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import cn.super12138.todo.constant.Constants
-import cn.super12138.todo.databinding.FragmentAboutBinding
-import cn.super12138.todo.utils.VersionUtils
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.utils.showToast
-import cn.super12138.todo.views.BaseFragment
-
-class AboutFragment : BaseFragment() {
- private var clickCount = 0
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- binding.appVersion.text = VersionUtils.getAppVersion(requireActivity())
-
- binding.toolBar.setNavigationOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- requireActivity().supportFragmentManager.popBackStack()
- }
-
- binding.checkUpdate.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- val intent = Intent(Intent.ACTION_VIEW).apply {
- data = Uri.parse(Constants.UPDATE_URL)
- }
- startActivity(intent)
- }
-
- binding.openSource.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- val intent = Intent(Intent.ACTION_VIEW).apply {
- data = Uri.parse(Constants.REPO_GITHUB_URL)
- }
- startActivity(intent)
- }
-
-
- binding.developerInfo.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- val intent = Intent(Intent.ACTION_VIEW).apply {
- data = Uri.parse(Constants.AUTHOR_GITHUB_URL)
- }
- startActivity(intent)
- }
-
- binding.appVersion.setOnClickListener {
- clickCount++
- when (clickCount) {
- 5 -> {
- clickCount = 0
- // GlobalValues.springFestivalTheme = !GlobalValues.springFestivalTheme
- "🍂".showToast()
- /*when (java.util.Calendar.getInstance(Locale.getDefault())
- .get(java.util.Calendar.MONTH) + 1) {
- 3, 4, 5 ->
- 6, 7, 8 -> SUMMER
- 9, 10, 11 -> AUTUMN
- 12, 1, 2 -> WINTER
- else -> -12
- }*/
- }
- }
- }
- }
-
- override fun getViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- attachToRoot: Boolean
- ): FragmentAboutBinding {
- return FragmentAboutBinding.inflate(inflater, container, attachToRoot)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AllTasksFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/AllTasksFragment.kt
deleted file mode 100644
index 0e69f0a..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AllTasksFragment.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package cn.super12138.todo.views.fragments
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.Observer
-import androidx.recyclerview.widget.LinearLayoutManager
-import cn.super12138.todo.ToDoApp
-import cn.super12138.todo.databinding.FragmentAllTasksBinding
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.views.BaseFragment
-import cn.super12138.todo.views.adapters.AllTasksAdapter
-import cn.super12138.todo.views.viewmodels.AllTasksViewModel
-import me.zhanghai.android.fastscroll.FastScrollerBuilder
-
-class AllTasksFragment : BaseFragment() {
- private val viewModel by viewModels()
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- val todoListAll = viewModel.todoListAll
- val layoutManager = LinearLayoutManager(ToDoApp.context)
- binding.allTasksList.layoutManager = layoutManager
- val adapter = AllTasksAdapter(todoListAll, childFragmentManager)
- binding.allTasksList.adapter = adapter
-
- FastScrollerBuilder(binding.allTasksList).apply {
- useMd2Style()
- build()
- }
-
- binding.toolBar.setNavigationOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- requireActivity().supportFragmentManager.popBackStack()
- }
-
- viewModel.refreshData.observe(viewLifecycleOwner, Observer {
- binding.allTasksList.adapter?.notifyItemRangeChanged(0, todoListAll.size + 1)
- })
- }
-
- override fun getViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- attachToRoot: Boolean
- ): FragmentAllTasksBinding {
- return FragmentAllTasksBinding.inflate(inflater, container, attachToRoot)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/InfoBottomSheet.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/InfoBottomSheet.kt
deleted file mode 100644
index a1f151a..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/InfoBottomSheet.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package cn.super12138.todo.views.fragments
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import cn.super12138.todo.R
-import cn.super12138.todo.constant.Constants
-import cn.super12138.todo.constant.GlobalValues
-import cn.super12138.todo.databinding.BottomSheetInfoBinding
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.android.material.bottomsheet.BottomSheetDialog
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment
-
-class InfoBottomSheet : BottomSheetDialogFragment() {
-
- private lateinit var binding: BottomSheetInfoBinding
- private lateinit var todoContent: String
- private lateinit var todoSubject: String
- private var todoState: Int = 0
- private lateinit var todoUUID: String
-
- companion object {
- const val TAG = Constants.TAG_INFO_BOTTOM_SHEET
- fun newInstance(
- todoContent: String,
- todoSubject: String,
- todoState: Int,
- todoUUID: String
- ) = InfoBottomSheet().apply {
- arguments = Bundle().apply {
- putString(Constants.BUNDLE_TODO_CONTENT, todoContent)
- putString(Constants.BUNDLE_TODO_SUBJECT, todoSubject)
- putInt(Constants.BUNDLE_TODO_STATE, todoState)
- putString(Constants.BUNDLE_TODO_UUID, todoUUID)
- }
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- arguments?.let {
- todoContent = it.getString(Constants.BUNDLE_TODO_CONTENT, "")
- todoSubject = it.getString(Constants.BUNDLE_TODO_SUBJECT, "")
- todoState = it.getInt(Constants.BUNDLE_TODO_STATE, 0)
- todoUUID = it.getString(Constants.BUNDLE_TODO_UUID, "")
- }
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- binding = BottomSheetInfoBinding.inflate(inflater, container, false)
-
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- val bottomSheetDialog = dialog as BottomSheetDialog
- bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
- bottomSheetDialog.dismissWithAnimation = true
-
- binding.todoContentInfo.text = todoContent
- binding.todoSubjectInfo.text = String.format(getString(R.string.info_subject), todoSubject)
- if (todoState == 0) {
- binding.todoState.text = getString(R.string.info_state_incomplete)
- } else {
- binding.todoState.text = getString(R.string.info_state_complete)
- }
- if (GlobalValues.devMode) {
- binding.todoUuid.apply {
- visibility = View.VISIBLE
- text = String.format(getString(R.string.info_uuid), todoUUID)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/MainFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/MainFragment.kt
deleted file mode 100644
index f21b23b..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/MainFragment.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package cn.super12138.todo.views.fragments
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import cn.super12138.todo.R
-import cn.super12138.todo.databinding.FragmentMainBinding
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.views.BaseFragment
-import cn.super12138.todo.views.activities.MainActivity
-
-class MainFragment : BaseFragment() {
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- binding.toolBar.setOnMenuItemClickListener { menuItem ->
- when (menuItem.itemId) {
- R.id.item_settings -> {
- (requireActivity() as MainActivity).startFragment(SettingsParentFragment())
-
- VibrationUtils.performHapticFeedback(binding.toolBar)
-
- true
- }
-
- else -> false
- }
- }
-
- // setSupportActionBar(binding.toolbar)
- }
-
- override fun getViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- attachToRoot: Boolean
- ): FragmentMainBinding {
- return FragmentMainBinding.inflate(inflater, container, attachToRoot)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ProgressFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/ProgressFragment.kt
deleted file mode 100644
index ebeed5f..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ProgressFragment.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package cn.super12138.todo.views.fragments
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.Observer
-import cn.super12138.todo.R
-import cn.super12138.todo.databinding.FragmentProgressBinding
-import cn.super12138.todo.views.BaseFragment
-import cn.super12138.todo.views.viewmodels.ProgressViewModel
-
-
-class ProgressFragment : BaseFragment() {
- private val viewModel: ProgressViewModel by viewModels({ requireActivity() })
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- viewModel.progress.observe(viewLifecycleOwner, Observer { value ->
- binding.progressBar.setProgressCompat(value, true)
- })
-
- viewModel.totalCount.observe(viewLifecycleOwner, Observer { total ->
- binding.totalCount.text = total.toString()
- })
-
- viewModel.completeCount.observe(viewLifecycleOwner, Observer { complete ->
- binding.completeCount.text = complete.toString()
- })
-
- viewModel.remainCount.observe(viewLifecycleOwner, Observer { remain ->
- if (remain == 0) {
- binding.remainCount.visibility = View.GONE
- } else {
- binding.remainCount.visibility = View.VISIBLE
- binding.remainCount.text =
- String.format(getString(R.string.remain_text), remain.toString())
- }
- })
-
- viewModel.updateProgress()
- }
-
- override fun getViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- attachToRoot: Boolean
- ): FragmentProgressBinding {
- return FragmentProgressBinding.inflate(inflater, container, attachToRoot)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsFragment.kt
deleted file mode 100644
index ccbd0f3..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsFragment.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-package cn.super12138.todo.views.fragments
-
-import android.content.Context
-import android.content.Intent
-import android.graphics.Color
-import android.graphics.drawable.ColorDrawable
-import android.graphics.drawable.Drawable
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.preference.ListPreference
-import androidx.preference.Preference
-import androidx.preference.PreferenceFragmentCompat
-import androidx.preference.SwitchPreferenceCompat
-import cn.super12138.todo.R
-import cn.super12138.todo.constant.Constants
-import cn.super12138.todo.constant.GlobalValues
-import cn.super12138.todo.logic.dao.ToDoRoomDB
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.views.activities.MainActivity
-import cn.super12138.todo.views.fragments.welcome.WelcomeFragment
-import com.google.android.material.snackbar.Snackbar
-import de.raphaelebner.roomdatabasebackup.core.RoomBackup
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
-import kotlin.system.exitProcess
-
-class SettingsFragment : PreferenceFragmentCompat() {
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.preferences, rootKey)
- val mainActivity = requireActivity() as MainActivity
- val roomBackup = mainActivity.roomBackup
-
- findPreference(Constants.PREF_DARK_MODE)?.apply {
- setOnPreferenceClickListener {
- VibrationUtils.performHapticFeedback(view)
- true
- }
- setOnPreferenceChangeListener { _, newValue ->
- when (newValue) {
- "0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
-
- "1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
-
- "2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
- }
- activity?.recreate()
- true
- }
- }
-
- findPreference(Constants.PREF_SECURE_MODE)?.apply {
- setOnPreferenceChangeListener { _, _ ->
- VibrationUtils.performHapticFeedback(view)
-
-
- view?.let {
- Snackbar.make(it, R.string.need_restart_app, Snackbar.LENGTH_LONG)
- .setAction(R.string.restart_app_now) {
- VibrationUtils.performHapticFeedback(view)
- restartApp(context)
- }
- .show()
- }
-
- true
- }
- }
-
- findPreference(Constants.PREF_HAPTIC_FEEDBACK)?.apply {
- setOnPreferenceChangeListener { _, _ ->
- VibrationUtils.performHapticFeedback(view)
-
- true
- }
- }
-
- findPreference(Constants.PREF_BACKUP_DB)?.apply {
- setOnPreferenceClickListener {
- VibrationUtils.performHapticFeedback(view)
- val simpleDateFormat =
- SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
- val formattedDate = simpleDateFormat.format(Date())
- roomBackup
- .database(ToDoRoomDB.getDatabase(requireContext()))
- .enableLogDebug(GlobalValues.devMode)
- .backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
- .customBackupFileName("ToDo-DataBase-$formattedDate.sqlite3")
- .apply {
- onCompleteListener { success, _, exitCode ->
- if (success) {
- view?.let { it1 ->
- Snackbar.make(
- it1, R.string.tips_backup_success, Snackbar.LENGTH_LONG
- ).show()
- }
- } else {
- view?.let { it1 ->
- Snackbar.make(
- it1, getString(
- R.string.tips_backup_failed,
- exitCode
- ), Snackbar.LENGTH_LONG
- ).show()
- }
- }
- }
- }
- .backup()
- true
- }
- }
-
- findPreference(Constants.PREF_RESTORE_DB)?.apply {
- setOnPreferenceClickListener {
- VibrationUtils.performHapticFeedback(view)
-
- roomBackup
- .database(ToDoRoomDB.getDatabase(requireContext()))
- .enableLogDebug(GlobalValues.devMode)
- .backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
- .apply {
- onCompleteListener { success, _, exitCode ->
- if (success) {
- view?.let { it1 ->
- Snackbar.make(
- it1,
- R.string.tips_restore_success,
- Snackbar.LENGTH_LONG
- )
- .setAction(R.string.restart_app_now) {
- VibrationUtils.performHapticFeedback(view)
- restartApp(context)
- }
- .show()
- }
-
- } else {
- view?.let { it1 ->
- Snackbar.make(
- it1, getString(
- R.string.tips_restore_failed,
- exitCode
- ), Snackbar.LENGTH_LONG
- ).show()
- }
- }
- }
- }
- .restore()
- true
- }
- }
-
- findPreference(Constants.PREF_ALL_TASKS)?.apply {
- setOnPreferenceClickListener {
- mainActivity.startFragment(AllTasksFragment())
- VibrationUtils.performHapticFeedback(view)
- true
- }
- }
-
- findPreference(Constants.PREF_REENTER_WELCOME_ACTIVITY)?.apply {
- setOnPreferenceClickListener {
- mainActivity.startFragment(WelcomeFragment())
- VibrationUtils.performHapticFeedback(view)
- true
- }
- }
-
- findPreference(Constants.PREF_ABOUT)?.apply {
- setOnPreferenceClickListener {
- mainActivity.startFragment(AboutFragment())
- VibrationUtils.performHapticFeedback(view)
- true
- }
- }
- }
-
- override fun setDivider(divider: Drawable?) {
- super.setDivider(ColorDrawable(Color.TRANSPARENT))
- }
-
- override fun setDividerHeight(height: Int) {
- super.setDividerHeight(0)
- }
-
- private fun restartApp(restartContext: Context) {
- val intent = Intent(restartContext, MainActivity::class.java).apply {
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- }
- restartContext.startActivity(intent)
- exitProcess(0)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsParentFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsParentFragment.kt
deleted file mode 100644
index 87b0c26..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsParentFragment.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package cn.super12138.todo.views.fragments
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import cn.super12138.todo.databinding.FragmentSettingsBinding
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.views.BaseFragment
-
-class SettingsParentFragment : BaseFragment() {
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- binding.toolBar.setNavigationOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- requireActivity().supportFragmentManager.popBackStack()
- }
- }
-
- override fun getViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- attachToRoot: Boolean
- ): FragmentSettingsBinding {
- return FragmentSettingsBinding.inflate(inflater, container, attachToRoot)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoBottomSheet.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoBottomSheet.kt
deleted file mode 100644
index 0138b2d..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoBottomSheet.kt
+++ /dev/null
@@ -1,192 +0,0 @@
-package cn.super12138.todo.views.fragments
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.viewModels
-import cn.super12138.todo.R
-import cn.super12138.todo.constant.Constants
-import cn.super12138.todo.constant.GlobalValues
-import cn.super12138.todo.databinding.BottomSheetTodoBinding
-import cn.super12138.todo.logic.dao.ToDoRoom
-import cn.super12138.todo.logic.model.ToDo
-import cn.super12138.todo.utils.TextUtils
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.utils.showToast
-import cn.super12138.todo.utils.toEditable
-import cn.super12138.todo.views.viewmodels.ProgressViewModel
-import cn.super12138.todo.views.viewmodels.ToDoViewModel
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.android.material.bottomsheet.BottomSheetDialog
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment
-import java.util.UUID
-
-class ToDoBottomSheet : BottomSheetDialogFragment() {
- private lateinit var binding: BottomSheetTodoBinding
-
- private val progressViewModel: ProgressViewModel by viewModels({ requireActivity() })
- private val todoViewModel: ToDoViewModel by viewModels({ requireActivity() })
-
- private var editMode: Boolean = false
- private var todoState: Int = 0
- private var todoPosition: Int = 0
- private lateinit var todoUUID: String
- private lateinit var todoOrigSubject: String
- private lateinit var todoOrigContent: String
-
- companion object {
- const val TAG = Constants.TAG_TODO_BOTTOM_SHEET
-
- fun newInstance(
- editMode: Boolean,
- todoPosition: Int,
- todoUUID: String,
- todoState: Int,
- todoSubject: String,
- todoContent: String,
- ) = ToDoBottomSheet().apply {
- arguments = Bundle().apply {
- putBoolean(Constants.BUNDLE_EDIT_MODE, editMode)
- putInt(Constants.BUNDLE_POSITION, todoPosition)
- putString(Constants.BUNDLE_TODO_UUID, todoUUID)
- putInt(Constants.BUNDLE_TODO_STATE, todoState)
- putString(Constants.BUNDLE_TODO_SUBJECT, todoSubject)
- putString(Constants.BUNDLE_TODO_CONTENT, todoContent)
- }
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- arguments?.let {
- editMode = it.getBoolean(Constants.BUNDLE_EDIT_MODE, false)
- todoPosition = it.getInt(Constants.BUNDLE_POSITION, 0)
- todoOrigContent = it.getString(Constants.BUNDLE_TODO_CONTENT, "")
- todoOrigSubject = it.getString(Constants.BUNDLE_TODO_SUBJECT, "")
- todoState = it.getInt(Constants.BUNDLE_TODO_STATE, 0)
- todoUUID = it.getString(Constants.BUNDLE_TODO_UUID, "")
- }
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- binding = BottomSheetTodoBinding.inflate(inflater, container, false)
-
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- // BottomSheet 基本参数设置
- val bottomSheetDialog = dialog as BottomSheetDialog
- bottomSheetDialog.behavior.apply {
- state = BottomSheetBehavior.STATE_EXPANDED
- saveFlags = BottomSheetBehavior.SAVE_ALL
- }
- bottomSheetDialog.dismissWithAnimation = true
-
- val todoList = todoViewModel.todoList
-
- binding.todoSubject.setOnCheckedStateChangeListener { _, _ ->
- VibrationUtils.performHapticFeedback(binding.todoSubject)
- }
-
- // 编辑模式
- if (editMode) {
- binding.btnCancel.visibility = View.GONE
- binding.btnDelete.visibility = View.VISIBLE
- binding.todoSheetTitle.text = getString(R.string.update_task)
- binding.todoContent.editText?.text = todoOrigContent.toEditable()
- binding.btnSave.text = getString(R.string.update)
-
- TextUtils.getSubjectID(todoOrigSubject)?.let { binding.todoSubject.check(it) }
-
- if (GlobalValues.devMode) {
- binding.todoUuid.apply {
- text = "UUID: $todoUUID"
- visibility = View.VISIBLE
- }
- }
- }
-
- binding.btnSave.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- val todoContent = binding.todoContent.editText?.text.toString()
- // 内容判空
- if (todoContent.isEmpty()) {
- binding.todoContent.error =
- getString(R.string.content_cannot_be_empty)
- return@setOnClickListener
- } else {
- // 开发者模式
- if (todoContent == Constants.STRING_DEV_MODE) {
- if (GlobalValues.devMode) {
- GlobalValues.devMode = false
- } else {
- GlobalValues.devMode = true
- "Dev Mode".showToast()
- }
- } else {
- // 随机 UUID
- val randomUUID = UUID.randomUUID().toString()
- // 待办学科
- val todoSubject = TextUtils.getSubjectName(binding.todoSubject.checkedChipId)
-
- // 更新待办
- if (editMode) {
- todoViewModel.updateTask(
- todoPosition,
- ToDoRoom(
- todoUUID,
- todoState,
- todoSubject,
- todoContent
- )
- )
- todoViewModel.refreshData.value = 1
- } else {
- // 添加到 RecyclerView
- todoList.add(
- ToDo(randomUUID, 0, todoContent, todoSubject)
- )
-
- // 插入数据库
- todoViewModel.insertTask(
- ToDoRoom(
- randomUUID,
- 0,
- todoSubject,
- todoContent
- )
- )
- progressViewModel.updateProgress()
- todoViewModel.addData.value = 1
- }
- }
- }
- dismiss()
- }
-
- binding.btnCancel.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- dismiss()
- }
-
- binding.btnDelete.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- todoViewModel.deleteTask(todoPosition, todoUUID)
- progressViewModel.updateProgress()
- todoViewModel.removeData.value = 1
-
- dismiss()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt
deleted file mode 100644
index 051252e..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-package cn.super12138.todo.views.fragments
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.updateLayoutParams
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import cn.super12138.todo.R
-import cn.super12138.todo.ToDoApp
-import cn.super12138.todo.databinding.FragmentTodoBinding
-import cn.super12138.todo.logic.Repository
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.views.adapters.ToDoAdapter
-import cn.super12138.todo.views.viewmodels.ProgressViewModel
-import cn.super12138.todo.views.viewmodels.ToDoViewModel
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import kotlinx.coroutines.launch
-import me.zhanghai.android.fastscroll.FastScrollerBuilder
-
-class ToDoFragment : Fragment() {
- private val progressViewModel: ProgressViewModel by viewModels({ requireActivity() })
- private val todoViewModel: ToDoViewModel by viewModels({ requireActivity() })
- private lateinit var binding: FragmentTodoBinding
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- binding = FragmentTodoBinding.inflate(inflater, container, false)
-
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- ViewCompat.setOnApplyWindowInsetsListener(binding.addItem) { v, windowInsets ->
- val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
- v.updateLayoutParams {
- leftMargin = if (insets.left == 0) 16 else insets.left
- bottomMargin = if (insets.bottom == 0) 48 else insets.bottom + 32
- rightMargin = if (insets.right == 0) 48 else insets.right + 32
- }
- WindowInsetsCompat.CONSUMED
- }
-
- FastScrollerBuilder(binding.todoList).apply {
- useMd2Style()
- build()
- }
-
- val layoutManager = LinearLayoutManager(ToDoApp.context)
- binding.todoList.layoutManager = layoutManager
- val todoList = todoViewModel.todoList
- val adapter = ToDoAdapter(todoList, requireActivity(), parentFragmentManager)
- binding.todoList.adapter = adapter
-
- binding.addItem.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- val toDoBottomSheet = ToDoBottomSheet()
- toDoBottomSheet.show(parentFragmentManager, ToDoBottomSheet.TAG)
- }
-
- binding.addItem.setOnLongClickListener {
- activity?.let { it1 ->
- MaterialAlertDialogBuilder(it1)
- .setTitle(R.string.warning)
- .setMessage(R.string.delete_confirm)
- .setPositiveButton(R.string.ok) { _, _ ->
- VibrationUtils.performHapticFeedback(it)
-
- // 清除 Recycler View
- todoList.clear()
- // 清除数据库
- lifecycleScope.launch {
- Repository.deleteAll()
- progressViewModel.updateProgress()
- }
- // 通知数据更改
- binding.todoList.adapter?.notifyDataSetChanged()
- }
- .setNegativeButton(R.string.cancel, null)
- .show()
- }
- true
- }
-
- binding.todoList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
- val fab = binding.addItem
- override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
- // 列表下滑,隐藏FAB
- if (dy > 0 && fab.isShown) {
- fab.hide()
- }
- // 列表上滑,显示FAB
- if (dy < 0 && !fab.isShown) {
- fab.show()
- }
- }
- })
-
- todoViewModel.addData.observe(viewLifecycleOwner) {
- binding.todoList.adapter?.notifyItemInserted(todoList.size + 1)
- }
-
- todoViewModel.removeData.observe(viewLifecycleOwner) {
- binding.todoList.adapter?.notifyItemRemoved(todoList.size + 1)
- }
-
- todoViewModel.refreshData.observe(viewLifecycleOwner) {
- binding.todoList.adapter?.notifyItemRangeChanged(0, todoList.size + 1)
- }
- }
-}
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/WelcomeFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/WelcomeFragment.kt
deleted file mode 100644
index 7fc68e8..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/WelcomeFragment.kt
+++ /dev/null
@@ -1,161 +0,0 @@
-package cn.super12138.todo.views.fragments.welcome
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.activity.OnBackPressedCallback
-import androidx.appcompat.content.res.AppCompatResources
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.commit
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import cn.super12138.todo.R
-import cn.super12138.todo.constant.GlobalValues
-import cn.super12138.todo.databinding.FragmentWelcomeBinding
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.utils.setOnIntervalClickListener
-import cn.super12138.todo.views.BaseFragment
-import cn.super12138.todo.views.activities.MainActivity
-import cn.super12138.todo.views.fragments.MainFragment
-import cn.super12138.todo.views.fragments.welcome.pages.IntroPage
-import cn.super12138.todo.views.fragments.welcome.pages.ProgressPage
-import cn.super12138.todo.views.fragments.welcome.pages.ToDoBtnPage
-import cn.super12138.todo.views.fragments.welcome.pages.ToDoItemPage
-import cn.super12138.todo.views.viewmodels.WelcomeViewModel
-import kotlinx.coroutines.launch
-
-class WelcomeFragment : BaseFragment() {
- private val viewModel by viewModels()
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- val previousBtn = binding.previousBtn
- val nextBtn = binding.nextBtn
- val centerBtn = binding.centerBtn
-
- val currentPage = viewModel.currentPage // 0: Intro 1: Progress 2: ToDo Btn 3: ToDo Item
-
- // 返回回调
- val callback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- if (currentPage.value == 0) {
- requireActivity().finishAffinity()
- } else {
- childFragmentManager.popBackStack()
- viewModel.decreasePage()
- }
- }
- }
-
-
- centerBtn.setOnIntervalClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- centerBtn.hide()
- nextBtn.show()
- previousBtn.show()
-
- if (currentPage.value == 3) {
- GlobalValues.welcomePage = true
- (requireActivity() as MainActivity).startFragment(MainFragment())
- } else {
- viewModel.setCurrentPage(1)
- nextPage(1)
- }
- }
-
- previousBtn.setOnIntervalClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- childFragmentManager.popBackStack()
-
- viewModel.decreasePage()
- }
-
- nextBtn.setOnIntervalClickListener {
- VibrationUtils.performHapticFeedback(it)
-
- nextPage(currentPage.value + 1)
-
- callback.isEnabled = false
- viewModel.increasePage()
- }
-
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.currentPage.collect { page ->
- when (page) {
- 0 -> {
- centerBtn.apply {
- text = getString(R.string.start)
- icon = AppCompatResources.getDrawable(
- requireContext(),
- R.drawable.ic_arrow_forward
- )
- show()
- }
- nextBtn.hide()
- previousBtn.hide()
- }
-
- in 1..2 -> {
- centerBtn.hide()
- previousBtn.show()
- nextBtn.show()
- }
-
- 3 -> {
- centerBtn.apply {
- text = getString(R.string.enter_app)
- icon = AppCompatResources.getDrawable(
- requireContext(),
- R.drawable.ic_focus
- )
- show()
- }
- previousBtn.show()
- nextBtn.hide()
- }
-
- else -> {
- if (page > 3) viewModel.setCurrentPage(3)
-
- if (page < 0) viewModel.setCurrentPage(0)
- }
- }
- }
- }
- }
- requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
- }
-
- private fun getCurrentPage(pageIndex: Int): Fragment {
- return when (pageIndex) {
- 0 -> IntroPage()
- 1 -> ProgressPage()
- 2 -> ToDoBtnPage()
- 3 -> ToDoItemPage()
- else -> IntroPage()
- }
- }
-
- private fun nextPage(page: Int) {
- childFragmentManager.commit {
- addToBackStack(System.currentTimeMillis().toString())
- hide(childFragmentManager.fragments.last())
- add(R.id.welcome_page_container, getCurrentPage(page))
- }
- }
-
- override fun getViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- attachToRoot: Boolean
- ): FragmentWelcomeBinding {
- return FragmentWelcomeBinding.inflate(inflater, container, attachToRoot)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/IntroPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/IntroPage.kt
deleted file mode 100644
index 515a337..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/IntroPage.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package cn.super12138.todo.views.fragments.welcome.pages
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import cn.super12138.todo.databinding.FragmentWelcomeIntroBinding
-import cn.super12138.todo.views.BaseFragment
-
-class IntroPage : BaseFragment() {
- override fun getViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- attachToRoot: Boolean
- ): FragmentWelcomeIntroBinding {
- return FragmentWelcomeIntroBinding.inflate(inflater, container, attachToRoot)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ProgressPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ProgressPage.kt
deleted file mode 100644
index 588ba0d..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ProgressPage.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package cn.super12138.todo.views.fragments.welcome.pages
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import cn.super12138.todo.databinding.FragmentWelcomeProgressBinding
-import cn.super12138.todo.views.BaseFragment
-
-class ProgressPage : BaseFragment() {
- override fun getViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- attachToRoot: Boolean
- ): FragmentWelcomeProgressBinding {
- return FragmentWelcomeProgressBinding.inflate(inflater, container, attachToRoot)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoBtnPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoBtnPage.kt
deleted file mode 100644
index d4ea77c..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoBtnPage.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package cn.super12138.todo.views.fragments.welcome.pages
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import cn.super12138.todo.databinding.FragmentWelcomeTodoBtnBinding
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.views.BaseFragment
-
-class ToDoBtnPage : BaseFragment() {
- override fun getViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- attachToRoot: Boolean
- ): FragmentWelcomeTodoBtnBinding {
- return FragmentWelcomeTodoBtnBinding.inflate(inflater, container, attachToRoot)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
-
- binding.addItem.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
- }
-
- binding.addItem.setOnLongClickListener {
- true
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoItemPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoItemPage.kt
deleted file mode 100644
index baa168a..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoItemPage.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package cn.super12138.todo.views.fragments.welcome.pages
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import cn.super12138.todo.databinding.FragmentWelcomeTodoItemBinding
-import cn.super12138.todo.utils.VibrationUtils
-import cn.super12138.todo.views.BaseFragment
-
-class ToDoItemPage : BaseFragment() {
- override fun getViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- attachToRoot: Boolean
- ): FragmentWelcomeTodoItemBinding {
- return FragmentWelcomeTodoItemBinding.inflate(inflater, container, attachToRoot)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.checkItemBtn.setOnClickListener {
- VibrationUtils.performHapticFeedback(it)
- }
- binding.todoItem.setOnLongClickListener {
- true
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/AllTasksViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/AllTasksViewModel.kt
deleted file mode 100644
index eeac4f6..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/AllTasksViewModel.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package cn.super12138.todo.views.viewmodels
-
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import cn.super12138.todo.logic.Repository
-import cn.super12138.todo.logic.model.ToDo
-import kotlinx.coroutines.launch
-
-class AllTasksViewModel : ViewModel() {
- val todoListAll = ArrayList()
- val refreshData = MutableLiveData(0)
-
- init {
- loadToDos()
- }
-
- private fun loadToDos() {
- viewModelScope.launch {
- val todos = Repository.getAll()
- for (todo in todos) {
- todoListAll.add(ToDo(todo.uuid, todo.state, todo.content, todo.subject))
- }
- if (todoListAll.size > 0) {
- refreshData.value = 1
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ProgressViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ProgressViewModel.kt
deleted file mode 100644
index ab45d6a..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ProgressViewModel.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package cn.super12138.todo.views.viewmodels
-
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import cn.super12138.todo.logic.Repository
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-class ProgressViewModel : ViewModel() {
- val totalCount: MutableLiveData = MutableLiveData()
- val completeCount: MutableLiveData = MutableLiveData()
- val remainCount: MutableLiveData = MutableLiveData()
- val progress: MutableLiveData = MutableLiveData()
-
- fun updateProgress() {
- viewModelScope.launch {
- delay(30)
- val total = Repository.getAll().size
- val complete = Repository.getAllComplete().size
-
- val calcProgress = (complete.toDouble() / total.toDouble()) * 100
- progress.postValue(calcProgress.toInt())
- completeCount.postValue(complete)
- totalCount.postValue(total)
- remainCount.postValue(total - complete)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ToDoViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ToDoViewModel.kt
deleted file mode 100644
index 6111382..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ToDoViewModel.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package cn.super12138.todo.views.viewmodels
-
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import cn.super12138.todo.logic.Repository
-import cn.super12138.todo.logic.dao.ToDoRoom
-import cn.super12138.todo.logic.model.ToDo
-import kotlinx.coroutines.launch
-
-class ToDoViewModel : ViewModel() {
- val addData = MutableLiveData(0)
- val removeData = MutableLiveData(0)
- val refreshData = MutableLiveData(0)
-
- val todoList = ArrayList()
-
- init {
- loadTasks()
- }
-
- private fun loadTasks() {
- viewModelScope.launch {
- val todos = Repository.getAllIncomplete()
- for (todo in todos) {
- todoList.add(ToDo(todo.uuid, todo.state, todo.content, todo.subject))
- }
- if (todoList.size > 0) {
- refreshData.value = 1
- }
- }
- }
-
- fun deleteTask(position: Int, uuid: String) {
- todoList.removeAt(position)
- viewModelScope.launch {
- Repository.deleteByUUID(uuid)
- }
- }
-
- fun insertTask(todo: ToDoRoom) {
- viewModelScope.launch {
- Repository.insert(todo)
- }
- }
-
- fun updateTaskState(uuid: String) {
- viewModelScope.launch {
- Repository.updateStateByUUID(uuid)
- }
- }
-
- fun updateTask(position: Int, todo: ToDoRoom) {
- todoList.removeAt(position)
- todoList.add(
- position,
- ToDo(
- todo.uuid,
- todo.state,
- todo.content,
- todo.subject
- )
- )
- viewModelScope.launch {
- Repository.update(todo)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/WelcomeViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/WelcomeViewModel.kt
deleted file mode 100644
index e8d1f7a..0000000
--- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/WelcomeViewModel.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package cn.super12138.todo.views.viewmodels
-
-import androidx.lifecycle.ViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-class WelcomeViewModel : ViewModel() {
- private val _currentPage = MutableStateFlow(0)
- val currentPage = _currentPage.asStateFlow()
-
- fun setCurrentPage(page: Int) {
- _currentPage.value = page
- }
-
- fun increasePage() {
- _currentPage.value += 1
- }
-
- fun decreasePage() {
- _currentPage.value -= 1
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionHelper.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionHelper.kt
deleted file mode 100644
index 3226c09..0000000
--- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionHelper.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-package de.raphaelebner.roomdatabasebackup.core
-
-import android.annotation.SuppressLint
-import android.content.SharedPreferences
-import java.io.*
-import java.security.NoSuchAlgorithmException
-import java.security.spec.InvalidKeySpecException
-import java.security.spec.KeySpec
-import javax.crypto.SecretKey
-import javax.crypto.SecretKeyFactory
-import javax.crypto.spec.PBEKeySpec
-import javax.crypto.spec.SecretKeySpec
-
-/**
- * MIT License
- *
- * Copyright (c) 2024 Raphael Ebner
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
- * associated documentation files (the "Software"), to deal in the Software without restriction,
- * including without limitation the rights to use, copy, modify, merge, publish, distribute,
- * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all copies or
- * substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
- * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
- * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-class AESEncryptionHelper {
-
- companion object {
- private const val BACKUP_SECRET_KEY = "backupsecretkey"
- private const val TAG = "debug_AESEncryptionHelper"
- }
-
- /**
- * This method will convert a file to ByteArray
- * @param file : the Path where the file is located
- * @return ByteArray of the file
- */
- @Throws(Exception::class)
- fun readFile(file: File): ByteArray {
- val fileContents = file.readBytes()
- val inputBuffer = BufferedInputStream(FileInputStream(file))
- inputBuffer.read(fileContents)
- inputBuffer.close()
- return fileContents
- }
-
- /**
- * This method will convert a ByteArray to a file, and saves it to the path
- * @param fileData : the ByteArray
- * @param file : the path where the ByteArray should be saved
- */
- @Throws(Exception::class)
- fun saveFile(fileData: ByteArray, file: File) {
- val bos = BufferedOutputStream(FileOutputStream(file, false))
- bos.write(fileData)
- bos.flush()
- bos.close()
- }
-
- /**
- * This method will convert a random password, saved in sharedPreferences to a SecretKey
- * @param sharedPref : the sharedPref, to fetch / save the key
- * @param iv : the encryption nonce
- * @return SecretKey
- */
- @SuppressLint("ApplySharedPref")
- fun getSecretKey(sharedPref: SharedPreferences, iv: ByteArray): SecretKey {
-
- // get key: String from sharedpref
- var password = sharedPref.getString(BACKUP_SECRET_KEY, null)
-
- // If no key is stored in shared pref, create one and save it
- if (password == null) {
- // generate random string
- val stringLength = 15
- val charset = ('a'..'z') + ('A'..'Z') + ('1'..'9')
- password = (1..stringLength).map { charset.random() }.joinToString("")
-
- val secretKey = generateSecretKey(password, iv)
- // the key can be saved plain, because i am using EncryptedSharedPreferences
- val editor = sharedPref.edit()
- editor.putString(BACKUP_SECRET_KEY, password)
- // I use .commit because when using .apply the needed app restart is faster then apply
- // and the preferences wont be saved
- editor.commit()
-
- return secretKey
- }
-
- // generate secretKey, and return it
- return generateSecretKey(password, iv)
- }
-
- /**
- * This method will convert a custom password to a SecretKey
- * @param encryptPassword : the custom user password as String
- * @param iv : the encryption nonce
- * @return SecretKey
- */
- fun getSecretKeyWithCustomPw(encryptPassword: String, iv: ByteArray): SecretKey {
- // generate secretKey, and return it
- return generateSecretKey(encryptPassword, iv)
- }
-
- /**
- * Function to generate a 128 bit key from the given password and iv
- * @param password
- * @param iv
- * @return Secret key
- * @throws NoSuchAlgorithmException
- * @throws InvalidKeySpecException
- */
- @Throws(NoSuchAlgorithmException::class, InvalidKeySpecException::class)
- private fun generateSecretKey(password: String, iv: ByteArray?): SecretKey {
- // convert random string to secretKey
- val spec: KeySpec = PBEKeySpec(password.toCharArray(), iv, 65536, 128) // AES-128
- val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
- val key = secretKeyFactory.generateSecret(spec).encoded
- return SecretKeySpec(key, "AES")
- }
-}
diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionManager.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionManager.kt
deleted file mode 100644
index 7611362..0000000
--- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionManager.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-package de.raphaelebner.roomdatabasebackup.core
-
-import android.content.SharedPreferences
-import java.nio.ByteBuffer
-import java.security.InvalidAlgorithmParameterException
-import java.security.InvalidKeyException
-import java.security.NoSuchAlgorithmException
-import java.security.SecureRandom
-import java.security.spec.InvalidKeySpecException
-import javax.crypto.BadPaddingException
-import javax.crypto.Cipher
-import javax.crypto.IllegalBlockSizeException
-import javax.crypto.NoSuchPaddingException
-import javax.crypto.spec.GCMParameterSpec
-
-/**
- * Encryption / Decryption service using the AES algorithm
- * example for nullbeans.com
- * https://nullbeans.com/how-to-encrypt-decrypt-files-byte-arrays-in-java-using-aes-gcm/#Generating_an_AES_key
- */
-class AESEncryptionManager {
-
- private val aesEncryptionHelper = AESEncryptionHelper()
-
- /**
- * This method will encrypt the given data
- * @param sharedPref : the sharedPref, to fetch the key
- * @param data : the data that will be encrypted
- * @return Encrypted data in a byte array
- */
- @Throws(
- NoSuchPaddingException::class,
- NoSuchAlgorithmException::class,
- InvalidAlgorithmParameterException::class,
- InvalidKeyException::class,
- BadPaddingException::class,
- IllegalBlockSizeException::class,
- InvalidKeySpecException::class
- )
- fun encryptData(sharedPref: SharedPreferences, encryptPassword: String?, data: ByteArray): ByteArray {
-
- //Prepare the nonce
- val secureRandom = SecureRandom()
-
- //Noonce should be 12 bytes
- val iv = ByteArray(12)
- secureRandom.nextBytes(iv)
-
- //Prepare your key/password
- val secretKey = if (encryptPassword != null) { aesEncryptionHelper.getSecretKeyWithCustomPw(encryptPassword, iv)
- } else aesEncryptionHelper.getSecretKey(sharedPref, iv)
-
- val cipher = Cipher.getInstance("AES/GCM/NoPadding")
- val parameterSpec = GCMParameterSpec(128, iv)
-
- //Encryption mode on!
- cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec)
-
- //Encrypt the data
- val encryptedData = cipher.doFinal(data)
-
- //Concatenate everything and return the final data
- val byteBuffer = ByteBuffer.allocate(4 + iv.size + encryptedData.size)
- byteBuffer.putInt(iv.size)
- byteBuffer.put(iv)
- byteBuffer.put(encryptedData)
- return byteBuffer.array()
- }
-
- /**
- * This method will decrypt the given data
- * @param sharedPref : the sharedPref, to fetch the key
- * @param encryptedData : the data that will be decrypted
- * @return decrypted data in a byte array
- */
- @Throws(
- NoSuchPaddingException::class,
- NoSuchAlgorithmException::class,
- InvalidAlgorithmParameterException::class,
- InvalidKeyException::class,
- BadPaddingException::class,
- IllegalBlockSizeException::class,
- InvalidKeySpecException::class
- )
- fun decryptData(sharedPref: SharedPreferences, encryptPassword: String?, encryptedData: ByteArray): ByteArray {
-
-
- //Wrap the data into a byte buffer to ease the reading process
- val byteBuffer = ByteBuffer.wrap(encryptedData)
- val noonceSize = byteBuffer.int
-
- //Make sure that the file was encrypted properly
- require(!(noonceSize < 12 || noonceSize >= 16)) { "Nonce size is incorrect. Make sure that the incoming data is an AES encrypted file." }
- val iv = ByteArray(noonceSize)
- byteBuffer[iv]
-
- //Prepare your key/password
- val secretKey = if (encryptPassword != null) { aesEncryptionHelper.getSecretKeyWithCustomPw(encryptPassword, iv)
- } else aesEncryptionHelper.getSecretKey(sharedPref, iv)
-
- //get the rest of encrypted data
- val cipherBytes = ByteArray(byteBuffer.remaining())
- byteBuffer[cipherBytes]
- val cipher = Cipher.getInstance("AES/GCM/NoPadding")
- val parameterSpec = GCMParameterSpec(128, iv)
-
- //Encryption mode on!
- cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec)
-
- //Encrypt the data
- return cipher.doFinal(cipherBytes)
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/OnCompleteListener.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/OnCompleteListener.kt
deleted file mode 100644
index 43dd44a..0000000
--- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/OnCompleteListener.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-package de.raphaelebner.roomdatabasebackup.core
-
-import de.raphaelebner.roomdatabasebackup.core.RoomBackup.Companion.BACKUP_FILE_LOCATION_CUSTOM_FILE
-
-/**
- * MIT License
- *
- * Copyright (c) 2024 Raphael Ebner
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
- * associated documentation files (the "Software"), to deal in the Software without restriction,
- * including without limitation the rights to use, copy, modify, merge, publish, distribute,
- * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all copies or
- * substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
- * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
- * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-interface OnCompleteListener {
- fun onComplete(success: Boolean, message: String, exitCode: Int)
-
- companion object {
-
- /** Other Error */
- const val EXIT_CODE_ERROR = 1
-
- /** Error while choosing backup to restore. Maybe no file selected */
- const val EXIT_CODE_ERROR_BACKUP_FILE_CHOOSER = 2
-
- /** Error while choosing backup file to create. Maybe no file selected */
- const val EXIT_CODE_ERROR_BACKUP_FILE_CREATOR = 3
-
- /**
- * [BACKUP_FILE_LOCATION_CUSTOM_FILE] is set but [RoomBackup.backupLocationCustomFile] is
- * not set
- */
- const val EXIT_CODE_ERROR_BACKUP_LOCATION_FILE_MISSING = 4
-
- /** [RoomBackup.backupLocation] is not set */
- const val EXIT_CODE_ERROR_BACKUP_LOCATION_MISSING = 5
-
- /** Restore dialog for internal/external storage was canceled by user */
- const val EXIT_CODE_ERROR_BY_USER_CANCELED = 6
-
- /** Cannot decrypt provided backup file */
- const val EXIT_CODE_ERROR_DECRYPTION_ERROR = 7
-
- /** Cannot encrypt database backup */
- const val EXIT_CODE_ERROR_ENCRYPTION_ERROR = 8
-
- /**
- * You tried to restore a encrypted backup but [RoomBackup.backupIsEncrypted] is set to
- * false
- */
- const val EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED = 9
-
- /** No backups to restore are available in internal/external sotrage */
- const val EXIT_CODE_ERROR_RESTORE_NO_BACKUPS_AVAILABLE = 10
-
- /** No room database to backup is provided */
- const val EXIT_CODE_ERROR_ROOM_DATABASE_MISSING = 11
-
- /** Storage permissions not granted for custom dialog */
- const val EXIT_CODE_ERROR_STORAGE_PERMISSONS_NOT_GRANTED = 12
-
- /** Cannot decrypt provided backup file because the password is incorrect */
- const val EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD = 13
-
- /** No error, action successful */
- const val EXIT_CODE_SUCCESS = 0
- }
-}
diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt
deleted file mode 100644
index a6b7018..0000000
--- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt
+++ /dev/null
@@ -1,853 +0,0 @@
-package de.raphaelebner.roomdatabasebackup.core
-
-import android.app.Activity
-import android.app.AlertDialog
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.os.Build
-import android.util.Log
-import android.widget.Toast
-import androidx.activity.ComponentActivity
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
-import androidx.room.RoomDatabase
-import androidx.security.crypto.EncryptedSharedPreferences
-import androidx.security.crypto.MasterKey
-import kotlinx.coroutines.runBlocking
-import java.io.BufferedOutputStream
-import java.io.File
-import java.io.FileOutputStream
-import java.io.IOException
-import java.io.InputStream
-import java.io.OutputStream
-import java.text.SimpleDateFormat
-import java.util.Calendar
-import java.util.Locale
-import javax.crypto.BadPaddingException
-
-/**
- * MIT License
- *
- * Copyright (c) 2024 Raphael Ebner
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
- * associated documentation files (the "Software"), to deal in the Software without restriction,
- * including without limitation the rights to use, copy, modify, merge, publish, distribute,
- * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all copies or
- * substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
- * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
- * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-class RoomBackup(var context: Context) {
-
- companion object {
- private const val SHARED_PREFS = "de.raphaelebner.roomdatabasebackup"
- private var TAG = "debug_RoomBackup"
- private lateinit var INTERNAL_BACKUP_PATH: File
- private lateinit var TEMP_BACKUP_PATH: File
- private lateinit var TEMP_BACKUP_FILE: File
- private lateinit var EXTERNAL_BACKUP_PATH: File
- private lateinit var DATABASE_FILE: File
-
- private var currentProcess: Int? = null
- private const val PROCESS_BACKUP = 1
- private const val PROCESS_RESTORE = 2
- private var backupFilename: String? = null
-
- /** Code for internal backup location, used for [backupLocation] */
- const val BACKUP_FILE_LOCATION_INTERNAL = 1
-
- /** Code for external backup location, used for [backupLocation] */
- const val BACKUP_FILE_LOCATION_EXTERNAL = 2
-
- /** Code for custom backup location dialog, used for [backupLocation] */
- const val BACKUP_FILE_LOCATION_CUSTOM_DIALOG = 3
-
- /** Code for custom backup file location, used for [backupLocation] */
- const val BACKUP_FILE_LOCATION_CUSTOM_FILE = 4
- }
-
- private lateinit var sharedPreferences: SharedPreferences
- private lateinit var dbName: String
-
- private var roomDatabase: RoomDatabase? = null
- private var enableLogDebug: Boolean = false
- private var restartIntent: Intent? = null
- private var onCompleteListener: OnCompleteListener? = null
- private var customRestoreDialogTitle: String = "Choose file to restore"
- private var customBackupFileName: String? = null
- private var backupIsEncrypted: Boolean = false
- private var maxFileCount: Int? = null
- private var encryptPassword: String? = null
- private var backupLocation: Int = BACKUP_FILE_LOCATION_INTERNAL
- private var backupLocationCustomFile: File? = null
-
- /**
- * Set RoomDatabase instance
- *
- * @param roomDatabase RoomDatabase
- */
- fun database(roomDatabase: RoomDatabase): RoomBackup {
- this.roomDatabase = roomDatabase
- return this
- }
-
- /**
- * Set LogDebug enabled / disabled
- *
- * @param enableLogDebug Boolean
- */
- fun enableLogDebug(enableLogDebug: Boolean): RoomBackup {
- this.enableLogDebug = enableLogDebug
- return this
- }
-
- /**
- * Set Intent in which to boot after App restart
- *
- * @param restartIntent Intent
- */
- fun restartApp(restartIntent: Intent): RoomBackup {
- this.restartIntent = restartIntent
- restartApp()
- return this
- }
-
- /**
- * Set onCompleteListener, to run code when tasks completed
- *
- * @param onCompleteListener OnCompleteListener
- */
- fun onCompleteListener(onCompleteListener: OnCompleteListener): RoomBackup {
- this.onCompleteListener = onCompleteListener
- return this
- }
-
- /**
- * Set onCompleteListener, to run code when tasks completed
- *
- * @param listener (success: Boolean, message: String) -> Unit
- */
- fun onCompleteListener(
- listener: (success: Boolean, message: String, exitCode: Int) -> Unit
- ): RoomBackup {
- this.onCompleteListener =
- object : OnCompleteListener {
- override fun onComplete(success: Boolean, message: String, exitCode: Int) {
- listener(success, message, exitCode)
- }
- }
- return this
- }
-
- /**
- * Set custom log tag, for detailed debugging
- *
- * @param customLogTag String
- */
- fun customLogTag(customLogTag: String): RoomBackup {
- TAG = customLogTag
- return this
- }
-
- /**
- * Set custom Restore Dialog Title, default = "Choose file to restore"
- *
- * @param customRestoreDialogTitle String
- */
- fun customRestoreDialogTitle(customRestoreDialogTitle: String): RoomBackup {
- this.customRestoreDialogTitle = customRestoreDialogTitle
- return this
- }
-
- /**
- * Set custom Backup File Name, default = "$dbName-$currentTime.sqlite3"
- *
- * @param customBackupFileName String
- */
- fun customBackupFileName(customBackupFileName: String): RoomBackup {
- this.customBackupFileName = customBackupFileName
- return this
- }
-
- /**
- * Set you backup location. Available values see: [BACKUP_FILE_LOCATION_INTERNAL],
- * [BACKUP_FILE_LOCATION_EXTERNAL], [BACKUP_FILE_LOCATION_CUSTOM_DIALOG] or
- * [BACKUP_FILE_LOCATION_CUSTOM_FILE]
- *
- * @param backupLocation Int, default = [BACKUP_FILE_LOCATION_INTERNAL]
- */
- fun backupLocation(backupLocation: Int): RoomBackup {
- this.backupLocation = backupLocation
- return this
- }
-
- /**
- * Set a custom file where to save or restore a backup. can be used for backup and restore
- *
- * Only available if [backupLocation] is set to [BACKUP_FILE_LOCATION_CUSTOM_FILE]
- *
- * @param backupLocationCustomFile File
- */
- fun backupLocationCustomFile(backupLocationCustomFile: File): RoomBackup {
- this.backupLocationCustomFile = backupLocationCustomFile
- return this
- }
-
- /**
- * Set file encryption to true / false can be used for backup and restore
- *
- * @param backupIsEncrypted Boolean, default = false
- */
- fun backupIsEncrypted(backupIsEncrypted: Boolean): RoomBackup {
- this.backupIsEncrypted = backupIsEncrypted
- return this
- }
-
- /**
- * Set max backup files count if fileCount is > maxFileCount the oldest backup file will be
- * deleted is for both internal and external storage
- *
- * @param maxFileCount Int, default = null
- */
- fun maxFileCount(maxFileCount: Int): RoomBackup {
- this.maxFileCount = maxFileCount
- return this
- }
-
- /**
- * Set custom backup encryption password
- *
- * @param encryptPassword String
- */
- fun customEncryptPassword(encryptPassword: String): RoomBackup {
- this.encryptPassword = encryptPassword
- return this
- }
-
- /** Init vars, and return true if no error occurred */
- private fun initRoomBackup(): Boolean {
- if (roomDatabase == null) {
- if (enableLogDebug) Log.d(TAG, "roomDatabase is missing")
- onCompleteListener?.onComplete(
- false,
- "roomDatabase is missing",
- OnCompleteListener.EXIT_CODE_ERROR_ROOM_DATABASE_MISSING
- )
- // throw IllegalArgumentException("roomDatabase is not initialized")
- return false
- }
-
- // Create or retrieve the Master Key for encryption/decryption
- val masterKeyAlias =
- MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
-
- if (backupLocation !in
- listOf(
- BACKUP_FILE_LOCATION_INTERNAL,
- BACKUP_FILE_LOCATION_EXTERNAL,
- BACKUP_FILE_LOCATION_CUSTOM_DIALOG,
- BACKUP_FILE_LOCATION_CUSTOM_FILE
- )
- ) {
- if (enableLogDebug) Log.d(TAG, "backupLocation is missing")
- onCompleteListener?.onComplete(
- false,
- "backupLocation is missing",
- OnCompleteListener.EXIT_CODE_ERROR_BACKUP_LOCATION_MISSING
- )
- return false
- }
-
- if (backupLocation == BACKUP_FILE_LOCATION_CUSTOM_FILE && backupLocationCustomFile == null
- ) {
- if (enableLogDebug)
- Log.d(
- TAG,
- "backupLocation is set to custom backup file, but no file is defined"
- )
- onCompleteListener?.onComplete(
- false,
- "backupLocation is set to custom backup file, but no file is defined",
- OnCompleteListener.EXIT_CODE_ERROR_BACKUP_LOCATION_FILE_MISSING
- )
- return false
- }
-
- // Initialize/open an instance of EncryptedSharedPreferences
- // Encryption key is stored in plain text in an EncryptedSharedPreferences --> it is saved
- // encrypted
- sharedPreferences =
- EncryptedSharedPreferences.create(
- context,
- SHARED_PREFS,
- masterKeyAlias,
- EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
- EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
- )
-
- dbName = roomDatabase!!.openHelper.databaseName!!
- INTERNAL_BACKUP_PATH = File("${context.filesDir}/databasebackup/")
- TEMP_BACKUP_PATH = File("${context.filesDir}/databasebackup-temp/")
- TEMP_BACKUP_FILE = File("$TEMP_BACKUP_PATH/tempbackup.sqlite3")
- EXTERNAL_BACKUP_PATH = File(context.getExternalFilesDir("backup")!!.toURI())
- DATABASE_FILE = File(context.getDatabasePath(dbName).toURI())
-
- // Create internal and temp backup directory if does not exist
- try {
- INTERNAL_BACKUP_PATH.mkdirs()
- TEMP_BACKUP_PATH.mkdirs()
- } catch (_: FileAlreadyExistsException) {
- } catch (_: IOException) {
- }
-
- if (enableLogDebug) {
- Log.d(TAG, "DatabaseName: $dbName")
- Log.d(TAG, "Database Location: $DATABASE_FILE")
- Log.d(TAG, "INTERNAL_BACKUP_PATH: $INTERNAL_BACKUP_PATH")
- Log.d(TAG, "EXTERNAL_BACKUP_PATH: $EXTERNAL_BACKUP_PATH")
- if (backupLocationCustomFile != null)
- Log.d(TAG, "backupLocationCustomFile: $backupLocationCustomFile")
- }
- return true
- }
-
- /** restart App with custom Intent */
- private fun restartApp() {
- restartIntent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- context.startActivity(restartIntent)
- if (context is Activity) {
- (context as Activity).finish()
- }
- Runtime.getRuntime().exit(0)
- }
-
- /**
- * Start Backup process, and set onComplete Listener to success, if no error occurred, else
- * onComplete Listener success is false and error message is passed
- *
- * if custom storage ist selected, the [openBackupfileCreator] will be launched
- */
- fun backup() {
- if (enableLogDebug) Log.d(TAG, "Starting Backup ...")
- val success = initRoomBackup()
- if (!success) return
-
- // Needed for storage permissions request
- currentProcess = PROCESS_BACKUP
-
- // Create name for backup file, if no custom name is set: Database name + currentTime +
- // .sqlite3
- var filename =
- if (customBackupFileName == null) "$dbName-${getTime()}.sqlite3"
- else customBackupFileName as String
- // Add .aes extension to filename, if file is encrypted
- if (backupIsEncrypted) filename += ".aes"
- if (enableLogDebug) Log.d(TAG, "backupFilename: $filename")
-
- when (backupLocation) {
- BACKUP_FILE_LOCATION_INTERNAL -> {
- val backupFile = File("$INTERNAL_BACKUP_PATH/$filename")
- doBackup(backupFile)
- }
-
- BACKUP_FILE_LOCATION_EXTERNAL -> {
- val backupFile = File("$EXTERNAL_BACKUP_PATH/$filename")
- doBackup(backupFile)
- }
-
- BACKUP_FILE_LOCATION_CUSTOM_DIALOG -> {
- backupFilename = filename
- permissionRequestLauncher.launch(arrayOf())
- return
- }
-
- BACKUP_FILE_LOCATION_CUSTOM_FILE -> {
- doBackup(backupLocationCustomFile!!)
- }
-
- else -> return
- }
- }
-
- /**
- * This method will do the backup action
- *
- * @param destination File
- */
- private fun doBackup(destination: File) {
- // Close the database
- roomDatabase!!.close()
- roomDatabase = null
- if (backupIsEncrypted) {
- val encryptedBytes = encryptBackup() ?: return
- val bos = BufferedOutputStream(FileOutputStream(destination, false))
- bos.write(encryptedBytes)
- bos.flush()
- bos.close()
- } else {
- // Copy current database to save location (/files dir)
- DATABASE_FILE.copyTo(destination)
- }
-
- // If maxFileCount is set and is reached, delete oldest file
- if (maxFileCount != null) {
- val deleted = deleteOldBackup()
- if (!deleted) return
- }
-
- if (enableLogDebug)
- Log.d(TAG, "Backup done, encrypted($backupIsEncrypted) and saved to $destination")
- onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS)
- }
-
- /**
- * This method will do the backup action
- *
- * @param destination OutputStream
- */
- private fun doBackup(destination: OutputStream) {
- // Close the database
- roomDatabase!!.close()
- roomDatabase = null
- if (backupIsEncrypted) {
- val encryptedBytes = encryptBackup() ?: return
- destination.write(encryptedBytes)
- } else {
- // Copy current database to save location (/files dir)
- DATABASE_FILE.inputStream().use { input ->
- destination.use { output ->
- input.copyTo(output)
- }
- }
- }
-
- // If maxFileCount is set and is reached, delete oldest file
- if (maxFileCount != null) {
- val deleted = deleteOldBackup()
- if (!deleted) return
- }
- if (enableLogDebug)
- Log.d(TAG, "Backup done, encrypted($backupIsEncrypted) and saved to $destination")
- onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS)
- }
-
- /**
- * Encrypts the current Database and return it's content as ByteArray. The original Database is
- * not encrypted only a current copy of this database
- *
- * @return encrypted backup as ByteArray
- */
- private fun encryptBackup(): ByteArray? {
- try {
- // Copy database you want to backup to temp directory
- DATABASE_FILE.copyTo(TEMP_BACKUP_FILE)
-
- // encrypt temp file, and save it to backup location
- val encryptDecryptBackup = AESEncryptionHelper()
- val fileData = encryptDecryptBackup.readFile(TEMP_BACKUP_FILE)
-
- val aesEncryptionManager = AESEncryptionManager()
- val encryptedBytes =
- aesEncryptionManager.encryptData(sharedPreferences, encryptPassword, fileData)
-
- // Delete temp file
- TEMP_BACKUP_FILE.delete()
-
- return encryptedBytes
- } catch (e: Exception) {
- if (enableLogDebug) Log.d(TAG, "error during encryption: ${e.message}")
- onCompleteListener?.onComplete(
- false,
- "error during encryption",
- OnCompleteListener.EXIT_CODE_ERROR_ENCRYPTION_ERROR
- )
- return null
- }
- }
-
- /**
- * Start Restore process, and set onComplete Listener to success, if no error occurred, else
- * onComplete Listener success is false and error message is passed
- *
- * if internal or external storage is selected, this function shows a list of all available
- * backup files in a MaterialAlertDialog and calls [restoreSelectedInternalExternalFile] to
- * restore selected file
- *
- * if custom storage ist selected, the [openBackupfileChooser] will be launched
- */
- fun restore() {
- if (enableLogDebug) Log.d(TAG, "Starting Restore ...")
- val success = initRoomBackup()
- if (!success) return
-
- // Needed for storage permissions request
- currentProcess = PROCESS_RESTORE
-
- // Path of Backup Directory
- val backupDirectory: File
-
- when (backupLocation) {
- BACKUP_FILE_LOCATION_INTERNAL -> {
- backupDirectory = INTERNAL_BACKUP_PATH
- }
-
- BACKUP_FILE_LOCATION_EXTERNAL -> {
- backupDirectory = File("$EXTERNAL_BACKUP_PATH/")
- }
-
- BACKUP_FILE_LOCATION_CUSTOM_DIALOG -> {
- permissionRequestLauncher.launch(arrayOf())
- return
- }
-
- BACKUP_FILE_LOCATION_CUSTOM_FILE -> {
- Log.d(
- TAG,
- "backupLocationCustomFile!!.exists()? : ${backupLocationCustomFile!!.exists()}"
- )
- doRestore(backupLocationCustomFile!!)
- return
- }
-
- else -> return
- }
-
- // All Files in an Array of type File
- val arrayOfFiles = backupDirectory.listFiles()
-
- // If array is null or empty show "error" and return
- if (arrayOfFiles.isNullOrEmpty()) {
- if (enableLogDebug) Log.d(TAG, "No backups available to restore")
- onCompleteListener?.onComplete(
- false,
- "No backups available",
- OnCompleteListener.EXIT_CODE_ERROR_RESTORE_NO_BACKUPS_AVAILABLE
- )
- Toast.makeText(context, "No backups available to restore", Toast.LENGTH_SHORT).show()
- return
- }
-
- // Sort Array: lastModified
- arrayOfFiles.sortBy { it.lastModified() }
-
- // New empty MutableList of String
- val mutableListOfFilesAsString = mutableListOf()
-
- // Add each filename to mutablelistOfFilesAsString
- runBlocking {
- for (i in arrayOfFiles.indices) {
- mutableListOfFilesAsString.add(arrayOfFiles[i].name)
- }
- }
-
- // Convert MutableList to Array
- val filesStringArray = mutableListOfFilesAsString.toTypedArray()
-
- // Show MaterialAlertDialog, with all available files, and on click Listener
-
- AlertDialog.Builder(context)
- .setTitle(customRestoreDialogTitle)
- .setItems(filesStringArray) { _, which ->
- restoreSelectedInternalExternalFile(filesStringArray[which])
- }
- .setOnCancelListener {
- if (enableLogDebug) Log.d(TAG, "Restore dialog canceled")
- onCompleteListener?.onComplete(
- false,
- "Restore dialog canceled",
- OnCompleteListener.EXIT_CODE_ERROR_BY_USER_CANCELED
- )
- }
- .show()
- }
-
- /**
- * This method will do the restore action
- *
- * @param source File
- */
- private fun doRestore(source: File) {
- // Close the database
- roomDatabase!!.close()
- roomDatabase = null
- val fileExtension = source.extension
- if (backupIsEncrypted) {
- source.copyTo(TEMP_BACKUP_FILE)
- val decryptedBytes = decryptBackup() ?: return
- val bos = BufferedOutputStream(FileOutputStream(DATABASE_FILE, false))
- bos.write(decryptedBytes)
- bos.flush()
- bos.close()
- } else {
- if (fileExtension == "aes") {
- if (enableLogDebug)
- Log.d(
- TAG,
- "Cannot restore database, it is encrypted. Maybe you forgot to add the property .fileIsEncrypted(true)"
- )
- onCompleteListener?.onComplete(
- false,
- "cannot restore database, see Log for more details (if enabled)",
- OnCompleteListener.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED
- )
- return
- }
- // Copy back database and replace current database
- source.copyTo(DATABASE_FILE)
- }
-
- if (enableLogDebug)
- Log.d(TAG, "Restore done, decrypted($backupIsEncrypted) and restored from $source")
- onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS)
- }
-
- /**
- * This method will do the restore action
- *
- * @param source InputStream
- */
- private fun doRestore(source: InputStream) {
- if (backupIsEncrypted) {
- // Save inputstream to temp file
- source.use { input ->
- TEMP_BACKUP_FILE.outputStream().use { output -> input.copyTo(output) }
- }
- // Decrypt tempfile and write to database file
- val decryptedBytes = decryptBackup() ?: return
-
- // Close the database if decryption is succesfull
- roomDatabase!!.close()
- roomDatabase = null
-
- val bos = BufferedOutputStream(FileOutputStream(DATABASE_FILE, false))
- bos.write(decryptedBytes)
- bos.flush()
- bos.close()
- } else {
- // Close the database
- roomDatabase!!.close()
- roomDatabase = null
-
- // Copy back database and replace current database
- source.use { input ->
- DATABASE_FILE.outputStream().use { output -> input.copyTo(output) }
- }
- }
-
- if (enableLogDebug)
- Log.d(TAG, "Restore done, decrypted($backupIsEncrypted) and restored from $source")
- onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS)
- }
-
- /**
- * Restores the selected file from internal or external storage
- *
- * @param filename String
- */
- private fun restoreSelectedInternalExternalFile(filename: String) {
- if (enableLogDebug) Log.d(TAG, "Restore selected file...")
-
- when (backupLocation) {
- BACKUP_FILE_LOCATION_INTERNAL -> {
- doRestore(File("$INTERNAL_BACKUP_PATH/$filename"))
- }
-
- BACKUP_FILE_LOCATION_EXTERNAL -> {
- doRestore(File("$EXTERNAL_BACKUP_PATH/$filename"))
- }
-
- else -> return
- }
- }
-
- /**
- * Decrypts the [TEMP_BACKUP_FILE] and return it's content as ByteArray. A valid encrypted
- * backup file must be present on [TEMP_BACKUP_FILE]
- *
- * @return decrypted backup as ByteArray
- */
- private fun decryptBackup(): ByteArray? {
- try {
- // Decrypt temp file, and save it to database location
- val encryptDecryptBackup = AESEncryptionHelper()
- val fileData = encryptDecryptBackup.readFile(TEMP_BACKUP_FILE)
-
- val aesEncryptionManager = AESEncryptionManager()
- val decryptedBytes =
- aesEncryptionManager.decryptData(sharedPreferences, encryptPassword, fileData)
-
- // Delete tem file
- TEMP_BACKUP_FILE.delete()
-
- return decryptedBytes
- } catch (e: BadPaddingException) {
- if (enableLogDebug) Log.d(TAG, "error during decryption (wrong password): ${e.message}")
- onCompleteListener?.onComplete(
- false,
- "error during decryption (wrong password) see Log for more details (if enabled)",
- OnCompleteListener.EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD
- )
- return null
- } catch (e: Exception) {
- if (enableLogDebug) Log.d(TAG, "error during decryption: ${e.message}")
- onCompleteListener?.onComplete(
- false,
- "error during decryption see Log for more details (if enabled)",
- OnCompleteListener.EXIT_CODE_ERROR_DECRYPTION_ERROR
- )
- return null
- }
- }
-
- /**
- * Opens the [ActivityResultContracts.RequestMultiplePermissions] and prompts the user to grant
- * storage permissions
- *
- * If granted backup or restore process starts
- */
- private val permissionRequestLauncher =
- (context as ComponentActivity).registerForActivityResult(
- ActivityResultContracts.RequestMultiplePermissions()
- ) { permissions ->
- permissions.entries.forEach {
- if (!it.value) {
- onCompleteListener?.onComplete(
- false,
- "storage permissions are required, please allow!",
- OnCompleteListener.EXIT_CODE_ERROR_STORAGE_PERMISSONS_NOT_GRANTED
- )
- return@registerForActivityResult
- }
- }
- when (currentProcess) {
- PROCESS_BACKUP -> {
- backupFilename?.let { openBackupfileCreator.launch(it) }
- }
-
- PROCESS_RESTORE -> {
- openBackupfileChooser.launch(arrayOf("application/octet-stream"))
- }
- }
- }
-
- /**
- * Opens the [ActivityResultContracts.OpenDocument] and prompts the user to open a document for
- * restoring a backup file
- */
- private val openBackupfileChooser =
- (context as ComponentActivity).registerForActivityResult(
- ActivityResultContracts.OpenDocument()
- ) { result ->
- if (result != null) {
- val inputstream = context.contentResolver.openInputStream(result)!!
- doRestore(inputstream)
- return@registerForActivityResult
- }
- onCompleteListener?.onComplete(
- false,
- "failure",
- OnCompleteListener.EXIT_CODE_ERROR_BACKUP_FILE_CHOOSER
- )
- }
-
- /**
- * Opens the [ActivityResultContracts.CreateDocument] and prompts the user to select a path for
- * creating the new backup file
- */
- private val openBackupfileCreator =
- (context as ComponentActivity).registerForActivityResult(
- CreateDocument("application/octet-stream")
- ) { result ->
- if (result != null) {
- val out = context.contentResolver.openOutputStream(result)!!
- doBackup(out)
- return@registerForActivityResult
- }
- onCompleteListener?.onComplete(
- false,
- "failure",
- OnCompleteListener.EXIT_CODE_ERROR_BACKUP_FILE_CREATOR
- )
- }
-
- /** @return current time formatted as String */
- private fun getTime(): String {
-
- val currentTime = Calendar.getInstance().time
-
- val sdf =
- if (Build.VERSION.SDK_INT <= 28) {
- SimpleDateFormat("yyyy-MM-dd-HH_mm_ss", Locale.getDefault())
- } else {
- SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.getDefault())
- }
-
- return sdf.format(currentTime)
- }
-
- /**
- * If maxFileCount is set, and reached, all old files will be deleted. Only if
- * [BACKUP_FILE_LOCATION_INTERNAL] or [BACKUP_FILE_LOCATION_EXTERNAL]
- *
- * @return true if old files deleted or nothing to do
- */
- private fun deleteOldBackup(): Boolean {
- // Path of Backup Directory
-
- val backupDirectory: File =
- when (backupLocation) {
- BACKUP_FILE_LOCATION_INTERNAL -> {
- INTERNAL_BACKUP_PATH
- }
-
- BACKUP_FILE_LOCATION_EXTERNAL -> {
- File("$EXTERNAL_BACKUP_PATH/")
- }
-
- BACKUP_FILE_LOCATION_CUSTOM_DIALOG -> {
- // In custom backup location no backups will be removed
- return true
- }
-
- else -> return true
- }
-
- // All Files in an Array of type File
- val arrayOfFiles = backupDirectory.listFiles()
-
- // If array is null or empty nothing to do and return
- if (arrayOfFiles.isNullOrEmpty()) {
- if (enableLogDebug) Log.d(TAG, "")
- onCompleteListener?.onComplete(
- false,
- "maxFileCount: Failed to get list of backups",
- OnCompleteListener.EXIT_CODE_ERROR
- )
- return false
- } else if (arrayOfFiles.size > maxFileCount!!) {
- // Sort Array: lastModified
- arrayOfFiles.sortBy { it.lastModified() }
-
- // Get count of files to delete
- val fileCountToDelete = arrayOfFiles.size - maxFileCount!!
-
- for (i in 1..fileCountToDelete) {
- // Delete all old files (i-1 because array starts a 0)
- arrayOfFiles[i - 1].delete()
-
- if (enableLogDebug)
- Log.d(TAG, "maxFileCount reached: ${arrayOfFiles[i - 1]} deleted")
- }
- }
- return true
- }
-}
diff --git a/app/src/main/play_store_512.png b/app/src/main/play_store_512.png
deleted file mode 100644
index a1a9f37..0000000
Binary files a/app/src/main/play_store_512.png and /dev/null differ
diff --git a/app/src/main/res/drawable/bg_item_complete.xml b/app/src/main/res/drawable/bg_item_complete.xml
deleted file mode 100644
index fc21ed9..0000000
--- a/app/src/main/res/drawable/bg_item_complete.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_about.xml b/app/src/main/res/drawable/ic_about.xml
deleted file mode 100644
index 1e1faf7..0000000
--- a/app/src/main/res/drawable/ic_about.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml
deleted file mode 100644
index a9503fd..0000000
--- a/app/src/main/res/drawable/ic_add.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_all_tasks.xml b/app/src/main/res/drawable/ic_all_tasks.xml
deleted file mode 100644
index 8c3bd45..0000000
--- a/app/src/main/res/drawable/ic_all_tasks.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml
deleted file mode 100644
index cd06f30..0000000
--- a/app/src/main/res/drawable/ic_arrow_back.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_arrow_forward.xml b/app/src/main/res/drawable/ic_arrow_forward.xml
deleted file mode 100644
index 9e27d98..0000000
--- a/app/src/main/res/drawable/ic_arrow_forward.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_backup.xml b/app/src/main/res/drawable/ic_backup.xml
deleted file mode 100644
index 59eaff5..0000000
--- a/app/src/main/res/drawable/ic_backup.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml
deleted file mode 100644
index 5623ef0..0000000
--- a/app/src/main/res/drawable/ic_check.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_dark_mode.xml b/app/src/main/res/drawable/ic_dark_mode.xml
deleted file mode 100644
index dedc9af..0000000
--- a/app/src/main/res/drawable/ic_dark_mode.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_exit.xml b/app/src/main/res/drawable/ic_exit.xml
deleted file mode 100644
index 2660d94..0000000
--- a/app/src/main/res/drawable/ic_exit.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_focus.xml b/app/src/main/res/drawable/ic_focus.xml
deleted file mode 100644
index 39fe24b..0000000
--- a/app/src/main/res/drawable/ic_focus.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml
deleted file mode 100644
index f32ae57..0000000
--- a/app/src/main/res/drawable/ic_github.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml
deleted file mode 100644
index 7b158f0..0000000
--- a/app/src/main/res/drawable/ic_launcher.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..a915451
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_next.xml b/app/src/main/res/drawable/ic_next.xml
deleted file mode 100644
index 0f71f1d..0000000
--- a/app/src/main/res/drawable/ic_next.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml
deleted file mode 100644
index ea2ea4a..0000000
--- a/app/src/main/res/drawable/ic_person.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_previous.xml b/app/src/main/res/drawable/ic_previous.xml
deleted file mode 100644
index 2e0b029..0000000
--- a/app/src/main/res/drawable/ic_previous.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_restore.xml b/app/src/main/res/drawable/ic_restore.xml
deleted file mode 100644
index 1698029..0000000
--- a/app/src/main/res/drawable/ic_restore.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_screenshot.xml b/app/src/main/res/drawable/ic_screenshot.xml
deleted file mode 100644
index f07d596..0000000
--- a/app/src/main/res/drawable/ic_screenshot.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml
deleted file mode 100644
index 09641d1..0000000
--- a/app/src/main/res/drawable/ic_settings.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml
deleted file mode 100644
index 14c24e7..0000000
--- a/app/src/main/res/drawable/ic_update.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_vibration.xml b/app/src/main/res/drawable/ic_vibration.xml
deleted file mode 100644
index 90f6ccf..0000000
--- a/app/src/main/res/drawable/ic_vibration.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/splash_screen.xml b/app/src/main/res/drawable/splash_screen.xml
deleted file mode 100644
index c3c4443..0000000
--- a/app/src/main/res/drawable/splash_screen.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout-land/fragment_main.xml b/app/src/main/res/layout-land/fragment_main.xml
deleted file mode 100644
index cf9c5d5..0000000
--- a/app/src/main/res/layout-land/fragment_main.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout-small/fragment_main.xml b/app/src/main/res/layout-small/fragment_main.xml
deleted file mode 100644
index e6b7a61..0000000
--- a/app/src/main/res/layout-small/fragment_main.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_crash.xml b/app/src/main/res/layout/activity_crash.xml
deleted file mode 100644
index 43436f4..0000000
--- a/app/src/main/res/layout/activity_crash.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 546db6c..0000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/bottom_sheet_info.xml b/app/src/main/res/layout/bottom_sheet_info.xml
deleted file mode 100644
index 044071e..0000000
--- a/app/src/main/res/layout/bottom_sheet_info.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/bottom_sheet_todo.xml b/app/src/main/res/layout/bottom_sheet_todo.xml
deleted file mode 100644
index 4c89ffd..0000000
--- a/app/src/main/res/layout/bottom_sheet_todo.xml
+++ /dev/null
@@ -1,194 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml
deleted file mode 100644
index 18e0005..0000000
--- a/app/src/main/res/layout/fragment_about.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_all_tasks.xml b/app/src/main/res/layout/fragment_all_tasks.xml
deleted file mode 100644
index 9a0c642..0000000
--- a/app/src/main/res/layout/fragment_all_tasks.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml
deleted file mode 100644
index 0345386..0000000
--- a/app/src/main/res/layout/fragment_main.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_progress.xml b/app/src/main/res/layout/fragment_progress.xml
deleted file mode 100644
index fce80c1..0000000
--- a/app/src/main/res/layout/fragment_progress.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
deleted file mode 100644
index 01037a1..0000000
--- a/app/src/main/res/layout/fragment_settings.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_todo.xml b/app/src/main/res/layout/fragment_todo.xml
deleted file mode 100644
index 3aab641..0000000
--- a/app/src/main/res/layout/fragment_todo.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome.xml b/app/src/main/res/layout/fragment_welcome.xml
deleted file mode 100644
index 6a51255..0000000
--- a/app/src/main/res/layout/fragment_welcome.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_intro.xml b/app/src/main/res/layout/fragment_welcome_intro.xml
deleted file mode 100644
index 7204238..0000000
--- a/app/src/main/res/layout/fragment_welcome_intro.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_progress.xml b/app/src/main/res/layout/fragment_welcome_progress.xml
deleted file mode 100644
index 5b4d61b..0000000
--- a/app/src/main/res/layout/fragment_welcome_progress.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_todo_btn.xml b/app/src/main/res/layout/fragment_welcome_todo_btn.xml
deleted file mode 100644
index 57875d4..0000000
--- a/app/src/main/res/layout/fragment_welcome_todo_btn.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_todo_item.xml b/app/src/main/res/layout/fragment_welcome_todo_item.xml
deleted file mode 100644
index 739bcaf..0000000
--- a/app/src/main/res/layout/fragment_welcome_todo_item.xml
+++ /dev/null
@@ -1,84 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_empty.xml b/app/src/main/res/layout/item_empty.xml
deleted file mode 100644
index 72d1aba..0000000
--- a/app/src/main/res/layout/item_empty.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_todo.xml b/app/src/main/res/layout/item_todo.xml
deleted file mode 100644
index 967ecf3..0000000
--- a/app/src/main/res/layout/item_todo.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/preference_switch_m3.xml b/app/src/main/res/layout/preference_switch_m3.xml
deleted file mode 100644
index 764709d..0000000
--- a/app/src/main/res/layout/preference_switch_m3.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml
deleted file mode 100644
index c1a01c9..0000000
--- a/app/src/main/res/menu/main.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index a83cd23..3ffb3dc 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,6 +1,6 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 7f2b607..3ffb3dc 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,6 +1,6 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 4bce769..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..488b66f
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
deleted file mode 100644
index f75a929..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
deleted file mode 100644
index 5cfaa2c..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
deleted file mode 100644
index b8a2995..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index d4457c9..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..6860501
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round_background.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round_background.png
deleted file mode 100644
index f75a929..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round_background.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round_foreground.png
deleted file mode 100644
index 5cfaa2c..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round_monochrome.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round_monochrome.png
deleted file mode 100644
index bdb9fb6..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round_monochrome.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 7c42fc8..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..952699c
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
deleted file mode 100644
index 62abb68..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
deleted file mode 100644
index b66eb7d..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
deleted file mode 100644
index b710d3a..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index 939206a..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..bc36a61
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round_background.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round_background.png
deleted file mode 100644
index 62abb68..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round_background.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round_foreground.png
deleted file mode 100644
index b66eb7d..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round_monochrome.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round_monochrome.png
deleted file mode 100644
index 6375041..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round_monochrome.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 4ec1f5f..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..9732a06
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
deleted file mode 100644
index ca84278..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 2711c0e..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
deleted file mode 100644
index 7537ab8..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index 1e0ea67..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..378823d
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round_background.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round_background.png
deleted file mode 100644
index ca84278..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round_background.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round_foreground.png
deleted file mode 100644
index 2711c0e..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round_monochrome.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round_monochrome.png
deleted file mode 100644
index 2dd0a42..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round_monochrome.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 485bb14..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..d312425
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
deleted file mode 100644
index 3ecea46..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 23a1723..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
deleted file mode 100644
index 87e0075..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index 96c249b..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..465e0b7
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round_background.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round_background.png
deleted file mode 100644
index 3ecea46..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round_background.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round_foreground.png
deleted file mode 100644
index 23a1723..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round_monochrome.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round_monochrome.png
deleted file mode 100644
index 34aabec..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round_monochrome.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 7275110..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..87a3e17
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
deleted file mode 100644
index bcdf6ad..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 0310a45..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
deleted file mode 100644
index 049ac1b..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index 30efdbb..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1fd68f3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_background.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_background.png
deleted file mode 100644
index bcdf6ad..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_background.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_foreground.png
deleted file mode 100644
index 0310a45..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_monochrome.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_monochrome.png
deleted file mode 100644
index 45efed0..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_monochrome.png and /dev/null differ
diff --git a/app/src/main/res/values-night-v27/themes.xml b/app/src/main/res/values-night-v27/themes.xml
new file mode 100644
index 0000000..61103ee
--- /dev/null
+++ b/app/src/main/res/values-night-v27/themes.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night-v29/themes.xml b/app/src/main/res/values-night-v29/themes.xml
new file mode 100644
index 0000000..e5a0783
--- /dev/null
+++ b/app/src/main/res/values-night-v29/themes.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
deleted file mode 100644
index 35b2a05..0000000
--- a/app/src/main/res/values-night/colors.xml
+++ /dev/null
@@ -1,194 +0,0 @@
-
- #A0CAFD
- #003258
- #1A4975
- #D1E4FF
- #BBC7DB
- #253140
- #3B4858
- #D7E3F8
- #D7BEE4
- #3B2948
- #523F5F
- #F3DAFF
- #FFB4AB
- #690005
- #93000A
- #FFDAD6
- #111418
- #E1E2E8
- #111418
- #E1E2E8
- #43474E
- #C3C6CF
- #8D9199
- #43474E
- #000000
- #E1E2E8
- #2E3135
- #36618E
- #D1E4FF
- #001D36
- #A0CAFD
- #1A4975
- #D7E3F8
- #101C2B
- #BBC7DB
- #3B4858
- #F3DAFF
- #251431
- #D7BEE4
- #523F5F
- #111418
- #36393E
- #0B0E13
- #191C20
- #1D2024
- #272A2F
- #32353A
- #A7CEFF
- #00172E
- #6B94C4
- #000000
- #BFCCDF
- #0A1725
- #8592A4
- #000000
- #DBC2E9
- #1F0F2C
- #9F88AD
- #000000
- #FFBAB1
- #370001
- #FF5449
- #000000
- #111418
- #E1E2E8
- #111418
- #FAFAFF
- #43474E
- #C7CBD3
- #9FA3AB
- #7F838B
- #000000
- #E1E2E8
- #272A2F
- #1B4A76
- #D1E4FF
- #001225
- #A0CAFD
- #003862
- #D7E3F8
- #051220
- #BBC7DB
- #2B3746
- #F3DAFF
- #1A0926
- #D7BEE4
- #412F4E
- #111418
- #36393E
- #0B0E13
- #191C20
- #1D2024
- #272A2F
- #32353A
- #FAFAFF
- #000000
- #A7CEFF
- #000000
- #FAFAFF
- #000000
- #BFCCDF
- #000000
- #FFF9FB
- #000000
- #DBC2E9
- #000000
- #FFF9F9
- #000000
- #FFBAB1
- #000000
- #111418
- #E1E2E8
- #111418
- #FFFFFF
- #43474E
- #FAFAFF
- #C7CBD3
- #C7CBD3
- #000000
- #E1E2E8
- #000000
- #002B4E
- #D9E8FF
- #000000
- #A7CEFF
- #00172E
- #DBE8FC
- #000000
- #BFCCDF
- #0A1725
- #F5DFFF
- #000000
- #DBC2E9
- #1F0F2C
- #111418
- #36393E
- #0B0E13
- #191C20
- #1D2024
- #272A2F
- #32353A
-
-
- #FFB4A8
- #690100
- #940100
- #FFD8D2
- #FFB4A8
- #630F08
- #751D13
- #FFC3B9
- #F1BE6F
- #442C00
- #614000
- #FFDAA6
- #FFB4AB
- #690005
- #93000A
- #FFDAD6
- #1E0F0D
- #FADCD7
- #1E0F0D
- #FADCD7
- #5C403B
- #E5BEB7
- #AC8983
- #5C403B
- #000000
- #FADCD7
- #3E2C29
- #BB190E
- #FFDAD4
- #410000
- #FFB4A8
- #930100
- #FFDAD4
- #410000
- #FFB4A8
- #82261B
- #FFDDAF
- #281800
- #F1BE6F
- #614000
- #1E0F0D
- #483531
- #190A08
- #281715
- #2C1B19
- #372623
- #43302D
-
- #282B24
-
diff --git a/app/src/main/res/values-night/theme_overlays.xml b/app/src/main/res/values-night/theme_overlays.xml
deleted file mode 100644
index 6d5e108..0000000
--- a/app/src/main/res/values-night/theme_overlays.xml
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index 878c8d8..3b8d68f 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,108 +1,9 @@
+
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml
new file mode 100644
index 0000000..60b6d70
--- /dev/null
+++ b/app/src/main/res/values-v27/themes.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v29/themes.xml b/app/src/main/res/values-v29/themes.xml
index d24e796..41d0610 100644
--- a/app/src/main/res/values-v29/themes.xml
+++ b/app/src/main/res/values-v29/themes.xml
@@ -1,10 +1,12 @@
-
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/arrays.xml b/app/src/main/res/values-zh-rCN/arrays.xml
deleted file mode 100644
index f12f49f..0000000
--- a/app/src/main/res/values-zh-rCN/arrays.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- - 诊断
- - E 听说
- - 卷子
- - 资料集
- - 改错
- - 每周一练
-
-
- - 跟随系统设置
- - 开启
- - 关闭
-
-
-
- - 0
- - 1
- - 2
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index fce948c..1db3fdc 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -1,89 +1,97 @@
待办
-
- 关于
- Super12138
- 检查更新
-
- 应用程序出现错误
- 别担心,下方是错误日志,请全选复制然后反馈到 GitHub Issues,我会尽快解决问题
- 没有日志传入
- 退出应用
-
- 未知
+ 设置
+ 添加待办
+ 目前没有要完成的任务,好好休息一下吧~
+ 待办内容
+ 取消
+ 保存
+ 没有输入待办内容
+ 选择该项
+ 标记为已完成
语文
数学
英语
生物
+ 地理
物理
+ 道法
化学
历史
- 地理
- 道法
- 其它
-
- 取消
- 确定
- 添加待办
- 警告
- 删除后,全部待办将无法恢复。确定删除?
- 待办内容
- 删除
- 标记为已完成
- 无待办事项
-
-
- 待办内容不能为空
- 设置
- 外观
- 深色模式
- 开启或关闭应用深色模式
- 在 GitHub 上查看源代码
- 配置
- 其它
- 阻止截屏
- 开启后任何人将不能对待办列表进行截屏
- 重启应用后生效
- 立即重启
- 备份数据库
- 备份待办数据库
- 恢复数据库
- 恢复先前备份过的待办数据库
- 需要重启应用载入数据
- JSON 数据格式错误,请检查粘贴数据格式
- 恢复失败,请检查是否重复恢复同一次备份的数据
- 查看全部待办
- 全部代办
- 查看包含已完成的待办在内的全部待办
- 学科:%s
- 状态:已完成
- 状态:未完成
- UUID:%s
- 剩余 %s 项任务
- 保存
- 更新待办
- 更新
- 触感反馈
- 部分点按操作会有轻微震动
- 上一页
- 下一页
- 开始
- 欢迎
- 欢迎使用待办
- 一个简单的待办应用
- 立即进入
- 再次进入欢迎页面
- 定睛看
- 这是待办进度条,它会提示你任务完成进度\n当你添加待办或把待办标记成已完成时,它都会更新\n左边的数字是你完成的任务数,右边的数字是全部任务数\n在它们下面显示了一行小字来提示你剩余任务数
- 掌控尽在指尖
- 这是添加待办按钮,点击它就可以添加一个新的待办事项\n长按它你就可以删除目前所有的待办(这不能恢复,谨慎点击哦)
- 长按一下,功能多多
- 这是待办内容
- 这是待办学科
- 这是待办列表的项目\n点击右边的 √ 就可以把这个待办标记为已完成\n长按需要修改的待办项目即可修改其信息
- 备份成功
- 备份失败(错误代码:%d)
- 恢复成功,重启应用以加载数据
- 恢复失败(错误代码:%d)
+ 其它
+ 修改待办
+ 剩余 %s 项任务
+ 应用程序出现错误
+ 没有日志传入
+ 退出应用
+ 删除
+ 清除已选择的项目
+ 全选
+ 已选择 %s 项
+ 确定
+ 警告
+ 即将删除 %s 项待办。删除后将无法恢复这些待办,确定删除?
+ 返回
+ 关于
+ 关于本应用
+ 版本
+ 开发者
+ 开放源代码许可
+ 查看应用使用的开源库及其许可
+ 学科
+ 不紧急
+ 不重要
+ 默认
+ 重要
+ 紧急
+ 优先级
+ 外观和个性化
+ 主题、色彩样式
+ 动态配色
+ 使用系统壁纸的主题颜色,仅在 Android 12+ 上生效
+ 主题样式
+ 选择应用主题色的配色方式
+ 色调点
+ 中性
+ 鲜艳
+ 表现力
+ 彩虹
+ 水果沙拉
+ 单色
+ 高保真
+ 内容
+ 深色模式
+ 开启或关闭应用深色模式
+ 跟随系统
+ 浅色模式
+ 深色模式
+ 对比度
+ 调整应用的颜色对比度
+ 极低
+ 低
+ 默认
+ 高
+ 极高
+ 更改颜色对比度
+ 界面与交互
+ 待办列表、触感反馈
+ 显示已完成待办
+ 在待办列表里显示已完成的待办
+ 待办列表
+ 在 GitHub 上查看
+ 查看源代码、提交错误报告和改进建议
+ 退出编辑后将无法找回你修改过的数据。确定退出编辑吗?
+ 日期
+ 学科
+ 优先级
+ 完成状态
+ 首字母(升序)
+ 首字母(降序)
+ 排序方式
+ 全局交互
+ 触感反馈
+ 为某些操作提供轻微的震动反馈
+ 提示
+ 使用此项前须先在系统设置中打开触感反馈(通常在“声音与震动”里的“触感反馈”一项)
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
deleted file mode 100644
index e88b805..0000000
--- a/app/src/main/res/values/arrays.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- - 诊断
- - E 听说
- - 卷子
- - 资料集
- - 改错
- - 每周一练
-
-
- - Follow system
- - On
- - Off
-
-
-
- - 0
- - 1
- - 2
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index bad958a..46f7692 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,197 +1,4 @@
- #FF000000
- #FFFFFFFF
- #0061A4
- #36618E
- #FFFFFF
- #D1E4FF
- #001D36
- #535F70
- #FFFFFF
- #D7E3F8
- #101C2B
- #6B5778
- #FFFFFF
- #F3DAFF
- #251431
- #BA1A1A
- #FFFFFF
- #FFDAD6
- #410002
- #F8F9FF
- #191C20
- #F8F9FF
- #191C20
- #DFE2EB
- #43474E
- #73777F
- #C3C6CF
- #000000
- #2E3135
- #EFF0F7
- #A0CAFD
- #D1E4FF
- #001D36
- #A0CAFD
- #1A4975
- #D7E3F8
- #101C2B
- #BBC7DB
- #3B4858
- #F3DAFF
- #251431
- #D7BEE4
- #523F5F
- #D8DAE0
- #F8F9FF
- #FFFFFF
- #F2F3FA
- #ECEEF4
- #E6E8EE
- #E1E2E8
- #144571
- #FFFFFF
- #4E77A6
- #FFFFFF
- #374454
- #FFFFFF
- #697687
- #FFFFFF
- #4E3B5B
- #FFFFFF
- #826D8F
- #FFFFFF
- #8C0009
- #FFFFFF
- #DA342E
- #FFFFFF
- #F8F9FF
- #191C20
- #F8F9FF
- #191C20
- #DFE2EB
- #3F434A
- #5B5F67
- #777B83
- #000000
- #2E3135
- #EFF0F7
- #A0CAFD
- #4E77A6
- #FFFFFF
- #335E8C
- #FFFFFF
- #697687
- #FFFFFF
- #515D6E
- #FFFFFF
- #826D8F
- #FFFFFF
- #685475
- #FFFFFF
- #D8DAE0
- #F8F9FF
- #FFFFFF
- #F2F3FA
- #ECEEF4
- #E6E8EE
- #E1E2E8
- #002341
- #FFFFFF
- #144571
- #FFFFFF
- #172332
- #FFFFFF
- #374454
- #FFFFFF
- #2C1B38
- #FFFFFF
- #4E3B5B
- #FFFFFF
- #4E0002
- #FFFFFF
- #8C0009
- #FFFFFF
- #F8F9FF
- #191C20
- #F8F9FF
- #000000
- #DFE2EB
- #20242B
- #3F434A
- #3F434A
- #000000
- #2E3135
- #FFFFFF
- #E2EDFF
- #144571
- #FFFFFF
- #002E52
- #FFFFFF
- #374454
- #FFFFFF
- #212E3D
- #FFFFFF
- #4E3B5B
- #FFFFFF
- #372644
- #FFFFFF
- #D8DAE0
- #F8F9FF
- #FFFFFF
- #F2F3FA
- #ECEEF4
- #E6E8EE
- #E1E2E8
-
- #7E0100
- #FFFFFF
- #BB1A0E
- #FFFFFF
- #A23E30
- #FFFFFF
- #FF9281
- #520201
- #533700
- #FFFFFF
- #7E5811
- #FFFFFF
- #BA1A1A
- #FFFFFF
- #FFDAD6
- #410002
- #FFF8F6
- #281715
- #FFF8F6
- #281715
- #FFDAD4
- #5C403B
- #906F6A
- #E5BEB7
- #000000
- #3E2C29
- #FFEDEA
- #FFB4A8
- #FFDAD4
- #410000
- #FFB4A8
- #930100
- #FFDAD4
- #410000
- #FFB4A8
- #82261B
- #FFDDAF
- #281800
- #F1BE6F
- #614000
- #F1D3CE
- #FFF8F6
- #FFFFFF
- #FFF0EE
- #FFE9E6
- #FFE2DD
- #FADCD7
-
- #E8E9DE
+ #0061A4
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d591bf9..752a26c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,89 +1,98 @@
To Do
-
- About
- Super12138
- Check for updates
- View source on GitHub
-
- Oops! App went wrong
- Don\'t worry, below is the error log. Please select all and copy it, and then provide feedback on GitHub Issues. I will resolve the issue as soon as possible.
- No crash logs
- Exit app
-
- Unknown
+ Settings
+ Add Task
+ There are no tasks to complete at the moment.\nTake a good rest!
+ Task content
+ Cancel
+ Save
+ No task content entered
+ Select this
+ Mark as completed
Chinese
Math
English
Biology
+ Geography
Physics
+ Morality and Rule of Law
Chemistry
History
- Geography
- Law
- Other
-
- Cancel
- OK
- Add Task
- Warning
- After deleting, all to-do items cannot be restored. Are you sure to delete it?
- Tasks
- Delete
- Checked this task
- No tasks
-
-
- Task content cannot be empty
-
- Settings
- Appearance
- Dark mode
- Enable or disable the dark mode for this app
- Config
- Others
- Prevent screenshot
- When it opens anybody cannot take screenshot for to do list
- Effective after restarting the app
- Restart now
- Backup database
- Backup to-do database
- Restore database
- Restore the previously backed up to-do database
- Need to restart the app to apply the recovered data
- JSON data format is wrong. Please check the paste data format
- Restore failed, please check if you are attempting to restore the same backup data again.
- View all tasks
- All tasks
- View all tasks including those that have been checked as completed
- Subject: %s
- State: complete
- State: incomplete
- UUID: %s
- %s tasks remaining
- Save
- Update task
- Update
- Haptic feedback
- Some tap operations will cause a slight vibration
- Previous page
- Next page
- Start
- Welcome
- Welcome to use To Do
- A simple to-do app
- Enter at once
- Reenter welcome page
- Take a look!
- This is the to-do progress bar that tracks your task completion.\n It updates when you add or mark tasks as done.\n The number on the left shows your completed tasks, while the number on the right indicates the total tasks.\n Below these numbers, a small line of text will show how many tasks are remaining.
- Control is at your fingertips!
- This is the button to add new to-dos.\n Click it to create a new task.\n Long press to delete all your current to-dos (this action cannot be undone, so be cautious).
- Long press for more options!
- This is the task content
- This is the task subject
- "This is an item in your to-do list.\n Click the checkmark on the right to mark it as completed.\n If you need to edit this to-do, just long press to make changes. "
- Backup successful
- Backup failed (Exit code: %d)
- Restore Successful
- Restore failed (Exit code: %d)
+ Others
+ Edit Task
+ %s tasks remaining
+ Oops! App went wrong
+ No crash logs
+ Exit app
+ Delete
+ Clear selected items
+ Select all
+ %s selected
+ Confirm
+ Warning
+ You are about to delete the %s tasks. Once deleted, these tasks cannot be recovered. Are you sure you want to delete them?
+ /
+ Back
+ About
+ About this app
+ Super12138
+ Version
+ Developer
+ Open Source Licences
+ Check the open source libraries used by the application and their licences.
+ Subject
+ Not Urgent
+ Not Important
+ Default
+ Important
+ Urgent
+ Priority
+ Appearance
+ Theme, color style
+ Dynamic Color
+ Use the system wallpaper\'s theme color, available on Android 12+
+ Palette Style
+ Choose the color scheme method for applying the theme color
+ Tonal Spot
+ Neutral
+ Vibrant
+ Expressive
+ Rainbow
+ Fruit Salad
+ Monochrome
+ Fidelity
+ Content
+ Dark Mode
+ Enable or disable the dark mode for this app
+ Follow System
+ Light
+ Dark
+ Contrast Level
+ Change the app\'s color contrast
+ Very Low
+ Low
+ Default
+ High
+ Very High
+ Change the contrast level
+ Interface & Interaction
+ To-Do List, haptic feedback
+ Show Completed Tasks
+ Show completed tasks in to-do list
+ To-Do List
+ View On GitHub
+ View source code, submit bug reports, and improvement suggestions
+ After exiting edit mode, you will not be able to retrieve the data you have modified. Are you sure you want to exit editing?
+ Date
+ Subject
+ Priority
+ Completion
+ Alphabetical (Ascending)
+ Alphabetical (Descending)
+ Sorting Method
+ Global Interaction
+ Haptic Feedback
+ Provide slight vibration feedback for some operations
+ Tips
+ Before using this feature, you need to enable haptic feedback in the system settings (usually under the \\\"Sound & Vibration\\\" section in the \\\"Haptic Feedback\\\" option)
\ No newline at end of file
diff --git a/app/src/main/res/values/theme_overlays.xml b/app/src/main/res/values/theme_overlays.xml
deleted file mode 100644
index 68f2305..0000000
--- a/app/src/main/res/values/theme_overlays.xml
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index cf9a405..7a880b5 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,147 +1,14 @@
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ - @android:color/transparent
+ - true
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
deleted file mode 100644
index 8e645de..0000000
--- a/app/src/main/res/xml/preferences.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 13fdfe0..7e2da06 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,6 +2,7 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.ksp) apply false
- alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.aboutlibraries) apply false
}
\ No newline at end of file
diff --git a/fastscroll/.gitignore b/fastscroll/.gitignore
deleted file mode 100644
index 42afabf..0000000
--- a/fastscroll/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/fastscroll/build.gradle.kts b/fastscroll/build.gradle.kts
deleted file mode 100644
index aad3a3f..0000000
--- a/fastscroll/build.gradle.kts
+++ /dev/null
@@ -1,42 +0,0 @@
-plugins {
- alias(libs.plugins.android.library)
- alias(libs.plugins.kotlin.android)
-}
-
-android {
- namespace = "me.zhanghai.android.fastscroll"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 21
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_18
- targetCompatibility = JavaVersion.VERSION_18
- }
- kotlinOptions {
- jvmTarget = "18"
- }
-}
-
-dependencies {
-
- implementation(libs.androidx.appcompat)
- implementation(libs.androidx.recyclerview)
- testImplementation(libs.junit)
- androidTestImplementation(libs.androidx.junit)
- androidTestImplementation(libs.androidx.espresso.core)
-}
\ No newline at end of file
diff --git a/fastscroll/consumer-rules.pro b/fastscroll/consumer-rules.pro
deleted file mode 100644
index e69de29..0000000
diff --git a/fastscroll/proguard-rules.pro b/fastscroll/proguard-rules.pro
deleted file mode 100644
index 481bb43..0000000
--- a/fastscroll/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/fastscroll/src/main/AndroidManifest.xml b/fastscroll/src/main/AndroidManifest.xml
deleted file mode 100644
index dacfad7..0000000
--- a/fastscroll/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/AutoMirrorDrawable.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/AutoMirrorDrawable.java
deleted file mode 100644
index 7ff5de3..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/AutoMirrorDrawable.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.annotation.SuppressLint;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.graphics.drawable.DrawableWrapperCompat;
-import androidx.core.graphics.drawable.DrawableCompat;
-
-@SuppressLint("RestrictedApi")
-class AutoMirrorDrawable extends DrawableWrapperCompat {
-
- public AutoMirrorDrawable(@NonNull Drawable drawable) {
- super(drawable);
- }
-
- @Override
- public void draw(@NonNull Canvas canvas) {
- if (needMirroring()) {
- float centerX = getBounds().exactCenterX();
- canvas.scale(-1, 1, centerX, 0);
- super.draw(canvas);
- canvas.scale(-1, 1, centerX, 0);
- } else {
- super.draw(canvas);
- }
- }
-
- @Override
- public boolean onLayoutDirectionChanged(int layoutDirection) {
- super.onLayoutDirectionChanged(layoutDirection);
- return true;
- }
-
- @Override
- public boolean isAutoMirrored() {
- return true;
- }
-
- private boolean needMirroring() {
- return DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL;
- }
-
- @Override
- public boolean getPadding(@NonNull Rect padding) {
- boolean hasPadding = super.getPadding(padding);
- if (needMirroring()) {
- int paddingStart = padding.left;
- int paddingEnd = padding.right;
- padding.left = paddingEnd;
- padding.right = paddingStart;
- }
- return hasPadding;
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/DefaultAnimationHelper.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/DefaultAnimationHelper.java
deleted file mode 100644
index 2c953b3..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/DefaultAnimationHelper.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.view.View;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.NonNull;
-import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
-import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
-
-public class DefaultAnimationHelper implements FastScroller.AnimationHelper {
-
- private static final int SHOW_DURATION_MILLIS = 150;
- private static final int HIDE_DURATION_MILLIS = 200;
- private static final Interpolator SHOW_SCROLLBAR_INTERPOLATOR =
- new LinearOutSlowInInterpolator();
- private static final Interpolator HIDE_SCROLLBAR_INTERPOLATOR =
- new FastOutLinearInInterpolator();
- private static final int AUTO_HIDE_SCROLLBAR_DELAY_MILLIS = 1500;
-
- @NonNull
- private final View mView;
-
- private boolean mScrollbarAutoHideEnabled = true;
-
- private boolean mShowingScrollbar = true;
- private boolean mShowingPopup;
-
- public DefaultAnimationHelper(@NonNull View view) {
- mView = view;
- }
-
- @Override
- public void showScrollbar(@NonNull View trackView, @NonNull View thumbView) {
-
- if (mShowingScrollbar) {
- return;
- }
- mShowingScrollbar = true;
-
- trackView.animate()
- .alpha(1)
- .translationX(0)
- .setDuration(SHOW_DURATION_MILLIS)
- .setInterpolator(SHOW_SCROLLBAR_INTERPOLATOR)
- .start();
- thumbView.animate()
- .alpha(1)
- .translationX(0)
- .setDuration(SHOW_DURATION_MILLIS)
- .setInterpolator(SHOW_SCROLLBAR_INTERPOLATOR)
- .start();
- }
-
- @Override
- public void hideScrollbar(@NonNull View trackView, @NonNull View thumbView) {
-
- if (!mShowingScrollbar) {
- return;
- }
- mShowingScrollbar = false;
-
- boolean isLayoutRtl = mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
- int width = Math.max(trackView.getWidth(), thumbView.getWidth());
- float translationX;
- if (isLayoutRtl) {
- translationX = trackView.getLeft() == 0 ? -width : 0;
- } else {
- translationX = trackView.getRight() == mView.getWidth() ? width : 0;
- }
- trackView.animate()
- .alpha(0)
- .translationX(translationX)
- .setDuration(HIDE_DURATION_MILLIS)
- .setInterpolator(HIDE_SCROLLBAR_INTERPOLATOR)
- .start();
- thumbView.animate()
- .alpha(0)
- .translationX(translationX)
- .setDuration(HIDE_DURATION_MILLIS)
- .setInterpolator(HIDE_SCROLLBAR_INTERPOLATOR)
- .start();
- }
-
- @Override
- public boolean isScrollbarAutoHideEnabled() {
- return mScrollbarAutoHideEnabled;
- }
-
- public void setScrollbarAutoHideEnabled(boolean enabled) {
- mScrollbarAutoHideEnabled = enabled;
- }
-
- @Override
- public int getScrollbarAutoHideDelayMillis() {
- return AUTO_HIDE_SCROLLBAR_DELAY_MILLIS;
- }
-
- @Override
- public void showPopup(@NonNull View popupView) {
-
- if (mShowingPopup) {
- return;
- }
- mShowingPopup = true;
-
- popupView.animate()
- .alpha(1)
- .setDuration(SHOW_DURATION_MILLIS)
- .start();
- }
-
- @Override
- public void hidePopup(@NonNull View popupView) {
-
- if (!mShowingPopup) {
- return;
- }
- mShowingPopup = false;
-
- popupView.animate()
- .alpha(0)
- .setDuration(HIDE_DURATION_MILLIS)
- .start();
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollNestedScrollView.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollNestedScrollView.java
deleted file mode 100644
index 960fc46..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollNestedScrollView.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-
-import androidx.annotation.AttrRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.widget.NestedScrollView;
-
-@SuppressLint("MissingSuperCall")
-public class FastScrollNestedScrollView extends NestedScrollView implements ViewHelperProvider {
-
- @NonNull
- private final ViewHelper mViewHelper = new ViewHelper();
-
- public FastScrollNestedScrollView(@NonNull Context context) {
- super(context);
-
- init();
- }
-
- public FastScrollNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
-
- init();
- }
-
- public FastScrollNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- init();
- }
-
- private void init() {
- setScrollContainer(true);
- }
-
- @NonNull
- @Override
- public FastScroller.ViewHelper getViewHelper() {
- return mViewHelper;
- }
-
- @Override
- public void draw(@NonNull Canvas canvas) {
- mViewHelper.draw(canvas);
- }
-
- @Override
- protected void onScrollChanged(int left, int top, int oldLeft, int oldTop) {
- mViewHelper.onScrollChanged(left, top, oldLeft, oldTop);
- }
-
- @Override
- public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
- return mViewHelper.onInterceptTouchEvent(event);
- }
-
- @Override
- @SuppressLint("ClickableViewAccessibility")
- public boolean onTouchEvent(@NonNull MotionEvent event) {
- return mViewHelper.onTouchEvent(event);
- }
-
- private class ViewHelper extends SimpleViewHelper {
-
- @Override
- public int getScrollRange() {
- return super.getScrollRange() + getPaddingTop() + getPaddingBottom();
- }
-
- @Override
- protected void superDraw(@NonNull Canvas canvas) {
- FastScrollNestedScrollView.super.draw(canvas);
- }
-
- @Override
- protected void superOnScrollChanged(int left, int top, int oldLeft, int oldTop) {
- FastScrollNestedScrollView.super.onScrollChanged(left, top, oldLeft, oldTop);
- }
-
- @Override
- protected boolean superOnInterceptTouchEvent(@NonNull MotionEvent event) {
- return FastScrollNestedScrollView.super.onInterceptTouchEvent(event);
- }
-
- @Override
- protected boolean superOnTouchEvent(@NonNull MotionEvent event) {
- return FastScrollNestedScrollView.super.onTouchEvent(event);
- }
-
- @Override
- @SuppressLint("RestrictedApi")
- protected int computeVerticalScrollRange() {
- return FastScrollNestedScrollView.this.computeVerticalScrollRange();
- }
-
- @Override
- @SuppressLint("RestrictedApi")
- protected int computeVerticalScrollOffset() {
- return FastScrollNestedScrollView.this.computeVerticalScrollOffset();
- }
-
- @Override
- protected int getScrollX() {
- return FastScrollNestedScrollView.this.getScrollX();
- }
-
- @Override
- protected void scrollTo(int x, int y) {
- FastScrollNestedScrollView.this.scrollTo(x, y);
- }
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollScrollView.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollScrollView.java
deleted file mode 100644
index 5d3790c..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollScrollView.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.widget.ScrollView;
-
-import androidx.annotation.AttrRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.StyleRes;
-
-@SuppressLint("MissingSuperCall")
-public class FastScrollScrollView extends ScrollView implements ViewHelperProvider {
-
- @NonNull
- private final ViewHelper mViewHelper = new ViewHelper();
-
- public FastScrollScrollView(@NonNull Context context) {
- super(context);
-
- init();
- }
-
- public FastScrollScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
-
- init();
- }
-
- public FastScrollScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- init();
- }
-
- public FastScrollScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
-
- init();
- }
-
- private void init() {
- setVerticalScrollBarEnabled(false);
- setScrollContainer(true);
- }
-
- @NonNull
- @Override
- public FastScroller.ViewHelper getViewHelper() {
- return mViewHelper;
- }
-
- @Override
- public void draw(@NonNull Canvas canvas) {
- mViewHelper.draw(canvas);
- }
-
- @Override
- protected void onScrollChanged(int left, int top, int oldLeft, int oldTop) {
- mViewHelper.onScrollChanged(left, top, oldLeft, oldTop);
- }
-
- @Override
- public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
- return mViewHelper.onInterceptTouchEvent(event);
- }
-
- @Override
- @SuppressLint("ClickableViewAccessibility")
- public boolean onTouchEvent(@NonNull MotionEvent event) {
- return mViewHelper.onTouchEvent(event);
- }
-
- private class ViewHelper extends SimpleViewHelper {
-
- @Override
- public int getScrollRange() {
- return super.getScrollRange() + getPaddingTop() + getPaddingBottom();
- }
-
- @Override
- protected void superDraw(@NonNull Canvas canvas) {
- FastScrollScrollView.super.draw(canvas);
- }
-
- @Override
- protected void superOnScrollChanged(int left, int top, int oldLeft, int oldTop) {
- FastScrollScrollView.super.onScrollChanged(left, top, oldLeft, oldTop);
- }
-
- @Override
- protected boolean superOnInterceptTouchEvent(@NonNull MotionEvent event) {
- return FastScrollScrollView.super.onInterceptTouchEvent(event);
- }
-
- @Override
- protected boolean superOnTouchEvent(@NonNull MotionEvent event) {
- return FastScrollScrollView.super.onTouchEvent(event);
- }
-
- @Override
- protected int computeVerticalScrollRange() {
- return FastScrollScrollView.this.computeVerticalScrollRange();
- }
-
- @Override
- protected int computeVerticalScrollOffset() {
- return FastScrollScrollView.this.computeVerticalScrollOffset();
- }
-
- @Override
- protected int getScrollX() {
- return FastScrollScrollView.this.getScrollX();
- }
-
- @Override
- protected void scrollTo(int x, int y) {
- FastScrollScrollView.this.scrollTo(x, y);
- }
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollWebView.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollWebView.java
deleted file mode 100644
index f6839e6..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollWebView.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.webkit.WebView;
-
-import androidx.annotation.AttrRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.StyleRes;
-
-@SuppressLint("MissingSuperCall")
-public class FastScrollWebView extends WebView implements ViewHelperProvider {
-
- @NonNull
- private final ViewHelper mViewHelper = new ViewHelper();
-
- public FastScrollWebView(@NonNull Context context) {
- super(context);
-
- init();
- }
-
- public FastScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
-
- init();
- }
-
- public FastScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- init();
- }
-
- public FastScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
-
- init();
- }
-
- private void init() {
- setVerticalScrollBarEnabled(false);
- setScrollContainer(true);
- }
-
- @NonNull
- @Override
- public FastScroller.ViewHelper getViewHelper() {
- return mViewHelper;
- }
-
- @Override
- public void draw(@NonNull Canvas canvas) {
- mViewHelper.draw(canvas);
- }
-
- @Override
- protected void onScrollChanged(int left, int top, int oldLeft, int oldTop) {
- mViewHelper.onScrollChanged(left, top, oldLeft, oldTop);
- }
-
- @Override
- public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
- return mViewHelper.onInterceptTouchEvent(event);
- }
-
- @Override
- @SuppressLint("ClickableViewAccessibility")
- public boolean onTouchEvent(@NonNull MotionEvent event) {
- return mViewHelper.onTouchEvent(event);
- }
-
- private class ViewHelper extends SimpleViewHelper {
-
- @Override
- protected void superDraw(@NonNull Canvas canvas) {
- FastScrollWebView.super.draw(canvas);
- }
-
- @Override
- protected void superOnScrollChanged(int left, int top, int oldLeft, int oldTop) {
- FastScrollWebView.super.onScrollChanged(left, top, oldLeft, oldTop);
- }
-
- @Override
- protected boolean superOnInterceptTouchEvent(@NonNull MotionEvent event) {
- return FastScrollWebView.super.onInterceptTouchEvent(event);
- }
-
- @Override
- protected boolean superOnTouchEvent(@NonNull MotionEvent event) {
- return FastScrollWebView.super.onTouchEvent(event);
- }
-
- @Override
- protected int computeVerticalScrollRange() {
- return FastScrollWebView.this.computeVerticalScrollRange();
- }
-
- @Override
- protected int computeVerticalScrollOffset() {
- return FastScrollWebView.this.computeVerticalScrollOffset();
- }
-
- @Override
- protected int getScrollX() {
- return FastScrollWebView.this.getScrollX();
- }
-
- @Override
- protected void scrollTo(int x, int y) {
- FastScrollWebView.this.scrollTo(x, y);
- }
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScroller.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScroller.java
deleted file mode 100644
index eff1895..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScroller.java
+++ /dev/null
@@ -1,465 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
-import android.view.Gravity;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewGroupOverlay;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import java.util.Objects;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.widget.AppCompatTextView;
-import androidx.core.math.MathUtils;
-import androidx.core.util.Consumer;
-
-public class FastScroller {
-
- private final int mMinTouchTargetSize;
- private final int mTouchSlop;
-
- @NonNull
- private final ViewGroup mView;
- @NonNull
- private final ViewHelper mViewHelper;
- @Nullable
- private Rect mUserPadding;
- @NonNull
- private final AnimationHelper mAnimationHelper;
-
- private final int mTrackWidth;
- private final int mThumbWidth;
- private final int mThumbHeight;
-
- @NonNull
- private final View mTrackView;
- @NonNull
- private final View mThumbView;
- @NonNull
- private final TextView mPopupView;
-
- private boolean mScrollbarEnabled;
- private int mThumbOffset;
-
- private float mDownX;
- private float mDownY;
- private float mLastY;
- private float mDragStartY;
- private int mDragStartThumbOffset;
- private boolean mDragging;
-
- @NonNull
- private final Runnable mAutoHideScrollbarRunnable = this::autoHideScrollbar;
-
- @NonNull
- private final Rect mTempRect = new Rect();
-
- public FastScroller(@NonNull ViewGroup view, @NonNull ViewHelper viewHelper,
- @Nullable Rect padding, @NonNull Drawable trackDrawable,
- @NonNull Drawable thumbDrawable, @NonNull Consumer popupStyle,
- @NonNull AnimationHelper animationHelper) {
-
- mMinTouchTargetSize = view.getResources().getDimensionPixelSize(
- R.dimen.afs_min_touch_target_size);
- Context context = view.getContext();
- mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
-
- mView = view;
- mViewHelper = viewHelper;
- mUserPadding = padding;
- mAnimationHelper = animationHelper;
-
- mTrackWidth = requireNonNegative(trackDrawable.getIntrinsicWidth(),
- "trackDrawable.getIntrinsicWidth() < 0");
- mThumbWidth = requireNonNegative(thumbDrawable.getIntrinsicWidth(),
- "thumbDrawable.getIntrinsicWidth() < 0");
- mThumbHeight = requireNonNegative(thumbDrawable.getIntrinsicHeight(),
- "thumbDrawable.getIntrinsicHeight() < 0");
-
- mTrackView = new View(context);
- mTrackView.setBackground(trackDrawable);
- mThumbView = new View(context);
- mThumbView.setBackground(thumbDrawable);
- mPopupView = new AppCompatTextView(context);
- mPopupView.setLayoutParams(new FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
- popupStyle.accept(mPopupView);
-
- ViewGroupOverlay overlay = mView.getOverlay();
- overlay.add(mTrackView);
- overlay.add(mThumbView);
- overlay.add(mPopupView);
-
- postAutoHideScrollbar();
- mPopupView.setAlpha(0);
-
- mViewHelper.addOnPreDrawListener(this::onPreDraw);
- mViewHelper.addOnScrollChangedListener(this::onScrollChanged);
- mViewHelper.addOnTouchEventListener(this::onTouchEvent);
- }
-
- private static int requireNonNegative(int value, @NonNull String message) {
- if (value < 0) {
- throw new IllegalArgumentException(message);
- }
- return value;
- }
-
- public void setPadding(int left, int top, int right, int bottom) {
- if (mUserPadding != null && mUserPadding.left == left && mUserPadding.top == top
- && mUserPadding.right == right && mUserPadding.bottom == bottom) {
- return;
- }
- if (mUserPadding == null) {
- mUserPadding = new Rect();
- }
- mUserPadding.set(left, top, right, bottom);
- mView.invalidate();
- }
-
- public void setPadding(@Nullable Rect padding) {
- if (Objects.equals(mUserPadding, padding)) {
- return;
- }
- if (padding != null) {
- if (mUserPadding == null) {
- mUserPadding = new Rect();
- }
- mUserPadding.set(padding);
- } else {
- mUserPadding = null;
- }
- mView.invalidate();
- }
-
- @NonNull
- private Rect getPadding() {
- if (mUserPadding != null) {
- mTempRect.set(mUserPadding);
- } else {
- mTempRect.set(mView.getPaddingLeft(), mView.getPaddingTop(), mView.getPaddingRight(),
- mView.getPaddingBottom());
- }
- return mTempRect;
- }
-
- private void onPreDraw() {
-
- updateScrollbarState();
- mTrackView.setVisibility(mScrollbarEnabled ? View.VISIBLE : View.INVISIBLE);
- mThumbView.setVisibility(mScrollbarEnabled ? View.VISIBLE : View.INVISIBLE);
- if (!mScrollbarEnabled) {
- mPopupView.setVisibility(View.INVISIBLE);
- return;
- }
-
- int layoutDirection = mView.getLayoutDirection();
- mTrackView.setLayoutDirection(layoutDirection);
- mThumbView.setLayoutDirection(layoutDirection);
- mPopupView.setLayoutDirection(layoutDirection);
-
- boolean isLayoutRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL;
- int viewWidth = mView.getWidth();
- int viewHeight = mView.getHeight();
-
- Rect padding = getPadding();
- int trackLeft = isLayoutRtl ? padding.left : viewWidth - padding.right - mTrackWidth;
- layoutView(mTrackView, trackLeft, padding.top, trackLeft + mTrackWidth,
- Math.max(viewHeight - padding.bottom, padding.top));
- int thumbLeft = isLayoutRtl ? padding.left : viewWidth - padding.right - mThumbWidth;
- int thumbTop = padding.top + mThumbOffset;
- layoutView(mThumbView, thumbLeft, thumbTop, thumbLeft + mThumbWidth,
- thumbTop + mThumbHeight);
-
- CharSequence popupText = mViewHelper.getPopupText();
- boolean hasPopup = !TextUtils.isEmpty(popupText);
- mPopupView.setVisibility(hasPopup ? View.VISIBLE : View.INVISIBLE);
- if (hasPopup) {
- FrameLayout.LayoutParams popupLayoutParams = (FrameLayout.LayoutParams)
- mPopupView.getLayoutParams();
- if (!Objects.equals(mPopupView.getText(), popupText)) {
- mPopupView.setText(popupText);
- int widthMeasureSpec = ViewGroup.getChildMeasureSpec(
- View.MeasureSpec.makeMeasureSpec(viewWidth, View.MeasureSpec.EXACTLY),
- padding.left + padding.right + mThumbWidth + popupLayoutParams.leftMargin
- + popupLayoutParams.rightMargin, popupLayoutParams.width);
- int heightMeasureSpec = ViewGroup.getChildMeasureSpec(
- View.MeasureSpec.makeMeasureSpec(viewHeight, View.MeasureSpec.EXACTLY),
- padding.top + padding.bottom + popupLayoutParams.topMargin
- + popupLayoutParams.bottomMargin, popupLayoutParams.height);
- mPopupView.measure(widthMeasureSpec, heightMeasureSpec);
- }
- int popupWidth = mPopupView.getMeasuredWidth();
- int popupHeight = mPopupView.getMeasuredHeight();
- int popupLeft = isLayoutRtl ? padding.left + mThumbWidth + popupLayoutParams.leftMargin
- : viewWidth - padding.right - mThumbWidth - popupLayoutParams.rightMargin
- - popupWidth;
- int popupAnchorY;
- switch (popupLayoutParams.gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
- case Gravity.LEFT:
- default:
- popupAnchorY = 0;
- break;
- case Gravity.CENTER_HORIZONTAL:
- popupAnchorY = popupHeight / 2;
- break;
- case Gravity.RIGHT:
- popupAnchorY = popupHeight;
- break;
- }
- int thumbAnchorY;
- switch (popupLayoutParams.gravity & Gravity.VERTICAL_GRAVITY_MASK) {
- case Gravity.TOP:
- default:
- thumbAnchorY = mThumbView.getPaddingTop();
- break;
- case Gravity.CENTER_VERTICAL: {
- int thumbPaddingTop = mThumbView.getPaddingTop();
- thumbAnchorY = thumbPaddingTop + (mThumbHeight - thumbPaddingTop
- - mThumbView.getPaddingBottom()) / 2;
- break;
- }
- case Gravity.BOTTOM:
- thumbAnchorY = mThumbHeight - mThumbView.getPaddingBottom();
- break;
- }
- int popupTop = MathUtils.clamp(thumbTop + thumbAnchorY - popupAnchorY,
- padding.top + popupLayoutParams.topMargin,
- viewHeight - padding.bottom - popupLayoutParams.bottomMargin - popupHeight);
- layoutView(mPopupView, popupLeft, popupTop, popupLeft + popupWidth,
- popupTop + popupHeight);
- }
- }
-
- private void updateScrollbarState() {
- int scrollOffsetRange = getScrollOffsetRange();
- mScrollbarEnabled = scrollOffsetRange > 0;
- mThumbOffset = mScrollbarEnabled ? (int) ((long) getThumbOffsetRange()
- * mViewHelper.getScrollOffset() / scrollOffsetRange) : 0;
- }
-
- private void layoutView(@NonNull View view, int left, int top, int right, int bottom) {
- int scrollX = mView.getScrollX();
- int scrollY = mView.getScrollY();
- view.layout(scrollX + left, scrollY + top, scrollX + right, scrollY + bottom);
- }
-
- private void onScrollChanged() {
-
- updateScrollbarState();
- if (!mScrollbarEnabled) {
- return;
- }
-
- mAnimationHelper.showScrollbar(mTrackView, mThumbView);
- postAutoHideScrollbar();
- }
-
- private boolean onTouchEvent(@NonNull MotionEvent event) {
-
- if (!mScrollbarEnabled) {
- return false;
- }
-
- float eventX = event.getX();
- float eventY = event.getY();
- Rect padding = getPadding();
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
-
- mDownX = eventX;
- mDownY = eventY;
-
- if (mThumbView.getAlpha() > 0 && isInViewTouchTarget(mThumbView, eventX, eventY)) {
- mDragStartY = eventY;
- mDragStartThumbOffset = mThumbOffset;
- setDragging(true);
- }
- break;
- case MotionEvent.ACTION_MOVE:
-
- if (!mDragging && isInViewTouchTarget(mTrackView, mDownX, mDownY)
- && Math.abs(eventY - mDownY) > mTouchSlop) {
- if (isInViewTouchTarget(mThumbView, mDownX, mDownY)) {
- mDragStartY = mLastY;
- mDragStartThumbOffset = mThumbOffset;
- } else {
- mDragStartY = eventY;
- mDragStartThumbOffset = (int) (eventY - padding.top - mThumbHeight / 2f);
- scrollToThumbOffset(mDragStartThumbOffset);
- }
- setDragging(true);
- }
-
- if (mDragging) {
- int thumbOffset = mDragStartThumbOffset + (int) (eventY - mDragStartY);
- scrollToThumbOffset(thumbOffset);
- }
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
-
- setDragging(false);
- break;
- }
-
- mLastY = eventY;
-
- return mDragging;
- }
-
- private boolean isInView(@NonNull View view, float x, float y) {
- int scrollX = mView.getScrollX();
- int scrollY = mView.getScrollY();
- return x >= view.getLeft() - scrollX && x < view.getRight() - scrollX
- && y >= view.getTop() - scrollY && y < view.getBottom() - scrollY;
- }
-
- private boolean isInViewTouchTarget(@NonNull View view, float x, float y) {
- int scrollX = mView.getScrollX();
- int scrollY = mView.getScrollY();
- return isInTouchTarget(x, view.getLeft() - scrollX, view.getRight() - scrollX, 0,
- mView.getWidth())
- && isInTouchTarget(y, view.getTop() - scrollY, view.getBottom() - scrollY, 0,
- mView.getHeight());
- }
-
- private boolean isInTouchTarget(float position, int viewStart, int viewEnd, int parentStart,
- int parentEnd) {
- int viewSize = viewEnd - viewStart;
- if (viewSize >= mMinTouchTargetSize) {
- return position >= viewStart && position < viewEnd;
- }
- int touchTargetStart = viewStart - (mMinTouchTargetSize - viewSize) / 2;
- if (touchTargetStart < parentStart) {
- touchTargetStart = parentStart;
- }
- int touchTargetEnd = touchTargetStart + mMinTouchTargetSize;
- if (touchTargetEnd > parentEnd) {
- touchTargetEnd = parentEnd;
- touchTargetStart = touchTargetEnd - mMinTouchTargetSize;
- if (touchTargetStart < parentStart) {
- touchTargetStart = parentStart;
- }
- }
- return position >= touchTargetStart && position < touchTargetEnd;
- }
-
- private void scrollToThumbOffset(int thumbOffset) {
- int thumbOffsetRange = getThumbOffsetRange();
- thumbOffset = MathUtils.clamp(thumbOffset, 0, thumbOffsetRange);
- int scrollOffset = (int) ((long) getScrollOffsetRange() * thumbOffset / thumbOffsetRange);
- mViewHelper.scrollTo(scrollOffset);
- }
-
- private int getScrollOffsetRange() {
- return mViewHelper.getScrollRange() - mView.getHeight();
- }
-
- private int getThumbOffsetRange() {
- Rect padding = getPadding();
- return mView.getHeight() - padding.top - padding.bottom - mThumbHeight;
- }
-
- private void setDragging(boolean dragging) {
-
- if (mDragging == dragging) {
- return;
- }
- mDragging = dragging;
-
- if (mDragging) {
- mView.getParent().requestDisallowInterceptTouchEvent(true);
- }
-
- mTrackView.setPressed(mDragging);
- mThumbView.setPressed(mDragging);
-
- if (mDragging) {
- cancelAutoHideScrollbar();
- mAnimationHelper.showScrollbar(mTrackView, mThumbView);
- mAnimationHelper.showPopup(mPopupView);
- } else {
- postAutoHideScrollbar();
- mAnimationHelper.hidePopup(mPopupView);
- }
- }
-
- private void postAutoHideScrollbar() {
- cancelAutoHideScrollbar();
- if (mAnimationHelper.isScrollbarAutoHideEnabled()) {
- mView.postDelayed(mAutoHideScrollbarRunnable,
- mAnimationHelper.getScrollbarAutoHideDelayMillis());
- }
- }
-
- private void autoHideScrollbar() {
- if (mDragging) {
- return;
- }
- mAnimationHelper.hideScrollbar(mTrackView, mThumbView);
- }
-
- private void cancelAutoHideScrollbar() {
- mView.removeCallbacks(mAutoHideScrollbarRunnable);
- }
-
- public interface ViewHelper {
-
- void addOnPreDrawListener(@NonNull Runnable onPreDraw);
-
- void addOnScrollChangedListener(@NonNull Runnable onScrollChanged);
-
- void addOnTouchEventListener(@NonNull Predicate onTouchEvent);
-
- int getScrollRange();
-
- int getScrollOffset();
-
- void scrollTo(int offset);
-
- @Nullable
- default CharSequence getPopupText() {
- return null;
- }
- }
-
- public interface AnimationHelper {
-
- void showScrollbar(@NonNull View trackView, @NonNull View thumbView);
-
- void hideScrollbar(@NonNull View trackView, @NonNull View thumbView);
-
- boolean isScrollbarAutoHideEnabled();
-
- int getScrollbarAutoHideDelayMillis();
-
- void showPopup(@NonNull View popupView);
-
- void hidePopup(@NonNull View popupView);
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollerBuilder.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollerBuilder.java
deleted file mode 100644
index 57ceaf3..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollerBuilder.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.view.ViewGroup;
-import android.webkit.WebView;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.util.Consumer;
-import androidx.core.widget.NestedScrollView;
-import androidx.recyclerview.widget.RecyclerView;
-
-public class FastScrollerBuilder {
-
- @NonNull
- private final ViewGroup mView;
-
- @Nullable
- private FastScroller.ViewHelper mViewHelper;
-
- @Nullable
- private PopupTextProvider mPopupTextProvider;
-
- @Nullable
- private Rect mPadding;
-
- @NonNull
- private Drawable mTrackDrawable;
-
- @NonNull
- private Drawable mThumbDrawable;
-
- @NonNull
- private Consumer mPopupStyle;
-
- @Nullable
- private FastScroller.AnimationHelper mAnimationHelper;
-
- public FastScrollerBuilder(@NonNull ViewGroup view) {
- mView = view;
- useDefaultStyle();
- }
-
- @NonNull
- public FastScrollerBuilder setViewHelper(@Nullable FastScroller.ViewHelper viewHelper) {
- mViewHelper = viewHelper;
- return this;
- }
-
- @NonNull
- public FastScrollerBuilder setPopupTextProvider(@Nullable PopupTextProvider popupTextProvider) {
- mPopupTextProvider = popupTextProvider;
- return this;
- }
-
- @NonNull
- public FastScrollerBuilder setPadding(int left, int top, int right, int bottom) {
- if (mPadding == null) {
- mPadding = new Rect();
- }
- mPadding.set(left, top, right, bottom);
- return this;
- }
-
- @NonNull
- public FastScrollerBuilder setPadding(@Nullable Rect padding) {
- if (padding != null) {
- if (mPadding == null) {
- mPadding = new Rect();
- }
- mPadding.set(padding);
- } else {
- mPadding = null;
- }
- return this;
- }
-
- @NonNull
- public FastScrollerBuilder setTrackDrawable(@NonNull Drawable trackDrawable) {
- mTrackDrawable = trackDrawable;
- return this;
- }
-
- @NonNull
- public FastScrollerBuilder setThumbDrawable(@NonNull Drawable thumbDrawable) {
- mThumbDrawable = thumbDrawable;
- return this;
- }
-
- @NonNull
- public FastScrollerBuilder setPopupStyle(@NonNull Consumer popupStyle) {
- mPopupStyle = popupStyle;
- return this;
- }
-
- @NonNull
- public FastScrollerBuilder useDefaultStyle() {
- Context context = mView.getContext();
- mTrackDrawable = Utils.getGradientDrawableWithTintAttr(R.drawable.afs_track,
- androidx.appcompat.R.attr.colorControlNormal, context);
- mThumbDrawable = Utils.getGradientDrawableWithTintAttr(R.drawable.afs_thumb,
- androidx.appcompat.R.attr.colorControlActivated, context);
- mPopupStyle = PopupStyles.DEFAULT;
- return this;
- }
-
- @NonNull
- public FastScrollerBuilder useMd2Style() {
- Context context = mView.getContext();
- mTrackDrawable = Utils.getGradientDrawableWithTintAttr(R.drawable.afs_md2_track,
- androidx.appcompat.R.attr.colorControlNormal, context);
- mThumbDrawable = Utils.getGradientDrawableWithTintAttr(R.drawable.afs_md2_thumb,
- androidx.appcompat.R.attr.colorControlActivated, context);
- mPopupStyle = PopupStyles.MD2;
- return this;
- }
-
- public void setAnimationHelper(@Nullable FastScroller.AnimationHelper animationHelper) {
- mAnimationHelper = animationHelper;
- }
-
- public void disableScrollbarAutoHide() {
- DefaultAnimationHelper animationHelper = new DefaultAnimationHelper(mView);
- animationHelper.setScrollbarAutoHideEnabled(false);
- mAnimationHelper = animationHelper;
- }
-
- @NonNull
- public FastScroller build() {
- return new FastScroller(mView, getOrCreateViewHelper(), mPadding, mTrackDrawable,
- mThumbDrawable, mPopupStyle, getOrCreateAnimationHelper());
- }
-
- @NonNull
- private FastScroller.ViewHelper getOrCreateViewHelper() {
- if (mViewHelper != null) {
- return mViewHelper;
- }
- if (mView instanceof ViewHelperProvider) {
- return ((ViewHelperProvider) mView).getViewHelper();
- } else if (mView instanceof RecyclerView) {
- return new RecyclerViewHelper((RecyclerView) mView, mPopupTextProvider);
- } else if (mView instanceof NestedScrollView) {
- throw new UnsupportedOperationException("Please use "
- + FastScrollNestedScrollView.class.getSimpleName() + " instead of "
- + NestedScrollView.class.getSimpleName() + "for fast scroll");
- } else if (mView instanceof ScrollView) {
- throw new UnsupportedOperationException("Please use "
- + FastScrollScrollView.class.getSimpleName() + " instead of "
- + ScrollView.class.getSimpleName() + "for fast scroll");
- } else if (mView instanceof WebView) {
- throw new UnsupportedOperationException("Please use "
- + FastScrollWebView.class.getSimpleName() + " instead of "
- + WebView.class.getSimpleName() + "for fast scroll");
- } else {
- throw new UnsupportedOperationException(mView.getClass().getSimpleName()
- + " is not supported for fast scroll");
- }
- }
-
- @NonNull
- private FastScroller.AnimationHelper getOrCreateAnimationHelper() {
- if (mAnimationHelper != null) {
- return mAnimationHelper;
- }
- return new DefaultAnimationHelper(mView);
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixItemDecorationRecyclerView.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixItemDecorationRecyclerView.java
deleted file mode 100644
index d8c3d33..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixItemDecorationRecyclerView.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.View;
-
-import androidx.annotation.AttrRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.RecyclerView;
-
-public class FixItemDecorationRecyclerView extends RecyclerView {
-
- public FixItemDecorationRecyclerView(@NonNull Context context) {
- super(context);
- }
-
- public FixItemDecorationRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
-
- public FixItemDecorationRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @Override
- protected void dispatchDraw(@NonNull Canvas canvas) {
- for (int i = 0, count = getItemDecorationCount(); i < count; ++i) {
- FixItemDecoration decor = (FixItemDecoration) super.getItemDecorationAt(i);
- decor.getItemDecoration().onDraw(canvas, this, decor.getState());
- }
- super.dispatchDraw(canvas);
- for (int i = 0, count = getItemDecorationCount(); i < count; ++i) {
- FixItemDecoration decor = (FixItemDecoration) super.getItemDecorationAt(i);
- decor.getItemDecoration().onDrawOver(canvas, this, decor.getState());
- }
- }
-
- @Override
- public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
- super.addItemDecoration(new FixItemDecoration(decor), index);
- }
-
- @NonNull
- @Override
- public ItemDecoration getItemDecorationAt(int index) {
- return ((FixItemDecoration) super.getItemDecorationAt(index)).getItemDecoration();
- }
-
- @Override
- public void removeItemDecoration(@NonNull ItemDecoration decor) {
- if (!(decor instanceof FixItemDecoration)) {
- for (int i = 0, count = getItemDecorationCount(); i < count; ++i) {
- FixItemDecoration fixDecor = (FixItemDecoration) super.getItemDecorationAt(i);
- if (fixDecor.getItemDecoration() == decor) {
- decor = fixDecor;
- break;
- }
- }
- }
- super.removeItemDecoration(decor);
- }
-
- private static class FixItemDecoration extends ItemDecoration {
-
- @NonNull
- private final ItemDecoration mItemDecoration;
-
- private State mState;
-
- private FixItemDecoration(@NonNull ItemDecoration itemDecoration) {
- mItemDecoration = itemDecoration;
- }
-
- @NonNull
- public ItemDecoration getItemDecoration() {
- return mItemDecoration;
- }
-
- public State getState() {
- return mState;
- }
-
- @Override
- public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
- mState = state;
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {}
-
- @Override
- public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
- @NonNull State state) {}
-
- @Override
- @SuppressWarnings("deprecation")
- public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {}
-
- @Override
- @SuppressWarnings("deprecation")
- public void getItemOffsets(@NonNull Rect outRect, int itemPosition,
- @NonNull RecyclerView parent) {
- mItemDecoration.getItemOffsets(outRect, itemPosition, parent);
- }
-
- @Override
- public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
- @NonNull RecyclerView parent, @NonNull State state) {
- mItemDecoration.getItemOffsets(outRect, view, parent, state);
- }
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixOnItemTouchListenerRecyclerView.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixOnItemTouchListenerRecyclerView.java
deleted file mode 100644
index 5400792..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixOnItemTouchListenerRecyclerView.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-
-import androidx.annotation.AttrRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.RecyclerView;
-
-public class FixOnItemTouchListenerRecyclerView extends RecyclerView {
-
- @NonNull
- private final OnItemTouchDispatcher mOnItemTouchDispatcher = new OnItemTouchDispatcher();
-
- public FixOnItemTouchListenerRecyclerView(@NonNull Context context) {
- super(context);
-
- init();
- }
-
- public FixOnItemTouchListenerRecyclerView(@NonNull Context context,
- @Nullable AttributeSet attrs) {
- super(context, attrs);
-
- init();
- }
-
- public FixOnItemTouchListenerRecyclerView(@NonNull Context context,
- @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- init();
- }
-
- private void init() {
- super.addOnItemTouchListener(mOnItemTouchDispatcher);
- }
-
- @Override
- public void addOnItemTouchListener(@NonNull OnItemTouchListener listener) {
- mOnItemTouchDispatcher.addListener(listener);
- }
-
- @Override
- public void removeOnItemTouchListener(@NonNull OnItemTouchListener listener) {
- mOnItemTouchDispatcher.removeListener(listener);
- }
-
- private static class OnItemTouchDispatcher implements OnItemTouchListener {
-
- @NonNull
- private final List mListeners = new ArrayList<>();
-
- @NonNull
- private final Set mTrackingListeners = new LinkedHashSet<>();
-
- @Nullable
- private OnItemTouchListener mInterceptingListener;
-
- public void addListener(@NonNull OnItemTouchListener listener) {
- mListeners.add(listener);
- }
-
- public void removeListener(@NonNull OnItemTouchListener listener) {
- mListeners.remove(listener);
- mTrackingListeners.remove(listener);
- if (mInterceptingListener == listener) {
- mInterceptingListener = null;
- }
- }
-
- // @see RecyclerView#findInterceptingOnItemTouchListener
- @Override
- public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
- @NonNull MotionEvent event) {
- int action = event.getAction();
- for (OnItemTouchListener listener : mListeners) {
- boolean intercepted = listener.onInterceptTouchEvent(recyclerView, event);
- if (action == MotionEvent.ACTION_CANCEL) {
- mTrackingListeners.remove(listener);
- continue;
- }
- if (intercepted) {
- mTrackingListeners.remove(listener);
- event.setAction(MotionEvent.ACTION_CANCEL);
- for (OnItemTouchListener trackingListener : mTrackingListeners) {
- trackingListener.onInterceptTouchEvent(recyclerView, event);
- }
- event.setAction(action);
- mTrackingListeners.clear();
- mInterceptingListener = listener;
- return true;
- } else {
- mTrackingListeners.add(listener);
- }
- }
- return false;
- }
-
- @Override
- public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
- if (mInterceptingListener == null) {
- return;
- }
- mInterceptingListener.onTouchEvent(recyclerView, event);
- int action = event.getAction();
- if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
- mInterceptingListener = null;
- }
- }
-
- @Override
- public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
- for (OnItemTouchListener listener : mListeners) {
- listener.onRequestDisallowInterceptTouchEvent(disallowIntercept);
- }
- }
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Md2PopupBackground.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Md2PopupBackground.java
deleted file mode 100644
index 453c20b..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Md2PopupBackground.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Matrix;
-import android.graphics.Outline;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.graphics.drawable.DrawableCompat;
-
-class Md2PopupBackground extends Drawable {
-
- @NonNull
- private final Paint mPaint;
- private final int mPaddingStart;
- private final int mPaddingEnd;
-
- @NonNull
- private final Path mPath = new Path();
-
- @NonNull
- private final Matrix mTempMatrix = new Matrix();
-
- public Md2PopupBackground(@NonNull Context context) {
- mPaint = new Paint();
- mPaint.setAntiAlias(true);
- mPaint.setColor(Utils.getColorFromAttrRes(androidx.appcompat.R.attr.colorControlActivated, context));
- mPaint.setStyle(Paint.Style.FILL);
- Resources resources = context.getResources();
- mPaddingStart = resources.getDimensionPixelOffset(R.dimen.afs_md2_popup_padding_start);
- mPaddingEnd = resources.getDimensionPixelOffset(R.dimen.afs_md2_popup_padding_end);
- }
-
- @Override
- public void draw(@NonNull Canvas canvas) {
- canvas.drawPath(mPath, mPaint);
- }
-
- @Override
- public boolean onLayoutDirectionChanged(int layoutDirection) {
- updatePath();
- return true;
- }
-
- @Override
- public void setAlpha(int alpha) {}
-
- @Override
- public void setColorFilter(@Nullable ColorFilter colorFilter) {}
-
- @Override
- public boolean isAutoMirrored() {
- return true;
- }
-
- private boolean needMirroring() {
- return DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL;
- }
-
- @Override
- public int getOpacity() {
- return PixelFormat.TRANSLUCENT;
- }
-
- @Override
- protected void onBoundsChange(@NonNull Rect bounds) {
- updatePath();
- }
-
- private void updatePath() {
-
- mPath.reset();
-
- Rect bounds = getBounds();
- float width = bounds.width();
- float height = bounds.height();
- float r = height / 2;
- float sqrt2 = (float) Math.sqrt(2);
- // Ensure we are convex.
- width = Math.max(r + sqrt2 * r, width);
- pathArcTo(mPath, r, r, r, 90, 180);
- float o1X = width - sqrt2 * r;
- pathArcTo(mPath, o1X, r, r, -90, 45f);
- float r2 = r / 5;
- float o2X = width - sqrt2 * r2;
- pathArcTo(mPath, o2X, r, r2, -45, 90);
- pathArcTo(mPath, o1X, r, r, 45f, 45f);
- mPath.close();
-
- if (needMirroring()) {
- mTempMatrix.setScale(-1, 1, width / 2, 0);
- } else {
- mTempMatrix.reset();
- }
- mTempMatrix.postTranslate(bounds.left, bounds.top);
- mPath.transform(mTempMatrix);
- }
-
- private static void pathArcTo(@NonNull Path path, float centerX, float centerY, float radius,
- float startAngle, float sweepAngle) {
- path.arcTo(centerX - radius, centerY - radius, centerX + radius, centerY + radius,
- startAngle, sweepAngle, false);
- }
-
- @Override
- public boolean getPadding(@NonNull Rect padding) {
- if (needMirroring()) {
- padding.set(mPaddingEnd, 0, mPaddingStart, 0);
- } else {
- padding.set(mPaddingStart, 0, mPaddingEnd, 0);
- }
- return true;
- }
-
- @Override
- public void getOutline(@NonNull Outline outline) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && !mPath.isConvex()) {
- // The outline path must be convex before Q, but we may run into floating point error
- // caused by calculation involving sqrt(2) or OEM implementation difference, so in this
- // case we just omit the shadow instead of crashing.
- super.getOutline(outline);
- return;
- }
- outline.setConvexPath(mPath);
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupStyles.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupStyles.java
deleted file mode 100644
index 10ea3f2..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupStyles.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.text.TextUtils;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import androidx.core.util.Consumer;
-
-public class PopupStyles {
-
- private PopupStyles() {}
-
- public static Consumer DEFAULT = popupView -> {
- Resources resources = popupView.getResources();
- int minimumSize = resources.getDimensionPixelSize(R.dimen.afs_popup_min_size);
- popupView.setMinimumWidth(minimumSize);
- popupView.setMinimumHeight(minimumSize);
- FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams)
- popupView.getLayoutParams();
- layoutParams.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
- layoutParams.setMarginEnd(resources.getDimensionPixelOffset(R.dimen.afs_popup_margin_end));
- popupView.setLayoutParams(layoutParams);
- Context context = popupView.getContext();
- popupView.setBackground(new AutoMirrorDrawable(Utils.getGradientDrawableWithTintAttr(
- R.drawable.afs_popup_background, androidx.appcompat.R.attr.colorControlActivated, context)));
- popupView.setEllipsize(TextUtils.TruncateAt.MIDDLE);
- popupView.setGravity(Gravity.CENTER);
- popupView.setIncludeFontPadding(false);
- popupView.setSingleLine(true);
- popupView.setTextColor(Utils.getColorFromAttrRes(android.R.attr.textColorPrimaryInverse,
- context));
- popupView.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimensionPixelSize(
- R.dimen.afs_popup_text_size));
- };
-
- public static Consumer MD2 = popupView -> {
- Resources resources = popupView.getResources();
- popupView.setMinimumWidth(resources.getDimensionPixelSize(
- R.dimen.afs_md2_popup_min_width));
- popupView.setMinimumHeight(resources.getDimensionPixelSize(
- R.dimen.afs_md2_popup_min_height));
- FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams)
- popupView.getLayoutParams();
- layoutParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
- layoutParams.setMarginEnd(resources.getDimensionPixelOffset(
- R.dimen.afs_md2_popup_margin_end));
- popupView.setLayoutParams(layoutParams);
- Context context = popupView.getContext();
- popupView.setBackground(new Md2PopupBackground(context));
- popupView.setElevation(resources.getDimensionPixelOffset(R.dimen.afs_md2_popup_elevation));
- popupView.setEllipsize(TextUtils.TruncateAt.MIDDLE);
- popupView.setGravity(Gravity.CENTER);
- popupView.setIncludeFontPadding(false);
- popupView.setSingleLine(true);
- popupView.setTextColor(Utils.getColorFromAttrRes(android.R.attr.textColorPrimaryInverse,
- context));
- popupView.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimensionPixelSize(
- R.dimen.afs_md2_popup_text_size));
- };
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupTextProvider.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupTextProvider.java
deleted file mode 100644
index 69cc848..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupTextProvider.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.view.View;
-
-import androidx.annotation.NonNull;
-
-public interface PopupTextProvider {
-
- @NonNull
- CharSequence getPopupText(@NonNull View view, int position);
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Predicate.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Predicate.java
deleted file mode 100644
index 0bc6793..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Predicate.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-@FunctionalInterface
-public interface Predicate {
-
- boolean test(T t);
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/RecyclerViewHelper.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/RecyclerViewHelper.java
deleted file mode 100644
index 18e799f..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/RecyclerViewHelper.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.view.MotionEvent;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.GridLayoutManager;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-class RecyclerViewHelper implements FastScroller.ViewHelper {
-
- @NonNull
- private final RecyclerView mView;
- @Nullable
- private final PopupTextProvider mPopupTextProvider;
-
- @NonNull
- private final Rect mTempRect = new Rect();
-
- public RecyclerViewHelper(@NonNull RecyclerView view,
- @Nullable PopupTextProvider popupTextProvider) {
- mView = view;
- mPopupTextProvider = popupTextProvider;
- }
-
- @Override
- public void addOnPreDrawListener(@NonNull Runnable onPreDraw) {
- mView.addItemDecoration(new RecyclerView.ItemDecoration() {
- @Override
- public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent,
- @NonNull RecyclerView.State state) {
- onPreDraw.run();
- }
- });
- }
-
- @Override
- public void addOnScrollChangedListener(@NonNull Runnable onScrollChanged) {
- mView.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @Override
- public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
- onScrollChanged.run();
- }
- });
- }
-
- @Override
- public void addOnTouchEventListener(@NonNull Predicate onTouchEvent) {
- mView.addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener() {
- @Override
- public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
- @NonNull MotionEvent event) {
- return onTouchEvent.test(event);
- }
- @Override
- public void onTouchEvent(@NonNull RecyclerView recyclerView,
- @NonNull MotionEvent event) {
- onTouchEvent.test(event);
- }
- });
- }
-
- @Override
- public int getScrollRange() {
- int itemCount = getItemCount();
- if (itemCount == 0) {
- return 0;
- }
- int itemHeight = getItemHeight();
- if (itemHeight == 0) {
- return 0;
- }
- return mView.getPaddingTop() + itemCount * itemHeight + mView.getPaddingBottom();
- }
-
- @Override
- public int getScrollOffset() {
- int firstItemPosition = getFirstItemPosition();
- if (firstItemPosition == RecyclerView.NO_POSITION) {
- return 0;
- }
- int itemHeight = getItemHeight();
- int firstItemTop = getFirstItemOffset();
- return mView.getPaddingTop() + firstItemPosition * itemHeight - firstItemTop;
- }
-
- @Override
- public void scrollTo(int offset) {
- // Stop any scroll in progress for RecyclerView.
- mView.stopScroll();
- offset -= mView.getPaddingTop();
- int itemHeight = getItemHeight();
- // firstItemPosition should be non-negative even if paddingTop is greater than item height.
- int firstItemPosition = Math.max(0, offset / itemHeight);
- int firstItemTop = firstItemPosition * itemHeight - offset;
- scrollToPositionWithOffset(firstItemPosition, firstItemTop);
- }
-
- @Nullable
- @Override
- public CharSequence getPopupText() {
- PopupTextProvider popupTextProvider = mPopupTextProvider;
- if (popupTextProvider == null) {
- RecyclerView.Adapter> adapter = mView.getAdapter();
- if (adapter instanceof PopupTextProvider) {
- popupTextProvider = (PopupTextProvider) adapter;
- }
- }
- if (popupTextProvider == null) {
- return null;
- }
- int position = getFirstItemAdapterPosition();
- if (position == RecyclerView.NO_POSITION) {
- return null;
- }
- return popupTextProvider.getPopupText(mView, position);
- }
-
- private int getItemCount() {
- LinearLayoutManager linearLayoutManager = getVerticalLinearLayoutManager();
- if (linearLayoutManager == null) {
- return 0;
- }
- int itemCount = linearLayoutManager.getItemCount();
- if (itemCount == 0) {
- return 0;
- }
- if (linearLayoutManager instanceof GridLayoutManager) {
- GridLayoutManager gridLayoutManager = (GridLayoutManager) linearLayoutManager;
- itemCount = (itemCount - 1) / gridLayoutManager.getSpanCount() + 1;
- }
- return itemCount;
- }
-
- private int getItemHeight() {
- if (mView.getChildCount() == 0) {
- return 0;
- }
- View itemView = mView.getChildAt(0);
- mView.getDecoratedBoundsWithMargins(itemView, mTempRect);
- return mTempRect.height();
- }
-
- private int getFirstItemPosition() {
- int position = getFirstItemAdapterPosition();
- LinearLayoutManager linearLayoutManager = getVerticalLinearLayoutManager();
- if (linearLayoutManager == null) {
- return RecyclerView.NO_POSITION;
- }
- if (linearLayoutManager instanceof GridLayoutManager) {
- GridLayoutManager gridLayoutManager = (GridLayoutManager) linearLayoutManager;
- position /= gridLayoutManager.getSpanCount();
- }
- return position;
- }
-
- private int getFirstItemAdapterPosition() {
- if (mView.getChildCount() == 0) {
- return RecyclerView.NO_POSITION;
- }
- View itemView = mView.getChildAt(0);
- LinearLayoutManager linearLayoutManager = getVerticalLinearLayoutManager();
- if (linearLayoutManager == null) {
- return RecyclerView.NO_POSITION;
- }
- return linearLayoutManager.getPosition(itemView);
- }
-
- private int getFirstItemOffset() {
- if (mView.getChildCount() == 0) {
- return RecyclerView.NO_POSITION;
- }
- View itemView = mView.getChildAt(0);
- mView.getDecoratedBoundsWithMargins(itemView, mTempRect);
- return mTempRect.top;
- }
-
- private void scrollToPositionWithOffset(int position, int offset) {
- LinearLayoutManager linearLayoutManager = getVerticalLinearLayoutManager();
- if (linearLayoutManager == null) {
- return;
- }
- if (linearLayoutManager instanceof GridLayoutManager) {
- GridLayoutManager gridLayoutManager = (GridLayoutManager) linearLayoutManager;
- position *= gridLayoutManager.getSpanCount();
- }
- // LinearLayoutManager actually takes offset from paddingTop instead of top of RecyclerView.
- offset -= mView.getPaddingTop();
- linearLayoutManager.scrollToPositionWithOffset(position, offset);
- }
-
- @Nullable
- private LinearLayoutManager getVerticalLinearLayoutManager() {
- RecyclerView.LayoutManager layoutManager = mView.getLayoutManager();
- if (!(layoutManager instanceof LinearLayoutManager)) {
- return null;
- }
- LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
- if (linearLayoutManager.getOrientation() != RecyclerView.VERTICAL) {
- return null;
- }
- return linearLayoutManager;
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/SimpleViewHelper.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/SimpleViewHelper.java
deleted file mode 100644
index 2a8d01d..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/SimpleViewHelper.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (c) 2019 Hai Zhang
- * All Rights Reserved.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.graphics.Canvas;
-import android.view.MotionEvent;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-public abstract class SimpleViewHelper implements FastScroller.ViewHelper {
-
- @Nullable
- private Runnable mOnPreDrawListener;
-
- @Nullable
- private Runnable mOnScrollChangedListener;
-
- @Nullable
- private Predicate mOnTouchEventListener;
- private boolean mListenerInterceptingTouchEvent;
-
- @Override
- public void addOnPreDrawListener(@Nullable Runnable listener) {
- mOnPreDrawListener = listener;
- }
-
- public void draw(@NonNull Canvas canvas) {
-
- if (mOnPreDrawListener != null) {
- mOnPreDrawListener.run();
- }
-
- superDraw(canvas);
- }
-
- @Override
- public void addOnScrollChangedListener(@Nullable Runnable listener) {
- mOnScrollChangedListener = listener;
- }
-
- public void onScrollChanged(int left, int top, int oldLeft, int oldTop) {
- superOnScrollChanged(left, top, oldLeft, oldTop);
-
- if (mOnScrollChangedListener != null) {
- mOnScrollChangedListener.run();
- }
- }
-
- @Override
- public void addOnTouchEventListener(@Nullable Predicate listener) {
- mOnTouchEventListener = listener;
- }
-
- public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
-
- if (mOnTouchEventListener != null && mOnTouchEventListener.test(event)) {
-
- int actionMasked = event.getActionMasked();
- if (actionMasked != MotionEvent.ACTION_UP
- && actionMasked != MotionEvent.ACTION_CANCEL) {
- mListenerInterceptingTouchEvent = true;
- }
-
- if (actionMasked != MotionEvent.ACTION_CANCEL) {
- MotionEvent cancelEvent = MotionEvent.obtain(event);
- cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
- superOnInterceptTouchEvent(cancelEvent);
- cancelEvent.recycle();
- } else {
- superOnInterceptTouchEvent(event);
- }
-
- return true;
- }
-
- return superOnInterceptTouchEvent(event);
- }
-
- public boolean onTouchEvent(@NonNull MotionEvent event) {
-
- if (mOnTouchEventListener != null) {
- if (mListenerInterceptingTouchEvent) {
-
- mOnTouchEventListener.test(event);
-
- int actionMasked = event.getActionMasked();
- if (actionMasked == MotionEvent.ACTION_UP
- || actionMasked == MotionEvent.ACTION_CANCEL) {
- mListenerInterceptingTouchEvent = false;
- }
-
- return true;
- } else {
- int actionMasked = event.getActionMasked();
- if (actionMasked != MotionEvent.ACTION_DOWN && mOnTouchEventListener.test(event)) {
-
- if (actionMasked != MotionEvent.ACTION_UP
- && actionMasked != MotionEvent.ACTION_CANCEL) {
- mListenerInterceptingTouchEvent = true;
- }
-
- if (actionMasked != MotionEvent.ACTION_CANCEL) {
- MotionEvent cancelEvent = MotionEvent.obtain(event);
- cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
- superOnTouchEvent(cancelEvent);
- cancelEvent.recycle();
- } else {
- superOnTouchEvent(event);
- }
-
- return true;
- }
- }
- }
-
- return superOnTouchEvent(event);
- }
-
- @Override
- public int getScrollRange() {
- return computeVerticalScrollRange();
- }
-
- @Override
- public int getScrollOffset() {
- return computeVerticalScrollOffset();
- }
-
- @Override
- public void scrollTo(int offset) {
- scrollTo(getScrollX(), offset);
- }
-
- protected abstract void superDraw(@NonNull Canvas canvas);
-
- protected abstract void superOnScrollChanged(int left, int top, int oldLeft, int oldTop);
-
- protected abstract boolean superOnInterceptTouchEvent(@NonNull MotionEvent event);
-
- protected abstract boolean superOnTouchEvent(@NonNull MotionEvent event);
-
- protected abstract int computeVerticalScrollRange();
-
- protected abstract int computeVerticalScrollOffset();
-
- protected abstract int getScrollX();
-
- protected abstract void scrollTo(int x, int y);
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Utils.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Utils.java
deleted file mode 100644
index 4ed5234..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Utils.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.os.Build;
-
-import androidx.annotation.AttrRes;
-import androidx.annotation.ColorInt;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.content.res.AppCompatResources;
-import androidx.core.graphics.drawable.DrawableCompat;
-
-class Utils {
-
- @ColorInt
- public static int getColorFromAttrRes(@AttrRes int attrRes, @NonNull Context context) {
- ColorStateList colorStateList = getColorStateListFromAttrRes(attrRes, context);
- return colorStateList != null ? colorStateList.getDefaultColor() : 0;
- }
-
- @Nullable
- public static ColorStateList getColorStateListFromAttrRes(@AttrRes int attrRes,
- @NonNull Context context) {
- TypedArray a = context.obtainStyledAttributes(new int[] { attrRes });
- int resId;
- try {
- resId = a.getResourceId(0, 0);
- if (resId != 0) {
- return AppCompatResources.getColorStateList(context, resId);
- }
- return a.getColorStateList(0);
- } finally {
- a.recycle();
- }
- }
-
- // Work around the bug that GradientDrawable didn't actually implement tinting until
- // Lollipop MR1 (API 22).
- @Nullable
- public static Drawable getGradientDrawableWithTintAttr(@DrawableRes int drawableRes,
- @AttrRes int tintAttrRes,
- @NonNull Context context) {
- Drawable drawable = AppCompatResources.getDrawable(context, drawableRes);
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1
- && drawable instanceof GradientDrawable) {
- drawable = DrawableCompat.wrap(drawable);
- drawable.setTintList(getColorStateListFromAttrRes(tintAttrRes, context));
- }
- return drawable;
- }
-}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/ViewHelperProvider.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/ViewHelperProvider.java
deleted file mode 100644
index 9bf2d30..0000000
--- a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/ViewHelperProvider.java
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Copyright (c) 2019 Hai Zhang
- * All Rights Reserved.
- */
-
-package me.zhanghai.android.fastscroll;
-
-import androidx.annotation.NonNull;
-
-public interface ViewHelperProvider {
-
- @NonNull
- FastScroller.ViewHelper getViewHelper();
-}
diff --git a/fastscroll/src/main/res/drawable/afs_md2_thumb.xml b/fastscroll/src/main/res/drawable/afs_md2_thumb.xml
deleted file mode 100644
index f45fa91..0000000
--- a/fastscroll/src/main/res/drawable/afs_md2_thumb.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/fastscroll/src/main/res/drawable/afs_md2_track.xml b/fastscroll/src/main/res/drawable/afs_md2_track.xml
deleted file mode 100644
index 02f03a5..0000000
--- a/fastscroll/src/main/res/drawable/afs_md2_track.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/fastscroll/src/main/res/drawable/afs_popup_background.xml b/fastscroll/src/main/res/drawable/afs_popup_background.xml
deleted file mode 100644
index f0a8c43..0000000
--- a/fastscroll/src/main/res/drawable/afs_popup_background.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/fastscroll/src/main/res/drawable/afs_thumb.xml b/fastscroll/src/main/res/drawable/afs_thumb.xml
deleted file mode 100644
index 23565a8..0000000
--- a/fastscroll/src/main/res/drawable/afs_thumb.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/fastscroll/src/main/res/drawable/afs_thumb_stateful.xml b/fastscroll/src/main/res/drawable/afs_thumb_stateful.xml
deleted file mode 100644
index c71f527..0000000
--- a/fastscroll/src/main/res/drawable/afs_thumb_stateful.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
diff --git a/fastscroll/src/main/res/drawable/afs_track.xml b/fastscroll/src/main/res/drawable/afs_track.xml
deleted file mode 100644
index e858cb5..0000000
--- a/fastscroll/src/main/res/drawable/afs_track.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/fastscroll/src/main/res/values/dimens.xml b/fastscroll/src/main/res/values/dimens.xml
deleted file mode 100644
index 73e24af..0000000
--- a/fastscroll/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
- 48dp
-
- 88dp
- 16dp
- 45dp
-
- 78dp
- 64dp
- 14dp
- 16dp
- 29dp
- 3dp
- 34dp
-
diff --git a/gradle.properties b/gradle.properties
index 8ea0209..20e2a01 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -8,8 +8,8 @@
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
@@ -20,5 +20,4 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
-
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8ed6ce8..404dcf4 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,69 +1,93 @@
[versions]
# Android X
-core = "1.15.0"
coreKtx = "1.15.0"
-appcompat = "1.7.0"
-activity = "1.10.0"
-activityKtx = "1.10.0"
-constraintlayout = "2.2.0"
-preference = "1.2.1"
-preferenceKtx = "1.2.1"
-lifecycleViewmodel = "2.8.7"
-lifecycleViewmodelKtx = "2.8.7"
-recyclerview = "1.4.0"
-fragment = "1.8.5"
-fragmentKtx = "1.8.5"
-roomRuntime = "2.6.1"
splashScreen = "1.2.0-alpha02"
-security = "1.1.0-alpha06"
-# Material
-material = "1.13.0-alpha10"
-# Fast Scroll
-fastScroll = "1.3.0"
-# Room Backup
-# roomBackup = "1.0.2"
+lifecycleRuntimeKtx = "2.8.7"
+# security = "1.1.0-alpha06"
+# Compose
+activityCompose = "1.10.0"
+composeBom = "2025.01.01"
+liveData = "1.7.6"
+icons = "1.7.7"
+room = "2.6.1"
+navigation = "2.8.6"
+material3 = "1.4.0-alpha07"
+adaptive = "1.1.0-beta01"
+# AboutLibraries
+aboutLibsRelease = "11.3.0-rc02"
+# M3 Color
+m3color = "2024.6"
+# Konfetti
+konfetti = "2.0.5"
+# Lazy Column Scrollbar
+lazycolumnscrollbar = "2.2.0"
+# Kotlin
+kotlinCoroutines = "1.9.0"
# Test
-espressoCore = "3.6.1"
junit = "4.13.2"
junitVersion = "1.2.1"
+espressoCore = "3.6.1"
# Plugins
agp = "8.8.0"
-kotlin-android = "2.1.10"
+kotlin = "2.1.10"
ksp = "2.1.10-1.0.29"
[libraries]
# Android X
-androidx-core = { group = "androidx.core", name = "core", version.ref = "core" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
-androidx-core-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashScreen" }
-androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
-androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
-androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
-androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
-androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
-androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" }
-androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "lifecycleViewmodel" }
-androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
-androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
-androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "fragment" }
-androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
-androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomRuntime" }
-androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomRuntime" }
-androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "roomRuntime" }
-androidx-security = { group = "androidx.security", name = "security-crypto", version.ref = "security" }
-# Material
-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
-# Fast Scroll
-fast-scroll = { group = "me.zhanghai.android.fastscroll", name = "library", version.ref = "fastScroll" }
-# Room Backup
-# room-backup = { group = "de.raphaelebner", name = "roomdatabasebackup", version = "roomBackup" }
+androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashScreen" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
+androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "liveData" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+# androidx-security = { group = "androidx.security", name = "security-crypto", version.ref = "security" }
+
+# Compose
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-animation = { group = "androidx.compose.animation", name = "animation" }
+androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
+androidx-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "adaptive" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
+androidx-material-icon-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "icons" }
+androidx-material-icon-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "icons" }
+
+# Room
+androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
+androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
+androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
+
+# About Libraries
+aboutlibraries-core = { group = "com.mikepenz", name = "aboutlibraries-core", version.ref = "aboutLibsRelease" }
+aboutlibraries-compose = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "aboutLibsRelease" }
+
+# M3 Color
+com-kyant0-m3color = { group = "com.github.Kyant0", name = "m3color", version.ref = "m3color" }
+
+# Konfetti
+nl-dionsegijn-konfetti-compose = { group = "nl.dionsegijn", name = "konfetti-compose", version.ref = "konfetti" }
+
+# Lazy Column Scrollbar
+lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazycolumnscrollbar" }
+
+# Kotlin
+kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinCoroutines" }
+kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinCoroutines" }
+
# Test
junit = { group = "junit", name = "junit", version.ref = "junit" }
-androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-android" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
-android-library = { id = "com.android.library", version.ref = "agp" }
\ No newline at end of file
+aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibsRelease" }
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 640ece8..f9b1b8c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,6 +1,12 @@
pluginManagement {
repositories {
- google()
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
mavenCentral()
gradlePluginPortal()
}
@@ -10,9 +16,9 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven("https://jitpack.io")
}
}
rootProject.name = "ToDo"
include(":app")
-include(":fastscroll")