Merge pull request #149 from Super12138/compose-refactor
使用 Jetpack Compose 重构大部分功能
This commit is contained in:
commit
ba2c979831
249 changed files with 4533 additions and 8517 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
16
app/licences/libraries/eastereggs.json
Normal file
16
app/licences/libraries/eastereggs.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
16
app/licences/libraries/libchecker.json
Normal file
16
app/licences/libraries/libchecker.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
16
app/licences/libraries/mauth.json
Normal file
16
app/licences/libraries/mauth.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
16
app/licences/libraries/readyou.json
Normal file
16
app/licences/libraries/readyou.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
13
app/licences/libraries/skipad.json
Normal file
13
app/licences/libraries/skipad.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
|
|
@ -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
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,9 @@
|
|||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:name=".ToDoApp"
|
||||
android:name=".TodoApp"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="productivity"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
|
|
@ -15,22 +13,20 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ToDo.Starting"
|
||||
tools:targetApi="35">
|
||||
|
||||
<activity
|
||||
android:name=".views.activities.MainActivity"
|
||||
android:name=".ui.activities.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".views.activities.CrashActivity"
|
||||
android:name=".ui.activities.CrashActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.ToDo" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -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)
|
||||
context = applicationContext
|
||||
|
||||
val crashHandler = CrashHandler(this)
|
||||
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
|
||||
|
||||
db = database
|
||||
context = applicationContext
|
||||
|
||||
val crashHandler = CrashHandler(applicationContext)
|
||||
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
suspend fun insertTodo(toDo: TodoEntity) {
|
||||
toDoDao.insert(toDo)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部未完成的待办
|
||||
* @return List<ToDoRoom>
|
||||
*/
|
||||
suspend fun getAllIncomplete(): List<ToDoRoom> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
todoDao.getAllUnfinished()
|
||||
}
|
||||
fun getAllTodos(): Flow<List<TodoEntity>> = toDoDao.getAll()
|
||||
|
||||
suspend fun updateTodo(toDo: TodoEntity) {
|
||||
toDoDao.update(toDo)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部已完成的待办
|
||||
* @return List<ToDoRoom>
|
||||
*/
|
||||
suspend fun getAllComplete(): List<ToDoRoom> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
todoDao.getAllComplete()
|
||||
}
|
||||
suspend fun deleteTodo(toDo: TodoEntity) {
|
||||
toDoDao.delete(toDo)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部待办
|
||||
* @return List<ToDoRoom>
|
||||
*/
|
||||
suspend fun getAll(): List<ToDoRoom> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
todoDao.getAll()
|
||||
}
|
||||
suspend fun deleteTodoFromIds(toDoItems: List<Int>) {
|
||||
toDoDao.deleteFromIds(toDoItems.toSet())
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据待办的UUID删除指定待办
|
||||
* @param uuid 待办的UUID
|
||||
*/
|
||||
suspend fun deleteByUUID(uuid: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
todoDao.deleteByUUID(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除全部代办
|
||||
*/
|
||||
suspend fun deleteAll() {
|
||||
withContext(Dispatchers.IO) {
|
||||
todoDao.deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据代办的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()
|
||||
}*/
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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<ToDoRoom>
|
||||
|
||||
@Query("SELECT * FROM todo WHERE state = 0")
|
||||
suspend fun getAllUnfinished(): List<ToDoRoom>
|
||||
|
||||
@Query("SELECT * FROM todo WHERE state = 1")
|
||||
suspend fun getAllComplete(): List<ToDoRoom>
|
||||
|
||||
@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)
|
||||
}
|
||||
|
|
@ -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<List<TodoEntity>>
|
||||
|
||||
@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<Int>)
|
||||
|
||||
/*@Query("DELETE FROM todo")
|
||||
suspend fun deleteAllTodo()*/
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
14
app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt
Normal file
14
app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>,
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
120
app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt
Normal file
120
app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt
Normal file
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
106
app/src/main/kotlin/cn/super12138/todo/ui/components/Konfetti.kt
Normal file
106
app/src/main/kotlin/cn/super12138/todo/ui/components/Konfetti.kt
Normal file
|
|
@ -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<Boolean>,
|
||||
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)
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package cn.super12138.todo.ui.navigation
|
||||
|
||||
enum class TodoScreen {
|
||||
Main,
|
||||
TodoEditor,
|
||||
SettingsMain,
|
||||
SettingsAppearance,
|
||||
SettingsInterface,
|
||||
SettingsAbout,
|
||||
SettingsAboutLicence
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
219
app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt
Normal file
219
app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt
Normal file
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
|
@ -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<TodoEntity>,
|
||||
onItemClick: (TodoEntity) -> Unit = {},
|
||||
onItemLongClick: (TodoEntity) -> Unit = {},
|
||||
onItemChecked: (TodoEntity) -> Unit = {},
|
||||
selectedTodoIds: List<Int>,
|
||||
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: 设置动画时间
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 =
|
||||
)
|
||||
}*/
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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<Int>,
|
||||
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 }
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SettingsRadioOptions>,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Boolean> {
|
||||
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<Boolean> {
|
||||
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
|
||||
)
|
||||
}
|
||||
87
app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt
Normal file
87
app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt
Normal file
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
48
app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt
Normal file
48
app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt
Normal file
|
|
@ -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
|
||||
)
|
||||
}
|
||||
91
app/src/main/kotlin/cn/super12138/todo/ui/theme/ThemeExt.kt
Normal file
91
app/src/main/kotlin/cn/super12138/todo/ui/theme/ThemeExt.kt
Normal file
|
|
@ -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<Color> = 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<Color> = spring()): Color {
|
||||
return animateColorAsState(this, animationSpec).value
|
||||
}
|
||||
5
app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt
Normal file
5
app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package cn.super12138.todo.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
|
||||
val Typography = Typography()
|
||||
|
|
@ -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<List<TodoEntity>> = Repository.getAllTodos()
|
||||
var appSortingMethod by mutableStateOf(SortingMethod.fromId(GlobalValues.sortingMethod))
|
||||
val sortedTodos: Flow<List<TodoEntity>> = 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<TodoEntity?>(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<Int>())
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package cn.super12138.todo.utils.sp
|
||||
package cn.super12138.todo.utils
|
||||
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
|
@ -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 <T> 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ package cn.super12138.todo.utils
|
|||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
||||
object VersionUtils {
|
||||
object SystemUtils {
|
||||
/**
|
||||
* 获取应用版本号
|
||||
* @return 版本名称(版本代码)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T : ViewBinding> : 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
|
||||
}
|
||||
|
|
@ -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<T : ViewBinding> : 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ActivityCrashBinding>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ActivityMainBinding>() {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ToDo>,
|
||||
private val fragmentManager: FragmentManager
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
// 默认待办项
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ToDo>,
|
||||
private val viewModelStoreOwner: ViewModelStoreOwner,
|
||||
private val fragmentManager: FragmentManager
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
// 默认待办项
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FragmentAboutBinding>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FragmentAllTasksBinding>() {
|
||||
private val viewModel by viewModels<AllTasksViewModel>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FragmentMainBinding>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FragmentProgressBinding>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ListPreference>(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<SwitchPreferenceCompat>(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<SwitchPreferenceCompat>(Constants.PREF_HAPTIC_FEEDBACK)?.apply {
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(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<Preference>(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<Preference>(Constants.PREF_ALL_TASKS)?.apply {
|
||||
setOnPreferenceClickListener {
|
||||
mainActivity.startFragment(AllTasksFragment())
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(Constants.PREF_REENTER_WELCOME_ACTIVITY)?.apply {
|
||||
setOnPreferenceClickListener {
|
||||
mainActivity.startFragment(WelcomeFragment())
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FragmentSettingsBinding>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ViewGroup.MarginLayoutParams> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FragmentWelcomeBinding>() {
|
||||
private val viewModel by viewModels<WelcomeViewModel>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FragmentWelcomeIntroBinding>() {
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentWelcomeIntroBinding {
|
||||
return FragmentWelcomeIntroBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FragmentWelcomeProgressBinding>() {
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentWelcomeProgressBinding {
|
||||
return FragmentWelcomeProgressBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FragmentWelcomeTodoBtnBinding>() {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FragmentWelcomeTodoItemBinding>() {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ToDo>()
|
||||
val refreshData = MutableLiveData<Int>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int> = MutableLiveData()
|
||||
val completeCount: MutableLiveData<Int> = MutableLiveData()
|
||||
val remainCount: MutableLiveData<Int> = MutableLiveData()
|
||||
val progress: MutableLiveData<Int> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int>(0)
|
||||
val removeData = MutableLiveData<Int>(0)
|
||||
val refreshData = MutableLiveData<Int>(0)
|
||||
|
||||
val todoList = ArrayList<ToDo>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue