From 2d7b3edf15af504e25f3c050219d8c1e77f67922 Mon Sep 17 00:00:00 2001 From: Super12138 <70494801+Super12138@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:50:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E5=A7=8B=E4=BD=BF=E7=94=A8=20Jetpack?= =?UTF-8?q?=20Compose=20=E5=AF=B9=E5=BE=85=E5=8A=9E=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/.gitignore | 3 +- app/build.gradle.kts | 91 +- app/proguard-rules.pro | 4 +- .../1.json | 52 -- .../todo/ExampleInstrumentedTest.kt | 6 +- app/src/main/AndroidManifest.xml | 20 +- .../kotlin/cn/super12138/todo/MainActivity.kt | 47 + .../main/kotlin/cn/super12138/todo/ToDoApp.kt | 29 - .../cn/super12138/todo/constant/Constants.kt | 38 - .../super12138/todo/constant/GlobalValues.kt | 12 - .../cn/super12138/todo/logic/Repository.kt | 87 -- .../cn/super12138/todo/logic/dao/ToDoRoom.kt | 19 - .../super12138/todo/logic/dao/ToDoRoomDB.kt | 28 - .../super12138/todo/logic/dao/ToDoRoomDao.kt | 33 - .../cn/super12138/todo/logic/model/ToDo.kt | 5 - .../cn/super12138/todo/ui/theme/Color.kt | 11 + .../cn/super12138/todo/ui/theme/Theme.kt | 58 ++ .../cn/super12138/todo/ui/theme/Type.kt | 34 + .../cn/super12138/todo/utils/TextUtils.kt | 49 - .../kotlin/cn/super12138/todo/utils/Utils.kt | 36 - .../cn/super12138/todo/utils/VersionUtils.kt | 21 - .../super12138/todo/utils/VibrationUtils.kt | 16 - .../super12138/todo/utils/sp/SPDelegates.kt | 14 - .../cn/super12138/todo/utils/sp/SPUtils.kt | 36 - .../cn/super12138/todo/views/BaseActivity.kt | 45 - .../cn/super12138/todo/views/BaseFragment.kt | 50 - .../todo/views/activities/CrashActivity.kt | 51 -- .../todo/views/activities/CrashHandler.kt | 29 - .../todo/views/activities/MainActivity.kt | 66 -- .../todo/views/adapters/AllTasksAdapter.kt | 85 -- .../todo/views/adapters/ToDoAdapter.kt | 108 --- .../todo/views/fragments/AboutFragment.kt | 85 -- .../todo/views/fragments/AllTasksFragment.kt | 53 -- .../todo/views/fragments/InfoBottomSheet.kt | 81 -- .../todo/views/fragments/MainFragment.kt | 41 - .../todo/views/fragments/ProgressFragment.kt | 52 -- .../todo/views/fragments/SettingsFragment.kt | 195 ---- .../views/fragments/SettingsParentFragment.kt | 29 - .../todo/views/fragments/ToDoBottomSheet.kt | 192 ---- .../todo/views/fragments/ToDoFragment.kt | 122 --- .../fragments/welcome/WelcomeFragment.kt | 161 ---- .../fragments/welcome/pages/IntroPage.kt | 16 - .../fragments/welcome/pages/ProgressPage.kt | 16 - .../fragments/welcome/pages/ToDoBtnPage.kt | 32 - .../fragments/welcome/pages/ToDoItemPage.kt | 29 - .../views/viewmodels/AllTasksViewModel.kt | 29 - .../views/viewmodels/ProgressViewModel.kt | 29 - .../todo/views/viewmodels/ToDoViewModel.kt | 68 -- .../todo/views/viewmodels/WelcomeViewModel.kt | 22 - .../core/AESEncryptionHelper.kt | 129 --- .../core/AESEncryptionManager.kt | 114 --- .../core/OnCompleteListener.kt | 78 -- .../roomdatabasebackup/core/RoomBackup.kt | 853 ------------------ app/src/main/play_store_512.png | Bin 52233 -> 0 bytes .../main/res/drawable/bg_item_complete.xml | 4 - app/src/main/res/drawable/ic_about.xml | 10 - app/src/main/res/drawable/ic_add.xml | 10 - app/src/main/res/drawable/ic_all_tasks.xml | 11 - app/src/main/res/drawable/ic_arrow_back.xml | 11 - .../main/res/drawable/ic_arrow_forward.xml | 13 - app/src/main/res/drawable/ic_backup.xml | 12 - app/src/main/res/drawable/ic_check.xml | 10 - app/src/main/res/drawable/ic_dark_mode.xml | 12 - app/src/main/res/drawable/ic_exit.xml | 13 - app/src/main/res/drawable/ic_focus.xml | 12 - app/src/main/res/drawable/ic_github.xml | 10 - app/src/main/res/drawable/ic_launcher.xml | 9 - .../res/drawable/ic_launcher_background.xml | 170 ++++ .../res/drawable/ic_launcher_foreground.xml | 30 + app/src/main/res/drawable/ic_next.xml | 13 - app/src/main/res/drawable/ic_person.xml | 12 - app/src/main/res/drawable/ic_previous.xml | 13 - app/src/main/res/drawable/ic_restore.xml | 16 - app/src/main/res/drawable/ic_screenshot.xml | 12 - app/src/main/res/drawable/ic_settings.xml | 12 - app/src/main/res/drawable/ic_update.xml | 10 - app/src/main/res/drawable/ic_vibration.xml | 12 - app/src/main/res/drawable/splash_screen.xml | 7 - .../main/res/layout-land/fragment_main.xml | 48 - .../main/res/layout-small/fragment_main.xml | 51 -- app/src/main/res/layout/activity_crash.xml | 79 -- app/src/main/res/layout/activity_main.xml | 17 - app/src/main/res/layout/bottom_sheet_info.xml | 60 -- app/src/main/res/layout/bottom_sheet_todo.xml | 194 ---- app/src/main/res/layout/fragment_about.xml | 170 ---- .../main/res/layout/fragment_all_tasks.xml | 37 - app/src/main/res/layout/fragment_main.xml | 50 - app/src/main/res/layout/fragment_progress.xml | 79 -- app/src/main/res/layout/fragment_settings.xml | 36 - app/src/main/res/layout/fragment_todo.xml | 22 - app/src/main/res/layout/fragment_welcome.xml | 58 -- .../res/layout/fragment_welcome_intro.xml | 46 - .../res/layout/fragment_welcome_progress.xml | 38 - .../res/layout/fragment_welcome_todo_btn.xml | 41 - .../res/layout/fragment_welcome_todo_item.xml | 84 -- app/src/main/res/layout/item_empty.xml | 17 - app/src/main/res/layout/item_todo.xml | 61 -- .../main/res/layout/preference_switch_m3.xml | 8 - app/src/main/res/menu/main.xml | 10 - .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 +- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 4797 -> 0 bytes app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../mipmap-hdpi/ic_launcher_background.png | Bin 854 -> 0 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 8594 -> 0 bytes .../mipmap-hdpi/ic_launcher_monochrome.png | Bin 3739 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 6838 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../ic_launcher_round_background.png | Bin 854 -> 0 bytes .../ic_launcher_round_foreground.png | Bin 8594 -> 0 bytes .../ic_launcher_round_monochrome.png | Bin 3932 -> 0 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2630 -> 0 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../mipmap-mdpi/ic_launcher_background.png | Bin 462 -> 0 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 5244 -> 0 bytes .../mipmap-mdpi/ic_launcher_monochrome.png | Bin 2300 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 3883 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../ic_launcher_round_background.png | Bin 462 -> 0 bytes .../ic_launcher_round_foreground.png | Bin 5244 -> 0 bytes .../ic_launcher_round_monochrome.png | Bin 2349 -> 0 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 6719 -> 0 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../mipmap-xhdpi/ic_launcher_background.png | Bin 1319 -> 0 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 12154 -> 0 bytes .../mipmap-xhdpi/ic_launcher_monochrome.png | Bin 5245 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 9947 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../ic_launcher_round_background.png | Bin 1319 -> 0 bytes .../ic_launcher_round_foreground.png | Bin 12154 -> 0 bytes .../ic_launcher_round_monochrome.png | Bin 5580 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 11578 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../mipmap-xxhdpi/ic_launcher_background.png | Bin 2951 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 20875 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_monochrome.png | Bin 9390 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 17116 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../ic_launcher_round_background.png | Bin 2951 -> 0 bytes .../ic_launcher_round_foreground.png | Bin 20875 -> 0 bytes .../ic_launcher_round_monochrome.png | Bin 9780 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 17670 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../mipmap-xxxhdpi/ic_launcher_background.png | Bin 4234 -> 0 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 30428 -> 0 bytes .../mipmap-xxxhdpi/ic_launcher_monochrome.png | Bin 13370 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 25156 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../ic_launcher_round_background.png | Bin 4234 -> 0 bytes .../ic_launcher_round_foreground.png | Bin 30428 -> 0 bytes .../ic_launcher_round_monochrome.png | Bin 13873 -> 0 bytes app/src/main/res/values-night/colors.xml | 194 ---- .../main/res/values-night/theme_overlays.xml | 129 --- app/src/main/res/values-night/themes.xml | 108 --- app/src/main/res/values-v29/themes.xml | 10 - app/src/main/res/values-zh-rCN/arrays.xml | 22 - app/src/main/res/values-zh-rCN/strings.xml | 89 -- app/src/main/res/values/arrays.xml | 22 - app/src/main/res/values/colors.xml | 197 +--- app/src/main/res/values/strings.xml | 88 +- app/src/main/res/values/theme_overlays.xml | 129 --- app/src/main/res/values/themes.xml | 146 +-- app/src/main/res/xml/preferences.xml | 64 -- .../cn/super12138/todo/ExampleUnitTest.kt | 3 +- build.gradle.kts | 3 +- fastscroll/.gitignore | 1 - fastscroll/build.gradle.kts | 42 - fastscroll/consumer-rules.pro | 0 fastscroll/proguard-rules.pro | 21 - fastscroll/src/main/AndroidManifest.xml | 19 - .../fastscroll/AutoMirrorDrawable.java | 74 -- .../fastscroll/DefaultAnimationHelper.java | 141 --- .../FastScrollNestedScrollView.java | 135 --- .../fastscroll/FastScrollScrollView.java | 142 --- .../android/fastscroll/FastScrollWebView.java | 137 --- .../android/fastscroll/FastScroller.java | 465 ---------- .../fastscroll/FastScrollerBuilder.java | 188 ---- .../FixItemDecorationRecyclerView.java | 133 --- .../FixOnItemTouchListenerRecyclerView.java | 143 --- .../fastscroll/Md2PopupBackground.java | 152 ---- .../android/fastscroll/PopupStyles.java | 80 -- .../android/fastscroll/PopupTextProvider.java | 27 - .../android/fastscroll/Predicate.java | 23 - .../fastscroll/RecyclerViewHelper.java | 224 ----- .../android/fastscroll/SimpleViewHelper.java | 153 ---- .../me/zhanghai/android/fastscroll/Utils.java | 72 -- .../fastscroll/ViewHelperProvider.java | 14 - .../src/main/res/drawable/afs_md2_thumb.xml | 38 - .../src/main/res/drawable/afs_md2_track.xml | 32 - .../res/drawable/afs_popup_background.xml | 41 - .../src/main/res/drawable/afs_thumb.xml | 32 - .../main/res/drawable/afs_thumb_stateful.xml | 39 - .../src/main/res/drawable/afs_track.xml | 30 - fastscroll/src/main/res/values/dimens.xml | 34 - gradle.properties | 7 +- gradle/libs.versions.toml | 101 ++- settings.gradle.kts | 9 +- 197 files changed, 478 insertions(+), 8746 deletions(-) delete mode 100644 app/schemas/cn.super12138.todo.logic.dao.ToDoRoomDB/1.json create mode 100644 app/src/main/kotlin/cn/super12138/todo/MainActivity.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/ToDoApp.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/constant/Constants.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/constant/GlobalValues.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoom.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDB.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDao.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/logic/model/ToDo.kt create mode 100644 app/src/main/kotlin/cn/super12138/todo/ui/theme/Color.kt create mode 100644 app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt create mode 100644 app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/utils/TextUtils.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/utils/Utils.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/utils/VersionUtils.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/utils/sp/SPDelegates.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/utils/sp/SPUtils.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/BaseActivity.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/BaseFragment.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/activities/CrashActivity.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/activities/CrashHandler.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/activities/MainActivity.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/adapters/AllTasksAdapter.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/adapters/ToDoAdapter.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/AboutFragment.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/AllTasksFragment.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/InfoBottomSheet.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/MainFragment.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/ProgressFragment.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsFragment.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsParentFragment.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoBottomSheet.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/WelcomeFragment.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/IntroPage.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ProgressPage.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoBtnPage.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoItemPage.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/viewmodels/AllTasksViewModel.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ProgressViewModel.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ToDoViewModel.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/views/viewmodels/WelcomeViewModel.kt delete mode 100644 app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionHelper.kt delete mode 100644 app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionManager.kt delete mode 100644 app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/OnCompleteListener.kt delete mode 100644 app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt delete mode 100644 app/src/main/play_store_512.png delete mode 100644 app/src/main/res/drawable/bg_item_complete.xml delete mode 100644 app/src/main/res/drawable/ic_about.xml delete mode 100644 app/src/main/res/drawable/ic_add.xml delete mode 100644 app/src/main/res/drawable/ic_all_tasks.xml delete mode 100644 app/src/main/res/drawable/ic_arrow_back.xml delete mode 100644 app/src/main/res/drawable/ic_arrow_forward.xml delete mode 100644 app/src/main/res/drawable/ic_backup.xml delete mode 100644 app/src/main/res/drawable/ic_check.xml delete mode 100644 app/src/main/res/drawable/ic_dark_mode.xml delete mode 100644 app/src/main/res/drawable/ic_exit.xml delete mode 100644 app/src/main/res/drawable/ic_focus.xml delete mode 100644 app/src/main/res/drawable/ic_github.xml delete mode 100644 app/src/main/res/drawable/ic_launcher.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml delete mode 100644 app/src/main/res/drawable/ic_next.xml delete mode 100644 app/src/main/res/drawable/ic_person.xml delete mode 100644 app/src/main/res/drawable/ic_previous.xml delete mode 100644 app/src/main/res/drawable/ic_restore.xml delete mode 100644 app/src/main/res/drawable/ic_screenshot.xml delete mode 100644 app/src/main/res/drawable/ic_settings.xml delete mode 100644 app/src/main/res/drawable/ic_update.xml delete mode 100644 app/src/main/res/drawable/ic_vibration.xml delete mode 100644 app/src/main/res/drawable/splash_screen.xml delete mode 100644 app/src/main/res/layout-land/fragment_main.xml delete mode 100644 app/src/main/res/layout-small/fragment_main.xml delete mode 100644 app/src/main/res/layout/activity_crash.xml delete mode 100644 app/src/main/res/layout/activity_main.xml delete mode 100644 app/src/main/res/layout/bottom_sheet_info.xml delete mode 100644 app/src/main/res/layout/bottom_sheet_todo.xml delete mode 100644 app/src/main/res/layout/fragment_about.xml delete mode 100644 app/src/main/res/layout/fragment_all_tasks.xml delete mode 100644 app/src/main/res/layout/fragment_main.xml delete mode 100644 app/src/main/res/layout/fragment_progress.xml delete mode 100644 app/src/main/res/layout/fragment_settings.xml delete mode 100644 app/src/main/res/layout/fragment_todo.xml delete mode 100644 app/src/main/res/layout/fragment_welcome.xml delete mode 100644 app/src/main/res/layout/fragment_welcome_intro.xml delete mode 100644 app/src/main/res/layout/fragment_welcome_progress.xml delete mode 100644 app/src/main/res/layout/fragment_welcome_todo_btn.xml delete mode 100644 app/src/main/res/layout/fragment_welcome_todo_item.xml delete mode 100644 app/src/main/res/layout/item_empty.xml delete mode 100644 app/src/main/res/layout/item_todo.xml delete mode 100644 app/src/main/res/layout/preference_switch_m3.xml delete mode 100644 app/src/main/res/menu/main.xml delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_background.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round_background.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round_foreground.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round_monochrome.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_background.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round_background.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round_foreground.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round_monochrome.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_background.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round_background.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round_foreground.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round_monochrome.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round_background.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round_foreground.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round_monochrome.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_background.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_foreground.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round_monochrome.png delete mode 100644 app/src/main/res/values-night/colors.xml delete mode 100644 app/src/main/res/values-night/theme_overlays.xml delete mode 100644 app/src/main/res/values-night/themes.xml delete mode 100644 app/src/main/res/values-v29/themes.xml delete mode 100644 app/src/main/res/values-zh-rCN/arrays.xml delete mode 100644 app/src/main/res/values-zh-rCN/strings.xml delete mode 100644 app/src/main/res/values/arrays.xml delete mode 100644 app/src/main/res/values/theme_overlays.xml delete mode 100644 app/src/main/res/xml/preferences.xml delete mode 100644 fastscroll/.gitignore delete mode 100644 fastscroll/build.gradle.kts delete mode 100644 fastscroll/consumer-rules.pro delete mode 100644 fastscroll/proguard-rules.pro delete mode 100644 fastscroll/src/main/AndroidManifest.xml delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/AutoMirrorDrawable.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/DefaultAnimationHelper.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollNestedScrollView.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollScrollView.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollWebView.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScroller.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollerBuilder.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixItemDecorationRecyclerView.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixOnItemTouchListenerRecyclerView.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/Md2PopupBackground.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupStyles.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupTextProvider.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/Predicate.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/RecyclerViewHelper.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/SimpleViewHelper.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/Utils.java delete mode 100644 fastscroll/src/main/java/me/zhanghai/android/fastscroll/ViewHelperProvider.java delete mode 100644 fastscroll/src/main/res/drawable/afs_md2_thumb.xml delete mode 100644 fastscroll/src/main/res/drawable/afs_md2_track.xml delete mode 100644 fastscroll/src/main/res/drawable/afs_popup_background.xml delete mode 100644 fastscroll/src/main/res/drawable/afs_thumb.xml delete mode 100644 fastscroll/src/main/res/drawable/afs_thumb_stateful.xml delete mode 100644 fastscroll/src/main/res/drawable/afs_track.xml delete mode 100644 fastscroll/src/main/res/values/dimens.xml diff --git a/app/.gitignore b/app/.gitignore index 956c004..42afabf 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1 @@ -/build -/release \ No newline at end of file +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index decde6c..1a78f45 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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() \ No newline at end of file + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f4ec64c..481bb43 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/schemas/cn.super12138.todo.logic.dao.ToDoRoomDB/1.json b/app/schemas/cn.super12138.todo.logic.dao.ToDoRoomDB/1.json deleted file mode 100644 index 0bd7a91..0000000 --- a/app/schemas/cn.super12138.todo.logic.dao.ToDoRoomDB/1.json +++ /dev/null @@ -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')" - ] - } -} \ 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 index 6f68032..491bd80 100644 --- a/app/src/androidTest/kotlin/cn/super12138/todo/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/kotlin/cn/super12138/todo/ExampleInstrumentedTest.kt @@ -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. * diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e3e6b4..9aa3ce1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,34 +3,26 @@ xmlns:tools="http://schemas.android.com/tools"> - + android:theme="@style/Theme.ToDo" + tools:targetApi="31"> + android:label="@string/app_name" + android:theme="@style/Theme.ToDo"> - - - + \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/MainActivity.kt b/app/src/main/kotlin/cn/super12138/todo/MainActivity.kt new file mode 100644 index 0000000..0589e32 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/MainActivity.kt @@ -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") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ToDoApp.kt b/app/src/main/kotlin/cn/super12138/todo/ToDoApp.kt deleted file mode 100644 index 8308a25..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ToDoApp.kt +++ /dev/null @@ -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 - } -} \ 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 deleted file mode 100644 index 8a54be8..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/constant/Constants.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/constant/GlobalValues.kt b/app/src/main/kotlin/cn/super12138/todo/constant/GlobalValues.kt deleted file mode 100644 index 430ed93..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/constant/GlobalValues.kt +++ /dev/null @@ -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) -} \ 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 deleted file mode 100644 index 077682f..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt +++ /dev/null @@ -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 - */ - suspend fun getAllIncomplete(): List { - return withContext(Dispatchers.IO) { - todoDao.getAllUnfinished() - } - } - - /** - * 获取全部已完成的待办 - * @return List - */ - suspend fun getAllComplete(): List { - return withContext(Dispatchers.IO) { - todoDao.getAllComplete() - } - } - - /** - * 获取全部待办 - * @return List - */ - suspend fun getAll(): List { - 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) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoom.kt b/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoom.kt deleted file mode 100644 index 69cf07c..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoom.kt +++ /dev/null @@ -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 -) \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDB.kt b/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDB.kt deleted file mode 100644 index bcc1f2f..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDB.kt +++ /dev/null @@ -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 - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDao.kt b/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDao.kt deleted file mode 100644 index c05fbd3..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/logic/dao/ToDoRoomDao.kt +++ /dev/null @@ -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 - - @Query("SELECT * FROM todo WHERE state = 0") - suspend fun getAllUnfinished(): List - - @Query("SELECT * FROM todo WHERE state = 1") - suspend fun getAllComplete(): List - - @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) -} \ No newline at end of file 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 deleted file mode 100644 index 86a7caa..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/logic/model/ToDo.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Color.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Color.kt new file mode 100644 index 0000000..9233036 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Color.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt new file mode 100644 index 0000000..0ff3880 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt new file mode 100644 index 0000000..c25d260 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Type.kt @@ -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 + ) + */ +) \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/TextUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/TextUtils.kt deleted file mode 100644 index 93adf32..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/TextUtils.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/Utils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/Utils.kt deleted file mode 100644 index 6d8d67f..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/Utils.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/VersionUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/VersionUtils.kt deleted file mode 100644 index 56c6dc9..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/VersionUtils.kt +++ /dev/null @@ -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)" - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt deleted file mode 100644 index 6d4d4bb..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt +++ /dev/null @@ -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) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPDelegates.kt b/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPDelegates.kt deleted file mode 100644 index b00405a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPDelegates.kt +++ /dev/null @@ -1,14 +0,0 @@ -package cn.super12138.todo.utils.sp - -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -class SPDelegates(private val key: String, private val default: T) : ReadWriteProperty { - 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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPUtils.kt deleted file mode 100644 index dfbf247..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/sp/SPUtils.kt +++ /dev/null @@ -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 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 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() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/BaseActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/BaseActivity.kt deleted file mode 100644 index 384583d..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/BaseActivity.kt +++ /dev/null @@ -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 : 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 -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/BaseFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/BaseFragment.kt deleted file mode 100644 index 2e58b76..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/BaseFragment.kt +++ /dev/null @@ -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 : 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 - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashActivity.kt deleted file mode 100644 index b8cfce2..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashActivity.kt +++ /dev/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() { - 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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashHandler.kt b/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashHandler.kt deleted file mode 100644 index 07325db..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/activities/CrashHandler.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/activities/MainActivity.kt b/app/src/main/kotlin/cn/super12138/todo/views/activities/MainActivity.kt deleted file mode 100644 index e718af2..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/activities/MainActivity.kt +++ /dev/null @@ -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() { - 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()) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/adapters/AllTasksAdapter.kt b/app/src/main/kotlin/cn/super12138/todo/views/adapters/AllTasksAdapter.kt deleted file mode 100644 index 260d503..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/adapters/AllTasksAdapter.kt +++ /dev/null @@ -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, - private val fragmentManager: FragmentManager -) : RecyclerView.Adapter() { - // 默认待办项 - 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 - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/adapters/ToDoAdapter.kt b/app/src/main/kotlin/cn/super12138/todo/views/adapters/ToDoAdapter.kt deleted file mode 100644 index 99c7537..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/adapters/ToDoAdapter.kt +++ /dev/null @@ -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, - private val viewModelStoreOwner: ViewModelStoreOwner, - private val fragmentManager: FragmentManager -) : RecyclerView.Adapter() { - // 默认待办项 - 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 - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AboutFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/AboutFragment.kt deleted file mode 100644 index f5fb487..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AboutFragment.kt +++ /dev/null @@ -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() { - 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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AllTasksFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/AllTasksFragment.kt deleted file mode 100644 index 0e69f0a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/AllTasksFragment.kt +++ /dev/null @@ -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() { - private val viewModel by viewModels() - - 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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/InfoBottomSheet.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/InfoBottomSheet.kt deleted file mode 100644 index a1f151a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/InfoBottomSheet.kt +++ /dev/null @@ -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) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/MainFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/MainFragment.kt deleted file mode 100644 index f21b23b..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/MainFragment.kt +++ /dev/null @@ -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() { - 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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ProgressFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/ProgressFragment.kt deleted file mode 100644 index ebeed5f..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ProgressFragment.kt +++ /dev/null @@ -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() { - 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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsFragment.kt deleted file mode 100644 index ccbd0f3..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsFragment.kt +++ /dev/null @@ -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(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(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(Constants.PREF_HAPTIC_FEEDBACK)?.apply { - setOnPreferenceChangeListener { _, _ -> - VibrationUtils.performHapticFeedback(view) - - true - } - } - - findPreference(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(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(Constants.PREF_ALL_TASKS)?.apply { - setOnPreferenceClickListener { - mainActivity.startFragment(AllTasksFragment()) - VibrationUtils.performHapticFeedback(view) - true - } - } - - findPreference(Constants.PREF_REENTER_WELCOME_ACTIVITY)?.apply { - setOnPreferenceClickListener { - mainActivity.startFragment(WelcomeFragment()) - VibrationUtils.performHapticFeedback(view) - true - } - } - - findPreference(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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsParentFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsParentFragment.kt deleted file mode 100644 index 87b0c26..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/SettingsParentFragment.kt +++ /dev/null @@ -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() { - 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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoBottomSheet.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoBottomSheet.kt deleted file mode 100644 index 0138b2d..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoBottomSheet.kt +++ /dev/null @@ -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() - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt deleted file mode 100644 index 051252e..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt +++ /dev/null @@ -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 { - 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) - } - } -} diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/WelcomeFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/WelcomeFragment.kt deleted file mode 100644 index 7fc68e8..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/WelcomeFragment.kt +++ /dev/null @@ -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() { - private val viewModel by viewModels() - - 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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/IntroPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/IntroPage.kt deleted file mode 100644 index 515a337..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/IntroPage.kt +++ /dev/null @@ -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() { - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentWelcomeIntroBinding { - return FragmentWelcomeIntroBinding.inflate(inflater, container, attachToRoot) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ProgressPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ProgressPage.kt deleted file mode 100644 index 588ba0d..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ProgressPage.kt +++ /dev/null @@ -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() { - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - attachToRoot: Boolean - ): FragmentWelcomeProgressBinding { - return FragmentWelcomeProgressBinding.inflate(inflater, container, attachToRoot) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoBtnPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoBtnPage.kt deleted file mode 100644 index d4ea77c..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoBtnPage.kt +++ /dev/null @@ -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() { - 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 - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoItemPage.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoItemPage.kt deleted file mode 100644 index baa168a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/welcome/pages/ToDoItemPage.kt +++ /dev/null @@ -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() { - 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 - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/AllTasksViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/AllTasksViewModel.kt deleted file mode 100644 index eeac4f6..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/AllTasksViewModel.kt +++ /dev/null @@ -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() - val refreshData = MutableLiveData(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 - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ProgressViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ProgressViewModel.kt deleted file mode 100644 index ab45d6a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ProgressViewModel.kt +++ /dev/null @@ -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 = MutableLiveData() - val completeCount: MutableLiveData = MutableLiveData() - val remainCount: MutableLiveData = MutableLiveData() - val progress: MutableLiveData = 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) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ToDoViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ToDoViewModel.kt deleted file mode 100644 index 6111382..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/ToDoViewModel.kt +++ /dev/null @@ -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(0) - val removeData = MutableLiveData(0) - val refreshData = MutableLiveData(0) - - val todoList = ArrayList() - - 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) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/WelcomeViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/WelcomeViewModel.kt deleted file mode 100644 index e8d1f7a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/views/viewmodels/WelcomeViewModel.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionHelper.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionHelper.kt deleted file mode 100644 index 3226c09..0000000 --- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionHelper.kt +++ /dev/null @@ -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") - } -} diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionManager.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionManager.kt deleted file mode 100644 index 7611362..0000000 --- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/AESEncryptionManager.kt +++ /dev/null @@ -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) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/OnCompleteListener.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/OnCompleteListener.kt deleted file mode 100644 index 43dd44a..0000000 --- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/OnCompleteListener.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt b/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt deleted file mode 100644 index a6b7018..0000000 --- a/app/src/main/kotlin/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt +++ /dev/null @@ -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() - - // 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 - } -} diff --git a/app/src/main/play_store_512.png b/app/src/main/play_store_512.png deleted file mode 100644 index a1a9f3734099dc0ccaa71334fc412465bd6e02f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52233 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 diff --git a/app/src/main/res/drawable/bg_item_complete.xml b/app/src/main/res/drawable/bg_item_complete.xml deleted file mode 100644 index fc21ed9..0000000 --- a/app/src/main/res/drawable/bg_item_complete.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_about.xml b/app/src/main/res/drawable/ic_about.xml deleted file mode 100644 index 1e1faf7..0000000 --- a/app/src/main/res/drawable/ic_about.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml deleted file mode 100644 index a9503fd..0000000 --- a/app/src/main/res/drawable/ic_add.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_all_tasks.xml b/app/src/main/res/drawable/ic_all_tasks.xml deleted file mode 100644 index 8c3bd45..0000000 --- a/app/src/main/res/drawable/ic_all_tasks.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml deleted file mode 100644 index cd06f30..0000000 --- a/app/src/main/res/drawable/ic_arrow_back.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_forward.xml b/app/src/main/res/drawable/ic_arrow_forward.xml deleted file mode 100644 index 9e27d98..0000000 --- a/app/src/main/res/drawable/ic_arrow_forward.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_backup.xml b/app/src/main/res/drawable/ic_backup.xml deleted file mode 100644 index 59eaff5..0000000 --- a/app/src/main/res/drawable/ic_backup.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml deleted file mode 100644 index 5623ef0..0000000 --- a/app/src/main/res/drawable/ic_check.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_dark_mode.xml b/app/src/main/res/drawable/ic_dark_mode.xml deleted file mode 100644 index dedc9af..0000000 --- a/app/src/main/res/drawable/ic_dark_mode.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_exit.xml b/app/src/main/res/drawable/ic_exit.xml deleted file mode 100644 index 2660d94..0000000 --- a/app/src/main/res/drawable/ic_exit.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_focus.xml b/app/src/main/res/drawable/ic_focus.xml deleted file mode 100644 index 39fe24b..0000000 --- a/app/src/main/res/drawable/ic_focus.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml deleted file mode 100644 index f32ae57..0000000 --- a/app/src/main/res/drawable/ic_github.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml deleted file mode 100644 index 7b158f0..0000000 --- a/app/src/main/res/drawable/ic_launcher.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_next.xml b/app/src/main/res/drawable/ic_next.xml deleted file mode 100644 index 0f71f1d..0000000 --- a/app/src/main/res/drawable/ic_next.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml deleted file mode 100644 index ea2ea4a..0000000 --- a/app/src/main/res/drawable/ic_person.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_previous.xml b/app/src/main/res/drawable/ic_previous.xml deleted file mode 100644 index 2e0b029..0000000 --- a/app/src/main/res/drawable/ic_previous.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_restore.xml b/app/src/main/res/drawable/ic_restore.xml deleted file mode 100644 index 1698029..0000000 --- a/app/src/main/res/drawable/ic_restore.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_screenshot.xml b/app/src/main/res/drawable/ic_screenshot.xml deleted file mode 100644 index f07d596..0000000 --- a/app/src/main/res/drawable/ic_screenshot.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml deleted file mode 100644 index 09641d1..0000000 --- a/app/src/main/res/drawable/ic_settings.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml deleted file mode 100644 index 14c24e7..0000000 --- a/app/src/main/res/drawable/ic_update.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_vibration.xml b/app/src/main/res/drawable/ic_vibration.xml deleted file mode 100644 index 90f6ccf..0000000 --- a/app/src/main/res/drawable/ic_vibration.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/splash_screen.xml b/app/src/main/res/drawable/splash_screen.xml deleted file mode 100644 index c3c4443..0000000 --- a/app/src/main/res/drawable/splash_screen.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_main.xml b/app/src/main/res/layout-land/fragment_main.xml deleted file mode 100644 index cf9c5d5..0000000 --- a/app/src/main/res/layout-land/fragment_main.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-small/fragment_main.xml b/app/src/main/res/layout-small/fragment_main.xml deleted file mode 100644 index e6b7a61..0000000 --- a/app/src/main/res/layout-small/fragment_main.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - \ 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 deleted file mode 100644 index 43436f4..0000000 --- a/app/src/main/res/layout/activity_crash.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ 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 deleted file mode 100644 index 546db6c..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_info.xml b/app/src/main/res/layout/bottom_sheet_info.xml deleted file mode 100644 index 044071e..0000000 --- a/app/src/main/res/layout/bottom_sheet_info.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_todo.xml b/app/src/main/res/layout/bottom_sheet_todo.xml deleted file mode 100644 index 4c89ffd..0000000 --- a/app/src/main/res/layout/bottom_sheet_todo.xml +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -