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 {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.compose)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
|
alias(libs.plugins.aboutlibraries)
|
||||||
}
|
}
|
||||||
|
|
||||||
ksp {
|
ksp {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
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 commitHash by lazy { "git rev-parse --short HEAD".exec() }
|
||||||
val verCode by lazy { "git rev-list --count HEAD".exec().toInt() }
|
val verCode by lazy { "git rev-list --count HEAD".exec().toInt() }
|
||||||
|
|
||||||
|
|
@ -53,50 +57,70 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_18
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
targetCompatibility = JavaVersion.VERSION_18
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "18"
|
jvmTarget = "21"
|
||||||
|
}
|
||||||
|
aboutLibraries {
|
||||||
|
configPath = "$projectDir/licences"
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
compose = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Android X
|
// Android X
|
||||||
implementation(libs.androidx.core)
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.core.core.splashscreen)
|
implementation(libs.androidx.core.splashscreen)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
implementation(libs.androidx.activity.ktx)
|
implementation(libs.androidx.compose.runtime.livedata)
|
||||||
implementation(libs.androidx.constraintlayout)
|
// implementation(libs.androidx.security)
|
||||||
implementation(libs.androidx.fragment)
|
// Compose
|
||||||
implementation(libs.androidx.fragment.ktx)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.recyclerview)
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.lifecycle.viewmodel)
|
implementation(libs.androidx.animation)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
implementation(libs.androidx.navigation)
|
||||||
implementation(libs.androidx.preference)
|
implementation(libs.androidx.material3.adaptive)
|
||||||
implementation(libs.androidx.preference.ktx)
|
implementation(libs.androidx.ui)
|
||||||
implementation(libs.androidx.security)
|
implementation(libs.androidx.ui.android)
|
||||||
// Material Design
|
implementation(libs.androidx.ui.graphics)
|
||||||
implementation(libs.material)
|
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
|
// Room
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
annotationProcessor(libs.androidx.room.compiler)
|
annotationProcessor(libs.androidx.room.compiler)
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
// FastScroll
|
|
||||||
implementation(project(":fastscroll"))
|
|
||||||
// Test
|
// Test
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
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 String.exec(): String = exec(this)
|
||||||
|
|
||||||
fun Project.exec(command: String): String = providers.exec {
|
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"
|
||||||
|
}
|
||||||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
|
@ -19,5 +19,3 @@
|
||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
-dontwarn javax.annotation.Nullable
|
|
||||||
-dontwarn javax.annotation.concurrent.GuardedBy
|
|
||||||
|
|
@ -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">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".ToDoApp"
|
android:name=".TodoApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:appCategory="productivity"
|
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|
@ -15,22 +13,20 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.ToDo.Starting"
|
android:theme="@style/Theme.ToDo.Starting"
|
||||||
tools:targetApi="35">
|
tools:targetApi="35">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".views.activities.MainActivity"
|
android:name=".ui.activities.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask">
|
android:label="@string/app_name">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".views.activities.CrashActivity"
|
android:name=".ui.activities.CrashActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Theme.ToDo" />
|
android:theme="@style/Theme.ToDo" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import cn.super12138.todo.logic.dao.ToDoRoomDB
|
import cn.super12138.todo.logic.database.TodoDatabase
|
||||||
import cn.super12138.todo.views.activities.CrashHandler
|
import cn.super12138.todo.ui.pages.crash.CrashHandler
|
||||||
import com.google.android.material.color.DynamicColors
|
|
||||||
|
|
||||||
class ToDoApp : Application() {
|
class TodoApp : Application() {
|
||||||
private val database by lazy { ToDoRoomDB.getDatabase(this) }
|
private val database by lazy { TodoDatabase.getDatabase(this) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
lateinit var context: Context
|
lateinit var context: Context
|
||||||
lateinit var db: ToDoRoomDB
|
lateinit var db: TodoDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
|
||||||
context = applicationContext
|
|
||||||
|
|
||||||
val crashHandler = CrashHandler(this)
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
|
|
||||||
|
|
||||||
db = database
|
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
|
package cn.super12138.todo.logic
|
||||||
|
|
||||||
import cn.super12138.todo.ToDoApp
|
import cn.super12138.todo.TodoApp
|
||||||
import cn.super12138.todo.logic.dao.ToDoRoom
|
import cn.super12138.todo.logic.database.TodoEntity
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
object Repository {
|
object Repository {
|
||||||
|
private val db get() = TodoApp.db
|
||||||
|
private val toDoDao = db.toDoDao()
|
||||||
|
|
||||||
// Room
|
suspend fun insertTodo(toDo: TodoEntity) {
|
||||||
private val db get() = ToDoApp.db
|
toDoDao.insert(toDo)
|
||||||
private val todoDao = db.toDoRoomDao()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param toDoRoom 要插入的数据
|
|
||||||
*/
|
|
||||||
suspend fun insert(toDoRoom: ToDoRoom) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
todoDao.insert(toDoRoom)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun getAllTodos(): Flow<List<TodoEntity>> = toDoDao.getAll()
|
||||||
* 获取全部未完成的待办
|
|
||||||
* @return List<ToDoRoom>
|
suspend fun updateTodo(toDo: TodoEntity) {
|
||||||
*/
|
toDoDao.update(toDo)
|
||||||
suspend fun getAllIncomplete(): List<ToDoRoom> {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
todoDao.getAllUnfinished()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
suspend fun deleteTodo(toDo: TodoEntity) {
|
||||||
* 获取全部已完成的待办
|
toDoDao.delete(toDo)
|
||||||
* @return List<ToDoRoom>
|
|
||||||
*/
|
|
||||||
suspend fun getAllComplete(): List<ToDoRoom> {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
todoDao.getAllComplete()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
suspend fun deleteTodoFromIds(toDoItems: List<Int>) {
|
||||||
* 获取全部待办
|
toDoDao.deleteFromIds(toDoItems.toSet())
|
||||||
* @return List<ToDoRoom>
|
|
||||||
*/
|
|
||||||
suspend fun getAll(): List<ToDoRoom> {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
todoDao.getAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*suspend fun deleteAllTodo() {
|
||||||
* 根据待办的UUID删除指定待办
|
toDoDao.deleteAllTodo()
|
||||||
* @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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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 android.content.Context
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
|
||||||
@Database(entities = [ToDoRoom::class], version = 1)
|
@Database(entities = [TodoEntity::class], version = 2)
|
||||||
abstract class ToDoRoomDB : RoomDatabase() {
|
abstract class TodoDatabase : RoomDatabase() {
|
||||||
abstract fun toDoRoomDao(): ToDoRoomDao
|
abstract fun toDoDao(): TodoDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile
|
@Volatile
|
||||||
private var INSTANCE: ToDoRoomDB? = null
|
private var INSTANCE: TodoDatabase? = null
|
||||||
fun getDatabase(context: Context): ToDoRoomDB {
|
fun getDatabase(context: Context): TodoDatabase {
|
||||||
return INSTANCE ?: synchronized(this) {
|
return INSTANCE ?: synchronized(this) {
|
||||||
val instance = Room.databaseBuilder(
|
val instance = Room.databaseBuilder(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
ToDoRoomDB::class.java,
|
TodoDatabase::class.java,
|
||||||
"todo"
|
"todo"
|
||||||
).build()
|
)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
|
|
||||||
INSTANCE = instance
|
INSTANCE = instance
|
||||||
return 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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
|
import cn.super12138.todo.ui.activities.CrashActivity
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
|
class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
|
||||||
|
|
||||||
private val defaultUEH = Thread.getDefaultUncaughtExceptionHandler()
|
private val defaultUEH = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
|
|
||||||
override fun uncaughtException(thread: Thread, ex: Throwable) {
|
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.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
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.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import cn.super12138.todo.ToDoApp
|
import cn.super12138.todo.TodoApp
|
||||||
import cn.super12138.todo.constant.Constants
|
import cn.super12138.todo.constants.Constants
|
||||||
|
|
||||||
object SPUtils {
|
object SPUtils {
|
||||||
private val sp: SharedPreferences by lazy {
|
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) {
|
fun <T> getValue(name: String, default: T): T = with(sp) {
|
||||||
|
|
@ -34,3 +34,4 @@ object SPUtils {
|
||||||
}.apply()
|
}.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3,7 +3,7 @@ package cn.super12138.todo.utils
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
|
||||||
object VersionUtils {
|
object SystemUtils {
|
||||||
/**
|
/**
|
||||||
* 获取应用版本号
|
* 获取应用版本号
|
||||||
* @return 版本名称(版本代码)
|
* @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.HapticFeedbackConstants
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import cn.super12138.todo.constant.GlobalValues
|
import cn.super12138.todo.constants.GlobalValues
|
||||||
|
|
||||||
object VibrationUtils {
|
object VibrationUtils {
|
||||||
|
private var isEnabled: Boolean = GlobalValues.hapticFeedback
|
||||||
|
|
||||||
|
fun setEnabled(enabled: Boolean) {
|
||||||
|
isEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
fun performHapticFeedback(
|
fun performHapticFeedback(
|
||||||
view: View?,
|
view: View,
|
||||||
hapticFeedbackConstants: Int = HapticFeedbackConstants.KEYBOARD_TAP
|
feedbackConstants: Int = HapticFeedbackConstants.CONTEXT_CLICK
|
||||||
) {
|
) {
|
||||||
if (GlobalValues.hapticFeedback) {
|
if (isEnabled) {
|
||||||
view?.performHapticFeedback(hapticFeedbackConstants)
|
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