开始使用 Jetpack Compose 对待办重构

This commit is contained in:
Super12138 2025-01-09 21:50:44 +08:00
parent 0b2384fb88
commit 2d7b3edf15
197 changed files with 478 additions and 8746 deletions

3
app/.gitignore vendored
View file

@ -1,2 +1 @@
/build
/release
/build

View file

@ -1,51 +1,28 @@
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 commitHash by lazy { "git rev-parse --short HEAD".exec() }
val verCode by lazy { "git rev-list --count HEAD".exec().toInt() }
android {
namespace = "cn.super12138.todo"
compileSdk = 35
// 获取 Release 签名
val releaseSigning = if (project.hasProperty("releaseStoreFile")) {
signingConfigs.create("release") {
storeFile = File(project.properties["releaseStoreFile"] as String)
storePassword = project.properties["releaseStorePassword"] as String
keyAlias = project.properties["releaseKeyAlias"] as String
keyPassword = project.properties["releaseKeyPassword"] as String
}
} else {
signingConfigs.getByName("debug")
}
defaultConfig {
applicationId = "cn.super12138.todo"
minSdk = 24
targetSdk = 35
versionCode = verCode
versionName = "${baseVersionName}-${commitHash}"
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
base.archivesName.set("todo-${baseVersionName}")
}
buildTypes {
all {
signingConfig = releaseSigning
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@ -53,52 +30,54 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_18
targetCompatibility = JavaVersion.VERSION_18
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlinOptions {
jvmTarget = "18"
jvmTarget = "21"
}
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.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.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)
// 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)
}
fun String.exec(): String = exec(this)
fun Project.exec(command: String): String = providers.exec {
commandLine(command.split(" "))
}.standardOutput.asText.get().trim()
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

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

@ -1,11 +1,13 @@
package cn.super12138.todo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*

View file

@ -3,34 +3,26 @@
xmlns:tools="http://schemas.android.com/tools">
<application
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"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ToDo.Starting"
tools:targetApi="35">
android:theme="@style/Theme.ToDo"
tools:targetApi="31">
<activity
android:name=".views.activities.MainActivity"
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
android:label="@string/app_name"
android:theme="@style/Theme.ToDo">
<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:exported="false"
android:theme="@style/Theme.ToDo" />
</application>
</manifest>

View file

@ -0,0 +1,47 @@
package cn.super12138.todo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import cn.super12138.todo.ui.theme.ToDoTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ToDoTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
ToDoTheme {
Greeting("Android")
}
}

View file

@ -1,29 +0,0 @@
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
class ToDoApp : Application() {
private val database by lazy { ToDoRoomDB.getDatabase(this) }
companion object {
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
lateinit var db: ToDoRoomDB
}
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
context = applicationContext
val crashHandler = CrashHandler(this)
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
db = database
}
}

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

@ -1,87 +0,0 @@
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
object Repository {
// Room
private val db get() = ToDoApp.db
private val todoDao = db.toDoRoomDao()
/**
* @param toDoRoom 要插入的数据
*/
suspend fun insert(toDoRoom: ToDoRoom) {
withContext(Dispatchers.IO) {
todoDao.insert(toDoRoom)
}
}
/**
* 获取全部未完成的待办
* @return List<ToDoRoom>
*/
suspend fun getAllIncomplete(): List<ToDoRoom> {
return withContext(Dispatchers.IO) {
todoDao.getAllUnfinished()
}
}
/**
* 获取全部已完成的待办
* @return List<ToDoRoom>
*/
suspend fun getAllComplete(): List<ToDoRoom> {
return withContext(Dispatchers.IO) {
todoDao.getAllComplete()
}
}
/**
* 获取全部待办
* @return List<ToDoRoom>
*/
suspend fun getAll(): List<ToDoRoom> {
return withContext(Dispatchers.IO) {
todoDao.getAll()
}
}
/**
* 根据待办的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)
}
}
}

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,28 +0,0 @@
package cn.super12138.todo.logic.dao
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
companion object {
@Volatile
private var INSTANCE: ToDoRoomDB? = null
fun getDatabase(context: Context): ToDoRoomDB {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ToDoRoomDB::class.java,
"todo"
).build()
INSTANCE = instance
return instance
}
}
}
}

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

@ -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,11 @@
package cn.super12138.todo.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View file

@ -0,0 +1,58 @@
package cn.super12138.todo.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun ToDoTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View file

@ -0,0 +1,34 @@
package cn.super12138.todo.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

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

@ -1,21 +0,0 @@
package cn.super12138.todo.utils
import android.content.Context
import android.os.Build
object VersionUtils {
/**
* 获取应用版本号
* @return 版本名称版本代码
*/
fun getAppVersion(context: Context): String {
val pkgInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val verName = pkgInfo.versionName
val verCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
pkgInfo.longVersionCode.toInt()
} else {
pkgInfo.versionCode
}
return "$verName ($verCode)"
}
}

View file

@ -1,16 +0,0 @@
package cn.super12138.todo.utils
import android.view.HapticFeedbackConstants
import android.view.View
import cn.super12138.todo.constant.GlobalValues
object VibrationUtils {
fun performHapticFeedback(
view: View?,
hapticFeedbackConstants: Int = HapticFeedbackConstants.KEYBOARD_TAP
) {
if (GlobalValues.hapticFeedback) {
view?.performHapticFeedback(hapticFeedbackConstants)
}
}
}

View file

@ -1,14 +0,0 @@
package cn.super12138.todo.utils.sp
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class SPDelegates<T>(private val key: String, private val default: T) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return SPUtils.getValue(key, default)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
SPUtils.putValue(key, value)
}
}

View file

@ -1,36 +0,0 @@
package cn.super12138.todo.utils.sp
import android.content.Context
import android.content.SharedPreferences
import cn.super12138.todo.ToDoApp
import cn.super12138.todo.constant.Constants
object SPUtils {
private val sp: SharedPreferences by lazy {
ToDoApp.context.getSharedPreferences(Constants.SP_NAME, Context.MODE_PRIVATE)
}
fun <T> getValue(name: String, default: T): T = with(sp) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default).orEmpty()
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw java.lang.IllegalArgumentException()
}
@Suppress("UNCHECKED_CAST")
res as T
}
fun <T> putValue(name: String, value: T) = with(sp.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can't be saved into Preferences")
}.apply()
}
}

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,29 +0,0 @@
package cn.super12138.todo.views.activities
import android.content.Context
import android.content.Intent
import android.os.Process
import kotlin.system.exitProcess
class CrashHandler(private val context: Context) : Thread.UncaughtExceptionHandler {
private val defaultUEH = Thread.getDefaultUncaughtExceptionHandler()
override fun uncaughtException(thread: Thread, ex: Throwable) {
val stackTrace = ex.stackTraceToString()
// 启动新的 Activity 来显示崩溃日志
val intent = Intent(context, CrashActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
putExtra("crash_logs", stackTrace)
}
context.startActivity(intent)
// 传递异常给默认的异常处理器
defaultUEH?.uncaughtException(thread, ex)
// 杀掉崩溃的应用程序进程
Process.killProcess(Process.myPid())
exitProcess(10)
}
}

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

View file

@ -1,129 +0,0 @@
package de.raphaelebner.roomdatabasebackup.core
import android.annotation.SuppressLint
import android.content.SharedPreferences
import java.io.*
import java.security.NoSuchAlgorithmException
import java.security.spec.InvalidKeySpecException
import java.security.spec.KeySpec
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
/**
* MIT License
*
* Copyright (c) 2024 Raphael Ebner
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
class AESEncryptionHelper {
companion object {
private const val BACKUP_SECRET_KEY = "backupsecretkey"
private const val TAG = "debug_AESEncryptionHelper"
}
/**
* This method will convert a file to ByteArray
* @param file : the Path where the file is located
* @return ByteArray of the file
*/
@Throws(Exception::class)
fun readFile(file: File): ByteArray {
val fileContents = file.readBytes()
val inputBuffer = BufferedInputStream(FileInputStream(file))
inputBuffer.read(fileContents)
inputBuffer.close()
return fileContents
}
/**
* This method will convert a ByteArray to a file, and saves it to the path
* @param fileData : the ByteArray
* @param file : the path where the ByteArray should be saved
*/
@Throws(Exception::class)
fun saveFile(fileData: ByteArray, file: File) {
val bos = BufferedOutputStream(FileOutputStream(file, false))
bos.write(fileData)
bos.flush()
bos.close()
}
/**
* This method will convert a random password, saved in sharedPreferences to a SecretKey
* @param sharedPref : the sharedPref, to fetch / save the key
* @param iv : the encryption nonce
* @return SecretKey
*/
@SuppressLint("ApplySharedPref")
fun getSecretKey(sharedPref: SharedPreferences, iv: ByteArray): SecretKey {
// get key: String from sharedpref
var password = sharedPref.getString(BACKUP_SECRET_KEY, null)
// If no key is stored in shared pref, create one and save it
if (password == null) {
// generate random string
val stringLength = 15
val charset = ('a'..'z') + ('A'..'Z') + ('1'..'9')
password = (1..stringLength).map { charset.random() }.joinToString("")
val secretKey = generateSecretKey(password, iv)
// the key can be saved plain, because i am using EncryptedSharedPreferences
val editor = sharedPref.edit()
editor.putString(BACKUP_SECRET_KEY, password)
// I use .commit because when using .apply the needed app restart is faster then apply
// and the preferences wont be saved
editor.commit()
return secretKey
}
// generate secretKey, and return it
return generateSecretKey(password, iv)
}
/**
* This method will convert a custom password to a SecretKey
* @param encryptPassword : the custom user password as String
* @param iv : the encryption nonce
* @return SecretKey
*/
fun getSecretKeyWithCustomPw(encryptPassword: String, iv: ByteArray): SecretKey {
// generate secretKey, and return it
return generateSecretKey(encryptPassword, iv)
}
/**
* Function to generate a 128 bit key from the given password and iv
* @param password
* @param iv
* @return Secret key
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
@Throws(NoSuchAlgorithmException::class, InvalidKeySpecException::class)
private fun generateSecretKey(password: String, iv: ByteArray?): SecretKey {
// convert random string to secretKey
val spec: KeySpec = PBEKeySpec(password.toCharArray(), iv, 65536, 128) // AES-128
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val key = secretKeyFactory.generateSecret(spec).encoded
return SecretKeySpec(key, "AES")
}
}

View file

@ -1,114 +0,0 @@
package de.raphaelebner.roomdatabasebackup.core
import android.content.SharedPreferences
import java.nio.ByteBuffer
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.security.spec.InvalidKeySpecException
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.GCMParameterSpec
/**
* Encryption / Decryption service using the AES algorithm
* example for nullbeans.com
* https://nullbeans.com/how-to-encrypt-decrypt-files-byte-arrays-in-java-using-aes-gcm/#Generating_an_AES_key
*/
class AESEncryptionManager {
private val aesEncryptionHelper = AESEncryptionHelper()
/**
* This method will encrypt the given data
* @param sharedPref : the sharedPref, to fetch the key
* @param data : the data that will be encrypted
* @return Encrypted data in a byte array
*/
@Throws(
NoSuchPaddingException::class,
NoSuchAlgorithmException::class,
InvalidAlgorithmParameterException::class,
InvalidKeyException::class,
BadPaddingException::class,
IllegalBlockSizeException::class,
InvalidKeySpecException::class
)
fun encryptData(sharedPref: SharedPreferences, encryptPassword: String?, data: ByteArray): ByteArray {
//Prepare the nonce
val secureRandom = SecureRandom()
//Noonce should be 12 bytes
val iv = ByteArray(12)
secureRandom.nextBytes(iv)
//Prepare your key/password
val secretKey = if (encryptPassword != null) { aesEncryptionHelper.getSecretKeyWithCustomPw(encryptPassword, iv)
} else aesEncryptionHelper.getSecretKey(sharedPref, iv)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val parameterSpec = GCMParameterSpec(128, iv)
//Encryption mode on!
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec)
//Encrypt the data
val encryptedData = cipher.doFinal(data)
//Concatenate everything and return the final data
val byteBuffer = ByteBuffer.allocate(4 + iv.size + encryptedData.size)
byteBuffer.putInt(iv.size)
byteBuffer.put(iv)
byteBuffer.put(encryptedData)
return byteBuffer.array()
}
/**
* This method will decrypt the given data
* @param sharedPref : the sharedPref, to fetch the key
* @param encryptedData : the data that will be decrypted
* @return decrypted data in a byte array
*/
@Throws(
NoSuchPaddingException::class,
NoSuchAlgorithmException::class,
InvalidAlgorithmParameterException::class,
InvalidKeyException::class,
BadPaddingException::class,
IllegalBlockSizeException::class,
InvalidKeySpecException::class
)
fun decryptData(sharedPref: SharedPreferences, encryptPassword: String?, encryptedData: ByteArray): ByteArray {
//Wrap the data into a byte buffer to ease the reading process
val byteBuffer = ByteBuffer.wrap(encryptedData)
val noonceSize = byteBuffer.int
//Make sure that the file was encrypted properly
require(!(noonceSize < 12 || noonceSize >= 16)) { "Nonce size is incorrect. Make sure that the incoming data is an AES encrypted file." }
val iv = ByteArray(noonceSize)
byteBuffer[iv]
//Prepare your key/password
val secretKey = if (encryptPassword != null) { aesEncryptionHelper.getSecretKeyWithCustomPw(encryptPassword, iv)
} else aesEncryptionHelper.getSecretKey(sharedPref, iv)
//get the rest of encrypted data
val cipherBytes = ByteArray(byteBuffer.remaining())
byteBuffer[cipherBytes]
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val parameterSpec = GCMParameterSpec(128, iv)
//Encryption mode on!
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec)
//Encrypt the data
return cipher.doFinal(cipherBytes)
}
}

View file

@ -1,78 +0,0 @@
package de.raphaelebner.roomdatabasebackup.core
import de.raphaelebner.roomdatabasebackup.core.RoomBackup.Companion.BACKUP_FILE_LOCATION_CUSTOM_FILE
/**
* MIT License
*
* Copyright (c) 2024 Raphael Ebner
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
interface OnCompleteListener {
fun onComplete(success: Boolean, message: String, exitCode: Int)
companion object {
/** Other Error */
const val EXIT_CODE_ERROR = 1
/** Error while choosing backup to restore. Maybe no file selected */
const val EXIT_CODE_ERROR_BACKUP_FILE_CHOOSER = 2
/** Error while choosing backup file to create. Maybe no file selected */
const val EXIT_CODE_ERROR_BACKUP_FILE_CREATOR = 3
/**
* [BACKUP_FILE_LOCATION_CUSTOM_FILE] is set but [RoomBackup.backupLocationCustomFile] is
* not set
*/
const val EXIT_CODE_ERROR_BACKUP_LOCATION_FILE_MISSING = 4
/** [RoomBackup.backupLocation] is not set */
const val EXIT_CODE_ERROR_BACKUP_LOCATION_MISSING = 5
/** Restore dialog for internal/external storage was canceled by user */
const val EXIT_CODE_ERROR_BY_USER_CANCELED = 6
/** Cannot decrypt provided backup file */
const val EXIT_CODE_ERROR_DECRYPTION_ERROR = 7
/** Cannot encrypt database backup */
const val EXIT_CODE_ERROR_ENCRYPTION_ERROR = 8
/**
* You tried to restore a encrypted backup but [RoomBackup.backupIsEncrypted] is set to
* false
*/
const val EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED = 9
/** No backups to restore are available in internal/external sotrage */
const val EXIT_CODE_ERROR_RESTORE_NO_BACKUPS_AVAILABLE = 10
/** No room database to backup is provided */
const val EXIT_CODE_ERROR_ROOM_DATABASE_MISSING = 11
/** Storage permissions not granted for custom dialog */
const val EXIT_CODE_ERROR_STORAGE_PERMISSONS_NOT_GRANTED = 12
/** Cannot decrypt provided backup file because the password is incorrect */
const val EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD = 13
/** No error, action successful */
const val EXIT_CODE_SUCCESS = 0
}
}

