Merge pull request #149 from Super12138/compose-refactor

使用 Jetpack Compose 重构大部分功能
This commit is contained in:
Super12138 2025-02-07 20:35:20 +08:00 committed by GitHub
commit ba2c979831
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
249 changed files with 4533 additions and 8517 deletions

View file

@ -1,13 +1,17 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.aboutlibraries)
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
val baseVersionName = "1.0.6"
// 配置版本信息
val baseVersionName = "compose-refactor"
val commitHash by lazy { "git rev-parse --short HEAD".exec() }
val verCode by lazy { "git rev-list --count HEAD".exec().toInt() }
@ -53,50 +57,70 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_18
targetCompatibility = JavaVersion.VERSION_18
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlinOptions {
jvmTarget = "18"
jvmTarget = "21"
}
aboutLibraries {
configPath = "$projectDir/licences"
}
buildFeatures {
viewBinding = true
compose = true
}
}
dependencies {
// Android X
implementation(libs.androidx.core)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.core.splashscreen)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.fragment)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.preference)
implementation(libs.androidx.preference.ktx)
implementation(libs.androidx.security)
// Material Design
implementation(libs.material)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.compose.runtime.livedata)
// implementation(libs.androidx.security)
// Compose
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.animation)
implementation(libs.androidx.navigation)
implementation(libs.androidx.material3.adaptive)
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.android)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.material.icon.core)
implementation(libs.androidx.material.icon.extended)
// About Libraries
implementation(libs.aboutlibraries.core)
implementation(libs.aboutlibraries.compose)
// M3 Color
implementation(libs.com.kyant0.m3color)
// Konfetti
implementation(libs.nl.dionsegijn.konfetti.compose)
// Lazy Column Scrollbar
implementation(libs.lazycolumnscrollbar)
// Kotlin Coroutines
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
// Room
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
annotationProcessor(libs.androidx.room.compiler)
ksp(libs.androidx.room.compiler)
// FastScroll
implementation(project(":fastscroll"))
// Test
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
// 命令执行工具类
fun String.exec(): String = exec(this)
fun Project.exec(command: String): String = providers.exec {

View 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"
]
}

View 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"
]
}

View 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"
]
}

View 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"
]
}

View 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"
}

View file

@ -18,6 +18,4 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.concurrent.GuardedBy
#-renamesourcefileattribute SourceFile

View file

@ -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')"
]
}
}

View file

@ -3,11 +3,9 @@
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".ToDoApp"
android:name=".TodoApp"
android:allowBackup="true"
android:appCategory="productivity"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@ -15,22 +13,20 @@
android:supportsRtl="true"
android:theme="@style/Theme.ToDo.Starting"
tools:targetApi="35">
<activity
android:name=".views.activities.MainActivity"
android:name=".ui.activities.MainActivity"
android:exported="true"
android:launchMode="singleTask">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".views.activities.CrashActivity"
android:name=".ui.activities.CrashActivity"
android:exported="false"
android:theme="@style/Theme.ToDo" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -3,27 +3,25 @@ package cn.super12138.todo
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import cn.super12138.todo.logic.dao.ToDoRoomDB
import cn.super12138.todo.views.activities.CrashHandler
import com.google.android.material.color.DynamicColors
import cn.super12138.todo.logic.database.TodoDatabase
import cn.super12138.todo.ui.pages.crash.CrashHandler
class ToDoApp : Application() {
private val database by lazy { ToDoRoomDB.getDatabase(this) }
class TodoApp : Application() {
private val database by lazy { TodoDatabase.getDatabase(this) }
companion object {
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
lateinit var db: ToDoRoomDB
lateinit var db: TodoDatabase
}
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
context = applicationContext
val crashHandler = CrashHandler(this)
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
db = database
context = applicationContext
val crashHandler = CrashHandler(applicationContext)
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
)
}

View file

@ -1,87 +1,32 @@
package cn.super12138.todo.logic
import cn.super12138.todo.ToDoApp
import cn.super12138.todo.logic.dao.ToDoRoom
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import cn.super12138.todo.TodoApp
import cn.super12138.todo.logic.database.TodoEntity
import kotlinx.coroutines.flow.Flow
object Repository {
private val db get() = TodoApp.db
private val toDoDao = db.toDoDao()
// Room
private val db get() = ToDoApp.db
private val todoDao = db.toDoRoomDao()
/**
* @param toDoRoom 要插入的数据
*/
suspend fun insert(toDoRoom: ToDoRoom) {
withContext(Dispatchers.IO) {
todoDao.insert(toDoRoom)
}
suspend fun insertTodo(toDo: TodoEntity) {
toDoDao.insert(toDo)
}
/**
* 获取全部未完成的待办
* @return List<ToDoRoom>
*/
suspend fun getAllIncomplete(): List<ToDoRoom> {
return withContext(Dispatchers.IO) {
todoDao.getAllUnfinished()
}
fun getAllTodos(): Flow<List<TodoEntity>> = toDoDao.getAll()
suspend fun updateTodo(toDo: TodoEntity) {
toDoDao.update(toDo)
}
/**
* 获取全部已完成的待办
* @return List<ToDoRoom>
*/
suspend fun getAllComplete(): List<ToDoRoom> {
return withContext(Dispatchers.IO) {
todoDao.getAllComplete()
}
suspend fun deleteTodo(toDo: TodoEntity) {
toDoDao.delete(toDo)
}
/**
* 获取全部待办
* @return List<ToDoRoom>
*/
suspend fun getAll(): List<ToDoRoom> {
return withContext(Dispatchers.IO) {
todoDao.getAll()
}
suspend fun deleteTodoFromIds(toDoItems: List<Int>) {
toDoDao.deleteFromIds(toDoItems.toSet())
}
/**
* 根据待办的UUID删除指定待办
* @param uuid 待办的UUID
*/
suspend fun deleteByUUID(uuid: String) {
withContext(Dispatchers.IO) {
todoDao.deleteByUUID(uuid)
}
}
/**
* 删除全部代办
*/
suspend fun deleteAll() {
withContext(Dispatchers.IO) {
todoDao.deleteAll()
}
}
/**
* 根据代办的UUID来把待办状态更新为已完成
* @param uuid 待办的UUID
*/
suspend fun updateStateByUUID(uuid: String) {
withContext(Dispatchers.IO) {
todoDao.updateStateByUUID(uuid)
}
}
suspend fun update(toDoRoom: ToDoRoom) {
withContext(Dispatchers.IO) {
todoDao.update(toDoRoom)
}
}
/*suspend fun deleteAllTodo() {
toDoDao.deleteAllTodo()
}*/
}

View file

@ -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
)

View file

@ -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)
}

View file

@ -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()*/
}

View file

@ -1,24 +1,26 @@
package cn.super12138.todo.logic.dao
package cn.super12138.todo.logic.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [ToDoRoom::class], version = 1)
abstract class ToDoRoomDB : RoomDatabase() {
abstract fun toDoRoomDao(): ToDoRoomDao
@Database(entities = [TodoEntity::class], version = 2)
abstract class TodoDatabase : RoomDatabase() {
abstract fun toDoDao(): TodoDao
companion object {
@Volatile
private var INSTANCE: ToDoRoomDB? = null
fun getDatabase(context: Context): ToDoRoomDB {
private var INSTANCE: TodoDatabase? = null
fun getDatabase(context: Context): TodoDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ToDoRoomDB::class.java,
TodoDatabase::class.java,
"todo"
).build()
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance

View file

@ -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,
)

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}

View 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
}

View file

@ -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()
)
}
}
}
}

View file

@ -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)
}
}
}
}
}

View file

@ -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)
)
}
}
}

View 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
)
}
}

View file

@ -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
)
}
}
}
}
}

View 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)

View file

@ -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) }
}
}

View file

@ -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

View file

@ -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() }
)
}
}
}
}

View file

