开始使用 Jetpack Compose 对待办重构
This commit is contained in:
parent
0b2384fb88
commit
2d7b3edf15
197 changed files with 478 additions and 8746 deletions
3
app/.gitignore
vendored
3
app/.gitignore
vendored
|
|
@ -1,2 +1 @@
|
|||
/build
|
||||
/release
|
||||
/build
|
||||
|
|
@ -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)
|
||||
}
|
||||
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
|
|
@ -18,6 +18,4 @@
|
|||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-dontwarn javax.annotation.Nullable
|
||||
-dontwarn javax.annotation.concurrent.GuardedBy
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "bf23f9a3f857e7cfe28dce9eb1657534",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "todo",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `state` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`uuid`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uuid",
|
||||
"columnName": "uuid",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "state",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subject",
|
||||
"columnName": "subject",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"uuid"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bf23f9a3f857e7cfe28dce9eb1657534')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
47
app/src/main/kotlin/cn/super12138/todo/MainActivity.kt
Normal file
47
app/src/main/kotlin/cn/super12138/todo/MainActivity.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
package cn.super12138.todo.constant
|
||||
|
||||
object Constants {
|
||||
const val WELCOME_PAGE= "welcome_page"
|
||||
|
||||
const val AUTHOR_GITHUB_URL = "https://github.com/Super12138/"
|
||||
const val REPO_GITHUB_URL = "https://github.com/Super12138/ToDo"
|
||||
const val UPDATE_URL = "https://github.com/Super12138/ToDo/releases"
|
||||
|
||||
const val SP_NAME = "cn.super12138.todo_preferences"
|
||||
|
||||
const val PREF_DARK_MODE = "dark_mode"
|
||||
const val PREF_SECURE_MODE = "secure_mode"
|
||||
const val PREF_HAPTIC_FEEDBACK = "haptic_feedback"
|
||||
const val PREF_ALL_TASKS = "all_tasks"
|
||||
const val PREF_REENTER_WELCOME_ACTIVITY = "reenter_welcome_activity"
|
||||
const val PREF_ABOUT = "about"
|
||||
const val PREF_DEV_MODE = "dev_mode"
|
||||
// const val PREF_SPRING_FESTIVAL_THEME = "spring_festival_theme"
|
||||
const val PREF_BACKUP_DB = "backup_db"
|
||||
const val PREF_RESTORE_DB = "restore_db"
|
||||
|
||||
|
||||
const val STRING_DEV_MODE = "/DEV_MODE"
|
||||
|
||||
const val TAG_INFO_BOTTOM_SHEET = "InfoBottomSheet"
|
||||
const val TAG_TODO_BOTTOM_SHEET = "ToDoBottomSheet"
|
||||
|
||||
const val BUNDLE_EDIT_MODE = "editMode"
|
||||
const val BUNDLE_POSITION = "todoPosition"
|
||||
const val BUNDLE_TODO_CONTENT = "todoContent"
|
||||
const val BUNDLE_TODO_SUBJECT = "todoSubject"
|
||||
const val BUNDLE_TODO_STATE = "todoState"
|
||||
const val BUNDLE_TODO_UUID = "todoUUID"
|
||||
|
||||
const val EMPTY_VIEW_TYPE = 0
|
||||
const val DEFAULT_VIEW_TYPE = 1
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package cn.super12138.todo.constant
|
||||
|
||||
import cn.super12138.todo.utils.sp.SPDelegates
|
||||
|
||||
object GlobalValues {
|
||||
var welcomePage: Boolean by SPDelegates(Constants.WELCOME_PAGE, false)
|
||||
var darkMode: String by SPDelegates(Constants.PREF_DARK_MODE, "0")
|
||||
var devMode: Boolean by SPDelegates(Constants.PREF_DEV_MODE, false)
|
||||
// var springFestivalTheme: Boolean by SPDelegates(Constants.PREF_SPRING_FESTIVAL_THEME, false)
|
||||
var secureMode: Boolean by SPDelegates(Constants.PREF_SECURE_MODE, false)
|
||||
var hapticFeedback: Boolean by SPDelegates(Constants.PREF_HAPTIC_FEEDBACK, true)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package cn.super12138.todo.logic.dao
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/**
|
||||
* @param uuid String 待办的uuid
|
||||
* @param state Int 待办的完成状态,0表示未完成,1表示完成
|
||||
* @param subject String 待办的学科
|
||||
* @param content String 待办的内容
|
||||
*/
|
||||
@Entity(tableName = "todo")
|
||||
data class ToDoRoom(
|
||||
@PrimaryKey @ColumnInfo(name = "uuid") val uuid: String,
|
||||
@ColumnInfo(name = "state") val state: Int,
|
||||
@ColumnInfo(name = "subject") val subject: String,
|
||||
@ColumnInfo(name = "content") val content: String
|
||||
)
|
||||
|
|
@ -1,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
11
app/src/main/kotlin/cn/super12138/todo/ui/theme/Color.kt
Normal file
11
app/src/main/kotlin/cn/super12138/todo/ui/theme/Color.kt
Normal 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)
|
||||
58
app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt
Normal file
58
app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt
Normal 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
|
||||
)
|
||||
}
|
||||
34
app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt
Normal file
34
app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt
Normal 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
|
||||
)
|
||||
*/
|
||||
)
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package cn.super12138.todo.utils
|
||||
|
||||
import android.text.Editable
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.ToDoApp
|
||||
|
||||
object TextUtils {
|
||||
// 更换为id匹配(数据库)
|
||||
private val subjectMap = mapOf(
|
||||
globalGetString(R.string.subject_chinese) to R.id.subject_chinese,
|
||||
globalGetString(R.string.subject_math) to R.id.subject_math,
|
||||
globalGetString(R.string.subject_english) to R.id.subject_english,
|
||||
globalGetString(R.string.subject_biology) to R.id.subject_biology,
|
||||
globalGetString(R.string.subject_geography) to R.id.subject_geography,
|
||||
globalGetString(R.string.subject_history) to R.id.subject_history,
|
||||
globalGetString(R.string.subject_physics) to R.id.subject_physics,
|
||||
globalGetString(R.string.subject_chemistry) to R.id.subject_chemistry,
|
||||
globalGetString(R.string.subject_law) to R.id.subject_law,
|
||||
globalGetString(R.string.subject_other) to R.id.subject_other
|
||||
)
|
||||
|
||||
fun getSubjectName(id: Int): String {
|
||||
return when (id) {
|
||||
R.id.subject_chinese -> globalGetString(R.string.subject_chinese)
|
||||
R.id.subject_math -> globalGetString(R.string.subject_math)
|
||||
R.id.subject_english -> globalGetString(R.string.subject_english)
|
||||
R.id.subject_biology -> globalGetString(R.string.subject_biology)
|
||||
R.id.subject_geography -> globalGetString(R.string.subject_geography)
|
||||
R.id.subject_history -> globalGetString(R.string.subject_history)
|
||||
R.id.subject_physics -> globalGetString(R.string.subject_physics)
|
||||
R.id.subject_chemistry -> globalGetString(R.string.subject_chemistry)
|
||||
R.id.subject_law -> globalGetString(R.string.subject_law)
|
||||
R.id.subject_other -> globalGetString(R.string.subject_other)
|
||||
else -> globalGetString(R.string.subject_unknown)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubjectID(name: String): Int? {
|
||||
return subjectMap[name]
|
||||
}
|
||||
}
|
||||
|
||||
fun globalGetString(resID: Int): String {
|
||||
return ToDoApp.context.resources.getString(resID)
|
||||
}
|
||||
|
||||
fun String.toEditable(): Editable? {
|
||||
return Editable.Factory.getInstance().newEditable(this)
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package cn.super12138.todo.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import cn.super12138.todo.ToDoApp
|
||||
|
||||
private var clickInterval = 380L
|
||||
private var lastTime = 0L
|
||||
|
||||
/**
|
||||
* 延迟点击
|
||||
*/
|
||||
fun View.setOnIntervalClickListener(onIntervalClickListener: (View) -> Unit) {
|
||||
this.setOnClickListener {
|
||||
if (SystemClock.elapsedRealtime() - lastTime > clickInterval) {
|
||||
lastTime = SystemClock.elapsedRealtime()
|
||||
onIntervalClickListener.invoke(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast
|
||||
*/
|
||||
fun String.showToast(
|
||||
context: Context = ToDoApp.context,
|
||||
duration: Int = Toast.LENGTH_SHORT
|
||||
) {
|
||||
Toast.makeText(context, this, duration).show()
|
||||
}
|
||||
|
||||
fun Int.showToast(context: Context = ToDoApp.context, duration: Int = Toast.LENGTH_SHORT) {
|
||||
Toast.makeText(context, this, duration).show()
|
||||
}
|
||||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
package cn.super12138.todo.views
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import cn.super12138.todo.constant.GlobalValues
|
||||
|
||||
abstract class BaseActivity<T : ViewBinding> : AppCompatActivity() {
|
||||
lateinit var binding: T
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
/*if (GlobalValues.springFestivalTheme) {
|
||||
setTheme(R.style.Theme_SpringFestival)
|
||||
}*/
|
||||
// enableEdgeToEdge(navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT))
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// 确保 Navigation Bar 区域会被显示
|
||||
// WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
binding = getViewBinding()
|
||||
setContentView(binding.root)
|
||||
|
||||
// 深色模式
|
||||
when (GlobalValues.darkMode) {
|
||||
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
|
||||
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
|
||||
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
|
||||
// 适配刘海屏
|
||||
val lp = window.attributes
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
lp.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
window.attributes = lp
|
||||
}
|
||||
|
||||
abstract fun getViewBinding(): T
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
package cn.super12138.todo.views
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
|
||||
abstract class BaseFragment<T : ViewBinding> : Fragment() {
|
||||
private var _binding: T? = null
|
||||
protected val binding get() = _binding!!
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true)
|
||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false)
|
||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true)
|
||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = getViewBinding(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
// https://github.com/material-components/material-components-android/issues/1984#issuecomment-1089710991
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
// Overlap colors.
|
||||
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.colorBackground))
|
||||
}
|
||||
|
||||
abstract fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): T
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package cn.super12138.todo.views.activities
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import cn.super12138.todo.databinding.ActivityCrashBinding
|
||||
import cn.super12138.todo.utils.VersionUtils
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.views.BaseActivity
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
class CrashActivity : BaseActivity<ActivityCrashBinding>() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val crashLogs = intent.getStringExtra("crash_logs")
|
||||
|
||||
val deviceBrand = Build.BRAND
|
||||
val deviceModel = Build.MODEL
|
||||
val sdkLevel = Build.VERSION.SDK_INT
|
||||
val currentDateTime = Calendar.getInstance().time
|
||||
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
val formattedDateTime = formatter.format(currentDateTime)
|
||||
|
||||
val deviceInfo = StringBuilder().apply {
|
||||
append("ToDo version: ").append(VersionUtils.getAppVersion(this@CrashActivity))
|
||||
.append('\n')
|
||||
append("Brand: ").append("").append(deviceBrand).append('\n')
|
||||
append("Model: ").append(deviceModel).append('\n')
|
||||
append("Device SDK: ").append(sdkLevel).append('\n').append('\n')
|
||||
append("Crash time: ").append(formattedDateTime).append('\n').append('\n')
|
||||
append("======beginning of crash======").append('\n')
|
||||
}
|
||||
|
||||
binding.crashLog.text = StringBuilder().apply {
|
||||
append(deviceInfo)
|
||||
append(crashLogs)
|
||||
}
|
||||
|
||||
binding.exitApp.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
this.finishAffinity()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewBinding(): ActivityCrashBinding {
|
||||
return ActivityCrashBinding.inflate(layoutInflater)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
package cn.super12138.todo.views.activities
|
||||
// 2023.11.18立项
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.constant.GlobalValues
|
||||
import cn.super12138.todo.databinding.ActivityMainBinding
|
||||
import cn.super12138.todo.views.BaseActivity
|
||||
import cn.super12138.todo.views.fragments.SettingsParentFragment
|
||||
import cn.super12138.todo.views.fragments.welcome.WelcomeFragment
|
||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||
|
||||
class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
lateinit var roomBackup: RoomBackup
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!GlobalValues.welcomePage) {
|
||||
startFragment(WelcomeFragment())
|
||||
}
|
||||
|
||||
when (GlobalValues.secureMode) {
|
||||
true -> window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
|
||||
false -> window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
handleIntent(intent)
|
||||
|
||||
roomBackup = RoomBackup(this)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
super.onNewIntent(intent, caller)
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
override fun getViewBinding(): ActivityMainBinding {
|
||||
return ActivityMainBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
fun startFragment(fragment: Fragment, args: (Bundle.() -> Unit)? = null) {
|
||||
supportFragmentManager.commit {
|
||||
addToBackStack(System.currentTimeMillis().toString())
|
||||
hide(supportFragmentManager.fragments.last())
|
||||
add(
|
||||
R.id.app_container,
|
||||
fragment.apply { args?.let { arguments = Bundle().apply(it) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_APPLICATION_PREFERENCES -> startFragment(SettingsParentFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
package cn.super12138.todo.views.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.ToDoApp
|
||||
import cn.super12138.todo.constant.Constants.DEFAULT_VIEW_TYPE
|
||||
import cn.super12138.todo.constant.Constants.EMPTY_VIEW_TYPE
|
||||
import cn.super12138.todo.logic.model.ToDo
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.views.fragments.InfoBottomSheet
|
||||
|
||||
class AllTasksAdapter(
|
||||
private val todoList: MutableList<ToDo>,
|
||||
private val fragmentManager: FragmentManager
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
// 默认待办项
|
||||
inner class DefaultViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val todoContext: TextView = view.findViewById(R.id.todo_content)
|
||||
val todoSubject: TextView = view.findViewById(R.id.todo_subject)
|
||||
val itemBackground: LinearLayout = view.findViewById(R.id.item_background)
|
||||
val checkBtn: Button = view.findViewById(R.id.check_item_btn)
|
||||
}
|
||||
|
||||
// 空项目提示
|
||||
inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return if (viewType == EMPTY_VIEW_TYPE) { // 如果列表是空的
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_empty, parent, false)
|
||||
EmptyViewHolder(view)
|
||||
} else {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_todo, parent, false)
|
||||
DefaultViewHolder(view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (todoList.isEmpty()) EMPTY_VIEW_TYPE else DEFAULT_VIEW_TYPE
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
// 判断当前的holder是不是待办项目的holder
|
||||
if (holder is DefaultViewHolder) {
|
||||
val todo = todoList[position]
|
||||
holder.apply {
|
||||
checkBtn.visibility = View.GONE
|
||||
todoContext.text = todo.content
|
||||
todoSubject.text = todo.subject
|
||||
}
|
||||
|
||||
if (todo.state == 1) {
|
||||
holder.itemBackground.background =
|
||||
ContextCompat.getDrawable(ToDoApp.context, R.drawable.bg_item_complete)
|
||||
} else {
|
||||
holder.itemBackground.background = null
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
val infoBottomSheet = InfoBottomSheet.newInstance(
|
||||
todo.content,
|
||||
todo.subject,
|
||||
todo.state,
|
||||
todo.uuid
|
||||
)
|
||||
infoBottomSheet.show(fragmentManager, InfoBottomSheet.TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return if (todoList.isEmpty()) 1 else todoList.size
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
package cn.super12138.todo.views.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.constant.Constants.DEFAULT_VIEW_TYPE
|
||||
import cn.super12138.todo.constant.Constants.EMPTY_VIEW_TYPE
|
||||
import cn.super12138.todo.constant.GlobalValues
|
||||
import cn.super12138.todo.logic.model.ToDo
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.utils.showToast
|
||||
import cn.super12138.todo.views.fragments.ToDoBottomSheet
|
||||
import cn.super12138.todo.views.viewmodels.ProgressViewModel
|
||||
import cn.super12138.todo.views.viewmodels.ToDoViewModel
|
||||
|
||||
class ToDoAdapter(
|
||||
private val todoList: MutableList<ToDo>,
|
||||
private val viewModelStoreOwner: ViewModelStoreOwner,
|
||||
private val fragmentManager: FragmentManager
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
// 默认待办项
|
||||
inner class DefaultViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val todoContext: TextView = view.findViewById(R.id.todo_content)
|
||||
val todoSubject: TextView = view.findViewById(R.id.todo_subject)
|
||||
val checkToDoBtn: Button = view.findViewById(R.id.check_item_btn)
|
||||
}
|
||||
|
||||
// 空项目提示
|
||||
inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
|
||||
|
||||
// 判断列表是否为空,为空显示空项目提示
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (todoList.isEmpty()) EMPTY_VIEW_TYPE else DEFAULT_VIEW_TYPE
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return if (viewType == EMPTY_VIEW_TYPE) { // 如果列表是空的
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_empty, parent, false)
|
||||
EmptyViewHolder(view)
|
||||
} else {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_todo, parent, false)
|
||||
DefaultViewHolder(view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
// 判断当前的holder是不是待办项目的holder
|
||||
if (holder is DefaultViewHolder) {
|
||||
val todo = todoList[position]
|
||||
holder.todoContext.text = todo.content
|
||||
holder.todoSubject.text = todo.subject
|
||||
|
||||
val progressViewModel =
|
||||
ViewModelProvider(viewModelStoreOwner)[ProgressViewModel::class.java]
|
||||
val todoViewModel =
|
||||
ViewModelProvider(viewModelStoreOwner)[ToDoViewModel::class.java]
|
||||
|
||||
holder.checkToDoBtn.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
if (position >= todoList.size) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
todoList.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, todoList.size)
|
||||
|
||||
todoViewModel.updateTaskState(todo.uuid)
|
||||
|
||||
progressViewModel.updateProgress()
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
if (GlobalValues.devMode) {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
"Current position: $position".showToast()
|
||||
}
|
||||
}
|
||||
|
||||
holder.itemView.setOnLongClickListener {
|
||||
val toDoBottomSheet = ToDoBottomSheet.newInstance(
|
||||
true,
|
||||
position,
|
||||
todo.uuid,
|
||||
todo.state,
|
||||
todo.subject,
|
||||
todo.content
|
||||
)
|
||||
toDoBottomSheet.show(fragmentManager, ToDoBottomSheet.TAG)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return if (todoList.isEmpty()) 1 else todoList.size
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import cn.super12138.todo.constant.Constants
|
||||
import cn.super12138.todo.databinding.FragmentAboutBinding
|
||||
import cn.super12138.todo.utils.VersionUtils
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.utils.showToast
|
||||
import cn.super12138.todo.views.BaseFragment
|
||||
|
||||
class AboutFragment : BaseFragment<FragmentAboutBinding>() {
|
||||
private var clickCount = 0
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.appVersion.text = VersionUtils.getAppVersion(requireActivity())
|
||||
|
||||
binding.toolBar.setNavigationOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
requireActivity().supportFragmentManager.popBackStack()
|
||||
}
|
||||
|
||||
binding.checkUpdate.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(Constants.UPDATE_URL)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.openSource.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(Constants.REPO_GITHUB_URL)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
||||
binding.developerInfo.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(Constants.AUTHOR_GITHUB_URL)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.appVersion.setOnClickListener {
|
||||
clickCount++
|
||||
when (clickCount) {
|
||||
5 -> {
|
||||
clickCount = 0
|
||||
// GlobalValues.springFestivalTheme = !GlobalValues.springFestivalTheme
|
||||
"🍂".showToast()
|
||||
/*when (java.util.Calendar.getInstance(Locale.getDefault())
|
||||
.get(java.util.Calendar.MONTH) + 1) {
|
||||
3, 4, 5 ->
|
||||
6, 7, 8 -> SUMMER
|
||||
9, 10, 11 -> AUTUMN
|
||||
12, 1, 2 -> WINTER
|
||||
else -> -12
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentAboutBinding {
|
||||
return FragmentAboutBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import cn.super12138.todo.ToDoApp
|
||||
import cn.super12138.todo.databinding.FragmentAllTasksBinding
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.views.BaseFragment
|
||||
import cn.super12138.todo.views.adapters.AllTasksAdapter
|
||||
import cn.super12138.todo.views.viewmodels.AllTasksViewModel
|
||||
import me.zhanghai.android.fastscroll.FastScrollerBuilder
|
||||
|
||||
class AllTasksFragment : BaseFragment<FragmentAllTasksBinding>() {
|
||||
private val viewModel by viewModels<AllTasksViewModel>()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val todoListAll = viewModel.todoListAll
|
||||
val layoutManager = LinearLayoutManager(ToDoApp.context)
|
||||
binding.allTasksList.layoutManager = layoutManager
|
||||
val adapter = AllTasksAdapter(todoListAll, childFragmentManager)
|
||||
binding.allTasksList.adapter = adapter
|
||||
|
||||
FastScrollerBuilder(binding.allTasksList).apply {
|
||||
useMd2Style()
|
||||
build()
|
||||
}
|
||||
|
||||
binding.toolBar.setNavigationOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
requireActivity().supportFragmentManager.popBackStack()
|
||||
}
|
||||
|
||||
viewModel.refreshData.observe(viewLifecycleOwner, Observer {
|
||||
binding.allTasksList.adapter?.notifyItemRangeChanged(0, todoListAll.size + 1)
|
||||
})
|
||||
}
|
||||
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentAllTasksBinding {
|
||||
return FragmentAllTasksBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.constant.Constants
|
||||
import cn.super12138.todo.constant.GlobalValues
|
||||
import cn.super12138.todo.databinding.BottomSheetInfoBinding
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
|
||||
class InfoBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding: BottomSheetInfoBinding
|
||||
private lateinit var todoContent: String
|
||||
private lateinit var todoSubject: String
|
||||
private var todoState: Int = 0
|
||||
private lateinit var todoUUID: String
|
||||
|
||||
companion object {
|
||||
const val TAG = Constants.TAG_INFO_BOTTOM_SHEET
|
||||
fun newInstance(
|
||||
todoContent: String,
|
||||
todoSubject: String,
|
||||
todoState: Int,
|
||||
todoUUID: String
|
||||
) = InfoBottomSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(Constants.BUNDLE_TODO_CONTENT, todoContent)
|
||||
putString(Constants.BUNDLE_TODO_SUBJECT, todoSubject)
|
||||
putInt(Constants.BUNDLE_TODO_STATE, todoState)
|
||||
putString(Constants.BUNDLE_TODO_UUID, todoUUID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
todoContent = it.getString(Constants.BUNDLE_TODO_CONTENT, "")
|
||||
todoSubject = it.getString(Constants.BUNDLE_TODO_SUBJECT, "")
|
||||
todoState = it.getInt(Constants.BUNDLE_TODO_STATE, 0)
|
||||
todoUUID = it.getString(Constants.BUNDLE_TODO_UUID, "")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = BottomSheetInfoBinding.inflate(inflater, container, false)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val bottomSheetDialog = dialog as BottomSheetDialog
|
||||
bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
bottomSheetDialog.dismissWithAnimation = true
|
||||
|
||||
binding.todoContentInfo.text = todoContent
|
||||
binding.todoSubjectInfo.text = String.format(getString(R.string.info_subject), todoSubject)
|
||||
if (todoState == 0) {
|
||||
binding.todoState.text = getString(R.string.info_state_incomplete)
|
||||
} else {
|
||||
binding.todoState.text = getString(R.string.info_state_complete)
|
||||
}
|
||||
if (GlobalValues.devMode) {
|
||||
binding.todoUuid.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = String.format(getString(R.string.info_uuid), todoUUID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.databinding.FragmentMainBinding
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.views.BaseFragment
|
||||
import cn.super12138.todo.views.activities.MainActivity
|
||||
|
||||
class MainFragment : BaseFragment<FragmentMainBinding>() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.toolBar.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.item_settings -> {
|
||||
(requireActivity() as MainActivity).startFragment(SettingsParentFragment())
|
||||
|
||||
VibrationUtils.performHapticFeedback(binding.toolBar)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// setSupportActionBar(binding.toolbar)
|
||||
}
|
||||
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentMainBinding {
|
||||
return FragmentMainBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.databinding.FragmentProgressBinding
|
||||
import cn.super12138.todo.views.BaseFragment
|
||||
import cn.super12138.todo.views.viewmodels.ProgressViewModel
|
||||
|
||||
|
||||
class ProgressFragment : BaseFragment<FragmentProgressBinding>() {
|
||||
private val viewModel: ProgressViewModel by viewModels({ requireActivity() })
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.progress.observe(viewLifecycleOwner, Observer { value ->
|
||||
binding.progressBar.setProgressCompat(value, true)
|
||||
})
|
||||
|
||||
viewModel.totalCount.observe(viewLifecycleOwner, Observer { total ->
|
||||
binding.totalCount.text = total.toString()
|
||||
})
|
||||
|
||||
viewModel.completeCount.observe(viewLifecycleOwner, Observer { complete ->
|
||||
binding.completeCount.text = complete.toString()
|
||||
})
|
||||
|
||||
viewModel.remainCount.observe(viewLifecycleOwner, Observer { remain ->
|
||||
if (remain == 0) {
|
||||
binding.remainCount.visibility = View.GONE
|
||||
} else {
|
||||
binding.remainCount.visibility = View.VISIBLE
|
||||
binding.remainCount.text =
|
||||
String.format(getString(R.string.remain_text), remain.toString())
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.updateProgress()
|
||||
}
|
||||
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentProgressBinding {
|
||||
return FragmentProgressBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.constant.Constants
|
||||
import cn.super12138.todo.constant.GlobalValues
|
||||
import cn.super12138.todo.logic.dao.ToDoRoomDB
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.views.activities.MainActivity
|
||||
import cn.super12138.todo.views.fragments.welcome.WelcomeFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
val mainActivity = requireActivity() as MainActivity
|
||||
val roomBackup = mainActivity.roomBackup
|
||||
|
||||
findPreference<ListPreference>(Constants.PREF_DARK_MODE)?.apply {
|
||||
setOnPreferenceClickListener {
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
true
|
||||
}
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
when (newValue) {
|
||||
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
|
||||
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
|
||||
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
activity?.recreate()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<SwitchPreferenceCompat>(Constants.PREF_SECURE_MODE)?.apply {
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
|
||||
|
||||
view?.let {
|
||||
Snackbar.make(it, R.string.need_restart_app, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.restart_app_now) {
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
restartApp(context)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<SwitchPreferenceCompat>(Constants.PREF_HAPTIC_FEEDBACK)?.apply {
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(Constants.PREF_BACKUP_DB)?.apply {
|
||||
setOnPreferenceClickListener {
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
val simpleDateFormat =
|
||||
SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||
val formattedDate = simpleDateFormat.format(Date())
|
||||
roomBackup
|
||||
.database(ToDoRoomDB.getDatabase(requireContext()))
|
||||
.enableLogDebug(GlobalValues.devMode)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.customBackupFileName("ToDo-DataBase-$formattedDate.sqlite3")
|
||||
.apply {
|
||||
onCompleteListener { success, _, exitCode ->
|
||||
if (success) {
|
||||
view?.let { it1 ->
|
||||
Snackbar.make(
|
||||
it1, R.string.tips_backup_success, Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
view?.let { it1 ->
|
||||
Snackbar.make(
|
||||
it1, getString(
|
||||
R.string.tips_backup_failed,
|
||||
exitCode
|
||||
), Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.backup()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(Constants.PREF_RESTORE_DB)?.apply {
|
||||
setOnPreferenceClickListener {
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
|
||||
roomBackup
|
||||
.database(ToDoRoomDB.getDatabase(requireContext()))
|
||||
.enableLogDebug(GlobalValues.devMode)
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
.apply {
|
||||
onCompleteListener { success, _, exitCode ->
|
||||
if (success) {
|
||||
view?.let { it1 ->
|
||||
Snackbar.make(
|
||||
it1,
|
||||
R.string.tips_restore_success,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setAction(R.string.restart_app_now) {
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
restartApp(context)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
} else {
|
||||
view?.let { it1 ->
|
||||
Snackbar.make(
|
||||
it1, getString(
|
||||
R.string.tips_restore_failed,
|
||||
exitCode
|
||||
), Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.restore()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(Constants.PREF_ALL_TASKS)?.apply {
|
||||
setOnPreferenceClickListener {
|
||||
mainActivity.startFragment(AllTasksFragment())
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(Constants.PREF_REENTER_WELCOME_ACTIVITY)?.apply {
|
||||
setOnPreferenceClickListener {
|
||||
mainActivity.startFragment(WelcomeFragment())
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>(Constants.PREF_ABOUT)?.apply {
|
||||
setOnPreferenceClickListener {
|
||||
mainActivity.startFragment(AboutFragment())
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDivider(divider: Drawable?) {
|
||||
super.setDivider(ColorDrawable(Color.TRANSPARENT))
|
||||
}
|
||||
|
||||
override fun setDividerHeight(height: Int) {
|
||||
super.setDividerHeight(0)
|
||||
}
|
||||
|
||||
private fun restartApp(restartContext: Context) {
|
||||
val intent = Intent(restartContext, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
restartContext.startActivity(intent)
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import cn.super12138.todo.databinding.FragmentSettingsBinding
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.views.BaseFragment
|
||||
|
||||
class SettingsParentFragment : BaseFragment<FragmentSettingsBinding>() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.toolBar.setNavigationOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
requireActivity().supportFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentSettingsBinding {
|
||||
return FragmentSettingsBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.constant.Constants
|
||||
import cn.super12138.todo.constant.GlobalValues
|
||||
import cn.super12138.todo.databinding.BottomSheetTodoBinding
|
||||
import cn.super12138.todo.logic.dao.ToDoRoom
|
||||
import cn.super12138.todo.logic.model.ToDo
|
||||
import cn.super12138.todo.utils.TextUtils
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.utils.showToast
|
||||
import cn.super12138.todo.utils.toEditable
|
||||
import cn.super12138.todo.views.viewmodels.ProgressViewModel
|
||||
import cn.super12138.todo.views.viewmodels.ToDoViewModel
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import java.util.UUID
|
||||
|
||||
class ToDoBottomSheet : BottomSheetDialogFragment() {
|
||||
private lateinit var binding: BottomSheetTodoBinding
|
||||
|
||||
private val progressViewModel: ProgressViewModel by viewModels({ requireActivity() })
|
||||
private val todoViewModel: ToDoViewModel by viewModels({ requireActivity() })
|
||||
|
||||
private var editMode: Boolean = false
|
||||
private var todoState: Int = 0
|
||||
private var todoPosition: Int = 0
|
||||
private lateinit var todoUUID: String
|
||||
private lateinit var todoOrigSubject: String
|
||||
private lateinit var todoOrigContent: String
|
||||
|
||||
companion object {
|
||||
const val TAG = Constants.TAG_TODO_BOTTOM_SHEET
|
||||
|
||||
fun newInstance(
|
||||
editMode: Boolean,
|
||||
todoPosition: Int,
|
||||
todoUUID: String,
|
||||
todoState: Int,
|
||||
todoSubject: String,
|
||||
todoContent: String,
|
||||
) = ToDoBottomSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putBoolean(Constants.BUNDLE_EDIT_MODE, editMode)
|
||||
putInt(Constants.BUNDLE_POSITION, todoPosition)
|
||||
putString(Constants.BUNDLE_TODO_UUID, todoUUID)
|
||||
putInt(Constants.BUNDLE_TODO_STATE, todoState)
|
||||
putString(Constants.BUNDLE_TODO_SUBJECT, todoSubject)
|
||||
putString(Constants.BUNDLE_TODO_CONTENT, todoContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
editMode = it.getBoolean(Constants.BUNDLE_EDIT_MODE, false)
|
||||
todoPosition = it.getInt(Constants.BUNDLE_POSITION, 0)
|
||||
todoOrigContent = it.getString(Constants.BUNDLE_TODO_CONTENT, "")
|
||||
todoOrigSubject = it.getString(Constants.BUNDLE_TODO_SUBJECT, "")
|
||||
todoState = it.getInt(Constants.BUNDLE_TODO_STATE, 0)
|
||||
todoUUID = it.getString(Constants.BUNDLE_TODO_UUID, "")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = BottomSheetTodoBinding.inflate(inflater, container, false)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// BottomSheet 基本参数设置
|
||||
val bottomSheetDialog = dialog as BottomSheetDialog
|
||||
bottomSheetDialog.behavior.apply {
|
||||
state = BottomSheetBehavior.STATE_EXPANDED
|
||||
saveFlags = BottomSheetBehavior.SAVE_ALL
|
||||
}
|
||||
bottomSheetDialog.dismissWithAnimation = true
|
||||
|
||||
val todoList = todoViewModel.todoList
|
||||
|
||||
binding.todoSubject.setOnCheckedStateChangeListener { _, _ ->
|
||||
VibrationUtils.performHapticFeedback(binding.todoSubject)
|
||||
}
|
||||
|
||||
// 编辑模式
|
||||
if (editMode) {
|
||||
binding.btnCancel.visibility = View.GONE
|
||||
binding.btnDelete.visibility = View.VISIBLE
|
||||
binding.todoSheetTitle.text = getString(R.string.update_task)
|
||||
binding.todoContent.editText?.text = todoOrigContent.toEditable()
|
||||
binding.btnSave.text = getString(R.string.update)
|
||||
|
||||
TextUtils.getSubjectID(todoOrigSubject)?.let { binding.todoSubject.check(it) }
|
||||
|
||||
if (GlobalValues.devMode) {
|
||||
binding.todoUuid.apply {
|
||||
text = "UUID: $todoUUID"
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnSave.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
val todoContent = binding.todoContent.editText?.text.toString()
|
||||
// 内容判空
|
||||
if (todoContent.isEmpty()) {
|
||||
binding.todoContent.error =
|
||||
getString(R.string.content_cannot_be_empty)
|
||||
return@setOnClickListener
|
||||
} else {
|
||||
// 开发者模式
|
||||
if (todoContent == Constants.STRING_DEV_MODE) {
|
||||
if (GlobalValues.devMode) {
|
||||
GlobalValues.devMode = false
|
||||
} else {
|
||||
GlobalValues.devMode = true
|
||||
"Dev Mode".showToast()
|
||||
}
|
||||
} else {
|
||||
// 随机 UUID
|
||||
val randomUUID = UUID.randomUUID().toString()
|
||||
// 待办学科
|
||||
val todoSubject = TextUtils.getSubjectName(binding.todoSubject.checkedChipId)
|
||||
|
||||
// 更新待办
|
||||
if (editMode) {
|
||||
todoViewModel.updateTask(
|
||||
todoPosition,
|
||||
ToDoRoom(
|
||||
todoUUID,
|
||||
todoState,
|
||||
todoSubject,
|
||||
todoContent
|
||||
)
|
||||
)
|
||||
todoViewModel.refreshData.value = 1
|
||||
} else {
|
||||
// 添加到 RecyclerView
|
||||
todoList.add(
|
||||
ToDo(randomUUID, 0, todoContent, todoSubject)
|
||||
)
|
||||
|
||||
// 插入数据库
|
||||
todoViewModel.insertTask(
|
||||
ToDoRoom(
|
||||
randomUUID,
|
||||
0,
|
||||
todoSubject,
|
||||
todoContent
|
||||
)
|
||||
)
|
||||
progressViewModel.updateProgress()
|
||||
todoViewModel.addData.value = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.btnCancel.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.btnDelete.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
todoViewModel.deleteTask(todoPosition, todoUUID)
|
||||
progressViewModel.updateProgress()
|
||||
todoViewModel.removeData.value = 1
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.ToDoApp
|
||||
import cn.super12138.todo.databinding.FragmentTodoBinding
|
||||
import cn.super12138.todo.logic.Repository
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.views.adapters.ToDoAdapter
|
||||
import cn.super12138.todo.views.viewmodels.ProgressViewModel
|
||||
import cn.super12138.todo.views.viewmodels.ToDoViewModel
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.launch
|
||||
import me.zhanghai.android.fastscroll.FastScrollerBuilder
|
||||
|
||||
class ToDoFragment : Fragment() {
|
||||
private val progressViewModel: ProgressViewModel by viewModels({ requireActivity() })
|
||||
private val todoViewModel: ToDoViewModel by viewModels({ requireActivity() })
|
||||
private lateinit var binding: FragmentTodoBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentTodoBinding.inflate(inflater, container, false)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.addItem) { v, windowInsets ->
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
leftMargin = if (insets.left == 0) 16 else insets.left
|
||||
bottomMargin = if (insets.bottom == 0) 48 else insets.bottom + 32
|
||||
rightMargin = if (insets.right == 0) 48 else insets.right + 32
|
||||
}
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
FastScrollerBuilder(binding.todoList).apply {
|
||||
useMd2Style()
|
||||
build()
|
||||
}
|
||||
|
||||
val layoutManager = LinearLayoutManager(ToDoApp.context)
|
||||
binding.todoList.layoutManager = layoutManager
|
||||
val todoList = todoViewModel.todoList
|
||||
val adapter = ToDoAdapter(todoList, requireActivity(), parentFragmentManager)
|
||||
binding.todoList.adapter = adapter
|
||||
|
||||
binding.addItem.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
val toDoBottomSheet = ToDoBottomSheet()
|
||||
toDoBottomSheet.show(parentFragmentManager, ToDoBottomSheet.TAG)
|
||||
}
|
||||
|
||||
binding.addItem.setOnLongClickListener {
|
||||
activity?.let { it1 ->
|
||||
MaterialAlertDialogBuilder(it1)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.delete_confirm)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
// 清除 Recycler View
|
||||
todoList.clear()
|
||||
// 清除数据库
|
||||
lifecycleScope.launch {
|
||||
Repository.deleteAll()
|
||||
progressViewModel.updateProgress()
|
||||
}
|
||||
// 通知数据更改
|
||||
binding.todoList.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
binding.todoList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
val fab = binding.addItem
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
// 列表下滑,隐藏FAB
|
||||
if (dy > 0 && fab.isShown) {
|
||||
fab.hide()
|
||||
}
|
||||
// 列表上滑,显示FAB
|
||||
if (dy < 0 && !fab.isShown) {
|
||||
fab.show()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
todoViewModel.addData.observe(viewLifecycleOwner) {
|
||||
binding.todoList.adapter?.notifyItemInserted(todoList.size + 1)
|
||||
}
|
||||
|
||||
todoViewModel.removeData.observe(viewLifecycleOwner) {
|
||||
binding.todoList.adapter?.notifyItemRemoved(todoList.size + 1)
|
||||
}
|
||||
|
||||
todoViewModel.refreshData.observe(viewLifecycleOwner) {
|
||||
binding.todoList.adapter?.notifyItemRangeChanged(0, todoList.size + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments.welcome
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.constant.GlobalValues
|
||||
import cn.super12138.todo.databinding.FragmentWelcomeBinding
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.utils.setOnIntervalClickListener
|
||||
import cn.super12138.todo.views.BaseFragment
|
||||
import cn.super12138.todo.views.activities.MainActivity
|
||||
import cn.super12138.todo.views.fragments.MainFragment
|
||||
import cn.super12138.todo.views.fragments.welcome.pages.IntroPage
|
||||
import cn.super12138.todo.views.fragments.welcome.pages.ProgressPage
|
||||
import cn.super12138.todo.views.fragments.welcome.pages.ToDoBtnPage
|
||||
import cn.super12138.todo.views.fragments.welcome.pages.ToDoItemPage
|
||||
import cn.super12138.todo.views.viewmodels.WelcomeViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class WelcomeFragment : BaseFragment<FragmentWelcomeBinding>() {
|
||||
private val viewModel by viewModels<WelcomeViewModel>()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val previousBtn = binding.previousBtn
|
||||
val nextBtn = binding.nextBtn
|
||||
val centerBtn = binding.centerBtn
|
||||
|
||||
val currentPage = viewModel.currentPage // 0: Intro 1: Progress 2: ToDo Btn 3: ToDo Item
|
||||
|
||||
// 返回回调
|
||||
val callback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (currentPage.value == 0) {
|
||||
requireActivity().finishAffinity()
|
||||
} else {
|
||||
childFragmentManager.popBackStack()
|
||||
viewModel.decreasePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
centerBtn.setOnIntervalClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
centerBtn.hide()
|
||||
nextBtn.show()
|
||||
previousBtn.show()
|
||||
|
||||
if (currentPage.value == 3) {
|
||||
GlobalValues.welcomePage = true
|
||||
(requireActivity() as MainActivity).startFragment(MainFragment())
|
||||
} else {
|
||||
viewModel.setCurrentPage(1)
|
||||
nextPage(1)
|
||||
}
|
||||
}
|
||||
|
||||
previousBtn.setOnIntervalClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
childFragmentManager.popBackStack()
|
||||
|
||||
viewModel.decreasePage()
|
||||
}
|
||||
|
||||
nextBtn.setOnIntervalClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
|
||||
nextPage(currentPage.value + 1)
|
||||
|
||||
callback.isEnabled = false
|
||||
viewModel.increasePage()
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.currentPage.collect { page ->
|
||||
when (page) {
|
||||
0 -> {
|
||||
centerBtn.apply {
|
||||
text = getString(R.string.start)
|
||||
icon = AppCompatResources.getDrawable(
|
||||
requireContext(),
|
||||
R.drawable.ic_arrow_forward
|
||||
)
|
||||
show()
|
||||
}
|
||||
nextBtn.hide()
|
||||
previousBtn.hide()
|
||||
}
|
||||
|
||||
in 1..2 -> {
|
||||
centerBtn.hide()
|
||||
previousBtn.show()
|
||||
nextBtn.show()
|
||||
}
|
||||
|
||||
3 -> {
|
||||
centerBtn.apply {
|
||||
text = getString(R.string.enter_app)
|
||||
icon = AppCompatResources.getDrawable(
|
||||
requireContext(),
|
||||
R.drawable.ic_focus
|
||||
)
|
||||
show()
|
||||
}
|
||||
previousBtn.show()
|
||||
nextBtn.hide()
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (page > 3) viewModel.setCurrentPage(3)
|
||||
|
||||
if (page < 0) viewModel.setCurrentPage(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
|
||||
}
|
||||
|
||||
private fun getCurrentPage(pageIndex: Int): Fragment {
|
||||
return when (pageIndex) {
|
||||
0 -> IntroPage()
|
||||
1 -> ProgressPage()
|
||||
2 -> ToDoBtnPage()
|
||||
3 -> ToDoItemPage()
|
||||
else -> IntroPage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun nextPage(page: Int) {
|
||||
childFragmentManager.commit {
|
||||
addToBackStack(System.currentTimeMillis().toString())
|
||||
hide(childFragmentManager.fragments.last())
|
||||
add(R.id.welcome_page_container, getCurrentPage(page))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentWelcomeBinding {
|
||||
return FragmentWelcomeBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments.welcome.pages
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import cn.super12138.todo.databinding.FragmentWelcomeIntroBinding
|
||||
import cn.super12138.todo.views.BaseFragment
|
||||
|
||||
class IntroPage : BaseFragment<FragmentWelcomeIntroBinding>() {
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentWelcomeIntroBinding {
|
||||
return FragmentWelcomeIntroBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments.welcome.pages
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import cn.super12138.todo.databinding.FragmentWelcomeProgressBinding
|
||||
import cn.super12138.todo.views.BaseFragment
|
||||
|
||||
class ProgressPage : BaseFragment<FragmentWelcomeProgressBinding>() {
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentWelcomeProgressBinding {
|
||||
return FragmentWelcomeProgressBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments.welcome.pages
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import cn.super12138.todo.databinding.FragmentWelcomeTodoBtnBinding
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.views.BaseFragment
|
||||
|
||||
class ToDoBtnPage : BaseFragment<FragmentWelcomeTodoBtnBinding>() {
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentWelcomeTodoBtnBinding {
|
||||
return FragmentWelcomeTodoBtnBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
binding.addItem.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
}
|
||||
|
||||
binding.addItem.setOnLongClickListener {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package cn.super12138.todo.views.fragments.welcome.pages
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import cn.super12138.todo.databinding.FragmentWelcomeTodoItemBinding
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.views.BaseFragment
|
||||
|
||||
class ToDoItemPage : BaseFragment<FragmentWelcomeTodoItemBinding>() {
|
||||
override fun getViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
attachToRoot: Boolean
|
||||
): FragmentWelcomeTodoItemBinding {
|
||||
return FragmentWelcomeTodoItemBinding.inflate(inflater, container, attachToRoot)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.checkItemBtn.setOnClickListener {
|
||||
VibrationUtils.performHapticFeedback(it)
|
||||
}
|
||||
binding.todoItem.setOnLongClickListener {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package cn.super12138.todo.views.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cn.super12138.todo.logic.Repository
|
||||
import cn.super12138.todo.logic.model.ToDo
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AllTasksViewModel : ViewModel() {
|
||||
val todoListAll = ArrayList<ToDo>()
|
||||
val refreshData = MutableLiveData<Int>(0)
|
||||
|
||||
init {
|
||||
loadToDos()
|
||||
}
|
||||
|
||||
private fun loadToDos() {
|
||||
viewModelScope.launch {
|
||||
val todos = Repository.getAll()
|
||||
for (todo in todos) {
|
||||
todoListAll.add(ToDo(todo.uuid, todo.state, todo.content, todo.subject))
|
||||
}
|
||||
if (todoListAll.size > 0) {
|
||||
refreshData.value = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package cn.super12138.todo.views.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cn.super12138.todo.logic.Repository
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ProgressViewModel : ViewModel() {
|
||||
val totalCount: MutableLiveData<Int> = MutableLiveData()
|
||||
val completeCount: MutableLiveData<Int> = MutableLiveData()
|
||||
val remainCount: MutableLiveData<Int> = MutableLiveData()
|
||||
val progress: MutableLiveData<Int> = MutableLiveData()
|
||||
|
||||
fun updateProgress() {
|
||||
viewModelScope.launch {
|
||||
delay(30)
|
||||
val total = Repository.getAll().size
|
||||
val complete = Repository.getAllComplete().size
|
||||
|
||||
val calcProgress = (complete.toDouble() / total.toDouble()) * 100
|
||||
progress.postValue(calcProgress.toInt())
|
||||
completeCount.postValue(complete)
|
||||
totalCount.postValue(total)
|
||||
remainCount.postValue(total - complete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
package cn.super12138.todo.views.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cn.super12138.todo.logic.Repository
|
||||
import cn.super12138.todo.logic.dao.ToDoRoom
|
||||
import cn.super12138.todo.logic.model.ToDo
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ToDoViewModel : ViewModel() {
|
||||
val addData = MutableLiveData<Int>(0)
|
||||
val removeData = MutableLiveData<Int>(0)
|
||||
val refreshData = MutableLiveData<Int>(0)
|
||||
|
||||
val todoList = ArrayList<ToDo>()
|
||||
|
||||
init {
|
||||
loadTasks()
|
||||
}
|
||||
|
||||
private fun loadTasks() {
|
||||
viewModelScope.launch {
|
||||
val todos = Repository.getAllIncomplete()
|
||||
for (todo in todos) {
|
||||
todoList.add(ToDo(todo.uuid, todo.state, todo.content, todo.subject))
|
||||
}
|
||||
if (todoList.size > 0) {
|
||||
refreshData.value = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteTask(position: Int, uuid: String) {
|
||||
todoList.removeAt(position)
|
||||
viewModelScope.launch {
|
||||
Repository.deleteByUUID(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
fun insertTask(todo: ToDoRoom) {
|
||||
viewModelScope.launch {
|
||||
Repository.insert(todo)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTaskState(uuid: String) {
|
||||
viewModelScope.launch {
|
||||
Repository.updateStateByUUID(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTask(position: Int, todo: ToDoRoom) {
|
||||
todoList.removeAt(position)
|
||||
todoList.add(
|
||||
position,
|
||||
ToDo(
|
||||
todo.uuid,
|
||||
todo.state,
|
||||
todo.content,
|
||||
todo.subject
|
||||
)
|
||||
)
|
||||
viewModelScope.launch {
|
||||
Repository.update(todo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package cn.super12138.todo.views.viewmodels
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class WelcomeViewModel : ViewModel() {
|
||||
private val _currentPage = MutableStateFlow(0)
|
||||
val currentPage = _currentPage.asStateFlow()
|
||||
|
||||
fun setCurrentPage(page: Int) {
|
||||
_currentPage.value = page
|
||||
}
|
||||
|
||||
fun increasePage() {
|
||||
_currentPage.value += 1
|
||||
}
|
||||
|
||||
fun decreasePage() {
|
||||
_currentPage.value -= 1
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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" />
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
Loading…
Reference in a new issue