View file

@ -1,853 +0,0 @@
package de.raphaelebner.roomdatabasebackup.core
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.room.RoomDatabase
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import kotlinx.coroutines.runBlocking
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import javax.crypto.BadPaddingException
/**
* MIT License
*
* Copyright (c) 2024 Raphael Ebner
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
class RoomBackup(var context: Context) {
companion object {
private const val SHARED_PREFS = "de.raphaelebner.roomdatabasebackup"
private var TAG = "debug_RoomBackup"
private lateinit var INTERNAL_BACKUP_PATH: File
private lateinit var TEMP_BACKUP_PATH: File
private lateinit var TEMP_BACKUP_FILE: File
private lateinit var EXTERNAL_BACKUP_PATH: File
private lateinit var DATABASE_FILE: File
private var currentProcess: Int? = null
private const val PROCESS_BACKUP = 1
private const val PROCESS_RESTORE = 2
private var backupFilename: String? = null
/** Code for internal backup location, used for [backupLocation] */
const val BACKUP_FILE_LOCATION_INTERNAL = 1
/** Code for external backup location, used for [backupLocation] */
const val BACKUP_FILE_LOCATION_EXTERNAL = 2
/** Code for custom backup location dialog, used for [backupLocation] */
const val BACKUP_FILE_LOCATION_CUSTOM_DIALOG = 3
/** Code for custom backup file location, used for [backupLocation] */
const val BACKUP_FILE_LOCATION_CUSTOM_FILE = 4
}
private lateinit var sharedPreferences: SharedPreferences
private lateinit var dbName: String
private var roomDatabase: RoomDatabase? = null
private var enableLogDebug: Boolean = false
private var restartIntent: Intent? = null
private var onCompleteListener: OnCompleteListener? = null
private var customRestoreDialogTitle: String = "Choose file to restore"
private var customBackupFileName: String? = null
private var backupIsEncrypted: Boolean = false
private var maxFileCount: Int? = null
private var encryptPassword: String? = null
private var backupLocation: Int = BACKUP_FILE_LOCATION_INTERNAL
private var backupLocationCustomFile: File? = null
/**
* Set RoomDatabase instance
*
* @param roomDatabase RoomDatabase
*/
fun database(roomDatabase: RoomDatabase): RoomBackup {
this.roomDatabase = roomDatabase
return this
}
/**
* Set LogDebug enabled / disabled
*
* @param enableLogDebug Boolean
*/
fun enableLogDebug(enableLogDebug: Boolean): RoomBackup {
this.enableLogDebug = enableLogDebug
return this
}
/**
* Set Intent in which to boot after App restart
*
* @param restartIntent Intent
*/
fun restartApp(restartIntent: Intent): RoomBackup {
this.restartIntent = restartIntent
restartApp()
return this
}
/**
* Set onCompleteListener, to run code when tasks completed
*
* @param onCompleteListener OnCompleteListener
*/
fun onCompleteListener(onCompleteListener: OnCompleteListener): RoomBackup {
this.onCompleteListener = onCompleteListener
return this
}
/**
* Set onCompleteListener, to run code when tasks completed
*
* @param listener (success: Boolean, message: String) -> Unit
*/
fun onCompleteListener(
listener: (success: Boolean, message: String, exitCode: Int) -> Unit
): RoomBackup {
this.onCompleteListener =
object : OnCompleteListener {
override fun onComplete(success: Boolean, message: String, exitCode: Int) {
listener(success, message, exitCode)
}
}
return this
}
/**
* Set custom log tag, for detailed debugging
*
* @param customLogTag String
*/
fun customLogTag(customLogTag: String): RoomBackup {
TAG = customLogTag
return this
}
/**
* Set custom Restore Dialog Title, default = "Choose file to restore"
*
* @param customRestoreDialogTitle String
*/
fun customRestoreDialogTitle(customRestoreDialogTitle: String): RoomBackup {
this.customRestoreDialogTitle = customRestoreDialogTitle
return this
}
/**
* Set custom Backup File Name, default = "$dbName-$currentTime.sqlite3"
*
* @param customBackupFileName String
*/
fun customBackupFileName(customBackupFileName: String): RoomBackup {
this.customBackupFileName = customBackupFileName
return this
}
/**
* Set you backup location. Available values see: [BACKUP_FILE_LOCATION_INTERNAL],
* [BACKUP_FILE_LOCATION_EXTERNAL], [BACKUP_FILE_LOCATION_CUSTOM_DIALOG] or
* [BACKUP_FILE_LOCATION_CUSTOM_FILE]
*
* @param backupLocation Int, default = [BACKUP_FILE_LOCATION_INTERNAL]
*/
fun backupLocation(backupLocation: Int): RoomBackup {
this.backupLocation = backupLocation
return this
}
/**
* Set a custom file where to save or restore a backup. can be used for backup and restore
*
* Only available if [backupLocation] is set to [BACKUP_FILE_LOCATION_CUSTOM_FILE]
*
* @param backupLocationCustomFile File
*/
fun backupLocationCustomFile(backupLocationCustomFile: File): RoomBackup {
this.backupLocationCustomFile = backupLocationCustomFile
return this
}
/**
* Set file encryption to true / false can be used for backup and restore
*
* @param backupIsEncrypted Boolean, default = false
*/
fun backupIsEncrypted(backupIsEncrypted: Boolean): RoomBackup {
this.backupIsEncrypted = backupIsEncrypted
return this
}
/**
* Set max backup files count if fileCount is > maxFileCount the oldest backup file will be
* deleted is for both internal and external storage
*
* @param maxFileCount Int, default = null
*/
fun maxFileCount(maxFileCount: Int): RoomBackup {
this.maxFileCount = maxFileCount
return this
}
/**
* Set custom backup encryption password
*
* @param encryptPassword String
*/
fun customEncryptPassword(encryptPassword: String): RoomBackup {
this.encryptPassword = encryptPassword
return this
}
/** Init vars, and return true if no error occurred */
private fun initRoomBackup(): Boolean {
if (roomDatabase == null) {
if (enableLogDebug) Log.d(TAG, "roomDatabase is missing")
onCompleteListener?.onComplete(
false,
"roomDatabase is missing",
OnCompleteListener.EXIT_CODE_ERROR_ROOM_DATABASE_MISSING
)
// throw IllegalArgumentException("roomDatabase is not initialized")
return false
}
// Create or retrieve the Master Key for encryption/decryption
val masterKeyAlias =
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
if (backupLocation !in
listOf(
BACKUP_FILE_LOCATION_INTERNAL,
BACKUP_FILE_LOCATION_EXTERNAL,
BACKUP_FILE_LOCATION_CUSTOM_DIALOG,
BACKUP_FILE_LOCATION_CUSTOM_FILE
)
) {
if (enableLogDebug) Log.d(TAG, "backupLocation is missing")
onCompleteListener?.onComplete(
false,
"backupLocation is missing",
OnCompleteListener.EXIT_CODE_ERROR_BACKUP_LOCATION_MISSING
)
return false
}
if (backupLocation == BACKUP_FILE_LOCATION_CUSTOM_FILE && backupLocationCustomFile == null
) {
if (enableLogDebug)
Log.d(
TAG,
"backupLocation is set to custom backup file, but no file is defined"
)
onCompleteListener?.onComplete(
false,
"backupLocation is set to custom backup file, but no file is defined",
OnCompleteListener.EXIT_CODE_ERROR_BACKUP_LOCATION_FILE_MISSING
)
return false
}
// Initialize/open an instance of EncryptedSharedPreferences
// Encryption key is stored in plain text in an EncryptedSharedPreferences --> it is saved
// encrypted
sharedPreferences =
EncryptedSharedPreferences.create(
context,
SHARED_PREFS,
masterKeyAlias,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
dbName = roomDatabase!!.openHelper.databaseName!!
INTERNAL_BACKUP_PATH = File("${context.filesDir}/databasebackup/")
TEMP_BACKUP_PATH = File("${context.filesDir}/databasebackup-temp/")
TEMP_BACKUP_FILE = File("$TEMP_BACKUP_PATH/tempbackup.sqlite3")
EXTERNAL_BACKUP_PATH = File(context.getExternalFilesDir("backup")!!.toURI())
DATABASE_FILE = File(context.getDatabasePath(dbName).toURI())
// Create internal and temp backup directory if does not exist
try {
INTERNAL_BACKUP_PATH.mkdirs()
TEMP_BACKUP_PATH.mkdirs()
} catch (_: FileAlreadyExistsException) {
} catch (_: IOException) {
}
if (enableLogDebug) {
Log.d(TAG, "DatabaseName: $dbName")
Log.d(TAG, "Database Location: $DATABASE_FILE")
Log.d(TAG, "INTERNAL_BACKUP_PATH: $INTERNAL_BACKUP_PATH")
Log.d(TAG, "EXTERNAL_BACKUP_PATH: $EXTERNAL_BACKUP_PATH")
if (backupLocationCustomFile != null)
Log.d(TAG, "backupLocationCustomFile: $backupLocationCustomFile")
}
return true
}
/** restart App with custom Intent */
private fun restartApp() {
restartIntent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(restartIntent)
if (context is Activity) {
(context as Activity).finish()
}
Runtime.getRuntime().exit(0)
}
/**
* Start Backup process, and set onComplete Listener to success, if no error occurred, else
* onComplete Listener success is false and error message is passed
*
* if custom storage ist selected, the [openBackupfileCreator] will be launched
*/
fun backup() {
if (enableLogDebug) Log.d(TAG, "Starting Backup ...")
val success = initRoomBackup()
if (!success) return
// Needed for storage permissions request
currentProcess = PROCESS_BACKUP
// Create name for backup file, if no custom name is set: Database name + currentTime +
// .sqlite3
var filename =
if (customBackupFileName == null) "$dbName-${getTime()}.sqlite3"
else customBackupFileName as String
// Add .aes extension to filename, if file is encrypted
if (backupIsEncrypted) filename += ".aes"
if (enableLogDebug) Log.d(TAG, "backupFilename: $filename")
when (backupLocation) {
BACKUP_FILE_LOCATION_INTERNAL -> {
val backupFile = File("$INTERNAL_BACKUP_PATH/$filename")
doBackup(backupFile)
}
BACKUP_FILE_LOCATION_EXTERNAL -> {
val backupFile = File("$EXTERNAL_BACKUP_PATH/$filename")
doBackup(backupFile)
}
BACKUP_FILE_LOCATION_CUSTOM_DIALOG -> {
backupFilename = filename
permissionRequestLauncher.launch(arrayOf())
return
}
BACKUP_FILE_LOCATION_CUSTOM_FILE -> {
doBackup(backupLocationCustomFile!!)
}
else -> return
}
}
/**
* This method will do the backup action
*
* @param destination File
*/
private fun doBackup(destination: File) {
// Close the database
roomDatabase!!.close()
roomDatabase = null
if (backupIsEncrypted) {
val encryptedBytes = encryptBackup() ?: return
val bos = BufferedOutputStream(FileOutputStream(destination, false))
bos.write(encryptedBytes)
bos.flush()
bos.close()
} else {
// Copy current database to save location (/files dir)
DATABASE_FILE.copyTo(destination)
}
// If maxFileCount is set and is reached, delete oldest file
if (maxFileCount != null) {
val deleted = deleteOldBackup()
if (!deleted) return
}
if (enableLogDebug)
Log.d(TAG, "Backup done, encrypted($backupIsEncrypted) and saved to $destination")
onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS)
}
/**
* This method will do the backup action
*
* @param destination OutputStream
*/
private fun doBackup(destination: OutputStream) {
// Close the database
roomDatabase!!.close()
roomDatabase = null
if (backupIsEncrypted) {
val encryptedBytes = encryptBackup() ?: return
destination.write(encryptedBytes)
} else {
// Copy current database to save location (/files dir)
DATABASE_FILE.inputStream().use { input ->
destination.use { output ->
input.copyTo(output)
}
}
}
// If maxFileCount is set and is reached, delete oldest file
if (maxFileCount != null) {
val deleted = deleteOldBackup()
if (!deleted) return
}
if (enableLogDebug)
Log.d(TAG, "Backup done, encrypted($backupIsEncrypted) and saved to $destination")
onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS)
}
/**
* Encrypts the current Database and return it's content as ByteArray. The original Database is
* not encrypted only a current copy of this database
*
* @return encrypted backup as ByteArray
*/
private fun encryptBackup(): ByteArray? {
try {
// Copy database you want to backup to temp directory
DATABASE_FILE.copyTo(TEMP_BACKUP_FILE)
// encrypt temp file, and save it to backup location
val encryptDecryptBackup = AESEncryptionHelper()
val fileData = encryptDecryptBackup.readFile(TEMP_BACKUP_FILE)
val aesEncryptionManager = AESEncryptionManager()
val encryptedBytes =
aesEncryptionManager.encryptData(sharedPreferences, encryptPassword, fileData)
// Delete temp file
TEMP_BACKUP_FILE.delete()
return encryptedBytes
} catch (e: Exception) {
if (enableLogDebug) Log.d(TAG, "error during encryption: ${e.message}")
onCompleteListener?.onComplete(
false,
"error during encryption",
OnCompleteListener.EXIT_CODE_ERROR_ENCRYPTION_ERROR
)
return null
}
}
/**
* Start Restore process, and set onComplete Listener to success, if no error occurred, else
* onComplete Listener success is false and error message is passed
*
* if internal or external storage is selected, this function shows a list of all available
* backup files in a MaterialAlertDialog and calls [restoreSelectedInternalExternalFile] to
* restore selected file
*
* if custom storage ist selected, the [openBackupfileChooser] will be launched
*/
fun restore() {
if (enableLogDebug) Log.d(TAG, "Starting Restore ...")
val success = initRoomBackup()
if (!success) return
// Needed for storage permissions request
currentProcess = PROCESS_RESTORE
// Path of Backup Directory
val backupDirectory: File
when (backupLocation) {
BACKUP_FILE_LOCATION_INTERNAL -> {
backupDirectory = INTERNAL_BACKUP_PATH
}
BACKUP_FILE_LOCATION_EXTERNAL -> {
backupDirectory = File("$EXTERNAL_BACKUP_PATH/")
}
BACKUP_FILE_LOCATION_CUSTOM_DIALOG -> {
permissionRequestLauncher.launch(arrayOf())
return
}
BACKUP_FILE_LOCATION_CUSTOM_FILE -> {
Log.d(
TAG,
"backupLocationCustomFile!!.exists()? : ${backupLocationCustomFile!!.exists()}"
)
doRestore(backupLocationCustomFile!!)
return
}
else -> return
}
// All Files in an Array of type File
val arrayOfFiles = backupDirectory.listFiles()
// If array is null or empty show "error" and return
if (arrayOfFiles.isNullOrEmpty()) {
if (enableLogDebug) Log.d(TAG, "No backups available to restore")
onCompleteListener?.onComplete(
false,
"No backups available",
OnCompleteListener.EXIT_CODE_ERROR_RESTORE_NO_BACKUPS_AVAILABLE
)
Toast.makeText(context, "No backups available to restore", Toast.LENGTH_SHORT).show()
return
}
// Sort Array: lastModified
arrayOfFiles.sortBy { it.lastModified() }
// New empty MutableList of String
val mutableListOfFilesAsString = mutableListOf<String>()
// Add each filename to mutablelistOfFilesAsString
runBlocking {
for (i in arrayOfFiles.indices) {
mutableListOfFilesAsString.add(arrayOfFiles[i].name)
}
}
// Convert MutableList to Array
val filesStringArray = mutableListOfFilesAsString.toTypedArray()
// Show MaterialAlertDialog, with all available files, and on click Listener
AlertDialog.Builder(context)
.setTitle(customRestoreDialogTitle)
.setItems(filesStringArray) { _, which ->
restoreSelectedInternalExternalFile(filesStringArray[which])
}
.setOnCancelListener {
if (enableLogDebug) Log.d(TAG, "Restore dialog canceled")
onCompleteListener?.onComplete(
false,
"Restore dialog canceled",
OnCompleteListener.EXIT_CODE_ERROR_BY_USER_CANCELED
)
}
.show()
}
/**
* This method will do the restore action
*
* @param source File
*/
private fun doRestore(source: File) {
// Close the database
roomDatabase!!.close()
roomDatabase = null
val fileExtension = source.extension
if (backupIsEncrypted) {
source.copyTo(TEMP_BACKUP_FILE)
val decryptedBytes = decryptBackup() ?: return
val bos = BufferedOutputStream(FileOutputStream(DATABASE_FILE, false))
bos.write(decryptedBytes)
bos.flush()
bos.close()
} else {
if (fileExtension == "aes") {
if (enableLogDebug)
Log.d(
TAG,
"Cannot restore database, it is encrypted. Maybe you forgot to add the property .fileIsEncrypted(true)"
)
onCompleteListener?.onComplete(
false,
"cannot restore database, see Log for more details (if enabled)",
OnCompleteListener.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED
)
return
}
// Copy back database and replace current database
source.copyTo(DATABASE_FILE)
}
if (enableLogDebug)
Log.d(TAG, "Restore done, decrypted($backupIsEncrypted) and restored from $source")
onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS)
}
/**
* This method will do the restore action
*
* @param source InputStream
*/
private fun doRestore(source: InputStream) {
if (backupIsEncrypted) {
// Save inputstream to temp file
source.use { input ->
TEMP_BACKUP_FILE.outputStream().use { output -> input.copyTo(output) }
}
// Decrypt tempfile and write to database file
val decryptedBytes = decryptBackup() ?: return
// Close the database if decryption is succesfull
roomDatabase!!.close()
roomDatabase = null
val bos = BufferedOutputStream(FileOutputStream(DATABASE_FILE, false))
bos.write(decryptedBytes)
bos.flush()
bos.close()
} else {
// Close the database
roomDatabase!!.close()
roomDatabase = null
// Copy back database and replace current database
source.use { input ->
DATABASE_FILE.outputStream().use { output -> input.copyTo(output) }
}
}
if (enableLogDebug)
Log.d(TAG, "Restore done, decrypted($backupIsEncrypted) and restored from $source")
onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS)
}
/**
* Restores the selected file from internal or external storage
*
* @param filename String
*/
private fun restoreSelectedInternalExternalFile(filename: String) {
if (enableLogDebug) Log.d(TAG, "Restore selected file...")
when (backupLocation) {
BACKUP_FILE_LOCATION_INTERNAL -> {
doRestore(File("$INTERNAL_BACKUP_PATH/$filename"))
}
BACKUP_FILE_LOCATION_EXTERNAL -> {
doRestore(File("$EXTERNAL_BACKUP_PATH/$filename"))
}
else -> return
}
}
/**
* Decrypts the [TEMP_BACKUP_FILE] and return it's content as ByteArray. A valid encrypted
* backup file must be present on [TEMP_BACKUP_FILE]
*
* @return decrypted backup as ByteArray
*/
private fun decryptBackup(): ByteArray? {
try {
// Decrypt temp file, and save it to database location
val encryptDecryptBackup = AESEncryptionHelper()
val fileData = encryptDecryptBackup.readFile(TEMP_BACKUP_FILE)
val aesEncryptionManager = AESEncryptionManager()
val decryptedBytes =
aesEncryptionManager.decryptData(sharedPreferences, encryptPassword, fileData)
// Delete tem file
TEMP_BACKUP_FILE.delete()
return decryptedBytes
} catch (e: BadPaddingException) {
if (enableLogDebug) Log.d(TAG, "error during decryption (wrong password): ${e.message}")
onCompleteListener?.onComplete(
false,
"error during decryption (wrong password) see Log for more details (if enabled)",
OnCompleteListener.EXIT_CODE_ERROR_WRONG_DECRYPTION_PASSWORD
)
return null
} catch (e: Exception) {
if (enableLogDebug) Log.d(TAG, "error during decryption: ${e.message}")
onCompleteListener?.onComplete(
false,
"error during decryption see Log for more details (if enabled)",
OnCompleteListener.EXIT_CODE_ERROR_DECRYPTION_ERROR
)
return null
}
}
/**
* Opens the [ActivityResultContracts.RequestMultiplePermissions] and prompts the user to grant
* storage permissions
*
* If granted backup or restore process starts
*/
private val permissionRequestLauncher =
(context as ComponentActivity).registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
permissions.entries.forEach {
if (!it.value) {
onCompleteListener?.onComplete(
false,
"storage permissions are required, please allow!",
OnCompleteListener.EXIT_CODE_ERROR_STORAGE_PERMISSONS_NOT_GRANTED
)
return@registerForActivityResult
}
}
when (currentProcess) {
PROCESS_BACKUP -> {
backupFilename?.let { openBackupfileCreator.launch(it) }
}
PROCESS_RESTORE -> {
openBackupfileChooser.launch(arrayOf("application/octet-stream"))
}
}
}
/**
* Opens the [ActivityResultContracts.OpenDocument] and prompts the user to open a document for
* restoring a backup file
*/
private val openBackupfileChooser =
(context as ComponentActivity).registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { result ->
if (result != null) {
val inputstream = context.contentResolver.openInputStream(result)!!
doRestore(inputstream)
return@registerForActivityResult
}
onCompleteListener?.onComplete(
false,
"failure",
OnCompleteListener.EXIT_CODE_ERROR_BACKUP_FILE_CHOOSER
)
}
/**
* Opens the [ActivityResultContracts.CreateDocument] and prompts the user to select a path for
* creating the new backup file
*/
private val openBackupfileCreator =
(context as ComponentActivity).registerForActivityResult(
CreateDocument("application/octet-stream")
) { result ->
if (result != null) {
val out = context.contentResolver.openOutputStream(result)!!
doBackup(out)
return@registerForActivityResult
}
onCompleteListener?.onComplete(
false,
"failure",
OnCompleteListener.EXIT_CODE_ERROR_BACKUP_FILE_CREATOR
)
}
/** @return current time formatted as String */
private fun getTime(): String {
val currentTime = Calendar.getInstance().time
val sdf =
if (Build.VERSION.SDK_INT <= 28) {
SimpleDateFormat("yyyy-MM-dd-HH_mm_ss", Locale.getDefault())
} else {
SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.getDefault())
}
return sdf.format(currentTime)
}
/**
* If maxFileCount is set, and reached, all old files will be deleted. Only if
* [BACKUP_FILE_LOCATION_INTERNAL] or [BACKUP_FILE_LOCATION_EXTERNAL]
*
* @return true if old files deleted or nothing to do
*/
private fun deleteOldBackup(): Boolean {
// Path of Backup Directory
val backupDirectory: File =
when (backupLocation) {
BACKUP_FILE_LOCATION_INTERNAL -> {
INTERNAL_BACKUP_PATH
}
BACKUP_FILE_LOCATION_EXTERNAL -> {
File("$EXTERNAL_BACKUP_PATH/")
}
BACKUP_FILE_LOCATION_CUSTOM_DIALOG -> {
// In custom backup location no backups will be removed
return true
}
else -> return true
}
// All Files in an Array of type File
val arrayOfFiles = backupDirectory.listFiles()
// If array is null or empty nothing to do and return
if (arrayOfFiles.isNullOrEmpty()) {
if (enableLogDebug) Log.d(TAG, "")
onCompleteListener?.onComplete(
false,
"maxFileCount: Failed to get list of backups",
OnCompleteListener.EXIT_CODE_ERROR
)
return false
} else if (arrayOfFiles.size > maxFileCount!!) {
// Sort Array: lastModified
arrayOfFiles.sortBy { it.lastModified() }
// Get count of files to delete
val fileCountToDelete = arrayOfFiles.size - maxFileCount!!
for (i in 1..fileCountToDelete) {
// Delete all old files (i-1 because array starts a 0)
arrayOfFiles[i - 1].delete()
if (enableLogDebug)
Log.d(TAG, "maxFileCount reached: ${arrayOfFiles[i - 1]} deleted")
}
}
return true
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/item_complete_color" />
</shape>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
</vector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View file

@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,10.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM4,4.5c-0.83,0 -1.5,0.67 -1.5,1.5S3.17,7.5 4,7.5 5.5,6.83 5.5,6 4.83,4.5 4,4.5zM4,16.5c-0.83,0 -1.5,0.68 -1.5,1.5s0.68,1.5 1.5,1.5 1.5,-0.68 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM7,19h14v-2L7,17v2zM7,13h14v-2L7,11v2zM7,5v2h14L21,5L7,5z" />
</vector>

View file

@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>

View file

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" />
</vector>

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16,9h-2.55L13.45,6h-2.9v3L8,9l4,4zM19,3L4.99,3C3.88,3 3,3.9 3,5v14c0,1.1 0.88,2 1.99,2L19,21c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19L5,19v-3h3.56c0.69,1.19 1.97,2 3.45,2s2.75,-0.81 3.45,-2L19,16v3zM19,14h-4.99c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2L5,14l-0.01,-9L19,5v9z" />
</vector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M9.37,5.51C9.19,6.15 9.1,6.82 9.1,7.5c0,4.08 3.32,7.4 7.4,7.4c0.68,0 1.35,-0.09 1.99,-0.27C17.45,17.19 14.93,19 12,19c-3.86,0 -7,-3.14 -7,-7C5,9.07 6.81,6.55 9.37,5.51zM12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9s9,-4.03 9,-9c0,-0.46 -0.04,-0.92 -0.1,-1.36c-0.98,1.37 -2.58,2.26 -4.4,2.26c-2.98,0 -5.4,-2.42 -5.4,-5.4c0,-1.81 0.89,-3.42 2.26,-4.4C12.92,3.04 12.46,3 12,3L12,3z" />
</vector>

View file

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z" />
</vector>

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M5,15L3,15v4c0,1.1 0.9,2 2,2h4v-2L5,19v-4zM5,5h4L9,3L5,3c-1.1,0 -2,0.9 -2,2v4h2L5,5zM19,3h-4v2h4v4h2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19h-4v2h4c1.1,0 2,-0.9 2,-2v-4h-2v4zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
</vector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:fillColor="#FF000000"
android:pathData="M8,0c4.42,0 8,3.58 8,8a8.013,8.013 0,0 1,-5.45 7.59c-0.4,0.08 -0.55,-0.17 -0.55,-0.38 0,-0.27 0.01,-1.13 0.01,-2.2 0,-0.75 -0.25,-1.23 -0.54,-1.48 1.78,-0.2 3.65,-0.88 3.65,-3.95 0,-0.88 -0.31,-1.59 -0.82,-2.15 0.08,-0.2 0.36,-1.02 -0.08,-2.12 0,0 -0.67,-0.22 -2.2,0.82 -0.64,-0.18 -1.32,-0.27 -2,-0.27 -0.68,0 -1.36,0.09 -2,0.27 -1.53,-1.03 -2.2,-0.82 -2.2,-0.82 -0.44,1.1 -0.16,1.92 -0.08,2.12 -0.51,0.56 -0.82,1.28 -0.82,2.15 0,3.06 1.86,3.75 3.64,3.95 -0.23,0.2 -0.44,0.55 -0.51,1.07 -0.46,0.21 -1.61,0.55 -2.33,-0.66 -0.15,-0.24 -0.6,-0.83 -1.23,-0.82 -0.67,0.01 -0.27,0.38 0.01,0.53 0.34,0.19 0.73,0.9 0.82,1.13 0.16,0.45 0.68,1.31 2.69,0.94 0,0.67 0.01,1.3 0.01,1.49 0,0.21 -0.15,0.45 -0.55,0.38A7.995,7.995 0,0 1,0 8c0,-4.42 3.58,-8 8,-8Z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="70dp"
android:height="70dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M21.29,5.89l-10,10c-0.39,0.39 -1.02,0.39 -1.41,0l-2.83,-2.83c-0.39,-0.39 -0.39,-1.02 0,-1.41l0,0c0.39,-0.39 1.02,-0.39 1.41,0l2.12,2.12l9.29,-9.29c0.39,-0.39 1.02,-0.39 1.41,0l0,0C21.68,4.87 21.68,5.5 21.29,5.89zM15.77,2.74c-1.69,-0.69 -3.61,-0.93 -5.61,-0.57C6.09,2.9 2.84,6.18 2.15,10.25C1.01,17 6.63,22.78 13.34,21.91c3.96,-0.51 7.28,-3.46 8.32,-7.31c0.4,-1.47 0.44,-2.89 0.21,-4.22c-0.13,-0.8 -1.12,-1.11 -1.7,-0.54v0c-0.23,0.23 -0.33,0.57 -0.27,0.89c0.22,1.33 0.12,2.75 -0.52,4.26c-1.16,2.71 -3.68,4.7 -6.61,4.97c-5.1,0.47 -9.33,-3.85 -8.7,-8.98c0.43,-3.54 3.28,-6.42 6.81,-6.91c1.73,-0.24 3.37,0.09 4.77,0.81c0.39,0.2 0.86,0.13 1.17,-0.18l0,0c0.48,-0.48 0.36,-1.29 -0.24,-1.6C16.31,2.98 16.04,2.85 15.77,2.74z" />
</vector>

View file

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M10.02,6L8.61,7.41 13.19,12l-4.58,4.59L10.02,18l6,-6 -6,-6z" />
</vector>

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,6c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2m0,10c2.7,0 5.8,1.29 6,2L6,18c0.23,-0.72 3.31,-2 6,-2m0,-12C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" />
</vector>

View file

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M15.61,7.41L14.2,6l-6,6 6,6 1.41,-1.41L11.03,12l4.58,-4.59z" />
</vector>

View file

@ -1,16 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11,9.83l0,4.17l2,0l0,-4.17l1.59,1.58l1.41,-1.41l-4,-4l-4,4l1.41,1.41z" />
<path
android:fillColor="@android:color/white"
android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM19,19H5v-3h3.02c0.91,1.21 2.35,2 3.98,2s3.06,-0.79 3.98,-2H19V19zM19,14h-4.18c-0.41,1.16 -1.51,2 -2.82,2s-2.4,-0.84 -2.82,-2H5V5h14V14z" />
</vector>

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,1.01L7,1C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1.01 17,1.01zM17,21H7v-1h10V21zM17,18H7V6h10V18zM17,4H7V3h10V4zM9.5,8.5H12V7H8v4h1.5V8.5zM12,17h4v-4h-1.5v2.5H12V17z" />
</vector>

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98 0,-0.34 -0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.09,-0.16 -0.26,-0.25 -0.44,-0.25 -0.06,0 -0.12,0.01 -0.17,0.03l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.06,-0.02 -0.12,-0.03 -0.18,-0.03 -0.17,0 -0.34,0.09 -0.43,0.25l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98 0,0.33 0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.09,0.16 0.26,0.25 0.44,0.25 0.06,0 0.12,-0.01 0.17,-0.03l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.06,0.02 0.12,0.03 0.18,0.03 0.17,0 0.34,-0.09 0.43,-0.25l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM17.45,11.27c0.04,0.31 0.05,0.52 0.05,0.73 0,0.21 -0.02,0.43 -0.05,0.73l-0.14,1.13 0.89,0.7 1.08,0.84 -0.7,1.21 -1.27,-0.51 -1.04,-0.42 -0.9,0.68c-0.43,0.32 -0.84,0.56 -1.25,0.73l-1.06,0.43 -0.16,1.13 -0.2,1.35h-1.4l-0.19,-1.35 -0.16,-1.13 -1.06,-0.43c-0.43,-0.18 -0.83,-0.41 -1.23,-0.71l-0.91,-0.7 -1.06,0.43 -1.27,0.51 -0.7,-1.21 1.08,-0.84 0.89,-0.7 -0.14,-1.13c-0.03,-0.31 -0.05,-0.54 -0.05,-0.74s0.02,-0.43 0.05,-0.73l0.14,-1.13 -0.89,-0.7 -1.08,-0.84 0.7,-1.21 1.27,0.51 1.04,0.42 0.9,-0.68c0.43,-0.32 0.84,-0.56 1.25,-0.73l1.06,-0.43 0.16,-1.13 0.2,-1.35h1.39l0.19,1.35 0.16,1.13 1.06,0.43c0.43,0.18 0.83,0.41 1.23,0.71l0.91,0.7 1.06,-0.43 1.27,-0.51 0.7,1.21 -1.07,0.85 -0.89,0.7 0.14,1.13zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
</vector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1c-2.73,2.71 -2.73,7.08 0,9.79s7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29c-3.51,3.48 -9.21,3.48 -12.72,0c-3.5,-3.47 -3.53,-9.11 -0.02,-12.58s9.14,-3.47 12.65,0L21,3V10.12zM12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z" />
</vector>

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M0,15h2L2,9L0,9v6zM3,17h2L5,7L3,7v10zM22,9v6h2L24,9h-2zM19,17h2L21,7h-2v10zM16.5,3h-9C6.67,3 6,3.67 6,4.5v15c0,0.83 0.67,1.5 1.5,1.5h9c0.83,0 1.5,-0.67 1.5,-1.5v-15c0,-0.83 -0.67,-1.5 -1.5,-1.5zM16,19L8,19L8,5h8v14z" />
</vector>

View file

@ -1,7 +0,0 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="70dp"
android:height="70dp"
android:drawable="@drawable/ic_launcher"
android:gravity="center" />
</layer-list>

View file

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".views.activities.MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/main"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:orientation="horizontal"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/progress_frag"
android:name="cn.super12138.todo.views.fragments.ProgressFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="3"
tools:layout="@layout/fragment_progress" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/todo_frag"
android:name="cn.super12138.todo.views.fragments.ToDoFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="2"
tools:layout="@layout/fragment_todo" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".views.activities.MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/main"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/small_screen_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/progress_frag"
android:name="cn.super12138.todo.views.fragments.ProgressFragment"
android:layout_width="match_parent"
android:layout_height="270dp"
tools:layout="@layout/fragment_progress" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/todo_frag"
android:name="cn.super12138.todo.views.fragments.ToDoFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_todo" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".views.activities.CrashActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/tool_bar"
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
app:collapsedTitleTextAppearance="?textAppearanceTitleLarge"
app:expandedTitleTextAppearance="?textAppearanceHeadlineLarge"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:titleCollapseMode="scale">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_app_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="0dp"
app:layout_collapseMode="pin"
app:title="@string/error_title" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<me.zhanghai.android.fastscroll.FastScrollNestedScrollView
android:id="@+id/nested_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="20dp"
android:text="@string/error_tips"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/crash_log"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="15dp"
android:layout_marginEnd="20dp"
android:fontFamily="monospace"
android:text="@string/no_crash_logs"
android:textColor="?attr/colorOnSurface"
android:textIsSelectable="true"
android:textSize="13sp" />
</LinearLayout>
</me.zhanghai.android.fastscroll.FastScrollNestedScrollView>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/exit_app"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:text="@string/exit_app"
app:icon="@drawable/ic_exit" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".views.activities.MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/app_container"
android:name="cn.super12138.todo.views.fragments.MainFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:layout="@layout/fragment_main" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_content_info"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="40dp"
android:layout_marginRight="40dp"
android:orientation="vertical">
<TextView
android:id="@+id/todo_content_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="?attr/colorOnSurface"
android:textSize="25sp" />
<TextView
android:id="@+id/todo_uuid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="?attr/colorOnSurface"
android:textSize="15sp"
android:visibility="gone" />
<TextView
android:id="@+id/todo_subject_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="?attr/colorOnSurface"
android:textSize="15sp" />
<TextView
android:id="@+id/todo_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="45dp"
android:textColor="?attr/colorOnSurface"
android:textSize="15sp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View file

@ -1,194 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="20dp"
android:paddingRight="20dp">
<TextView
android:id="@+id/todo_sheet_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_task"
android:textAlignment="center"
android:textColor="?attr/colorOnSurface"
android:textSize="25sp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/todo_content"
style="?attr/textInputFilledExposedDropdownMenuStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="25dp"
android:gravity="center_horizontal"
android:hint="@string/tasks_textfield_hint"
app:errorEnabled="true"
app:helperText=" "
app:helperTextEnabled="true">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:labelFor="@string/tasks_textfield_hint"
app:simpleItems="@array/todo_predicts" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.chip.ChipGroup
android:id="@+id/todo_subject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:chipSpacing="5dp"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.chip.Chip
android:id="@+id/unknown_subject"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/subject_unknown" />
<com.google.android.material.chip.Chip
android:id="@+id/subject_chinese"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/subject_chinese" />
<com.google.android.material.chip.Chip
android:id="@+id/subject_math"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/subject_math" />
<com.google.android.material.chip.Chip
android:id="@+id/subject_english"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/subject_english" />
<com.google.android.material.chip.Chip
android:id="@+id/subject_biology"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/subject_biology" />
<com.google.android.material.chip.Chip
android:id="@+id/subject_physics"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/subject_physics" />
<com.google.android.material.chip.Chip
android:id="@+id/subject_chemistry"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/subject_chemistry" />
<com.google.android.material.chip.Chip
android:id="@+id/subject_history"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/subject_history" />
<com.google.android.material.chip.Chip
android:id="@+id/subject_geography"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/subject_geography" />
<com.google.android.material.chip.Chip
android:id="@+id/subject_law"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/subject_law" />
<com.google.android.material.chip.Chip
android:id="@+id/subject_other"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/subject_other" />
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/todo_checked"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:checked="false"
android:text="@string/check_one"
android:visibility="gone" />
<TextView
android:id="@+id/todo_uuid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="?attr/colorOnSurface"
android:textSize="15sp"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="35dp"
android:layout_marginBottom="50dp"
android:gravity="center_horizontal"
android:orientation="horizontal">
<Button
android:id="@+id/btn_cancel"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel" />
<Button
android:id="@+id/btn_delete"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/delete_one"
android:textColor="?attr/colorOnSurface"
android:visibility="gone"
app:backgroundTint="?attr/colorErrorContainer" />
<Button
android:id="@+id/btn_save"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@string/save" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View file

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".views.fragments.AboutFragment">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_arrow_back"
app:title="@string/about_label" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="30dp"
android:layout_marginBottom="20dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/app_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="128dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@string/app_name"
android:src="@mipmap/ic_launcher" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/app_name"
android:textAlignment="center"
android:textColor="?attr/colorOnSurface"
android:textSize="20sp" />
<TextView
android:id="@+id/app_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="7dp"
android:textAlignment="center"
android:textSize="11sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/check_update"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="10dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/check_update_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:contentDescription="@string/check_update"
app:srcCompat="@drawable/ic_update" />
<TextView
android:id="@+id/check_update_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:text="@string/check_update"
android:textColor="?attr/colorOnSurface"
android:textSize="15sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/open_source"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/github_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:contentDescription="@string/view_source"
app:srcCompat="@drawable/ic_github" />
<TextView
android:id="@+id/open_source_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:text="@string/view_source"
android:textColor="?attr/colorOnSurface"
android:textSize="15sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/developer_info"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_horizontal|center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/developer_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:contentDescription="@string/author"
app:srcCompat="@drawable/ic_person" />
<TextView
android:id="@+id/developer_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:text="@string/author"
android:textColor="?attr/colorOnSurface"
android:textSize="15sp" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".views.fragments.AboutFragment">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:liftOnScrollTargetViewId="@+id/all_tasks_list">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_arrow_back"
app:title="@string/all_tasks_label" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/all_tasks_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".views.activities.MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:liftOnScroll="false"
app:liftOnScrollTargetViewId="@id/progress_frag">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/main"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/progress_frag"
android:name="cn.super12138.todo.views.fragments.ProgressFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:minHeight="160dp"
tools:layout="@layout/fragment_progress" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/todo_frag"
android:name="cn.super12138.todo.views.fragments.ToDoFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
tools:layout="@layout/fragment_todo" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="270dp"
android:animateLayoutChanges="true">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:max="100"
app:indicatorSize="170dp"
app:indicatorTrackGapSize="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:trackColor="?attr/colorSurfaceContainerHighest"
app:trackCornerRadius="25dp"
app:trackThickness="10dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/complete_count"
style="?attr/textAppearanceDisplaySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="0"
android:textColor="?attr/colorOnSurface"
android:textStyle="bold" />
<TextView
style="?attr/textAppearanceDisplayMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="/"
android:textColor="?attr/colorOnSurface"
android:textStyle="bold" />
<TextView
android:id="@+id/total_count"
style="?attr/textAppearanceDisplayMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="0"
android:textColor="?attr/colorOnSurface"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/remain_count"
style="?attr/textAppearanceLabelMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textSize="12sp"
android:visibility="gone" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".views.fragments.SettingsParentFragment">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_arrow_back"
app:title="@string/settings_label" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/settings_container"
android:name="cn.super12138.todo.views.fragments.SettingsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/todo_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/add_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/add_task"
android:text="@string/add_task"
app:icon="@drawable/ic_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/welcome_page_container"
android:name="cn.super12138.todo.views.fragments.welcome.pages.IntroPage"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/control_area"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout="@layout/fragment_welcome_intro" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/control_area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/welcome_page_container">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/previous_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_margin="16dp"
android:contentDescription="@string/previous_page"
android:visibility="invisible"
app:srcCompat="@drawable/ic_previous" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/center_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:layout_margin="16dp"
android:text="@string/start"
app:icon="@drawable/ic_arrow_forward" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/next_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/next_page"
android:visibility="invisible"
app:srcCompat="@drawable/ic_next" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:padding="20dp">
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="128dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@string/app_name"
android:src="@mipmap/ic_launcher" />
</com.google.android.material.card.MaterialCardView>
<TextView
style="?attr/textAppearanceHeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@string/intro_title"
android:textAlignment="center"
android:textColor="?attr/colorOnSurface" />
<TextView
style="?attr/textAppearanceBodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/intro_description"
android:textAlignment="center"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="100dp"
android:gravity="center"
android:orientation="vertical"
android:padding="20dp">
<TextView
style="?attr/textAppearanceHeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/progress_title"
android:textAlignment="center"
android:textColor="?attr/colorOnSurface" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/welcome_progress"
android:name="cn.super12138.todo.views.fragments.ProgressFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout="@layout/fragment_progress" />
<TextView
style="?attr/textAppearanceBodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/progress_description"
android:textAlignment="center"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="100dp"
android:gravity="center"
android:orientation="vertical"
android:padding="20dp">
<TextView
style="?attr/textAppearanceHeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/todo_btn_title"
android:textAlignment="center"
android:textColor="?attr/colorOnSurface" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/add_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginBottom="30dp"
android:contentDescription="@string/add_task"
android:text="@string/add_task"
app:icon="@drawable/ic_add" />
<TextView
style="?attr/textAppearanceBodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/todo_btn_description"
android:textAlignment="center"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="100dp"
android:gravity="center"
android:orientation="vertical"
android:padding="20dp">
<TextView
style="?attr/textAppearanceHeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/todo_item_title"
android:textAlignment="center"
android:textColor="?attr/colorOnSurface" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/todo_item"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="30dp"
android:layout_marginBottom="30dp"
android:clickable="true"
android:focusable="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/todo_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="18dp"
android:text="@string/todo_item_item_title"
android:textColor="?attr/colorOnSurface"
android:textSize="20sp" />
<TextView
android:id="@+id/todo_subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:text="@string/todo_item_item_subject"
android:textColor="?attr/colorOnSurface"
android:textSize="11sp" />
</LinearLayout>
<Button
android:id="@+id/check_item_btn"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:tooltipText="@string/check_one"
app:icon="@drawable/ic_check" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
style="?attr/textAppearanceBodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/todo_item_description_1"
android:textAlignment="center"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/empty_tip"
style="?attr/textAppearanceLabelLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/no_tasks"
android:textAlignment="center"
android:textColor="?attr/colorOnSurfaceVariant"
android:textSize="14sp" />
</LinearLayout>

View file

@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="5dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="5dp"
android:clickable="true"
android:focusable="true">
<LinearLayout
android:id="@+id/item_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/todo_content"
style="?attr/textAppearanceTitleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="18dp"
android:ellipsize="marquee"
android:singleLine="true"
android:textColor="?attr/colorOnSurface"
tools:text="Title" />
<TextView
android:id="@+id/todo_subject"
style="?attr/textAppearanceLabelLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:singleLine="true"
android:textColor="?attr/colorOnSurface"
android:textSize="11sp"
tools:text="Subject" />
</LinearLayout>
<Button
android:id="@+id/check_item_btn"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:tooltipText="@string/check_one"
app:icon="@drawable/ic_check" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.materialswitch.MaterialSwitch xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:focusable="false" />

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item_settings"
android:icon="@drawable/ic_settings"
android:title="@string/settings_label"
android:tooltipText="@string/settings_label"
app:showAsAction="ifRoom" />
</menu>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Some files were not shown because too many files have changed in this diff Show more