@ -0,0 +1,11 @@
package cn.super12138.todo.ui.navigation
enum class TodoScreen {
Main,
TodoEditor,
SettingsMain,
SettingsAppearance,
SettingsInterface,
SettingsAbout,
SettingsAboutLicence
}

View file

@ -1,12 +1,12 @@
package cn.super12138.todo.views.activities
package cn.super12138.todo.ui.pages.crash
import android.content.Context
import android.content.Intent
import android.os.Process
import cn.super12138.todo.ui.activities.CrashActivity
import kotlin.system.exitProcess
class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
private val defaultUEH = Thread.getDefaultUncaughtExceptionHandler()
override fun uncaughtException(thread: Thread, ex: Throwable) {

View file

@ -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
)
}
}
}
}

View file

@ -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 }
)
}

View 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 }
)
}

View file

@ -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: 设置动画时间
)
}
}
}
}
}

View file

@ -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
)
}
}
}
}

View file

@ -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 =
)
}*/

View file

@ -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
)
}

View file

@ -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 }
)
}

View file

@ -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
)
}
}
}

View file

@ -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()
}
}
}

View file

@ -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 },
)
}
}
}

View file

@ -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 }
)
}

View file

@ -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
)
}
}
}

View file

@ -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()
}
}

View file

@ -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
)
}
}

View file

@ -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
)
}

View file

@ -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()
}
}
}

View file

@ -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
)
)
}
}

View file

@ -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
)
}

View file

@ -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))
}
}
}
}

View file

@ -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
)
}
}

View file

@ -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)
}
)
}
}
}

View file

@ -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
)
}
}

View file

@ -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)
}
)
}
}
}

View file

@ -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
)
}

View 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
)
)

View file

@ -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
}
}
}

View 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
)
}

View 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
}

View file

@ -0,0 +1,5 @@
package cn.super12138.todo.ui.theme
import androidx.compose.material3.Typography
val Typography = Typography()

View file

@ -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
}
}

View file

@ -1,4 +1,4 @@
package cn.super12138.todo.utils.sp
package cn.super12138.todo.utils
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

View file

@ -1,13 +1,13 @@
package cn.super12138.todo.utils.sp
package cn.super12138.todo.utils
import android.content.Context
import android.content.SharedPreferences
import cn.super12138.todo.ToDoApp
import cn.super12138.todo.constant.Constants
import cn.super12138.todo.TodoApp
import cn.super12138.todo.constants.Constants
object SPUtils {
private val sp: SharedPreferences by lazy {
ToDoApp.context.getSharedPreferences(Constants.SP_NAME, Context.MODE_PRIVATE)
TodoApp.context.getSharedPreferences(Constants.SP_NAME, Context.MODE_PRIVATE)
}
fun <T> getValue(name: String, default: T): T = with(sp) {
@ -33,4 +33,5 @@ object SPUtils {
else -> throw IllegalArgumentException("This type can't be saved into Preferences")
}.apply()
}
}
}

View file

@ -3,7 +3,7 @@ package cn.super12138.todo.utils
import android.content.Context
import android.os.Build
object VersionUtils {
object SystemUtils {
/**
* 获取应用版本号
* @return 版本名称版本代码

View file

@ -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)
}

View file

@ -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()
}

View file

@ -2,15 +2,21 @@ package cn.super12138.todo.utils
import android.view.HapticFeedbackConstants
import android.view.View
import cn.super12138.todo.constant.GlobalValues
import cn.super12138.todo.constants.GlobalValues
object VibrationUtils {
private var isEnabled: Boolean = GlobalValues.hapticFeedback
fun setEnabled(enabled: Boolean) {
isEnabled = enabled
}
fun performHapticFeedback(
view: View?,
hapticFeedbackConstants: Int = HapticFeedbackConstants.KEYBOARD_TAP
view: View,
feedbackConstants: Int = HapticFeedbackConstants.CONTEXT_CLICK
) {
if (GlobalValues.hapticFeedback) {
view?.performHapticFeedback(hapticFeedbackConstants)
if (isEnabled) {
view.performHapticFeedback(feedbackConstants)
}
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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())
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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