diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml new file mode 100644 index 0000000..c4aa63d --- /dev/null +++ b/.github/workflows/android_ci.yml @@ -0,0 +1,42 @@ +name: Android CI +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + - name: set up JDK 17 + uses: actions/setup-java@v4.0.0 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew assembleRelease + + - uses: ilharp/sign-android-release@nightly + name: Sign APK + id: sign_app + with: + releaseDir: app/build/outputs/apk/release + signingKey: ${{ secrets.ANDROID_SIGNING_KEY }} + keyAlias: ${{ secrets.ANDROID_KEY_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} + buildToolsVersion: 33.0.0 + + - name: Upload APK + uses: actions/upload-artifact@v4.0.0 + with: + name: Release + path: ${{steps.sign_app.outputs.signedFile}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 347e252..c4b6c3b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ google-services.json # Android Profiling *.hprof + +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index f17a84f..4f34b01 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# ToDo +# 待办 | ToDo 简单的待办应用,遵循Material Design 3 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..44cb119 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,72 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +val baseVersionName = "1.0.0" +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 = 34 + + defaultConfig { + applicationId = "cn.super12138.todo" + minSdk = 24 + targetSdk = 34 + versionCode = verCode + versionName = "${baseVersionName}-${commitHash}" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + + // AndroidX + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.fragment:fragment:1.6.2") + implementation("androidx.fragment:fragment-ktx:1.6.2") + implementation("androidx.recyclerview:recyclerview:1.3.2") + implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") + implementation("androidx.preference:preference:1.2.1") + implementation("androidx.preference:preference-ktx:1.2.1") + // Material Design + implementation("com.google.android.material:material:1.12.0-alpha02") + // Test + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} + +fun String.exec(): String = exec(this) + +@Suppress("UnstableApiUsage") +fun Project.exec(command: String): String = providers.exec { + commandLine(command.split(" ")) +}.standardOutput.asText.get().trim() \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..54a4fa5 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,20 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "cn.super12138.todo", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 1, + "versionName": "1.0.0", + "outputFile": "app-release.apk" + } + ], + "elementType": "File" +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/cn/super12138/todo/ExampleInstrumentedTest.kt b/app/src/androidTest/kotlin/cn/super12138/todo/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..1a9a74c --- /dev/null +++ b/app/src/androidTest/kotlin/cn/super12138/todo/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package cn.super12138.todo + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("cn.super12138.todo", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b9d28c8 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ToDoApplication.kt b/app/src/main/kotlin/cn/super12138/todo/ToDoApplication.kt new file mode 100644 index 0000000..1b03ab0 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ToDoApplication.kt @@ -0,0 +1,23 @@ +package cn.super12138.todo + +import android.annotation.SuppressLint +import android.app.Application +import android.content.Context +import cn.super12138.todo.views.crash.CrashHandler +import com.google.android.material.color.DynamicColors + +class ToDoApplication : Application() { + companion object { + @SuppressLint("StaticFieldLeak") + lateinit var context: Context + } + + override fun onCreate() { + super.onCreate() + DynamicColors.applyToActivitiesIfAvailable(this) + context = applicationContext + + val crashHandler = CrashHandler(this) + Thread.setDefaultUncaughtExceptionHandler(crashHandler) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/constant/Constants.kt b/app/src/main/kotlin/cn/super12138/todo/constant/Constants.kt new file mode 100644 index 0000000..ca57bd5 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/constant/Constants.kt @@ -0,0 +1,11 @@ +package cn.super12138.todo.constant + +object Constants { + const val DB_NAME = "ToDo.db" + const val DB_VERSION = 1 + const val TABLE_NAME = "ToDo" + + 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" +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt b/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt new file mode 100644 index 0000000..13bbd7c --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt @@ -0,0 +1,20 @@ +package cn.super12138.todo.logic + +import android.content.ContentValues +import android.content.Context +import cn.super12138.todo.logic.database.DBHelper +import cn.super12138.todo.logic.database.SPHelper + +object Repository { + fun getCompleteTotalCount() = DBHelper.getCompleteTotalCount() + + fun insertData(data: ContentValues) = DBHelper.insertData(data) + + fun deleteData(deleteAll: Boolean, uuid: String?) = DBHelper.deleteData(deleteAll, uuid) + + fun updateData(uuid: String, newData: ContentValues) = DBHelper.updateData(uuid, newData) + + fun getAllData() = DBHelper.getAllData() + + fun getPreferenceString(context: Context, key: String, defaultValue: String) = SPHelper.getPreferenceString(context, key, defaultValue) +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/database/DBHelper.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/DBHelper.kt new file mode 100644 index 0000000..569d9be --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/DBHelper.kt @@ -0,0 +1,109 @@ +package cn.super12138.todo.logic.database + +import android.annotation.SuppressLint +import android.content.ContentValues +import cn.super12138.todo.ToDoApplication +import cn.super12138.todo.constant.Constants +import cn.super12138.todo.logic.model.Progress +import cn.super12138.todo.logic.model.ToDo +import cn.super12138.todo.logic.model.ToDoDatabase + +object DBHelper { + + fun insertData(data: ContentValues) { + ToDoDatabase( + ToDoApplication.context, + Constants.DB_NAME, + Constants.DB_VERSION + ).writableDatabase.use {db -> + db.insert(Constants.TABLE_NAME, null, data) + } + } + + fun deleteData(delAll: Boolean, uuid: String?) { + ToDoDatabase( + ToDoApplication.context, + Constants.DB_NAME, + Constants.DB_VERSION + ).writableDatabase.use { db -> + if (delAll) { + db.delete(Constants.TABLE_NAME, null, null) + } else { + if (uuid == null) { + throw RuntimeException("uuid cannot be null") + } + db.delete(Constants.TABLE_NAME, "uuid = ?", arrayOf(uuid)) + } + } + } + + fun updateData(uuid: String, newData: ContentValues) { + val dbHelper = ToDoDatabase( + ToDoApplication.context, + Constants.DB_NAME, + Constants.DB_VERSION + ).writableDatabase + dbHelper.update(Constants.TABLE_NAME, newData, "uuid = ?", arrayOf(uuid)) + } + + @SuppressLint("Range") + fun getCompleteTotalCount(): Progress { + var total = 0 + var complete = 0 + + ToDoDatabase( + ToDoApplication.context, + Constants.DB_NAME, + Constants.DB_VERSION + ).writableDatabase.use { db -> + db.query(Constants.TABLE_NAME, null, null, null, null, null, null, null).use { cursor -> + if (cursor.moveToFirst()) { + do { + val state = cursor.getString(cursor.getColumnIndex("state")) + if (state.toInt() == 1) { + complete += 1 + } + total += 1 + } while (cursor.moveToNext()) + cursor.close() + } + } + } + + return Progress(complete, total) + } + + @SuppressLint("Range") + fun getAllData() : MutableList { + val todoList = mutableListOf() + ToDoDatabase( + ToDoApplication.context, + Constants.DB_NAME, + Constants.DB_VERSION + ).writableDatabase.query( + Constants.TABLE_NAME, + null, + null, + null, + null, + null, + null, + null + ).use { cursor -> + if (cursor.moveToFirst()) { + do { + val uuid = cursor.getString(cursor.getColumnIndex("uuid")) + val subject = cursor.getString(cursor.getColumnIndex("subject")) + val state = cursor.getString(cursor.getColumnIndex("state")) + val todoContext = cursor.getString(cursor.getColumnIndex("context")) + + if (state.toInt() != 1) { + todoList.add(ToDo(uuid, todoContext, subject)) + } + } while (cursor.moveToNext()) + cursor.close() + } + } + return todoList + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/database/SPHelper.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/SPHelper.kt new file mode 100644 index 0000000..f3aab33 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/SPHelper.kt @@ -0,0 +1,11 @@ +package cn.super12138.todo.logic.database + +import android.content.Context +import androidx.preference.PreferenceManager + +object SPHelper { + fun getPreferenceString(context: Context, name: String, defaultValue: String): String? { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context /* Activity context */) + return sharedPreferences.getString(name, defaultValue) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/Progress.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/Progress.kt new file mode 100644 index 0000000..897432c --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/Progress.kt @@ -0,0 +1,3 @@ +package cn.super12138.todo.logic.model + +data class Progress(val complete: Int, val total: Int) diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDo.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDo.kt new file mode 100644 index 0000000..b670c7b --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDo.kt @@ -0,0 +1,19 @@ +package cn.super12138.todo.logic.model + +data class ToDo(val uuid: String, val context: String, val subject: String) + +/* +package cn.super12138.todo.logic.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "todo") +data class ToDo ( + @PrimaryKey @ColumnInfo(name = "uuid") val uuid: String, + @ColumnInfo(name = "state") val state: Int, + @ColumnInfo(name = "subject") val subject: String, + @ColumnInfo(name = "context") val context: String +) + */ \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDoDatabase.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDoDatabase.kt new file mode 100644 index 0000000..8e9d0d3 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDoDatabase.kt @@ -0,0 +1,28 @@ +package cn.super12138.todo.logic.model + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper + +class ToDoDatabase(val context: Context, name: String, version: Int) : + SQLiteOpenHelper(context, name, null, version) { + /* + * @param uuid: String 待办的uuid + * @param state: Int 待办的完成状态,0表示未完成,1表示完成 + * @param subject: String 待办的学科 + * @param context: String 待办的内容 + */ + private val createToDo = "create table ToDo (" + + "uuid text primary key," + + "state integer," + + "subject text," + + "context text)" + + override fun onCreate(db: SQLiteDatabase) { + db.execSQL(createToDo) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + + } +} diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/Toast.kt b/app/src/main/kotlin/cn/super12138/todo/utils/Toast.kt new file mode 100644 index 0000000..04d9583 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/utils/Toast.kt @@ -0,0 +1,14 @@ +import android.content.Context +import android.widget.Toast +import cn.super12138.todo.ToDoApplication + +fun String.showToast( + context: Context = ToDoApplication.context, + duration: Int = Toast.LENGTH_SHORT +) { + Toast.makeText(context, this, duration).show() +} + +fun Int.showToast(context: Context = ToDoApplication.context, duration: Int = Toast.LENGTH_SHORT) { + Toast.makeText(context, this, duration).show() +} diff --git a/app/src/main/kotlin/cn/super12138/todo/views/about/AboutActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/about/AboutActivity.kt new file mode 100644 index 0000000..230a9f9 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/views/about/AboutActivity.kt @@ -0,0 +1,77 @@ +package cn.super12138.todo.views.about + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.view.WindowManager +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import cn.super12138.todo.constant.Constants +import cn.super12138.todo.databinding.ActivityAboutBinding +import showToast + +class AboutActivity : AppCompatActivity() { + private lateinit var binding: ActivityAboutBinding + private var clickCount = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + // 适配刘海屏 + 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 + + binding = ActivityAboutBinding.inflate(layoutInflater) + setContentView(binding.root) + + val pkgInfo = packageManager.getPackageInfo(packageName, 0) + val verName = pkgInfo.versionName + val verCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + pkgInfo.longVersionCode.toInt() + } else { + pkgInfo.versionCode + } + binding.appVersion.text = "$verName($verCode)" + + binding.toolbar.setNavigationOnClickListener { + finish() + } + + binding.checkUpdate.setOnClickListener { + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(Constants.UPDATE_URL) + } + startActivity(intent) + } + + binding.openSource.setOnClickListener { + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(Constants.REPO_GITHUB_URL) + } + startActivity(intent) + } + + + binding.developerInfo.setOnClickListener { + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(Constants.AUTHOR_GITHUB_URL) + } + startActivity(intent) + } + + binding.appVersion.setOnClickListener { + clickCount++ + + if (clickCount == 5) { + clickCount = 0 + "🧧".showToast() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/crash/CrashActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/crash/CrashActivity.kt new file mode 100644 index 0000000..1094b5d --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/views/crash/CrashActivity.kt @@ -0,0 +1,50 @@ +package cn.super12138.todo.views.crash + +import android.os.Build +import android.os.Bundle +import android.view.ViewGroup +import android.view.WindowManager +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import cn.super12138.todo.databinding.ActivityCrashBinding + +class CrashActivity : AppCompatActivity() { + private lateinit var binding: ActivityCrashBinding + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + // 适配刘海屏 + 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 + + binding = ActivityCrashBinding.inflate(layoutInflater) + setContentView(binding.root) + + ViewCompat.setOnApplyWindowInsetsListener(binding.exitApp) { view, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.updateLayoutParams { + leftMargin = insets.left + bottomMargin = insets.bottom + rightMargin = insets.right + } + + WindowInsetsCompat.CONSUMED + } + + val crashLogs = intent.getStringExtra("crash_logs") + binding.crashLog.text = crashLogs + + binding.exitApp.setOnClickListener { + this.finishAffinity() + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/crash/CrashHandler.kt b/app/src/main/kotlin/cn/super12138/todo/views/crash/CrashHandler.kt new file mode 100644 index 0000000..fa92578 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/views/crash/CrashHandler.kt @@ -0,0 +1,28 @@ +package cn.super12138.todo.views.crash + +import android.content.Context +import android.content.Intent +import android.os.Process + +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) + + // 杀掉崩溃的应用程序进程 + Process.killProcess(Process.myPid()) + System.exit(10) + + // 传递异常给默认的异常处理器 + defaultUEH?.uncaughtException(thread, ex) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/main/MainActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/main/MainActivity.kt new file mode 100644 index 0000000..0d0af3f --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/views/main/MainActivity.kt @@ -0,0 +1,76 @@ +package cn.super12138.todo.views.main +// 2023.11.18立项 +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.WindowManager +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.PreferenceManager +import cn.super12138.todo.R +import cn.super12138.todo.ToDoApplication +import cn.super12138.todo.databinding.ActivityMainBinding +import cn.super12138.todo.logic.Repository +import cn.super12138.todo.views.settings.SettingsActivity + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + // 适配刘海屏 + 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 + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + /*val pref = getSharedPreferences("data",Context.MODE_PRIVATE) + val isFirstUse = pref.getBoolean("first_use", false) + + if (!isFirstUse) { + MaterialAlertDialogBuilder(this) + .setTitle("欢迎使用待办") + .setMessage("") + .setPositiveButton("确定") { dialog, which -> + val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit() + editor.putBoolean("first_use", true) + editor.apply() + } + .show() + } +*/ + val isDarkMode = Repository.getPreferenceString(this, "dark_mode", "0") + when (isDarkMode) { + "0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + + "1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + + "2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.item_settings -> { + val intent = Intent(ToDoApplication.context, SettingsActivity::class.java) + startActivity(intent) + } + } + return true + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/progress/ProgressFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/progress/ProgressFragment.kt new file mode 100644 index 0000000..1668d48 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/views/progress/ProgressFragment.kt @@ -0,0 +1,45 @@ +package cn.super12138.todo.views.progress + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import cn.super12138.todo.databinding.FragmentProgressBinding + + +class ProgressFragment : Fragment() { + private lateinit var viewModel: ProgressFragmentViewModel + private lateinit var binding: FragmentProgressBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentProgressBinding.inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel = ViewModelProvider(requireActivity()).get(ProgressFragmentViewModel::class.java) + + viewModel.progress.observe(viewLifecycleOwner, Observer { value -> + binding.progressBar.setProgressCompat(value, true) + }) + + viewModel.totalCount.observe(viewLifecycleOwner, Observer { total -> + binding.totalTextView.text = total.toString() + }) + + viewModel.completeCount.observe(viewLifecycleOwner, Observer { complete -> + binding.completeTextView.text = complete.toString() + }) + + viewModel.updateProgress() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/progress/ProgressFragmentViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/progress/ProgressFragmentViewModel.kt new file mode 100644 index 0000000..8042096 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/views/progress/ProgressFragmentViewModel.kt @@ -0,0 +1,22 @@ +package cn.super12138.todo.views.progress + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import cn.super12138.todo.logic.Repository + +class ProgressFragmentViewModel : ViewModel() { + val totalCount: MutableLiveData = MutableLiveData() + val completeCount: MutableLiveData = MutableLiveData() + val progress: MutableLiveData = MutableLiveData() + + fun updateProgress() { + val progressData = Repository.getCompleteTotalCount() + val total = progressData.total + val complete = progressData.complete + val calcProgress = (complete.toDouble() / total.toDouble()) * 100 + + progress.value = calcProgress.toInt() + totalCount.value = total + completeCount.value = complete + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/settings/SettingsActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/settings/SettingsActivity.kt new file mode 100644 index 0000000..679a967 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/views/settings/SettingsActivity.kt @@ -0,0 +1,62 @@ +package cn.super12138.todo.views.settings + +import android.os.Build +import android.os.Bundle +import android.view.WindowManager +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.ListPreference +import androidx.preference.PreferenceFragmentCompat +import cn.super12138.todo.R +import cn.super12138.todo.databinding.ActivitySettingsBinding + +class SettingsActivity : AppCompatActivity() { + private lateinit var binding: ActivitySettingsBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // 适配刘海屏 + 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 + + binding = ActivitySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) + + if (savedInstanceState == null) { + supportFragmentManager + .beginTransaction() + .replace(R.id.settings_frame, SettingsFragment()) + .commit() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + binding.toolbar.setNavigationOnClickListener { + finish() + } + } + + + class SettingsFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.preferences, rootKey) + + findPreference("dark_mode")?.apply { + setOnPreferenceChangeListener { preference, 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 + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/todo/ToDoAdapter.kt b/app/src/main/kotlin/cn/super12138/todo/views/todo/ToDoAdapter.kt new file mode 100644 index 0000000..48cdd5b --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/views/todo/ToDoAdapter.kt @@ -0,0 +1,118 @@ +package cn.super12138.todo.views.todo + +import android.content.ContentValues +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import androidx.recyclerview.widget.RecyclerView +import cn.super12138.todo.R +import cn.super12138.todo.logic.Repository +import cn.super12138.todo.logic.model.ToDo +import cn.super12138.todo.views.progress.ProgressFragmentViewModel +import com.google.android.material.snackbar.Snackbar + +class ToDoAdapter(val todoList: MutableList, val viewModelStoreOwner: ViewModelStoreOwner) : + RecyclerView.Adapter() { + + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val todoContext: TextView = view.findViewById(R.id.todo_context) + val todoSubject: TextView = view.findViewById(R.id.todo_subject) + val checkToDoBtn: Button = view.findViewById(R.id.check_item_btn) + val delToDoBtn: Button = view.findViewById(R.id.delete_item_btn) + } + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val progressViewModel = + ViewModelProvider(viewModelStoreOwner).get(ProgressFragmentViewModel::class.java) + val todoViewModel = + ViewModelProvider(viewModelStoreOwner).get(ToDoFragmentViewModel::class.java) + + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.todo_item, parent, false) + val holder = ViewHolder(view) + + holder.checkToDoBtn.setOnClickListener { + val position = holder.absoluteAdapterPosition + if (position < 0 || position >= todoList.size) { + return@setOnClickListener + } + val todo = todoList[position] + + todoList.removeAt(position) + notifyItemRemoved(position) + notifyItemRangeChanged(position, todoList.size) + + val newState = ContentValues().apply { + put("state", true) + } + Repository.updateData(todo.uuid, newState) + + progressViewModel.updateProgress() + + // 设置空项目提示可见性 + if (todoList.isEmpty()) { + todoViewModel.emptyTipVis.value = View.VISIBLE + } else { + todoViewModel.emptyTipVis.value = View.GONE + } + } + + holder.delToDoBtn.setOnClickListener { + val position = holder.absoluteAdapterPosition + if (position < 0 || position >= todoList.size) { + return@setOnClickListener + } + val todo = todoList[position] + + todoList.removeAt(position) + notifyItemRemoved(position) + notifyItemRangeChanged(position, todoList.size) + + val tempTaskInfo = ContentValues().apply { + put("uuid", todo.uuid) + put("state", false) + put("subject", todo.subject) + put("context", todo.context) + } + + Repository.deleteData(false, todo.uuid) + + progressViewModel.updateProgress() + + // 设置空项目提示可见性 + if (todoList.isEmpty()) { + todoViewModel.emptyTipVis.value = View.VISIBLE + } else { + todoViewModel.emptyTipVis.value = View.GONE + } + + Snackbar.make(it, R.string.task_deleted, Snackbar.LENGTH_LONG) + .setAction(R.string.delete_undo) { + if (todoList.size + 1 > 0) { + todoViewModel.emptyTipVis.value = View.GONE + } + todoList.add(ToDo(todo.uuid, todo.context, todo.subject)) + + todoViewModel.refreshData.value = 1 + Repository.insertData(tempTaskInfo) + progressViewModel.updateProgress() + } + .show() + } + return holder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val todo = todoList[position] + holder.todoContext.text = todo.context + holder.todoSubject.text = todo.subject + + } + + override fun getItemCount() = todoList.size +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/todo/ToDoFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/todo/ToDoFragment.kt new file mode 100644 index 0000000..7358376 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/views/todo/ToDoFragment.kt @@ -0,0 +1,168 @@ +package cn.super12138.todo.views.todo + +import android.content.ContentValues +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import cn.super12138.todo.R +import cn.super12138.todo.ToDoApplication +import cn.super12138.todo.databinding.DialogAddTodoBinding +import cn.super12138.todo.databinding.FragmentTodoBinding +import cn.super12138.todo.logic.Repository +import cn.super12138.todo.logic.model.ToDo +import cn.super12138.todo.views.progress.ProgressFragmentViewModel +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.util.UUID + +class ToDoFragment : Fragment() { + + private lateinit var binding: FragmentTodoBinding + private lateinit var ToDoDialogBinding: DialogAddTodoBinding + private val todoList = ArrayList() + + 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) + val todos = Repository.getAllData() + for (todo in todos) { + todoList.add(ToDo(todo.uuid, todo.context, todo.subject)) + } + + /*ViewCompat.setOnApplyWindowInsetsListener(binding.todoList) { view, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.updateLayoutParams { + leftMargin = insets.left + bottomMargin = insets.bottom + rightMargin = insets.right + } + + WindowInsetsCompat.CONSUMED + }*/ + + val layoutManager = LinearLayoutManager(ToDoApplication.context) + binding.todoList.layoutManager = layoutManager + val adapter = ToDoAdapter(todoList, requireActivity()) + binding.todoList.adapter = adapter + + val progressViewModel = + ViewModelProvider(requireActivity()).get(ProgressFragmentViewModel::class.java) + val todoViewModel = + ViewModelProvider(requireActivity()).get(ToDoFragmentViewModel::class.java) + + + if (todoList.size == 0) { + todoViewModel.emptyTipVis.value = View.VISIBLE + } + binding.addItem.setOnClickListener { + ToDoDialogBinding = DialogAddTodoBinding.inflate(layoutInflater) + + activity?.let { it1 -> + MaterialAlertDialogBuilder(it1) + .setTitle(R.string.add_task) + .setView(ToDoDialogBinding.root) + .setPositiveButton(R.string.ok) { dialog, which -> + val randomUUID = UUID.randomUUID().toString() + val todoContext = ToDoDialogBinding.todoContext.editText?.text.toString() + val todoSubject = when (ToDoDialogBinding.todoSubject.checkedChipId) { + R.id.subject_chinese -> "语文" + R.id.subject_math -> "数学" + R.id.subject_english -> "英语" + R.id.subject_biology -> "生物" + R.id.subject_geography -> "地理" + R.id.subject_history -> "历史" + R.id.subject_physics -> "物理" + R.id.subject_law -> "道法" + else -> "未知" + } + + // 显示RecyclerView + if (todoList.size + 1 > 0) { + todoViewModel.emptyTipVis.value = View.GONE + } + + // 添加到RecyclerView + todoList.add( + ToDo(randomUUID, todoContext, todoSubject) + ) + + // 添加到数据库 + val todoData = ContentValues().apply { + put("uuid", randomUUID) + put("state", 0) + put("subject", todoSubject) + put("context", todoContext) + } + Repository.insertData(todoData) + // dbHelper.writableDatabase.insert("ToDo", null, todoData) + + binding.todoList.adapter?.notifyItemInserted(todoList.size + 1) + + progressViewModel.updateProgress() + } + .setNegativeButton(R.string.cancel, null) + .show() + } + } + + binding.addItem.setOnLongClickListener { + activity?.let { it1 -> + MaterialAlertDialogBuilder(it1) + .setTitle(R.string.warning) + .setMessage(R.string.delete_confirm) + .setPositiveButton(R.string.ok) { dialog, which -> + todoList.clear() + Repository.deleteData(true, null) + // dbHelper.writableDatabase.delete("ToDo", null, null) + binding.todoList.adapter?.notifyItemRangeRemoved(0, todoList.size + 1) + + progressViewModel.updateProgress() + + todoViewModel.emptyTipVis.value = View.VISIBLE + } + .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) { + if (dy > 0 && fab.isShown) { + fab.hide() + } + if (dy < 0 && !fab.isShown) { + fab.show() + } + } + }) + + todoViewModel.emptyTipVis.observe(viewLifecycleOwner, Observer { visibility -> + if (visibility == View.VISIBLE) { + binding.todoList.visibility = View.GONE + binding.emptyTip.visibility = View.VISIBLE + } else { + binding.todoList.visibility = View.VISIBLE + binding.emptyTip.visibility = View.GONE + } + }) + todoViewModel.refreshData.observe(viewLifecycleOwner, Observer { + binding.todoList.adapter?.notifyItemInserted(todoList.size + 1) + }) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/todo/ToDoFragmentViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/todo/ToDoFragmentViewModel.kt new file mode 100644 index 0000000..1902b67 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/views/todo/ToDoFragmentViewModel.kt @@ -0,0 +1,10 @@ +package cn.super12138.todo.views.todo + +import android.view.View +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class ToDoFragmentViewModel : ViewModel() { + val emptyTipVis = MutableLiveData(View.GONE) + val refreshData = MutableLiveData(0) +} \ No newline at end of file diff --git a/app/src/main/play_store_512.png b/app/src/main/play_store_512.png new file mode 100644 index 0000000..a1a9f37 Binary files /dev/null and b/app/src/main/play_store_512.png differ diff --git a/app/src/main/res/drawable/add_24.xml b/app/src/main/res/drawable/add_24.xml new file mode 100644 index 0000000..a9503fd --- /dev/null +++ b/app/src/main/res/drawable/add_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/arrow_back_24.xml b/app/src/main/res/drawable/arrow_back_24.xml new file mode 100644 index 0000000..cd06f30 --- /dev/null +++ b/app/src/main/res/drawable/arrow_back_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/check_24.xml b/app/src/main/res/drawable/check_24.xml new file mode 100644 index 0000000..5623ef0 --- /dev/null +++ b/app/src/main/res/drawable/check_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/delete_24.xml b/app/src/main/res/drawable/delete_24.xml new file mode 100644 index 0000000..288869d --- /dev/null +++ b/app/src/main/res/drawable/delete_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/delete_forever_24.xml b/app/src/main/res/drawable/delete_forever_24.xml new file mode 100644 index 0000000..5615a26 --- /dev/null +++ b/app/src/main/res/drawable/delete_forever_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/exit_to_app_24.xml b/app/src/main/res/drawable/exit_to_app_24.xml new file mode 100644 index 0000000..348fab8 --- /dev/null +++ b/app/src/main/res/drawable/exit_to_app_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/github_24.xml b/app/src/main/res/drawable/github_24.xml new file mode 100644 index 0000000..f32ae57 --- /dev/null +++ b/app/src/main/res/drawable/github_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000..6e4f78d --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/person_24.xml b/app/src/main/res/drawable/person_24.xml new file mode 100644 index 0000000..74ba415 --- /dev/null +++ b/app/src/main/res/drawable/person_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/settings_24.xml b/app/src/main/res/drawable/settings_24.xml new file mode 100644 index 0000000..a7c7678 --- /dev/null +++ b/app/src/main/res/drawable/settings_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/update_24.xml b/app/src/main/res/drawable/update_24.xml new file mode 100644 index 0000000..14c24e7 --- /dev/null +++ b/app/src/main/res/drawable/update_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 0000000..d5ea165 --- /dev/null +++ b/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..a15f87b --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_crash.xml b/app/src/main/res/layout/activity_crash.xml new file mode 100644 index 0000000..7edd0ee --- /dev/null +++ b/app/src/main/res/layout/activity_crash.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b319978 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..0a79d29 --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_add_todo.xml b/app/src/main/res/layout/dialog_add_todo.xml new file mode 100644 index 0000000..5e35bb1 --- /dev/null +++ b/app/src/main/res/layout/dialog_add_todo.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_progress.xml b/app/src/main/res/layout/fragment_progress.xml new file mode 100644 index 0000000..64e65cb --- /dev/null +++ b/app/src/main/res/layout/fragment_progress.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_todo.xml b/app/src/main/res/layout/fragment_todo.xml new file mode 100644 index 0000000..4f9a32d --- /dev/null +++ b/app/src/main/res/layout/fragment_todo.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/todo_item.xml b/app/src/main/res/layout/todo_item.xml new file mode 100644 index 0000000..6767100 --- /dev/null +++ b/app/src/main/res/layout/todo_item.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + +