├── .gitignore ├── .idea ├── .gitignore ├── .name ├── MarsCodeWorkspaceAppSettings.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── deploymentTargetSelector.xml ├── dictionaries │ └── tao_wu.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README-CN.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ ├── com │ │ └── ldlywt │ │ │ └── note │ │ │ ├── App.kt │ │ │ ├── backup │ │ │ ├── BackupScheduler.kt │ │ │ ├── BackupWorker.kt │ │ │ ├── SyncManager.kt │ │ │ ├── api │ │ │ │ ├── Encryption.kt │ │ │ │ └── OnSyncResultListener.kt │ │ │ ├── model │ │ │ │ └── DavData.java │ │ │ └── utils │ │ │ │ ├── Base64Util.java │ │ │ │ └── DefaultEncryption.kt │ │ │ ├── bean │ │ │ ├── Attachment.kt │ │ │ ├── LocationInfo.kt │ │ │ ├── Note.kt │ │ │ ├── NoteTagCrossRef.kt │ │ │ ├── Option.kt │ │ │ ├── Reminder.kt │ │ │ └── Tag.kt │ │ │ ├── biometric │ │ │ ├── AppBioMetricManager.kt │ │ │ └── BiometricAuthListener.kt │ │ │ ├── component │ │ │ ├── ActionBottomSheet.kt │ │ │ ├── CardCalender.kt │ │ │ ├── DraggableCard.kt │ │ │ ├── EmptyComponent.kt │ │ │ ├── ImageCard.kt │ │ │ ├── LoadingComponent.kt │ │ │ ├── MySaltUi.kt │ │ │ ├── NoteCard.kt │ │ │ ├── PIconButton.kt │ │ │ ├── RYDialog.kt │ │ │ ├── RYOutlineTextField.kt │ │ │ ├── RYScaffold.kt │ │ │ ├── StateHandler.kt │ │ │ └── Wave.kt │ │ │ ├── db │ │ │ ├── AppDatabase.kt │ │ │ ├── DatabaseConverters.kt │ │ │ ├── dao │ │ │ │ ├── NoteDao.kt │ │ │ │ ├── NoteTagCrossRefDao.kt │ │ │ │ ├── TagDao.kt │ │ │ │ └── TagNoteDao.kt │ │ │ └── repo │ │ │ │ └── TagNoteRepo.kt │ │ │ ├── hilt │ │ │ ├── DatabaseModule.kt │ │ │ ├── RepositoryModule.kt │ │ │ └── SyncModule.kt │ │ │ ├── state │ │ │ └── NoteState.kt │ │ │ ├── ui │ │ │ └── page │ │ │ │ ├── NoteViewModel.kt │ │ │ │ ├── PictureDisplayPage.kt │ │ │ │ ├── data │ │ │ │ ├── DataManagerPage.kt │ │ │ │ ├── DataManagerViewModel.kt │ │ │ │ └── WebdavConfigPage.kt │ │ │ │ ├── home │ │ │ │ ├── AllNotePage.kt │ │ │ │ ├── CalenderContent.kt │ │ │ │ └── CalenderPage.kt │ │ │ │ ├── input │ │ │ │ ├── ChatInputDialog.kt │ │ │ │ ├── InputImage.kt │ │ │ │ ├── MemoInputPage.kt │ │ │ │ └── MemoInputViewModel.kt │ │ │ │ ├── main │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainScreen.kt │ │ │ │ └── NavigationBar.kt │ │ │ │ ├── router │ │ │ │ ├── App.kt │ │ │ │ └── Screen.kt │ │ │ │ ├── search │ │ │ │ ├── SearchPage.kt │ │ │ │ └── SearchViewModel.kt │ │ │ │ ├── settings │ │ │ │ ├── AboutPage.kt │ │ │ │ ├── ExplorePage.kt │ │ │ │ ├── GalleryPage.kt │ │ │ │ ├── HeatPage.kt │ │ │ │ ├── HomeSettingsPage.kt │ │ │ │ ├── MoreInfoPage.kt │ │ │ │ └── SettingsViewModel.kt │ │ │ │ ├── share │ │ │ │ └── SharePage.kt │ │ │ │ └── tag │ │ │ │ ├── LocationDetailPage.kt │ │ │ │ ├── LocationListPage.kt │ │ │ │ ├── TagDetailPage.kt │ │ │ │ ├── TagListPage.kt │ │ │ │ └── YearDetailPage.kt │ │ │ └── utils │ │ │ ├── ActivityResultUtils.kt │ │ │ ├── BackUp.kt │ │ │ ├── BioMetricUtil.kt │ │ │ ├── BlurTransformation.kt │ │ │ ├── Constant.kt │ │ │ ├── CoroutinesHelper.kt │ │ │ ├── DateUtils.kt │ │ │ ├── DonateUtils.kt │ │ │ ├── File.kt │ │ │ ├── FirstTimeManager.kt │ │ │ ├── PathUtils.java │ │ │ ├── ReceiveFileKtx.kt │ │ │ ├── Resource.kt │ │ │ ├── SettingsPreferences.kt │ │ │ ├── SharedPreferencesUtils.kt │ │ │ ├── String.kt │ │ │ ├── Tools.kt │ │ │ ├── TopicUtils.kt │ │ │ └── ktx.kt │ ├── dev │ │ └── jeziellago │ │ │ └── compose │ │ │ └── markdowntext │ │ │ ├── AutoSizeConfig.kt │ │ │ ├── CustomTextView.kt │ │ │ ├── MarkdownRender.kt │ │ │ ├── MarkdownText.kt │ │ │ └── TextAppearanceExt.kt │ └── top │ │ └── zibin │ │ └── luban │ │ ├── Checker.java │ │ ├── CompressionPredicate.java │ │ ├── Engine.java │ │ ├── InputStreamAdapter.java │ │ ├── InputStreamProvider.java │ │ ├── Luban.java │ │ ├── OnCompressListener.java │ │ └── OnRenameListener.java │ └── res │ ├── drawable │ ├── about.xml │ ├── agreement.xml │ ├── android.xml │ ├── app_theme.xml │ ├── auto_check_update.xml │ ├── coffee.xml │ ├── color.xml │ ├── complete.xml │ ├── dark_color.xml │ ├── haptic.xml │ ├── home_screen.xml │ ├── ic_arrow_drop_down.xml │ ├── ic_database.xml │ ├── ic_empty.xml │ ├── ic_info.xml │ ├── json_file.xml │ └── light_color.xml │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_start.png │ └── pic_thinking.png │ ├── values-de │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-hi │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── strings.xml │ ├── styles.xml │ └── themes.xml │ └── xml │ ├── file_provider_paths.xml │ └── network__config.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /keystore/ 17 | /keystore/noteKeyStore.jks 18 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Note -------------------------------------------------------------------------------- /.idea/MarsCodeWorkspaceAppSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 127 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/dictionaries/tao_wu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 69 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 31 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

IdeaMemo

5 |

一个用 Jetpack Compose 编写的快速记录应用,数据存在本地或者Webdav。

6 | pEK8AXT.jpg 7 |
8 |
9 |
10 | 11 | ## 🖌️ 截图 12 | 13 |
14 | 15 | 16 | 17 |
18 | 19 | ## ✨ 特征 20 | 21 | **IdeaMemo** 是一款 Android 轻量级以标签 Tag 为核心的卡片便签App。 22 | 23 | 以下是当前功能: 24 | 25 | - **隐私优先**:不需要申请任何权限,所有运行时数据都牢固地存储在您的本地数据库中,也可以上传到您自己的WEBDAV私有云。 26 | - **简洁干净**:以 #标签(TAG) 为索引,支持图文混排来记录和整理您的突发灵感。但是作为卡片笔记,不建议用来记录太长的文字。 27 | - **随时回顾**:支持日历视图、热力图和随机漫步等方式来回顾您的笔记。 28 | - **代码开源**:所有代码都开源在Github上,您可以随时查看和协作开发。 29 | - **免费使用**:完全免费享受所有功能,没有任何内容的费用。 30 | - **持续更新**:IdeaMemo App 自己一直在使用中,会持续更新。 31 | - ... 32 | 33 | 34 | ## ✈️ 下载 35 | 36 | [Get it on Google Play](https://play.google.com/store/apps/details?id=com.ldlywt.note) 39 | [Get it on GitHub](https://github.com/ldlywt/IdeaMemo/releases/latest) 40 | 41 | ## 🤝 贡献指南 42 | 欢迎贡献!请按照以下步骤进行贡献: 43 | 1. Fork 本仓库。 44 | 2. 创建一个新分支(git checkout -b feature-branch-name)。 45 | 3. 提交你的修改(git commit -am 'Add some feature')。 46 | 4. 推送到分支(git push origin feature-branch-name)。 47 | 5. 创建一个 Pull Request。 48 | 49 | ## 🤗 鸣谢 50 | 51 | 该项目由 Compose 创建。该项目的部分代码源自优秀的开源项目。 52 | 53 | 54 | **Open Source Projects** 55 | 56 | - [ReadYou](https://github.com/Ashinch/ReadYou) 57 | - [MoeMemosAndroid](https://github.com/mudkipme/MoeMemosAndroid) 58 | - [Animius](https://github.com/lanlinju/Animius) 59 | - [SaltUI](https://github.com/Moriafly/SaltUI) 60 | - ... 61 | 62 | ## 🧾 License 63 | GNU GPL v3.0 © [IdeaMemo](https://github.com/ldlywt/IdeaMemo/blob/master/LICENSE) 64 | 65 | 66 | 您的star是我最大的动力,谢谢! **🌟** 67 | 68 | ## ☕️ 捐助 69 | 可以给我买一杯咖啡,让我更有动力继续开发。 70 | 71 | 1742624588864 72 | 73 | mm_facetoface_collect_qrcode_1742624668599 74 | 75 | 76 | ## 🌸 捐赠名单 77 | 78 | | 捐赠者名字 | 金额 | 79 | |-------------------|------| 80 | | 尧孟张 | 8.8 | 81 | | +(李刚) | 5 | 82 | | Tomo Ebizuka | 15 | 83 | | Corcube | 10 | 84 | | 奶酪很热情 | 10 | 85 | | KavenSu | 10 | 86 | 87 | 88 | ## ⭐ Star History 89 | 90 | [![Star History Chart]( https://api.star-history.com/svg?repos=ldlywt/IdeaMemo&type=Date)]( https://star-history.com/#ldlywt/IdeaMemo&Date) 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

IdeaMemo

3 |

A quick record app written with Jetpack Compose.

4 | 简体中文  |   English  

5 | 6 |
7 |
8 |
9 | 10 | ## 🖌️ Screenshots 11 | 12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 | 21 | ## ✨ Features 22 | 23 | **IdeaMemo** is an Android lightweight note-taking app. 24 | 25 | Here are the current features: 26 | 27 | - **Privacy First** : No need to apply any permissions, all runtime data is firmly stored in your local database or can be uploaded to your own WEBDAV private cloud. 28 | - **Simplicity and cleanliness** : Indexed by #TAG , it supports mixed text and graphics to record and organize your sudden inspirations. But as a card note, it is not recommended to record too long text. 29 | - **Review at any time** : Support calendar view, heatmap and random walk to review your notes. 30 | - **Code Open Source** : All code is open source on Github, you can view and collaborate on development at any time. 31 | - **Free to use** : Enjoy all features completely free of charge with no content fees. 32 | - **Continuously Updated** : IdeaMemo is always in use by itself and will be continuously updated. 33 | - ... 34 | 35 | 36 | ## ✈️ Download 37 | 38 | [Get it on Google Play](https://play.google.com/store/apps/details?id=com.ldlywt.note) 41 | [Get it on GitHub](https://github.com/ldlywt/IdeaMemo/releases/latest) 42 | 43 | ## 🤝 How to contribute 44 | - **Feel free to contribute code**, but please don't mention simple fix code! 45 | - **Share your ideas and suggestions.** If you’re missing a feature or have an interesting idea, 46 | feel free to create a new *Issue*. 47 | - **Report bugs.** Encountered a crash or something went wrong? Create a new *Issue* with as much 48 | detail as possible to help resolve it. 49 | - **Enjoy the app.** The best contribution is simply using and enjoying the app I spent so much time on! 50 | 51 | ## 🌍 Translation 52 | Multi-language translation is done by AI, there may be errors, please point them out.Thanks! 53 | 54 | ## 🤗 Thanks 55 | This project was created by Compose. Some of the project's code is derived from excellent open-source projects. 56 | 57 | I'll gradually improve it, but the power of one person is limited. Pull requests are welcome. 58 | 59 | - [ReadYou](https://github.com/Ashinch/ReadYou) 60 | - [MoeMemosAndroid](https://github.com/mudkipme/MoeMemosAndroid) 61 | - [Animius](https://github.com/lanlinju/Animius) 62 | - [SaltUI](https://github.com/Moriafly/SaltUI) 63 | - [memos](https://github.com/usememos/memos) 64 | - ... 65 | 66 | ## 🧾 License 67 | GNU GPL v3.0 © [IdeaMemo](https://github.com/ldlywt/IdeaMemo/blob/master/LICENSE) 68 | 69 | Your star is my biggest motivation! **🌟** 70 | 71 | ## ⭐ Star History 72 | 73 | [![Star History Chart]( https://api.star-history.com/svg?repos=ldlywt/IdeaMemo&type=Date)]( https://star-history.com/#ldlywt/IdeaMemo&Date) 74 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | # Please add these rules to your existing keep rules in order to suppress warnings. 23 | # This is generated automatically by the Android Gradle plugin. 24 | -dontwarn org.bouncycastle.jsse.BCSSLParameters 25 | -dontwarn org.bouncycastle.jsse.BCSSLSocket 26 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider 27 | -dontwarn org.conscrypt.Conscrypt$Version 28 | -dontwarn org.conscrypt.Conscrypt 29 | -dontwarn org.conscrypt.ConscryptHostnameVerifier 30 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters 31 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket 32 | -dontwarn org.openjsse.net.ssl.OpenJSSE 33 | 34 | # https://developer.android.com/build/shrink-code?utm_source=android-studio&hl=zh-cn#retracing 35 | # 对堆栈轨迹进行轨迹还原 36 | -keepattributes LineNumberTable,SourceFile 37 | -renamesourcefileattribute SourceFile 38 | 39 | -dontwarn org.xmlpull.v1.** 40 | -dontwarn org.kxml2.io.** 41 | -dontwarn android.content.res.** 42 | -dontwarn org.slf4j.impl.StaticLoggerBinder 43 | 44 | -keep class org.xmlpull.** { *; } 45 | -keepclassmembers class org.xmlpull.** { *; } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/App.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.asLiveData 5 | import com.ldlywt.note.backup.BackupScheduler 6 | import com.ldlywt.note.utils.SettingsPreferences 7 | import com.ldlywt.note.utils.SharedPreferencesUtils 8 | import dagger.hilt.android.HiltAndroidApp 9 | import kotlinx.coroutines.DelicateCoroutinesApi 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.GlobalScope 12 | import kotlinx.coroutines.launch 13 | 14 | fun getAppName(): String { 15 | return "IdeaMemo" 16 | } 17 | 18 | 19 | @HiltAndroidApp 20 | class App : Application() { 21 | 22 | override fun onCreate() { 23 | super.onCreate() 24 | instance = this 25 | val localAutoBackup = SharedPreferencesUtils.localAutoBackup.asLiveData().value 26 | if (localAutoBackup == true) { 27 | BackupScheduler.scheduleDailyBackup(this) 28 | } else { 29 | BackupScheduler.cancelDailyBackup(this) 30 | } 31 | 32 | GlobalScope.launch(Dispatchers.Main) { 33 | SettingsPreferences.themeMode.collect { 34 | SettingsPreferences.applyAppCompatThemeMode(it) 35 | } 36 | } 37 | } 38 | 39 | companion object { 40 | lateinit var instance: App 41 | private set 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/backup/BackupScheduler.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.backup 2 | 3 | import android.content.Context 4 | import androidx.work.ExistingPeriodicWorkPolicy 5 | import androidx.work.PeriodicWorkRequestBuilder 6 | import androidx.work.WorkManager 7 | import java.util.concurrent.TimeUnit 8 | 9 | class BackupScheduler { 10 | companion object { 11 | private const val BACKUP_WORK_TAG = "backup_work" 12 | 13 | fun scheduleDailyBackup(context: Context) { 14 | // 创建 PeriodicWorkRequest,间隔时间为 7 天 15 | val workRequest = PeriodicWorkRequestBuilder(3, TimeUnit.DAYS) 16 | .addTag(BACKUP_WORK_TAG) 17 | .build() 18 | 19 | // 将任务添加到 WorkManager,并设定唯一的标签 20 | val workManager = WorkManager.getInstance(context) 21 | workManager.enqueueUniquePeriodicWork( 22 | BACKUP_WORK_TAG, 23 | ExistingPeriodicWorkPolicy.KEEP, 24 | workRequest 25 | ) 26 | } 27 | 28 | fun cancelDailyBackup(context: Context) { 29 | // 取消已经调度的任务 30 | val workManager = WorkManager.getInstance(context) 31 | workManager.cancelUniqueWork(BACKUP_WORK_TAG) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/backup/BackupWorker.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.backup 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import androidx.documentfile.provider.DocumentFile 6 | import androidx.work.CoroutineWorker 7 | import androidx.work.WorkerParameters 8 | import com.ldlywt.note.utils.BackUp 9 | import com.ldlywt.note.utils.SharedPreferencesUtils 10 | import com.ldlywt.note.utils.backUpFileName 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.first 13 | import kotlinx.coroutines.withContext 14 | import javax.inject.Inject 15 | 16 | class BackupWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { 17 | 18 | @Inject 19 | lateinit var syncManager: SyncManager 20 | 21 | override suspend fun doWork(): Result = withContext(Dispatchers.IO) { 22 | // 执行数据备份逻辑 23 | // 这里是每天备份数据的具体操作 24 | 25 | SharedPreferencesUtils.localBackupUri.first()?.let { 26 | val uri = Uri.parse(it) 27 | val folder = requireNotNull(DocumentFile.fromTreeUri(context, uri)) 28 | val file = requireNotNull(folder.createFile("application/zip", "Auto".plus(backUpFileName))) 29 | BackUp.exportEncrypted(context, file.uri) 30 | Result.success() 31 | } ?: Result.failure() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/backup/api/Encryption.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.backup.api 2 | 3 | interface Encryption { 4 | //加密 5 | fun encode(key: String?): String? 6 | 7 | //解密 8 | fun decode(password: String?): String? 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/backup/api/OnSyncResultListener.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.backup.api 2 | 3 | interface OnSyncResultListener { 4 | fun onSuccess(result: String?) 5 | fun onError(errorMsg: String?) 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/backup/utils/Base64Util.java: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.backup.utils; 2 | 3 | import android.util.Base64; 4 | 5 | import java.io.UnsupportedEncodingException; 6 | 7 | public class Base64Util { 8 | /** 9 | * 字符Base64加密 10 | * @param str 11 | * @return 12 | */ 13 | public static String encodeToString(String str){ 14 | try { 15 | return Base64.encodeToString(str.getBytes("UTF-8"), Base64.DEFAULT); 16 | } catch (UnsupportedEncodingException e) { 17 | e.printStackTrace(); 18 | } 19 | return ""; 20 | } 21 | /** 22 | * 字符Base64解密 23 | * @param str 24 | * @return 25 | */ 26 | public static String decodeToString(String str){ 27 | try { 28 | return new String(Base64.decode(str.getBytes("UTF-8"), Base64.DEFAULT)); 29 | } catch (UnsupportedEncodingException e) { 30 | e.printStackTrace(); 31 | } 32 | return ""; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/backup/utils/DefaultEncryption.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.backup.utils 2 | 3 | import com.ldlywt.note.backup.api.Encryption 4 | import javax.inject.Inject 5 | 6 | 7 | class DefaultEncryption @Inject constructor() : Encryption { 8 | 9 | override fun encode(key: String?): String? { 10 | return Base64Util.encodeToString(key) 11 | } 12 | 13 | override fun decode(password: String?): String? { 14 | return Base64Util.decodeToString(password) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/bean/Attachment.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.bean 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | @Parcelize 9 | data class Attachment( 10 | val type: Type = Type.IMAGE, 11 | val path: String = "", 12 | val description: String = "", 13 | val fileName: String = "", 14 | ) : Parcelable { 15 | enum class Type { AUDIO, IMAGE, VIDEO, GENERIC, FILE } 16 | 17 | fun isMedia(): Boolean { 18 | return type == Attachment.Type.IMAGE || type == Attachment.Type.VIDEO 19 | } 20 | 21 | fun isEmpty() = path.isEmpty() && description.isEmpty() && fileName.isEmpty() 22 | } 23 | 24 | data class NoteIdWithPath( 25 | val noteId: Long, 26 | val path: String 27 | ) 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/bean/LocationInfo.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.bean 2 | 3 | data class LocationInfo(val city: String?, val weather: String?, val addressList: List?) -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/bean/Note.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.bean 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Embedded 6 | import androidx.room.Entity 7 | import androidx.room.ForeignKey 8 | import androidx.room.ForeignKey.Companion.CASCADE 9 | import androidx.room.Ignore 10 | import androidx.room.Junction 11 | import androidx.room.PrimaryKey 12 | import androidx.room.Relation 13 | import kotlinx.parcelize.Parcelize 14 | import kotlinx.serialization.Serializable 15 | import kotlinx.serialization.json.JsonNull.content 16 | 17 | @Serializable 18 | @Parcelize 19 | data class NoteShowBean( 20 | @Embedded val note: Note, 21 | @Relation( 22 | parentColumn = "note_id", 23 | entityColumn = "tag", 24 | associateBy = Junction(NoteTagCrossRef::class) 25 | ) val tagList: List, 26 | @Relation( 27 | parentColumn = "note_id", 28 | entityColumn = "note_comment_id" 29 | ) val commentList: List? = null, 30 | @Relation( 31 | parentColumn = "note_id", 32 | entityColumn = "noteId", 33 | ) 34 | val reminders: List = listOf(), 35 | ) : Parcelable { 36 | fun doesMatchSearchQuery(query: String): Boolean { 37 | val matchingCombinations = listOf( 38 | "${note.noteTitle}${note.content}", 39 | "${note.noteTitle} $content", 40 | "${note.noteTitle?.firstOrNull()} ${note.content.first()}" 41 | ) 42 | return matchingCombinations.any { 43 | it.contains(query, true) 44 | } 45 | } 46 | } 47 | 48 | @Serializable 49 | @Parcelize 50 | @Entity 51 | data class Note( 52 | @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "note_id") var noteId: Long = 0, 53 | @ColumnInfo(name = "note_title") var noteTitle: String? = null, 54 | @ColumnInfo(name = "note_content") var content: String = "", 55 | @ColumnInfo(name = "location_info") var locationInfo: String? = null, 56 | @ColumnInfo(name = "weather_info") var weatherInfo: String? = null, 57 | @ColumnInfo(name = "city") var city: String? = null, 58 | @ColumnInfo(name = "create_time") var createTime: Long = System.currentTimeMillis(), 59 | @ColumnInfo(name = "update_time") var updateTime: Long = System.currentTimeMillis(), 60 | @ColumnInfo(name = "is_collected") var isCollected: Boolean = false, 61 | @ColumnInfo(name = "is_deleted") var isDeleted: Boolean = false, 62 | var attachments: List = arrayListOf(), 63 | @Ignore var isHide: Boolean = false, 64 | ) : Parcelable 65 | 66 | @Serializable 67 | @Parcelize 68 | @Entity( 69 | foreignKeys = [ForeignKey( 70 | entity = Note::class, 71 | parentColumns = arrayOf("note_id"), 72 | childColumns = arrayOf("note_comment_id"), 73 | onDelete = CASCADE 74 | )] 75 | ) 76 | data class Comment( 77 | @PrimaryKey(autoGenerate = true) var id: Long = 0, 78 | @ColumnInfo(name = "note_comment_id", index = true) val noteCommentId: Long, 79 | var text: String = "", 80 | @ColumnInfo(name = "create_time") var createTime: Long = System.currentTimeMillis(), 81 | @ColumnInfo(name = "update_time") var updateTime: Long = System.currentTimeMillis(), 82 | ) : Parcelable 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/bean/NoteTagCrossRef.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.bean 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.ForeignKey 7 | import kotlinx.parcelize.Parcelize 8 | import kotlinx.serialization.Serializable 9 | 10 | @Serializable 11 | @Parcelize 12 | @Entity( 13 | primaryKeys = ["note_id", "tag"], 14 | foreignKeys = [ForeignKey( 15 | entity = Note::class, 16 | parentColumns = arrayOf("note_id"), 17 | childColumns = arrayOf("note_id"), 18 | onDelete = ForeignKey.CASCADE, 19 | onUpdate = ForeignKey.CASCADE, 20 | ), ForeignKey( 21 | entity = Tag::class, 22 | parentColumns = arrayOf("tag"), 23 | childColumns = arrayOf("tag"), 24 | onDelete = ForeignKey.CASCADE, 25 | onUpdate = ForeignKey.CASCADE 26 | )] 27 | ) 28 | data class NoteTagCrossRef( 29 | @ColumnInfo(name = "note_id") val noteId: Long, @ColumnInfo(index = true) val tag: String 30 | ) : Parcelable -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/bean/Option.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.bean 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | 6 | data class Option( 7 | /** 8 | * ID of the option. Will be used as the saved value in [SharedPreferences]. 9 | */ 10 | val id: Int, 11 | /** 12 | * The drawable resource ID of the icon of the option to show in the preferences screen. 13 | */ 14 | @field:DrawableRes @param:DrawableRes val icon: Int, 15 | /** 16 | * The string resource ID of the human readable description of the option to show in the 17 | * preferences screen. 18 | */ 19 | @field:StringRes @param:StringRes val description: Int 20 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/bean/Reminder.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.bean 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.ForeignKey 7 | import androidx.room.PrimaryKey 8 | import kotlinx.parcelize.Parcelize 9 | import kotlinx.serialization.Serializable 10 | 11 | @Entity( 12 | tableName = "reminders", 13 | foreignKeys = [ 14 | ForeignKey( 15 | onDelete = ForeignKey.CASCADE, 16 | entity = Note::class, 17 | parentColumns = ["note_id"], 18 | childColumns = ["noteId"] 19 | ), 20 | ] 21 | ) 22 | @Serializable 23 | @Parcelize 24 | data class Reminder( 25 | val name: String, 26 | @ColumnInfo(index = true) 27 | val noteId: Long, 28 | val date: Long, 29 | @PrimaryKey(autoGenerate = true) 30 | val id: Long = 0L, 31 | ) : Parcelable { 32 | 33 | // fun hasExpired(): Boolean { 34 | // val dateInstant = Instant.ofEpochSecond(date) 35 | // return dateInstant.isBefore(Instant.now()) 36 | // } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/bean/Tag.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.bean 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.PrimaryKey 7 | import kotlinx.parcelize.Parcelize 8 | import kotlinx.serialization.Serializable 9 | 10 | @Serializable 11 | @Parcelize 12 | @Entity 13 | data class Tag( 14 | @PrimaryKey val tag: String, 15 | @ColumnInfo(name = "create_time") val createTime: Long = System.currentTimeMillis(), 16 | @ColumnInfo(name = "update_time") val updateTime: Long = System.currentTimeMillis(), 17 | @ColumnInfo(name = "is_collected") var isCollected: Boolean = false, 18 | @ColumnInfo(name = "is_deleted") var isDeleted: Boolean = false, 19 | @ColumnInfo(name = "is_city_tag") var isCityTag: Boolean = false, 20 | @ColumnInfo(name = "count") var count: Int = 0, 21 | ) : Parcelable { 22 | override fun toString(): String { 23 | return tag 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/biometric/AppBioMetricManager.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.biometric 2 | 3 | import android.content.Context 4 | import androidx.biometric.BiometricManager 5 | import androidx.biometric.BiometricPrompt 6 | import com.ldlywt.note.R 7 | import com.ldlywt.note.ui.page.main.MainActivity 8 | import com.ldlywt.note.utils.str 9 | import javax.inject.Inject 10 | 11 | class AppBioMetricManager @Inject constructor(appContext: Context) { 12 | 13 | private var biometricPrompt: BiometricPrompt? = null 14 | private val biometricManager = BiometricManager.from(appContext) 15 | 16 | fun canAuthenticate(): Boolean { 17 | return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) { 18 | BiometricManager.BIOMETRIC_SUCCESS -> { 19 | true 20 | } 21 | 22 | BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { 23 | false 24 | } 25 | 26 | else -> { 27 | false 28 | } 29 | } 30 | } 31 | 32 | fun initBiometricPrompt(activity: MainActivity, listener: BiometricAuthListener) { 33 | biometricPrompt = BiometricPrompt( 34 | activity, 35 | object : BiometricPrompt.AuthenticationCallback() { 36 | override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { 37 | super.onAuthenticationError(errorCode, errString) 38 | val cancelled = errorCode in arrayListOf( 39 | BiometricPrompt.ERROR_CANCELED, 40 | BiometricPrompt.ERROR_USER_CANCELED, 41 | BiometricPrompt.ERROR_NEGATIVE_BUTTON 42 | ) 43 | if (cancelled) { 44 | listener.onUserCancelled() 45 | } else { 46 | listener.onErrorOccurred() 47 | } 48 | } 49 | 50 | override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { 51 | super.onAuthenticationSucceeded(result) 52 | listener.onBiometricAuthSuccess() 53 | } 54 | } 55 | ) 56 | 57 | val promptInfo = BiometricPrompt.PromptInfo.Builder() 58 | .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG) 59 | .setTitle(R.string.biometric_authentication.str) 60 | // .setSubtitle("Log in with biometric auth") 61 | .setNegativeButtonText(R.string.cancel.str) 62 | .build() 63 | biometricPrompt?.authenticate(promptInfo) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/biometric/BiometricAuthListener.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.biometric 2 | 3 | interface BiometricAuthListener { 4 | fun onBiometricAuthSuccess() 5 | fun onUserCancelled() 6 | fun onErrorOccurred() 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/component/CardCalender.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.component 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.combinedClickable 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.Card 11 | import androidx.compose.material3.CardDefaults 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.saveable.rememberSaveable 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.sp 25 | import androidx.navigation.NavHostController 26 | import com.ldlywt.note.bean.NoteShowBean 27 | import com.ldlywt.note.ui.page.router.Screen 28 | import com.ldlywt.note.utils.toMinute 29 | import com.moriafly.salt.ui.SaltTheme 30 | import dev.jeziellago.compose.markdowntext.MarkdownText 31 | 32 | 33 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) 34 | @Composable 35 | fun CardCalender( 36 | noteShowBean: NoteShowBean, navHostController: NavHostController, modifier: Modifier = Modifier 37 | ) { 38 | 39 | var openBottomSheet by rememberSaveable { mutableStateOf(false) } 40 | val context = LocalContext.current 41 | val note = noteShowBean.note 42 | val tags = noteShowBean.tagList 43 | 44 | 45 | Card( 46 | colors = CardDefaults.cardColors(containerColor = SaltTheme.colors.subBackground), 47 | modifier = modifier 48 | .padding(horizontal = 16.dp, vertical = 4.dp) 49 | .fillMaxWidth() 50 | .combinedClickable( 51 | onClick = { 52 | navHostController.navigate(route = Screen.InputDetail(noteShowBean.note.noteId)) 53 | }, 54 | onLongClick = { 55 | openBottomSheet = true 56 | }, 57 | ), 58 | ) { 59 | 60 | Column( 61 | modifier = Modifier.padding(12.dp) 62 | ) { 63 | Text( 64 | text = note.createTime.toMinute(), 65 | style = SaltTheme.textStyles.paragraph.copy(fontWeight = FontWeight.SemiBold), 66 | ) 67 | Spacer(modifier = Modifier.height(10.dp)) 68 | if (!note.noteTitle.isNullOrEmpty()) { 69 | Text( 70 | text = note.noteTitle ?: "", 71 | style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), 72 | color = MaterialTheme.colorScheme.primary, 73 | ) 74 | Spacer(modifier = Modifier.height(4.dp)) 75 | } 76 | MarkdownText(markdown = note.content, style = SaltTheme.textStyles.paragraph.copy(fontSize = 15.sp, lineHeight = 24.sp), onTagClick = { 77 | navHostController.navigate(Screen.TagDetail(it)) 78 | }) 79 | if (note.attachments.isNotEmpty()) { 80 | Spacer(modifier = Modifier.height(8.dp)) 81 | ImageCard(note, navHostController) 82 | } 83 | Spacer(modifier = Modifier.height(8.dp)) 84 | showLocationInfoContent(note) 85 | // val filterTagList = tags.filterNot { it.tag.isBlank() || it.isCityTag } 86 | // if (filterTagList.isNotEmpty()) { 87 | // Spacer(modifier = Modifier.height(4.dp)) 88 | // LazyRow { 89 | // filterTagList.forEachIndexed { index, tag -> 90 | // item(tag.tag) { 91 | // val startPadding = if (index == 0) 0.dp else 6.dp 92 | // Text(tag.tag, 93 | // style = MaterialTheme.typography.labelMedium, 94 | // color = MaterialTheme.colorScheme.primary, 95 | // modifier = Modifier 96 | // .padding(horizontal = startPadding) 97 | // .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)) 98 | // .clickable { 99 | // navHostController.navigate("${RouteName.TAG_DETAIL}/${tag.tag}") 100 | // } 101 | // ) 102 | // } 103 | // } 104 | // } 105 | // } 106 | } 107 | } 108 | 109 | ActionBottomSheet(navHostController, noteShowBean = noteShowBean, show = openBottomSheet) { 110 | openBottomSheet = false 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/component/DraggableCard.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.component 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.AnimationVector1D 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.gestures.detectDragGestures 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material3.Card 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.rememberCoroutineScope 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.composed 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.graphics.graphicsLayer 16 | import androidx.compose.ui.input.pointer.consumePositionChange 17 | import androidx.compose.ui.input.pointer.pointerInput 18 | import androidx.compose.ui.platform.LocalConfiguration 19 | import androidx.compose.ui.unit.dp 20 | import kotlinx.coroutines.launch 21 | import kotlin.math.abs 22 | 23 | enum class SwipeResult { 24 | ACCEPTED, REJECTED 25 | } 26 | 27 | @Composable 28 | fun DraggableCard( 29 | item: Any, 30 | modifier: Modifier = Modifier, 31 | onSwiped: (Any, Any) -> Unit, 32 | content: @Composable () -> Unit 33 | ) { 34 | val screenWidth = LocalConfiguration.current.screenWidthDp.dp 35 | val swipeXLeft = -(screenWidth.value * 3.2).toFloat() 36 | val swipeXRight = (screenWidth.value * 3.2).toFloat() 37 | val swipeYTop = -1000f 38 | val swipeYBottom = 1000f 39 | val swipeX = remember { Animatable(0f) } 40 | val swipeY = remember { Animatable(0f) } 41 | swipeX.updateBounds(swipeXLeft, swipeXRight) 42 | swipeY.updateBounds(swipeYTop, swipeYBottom) 43 | if (abs(swipeX.value) < swipeXRight - 50f) { 44 | val rotationFraction = (swipeX.value / 60).coerceIn(-40f, 40f) 45 | Card( 46 | // elevation = 16.dp, 47 | modifier = modifier 48 | .dragContent( 49 | swipeX = swipeX, 50 | swipeY = swipeY, 51 | maxX = swipeXRight, 52 | onSwiped = { _, _ -> } 53 | ) 54 | .graphicsLayer( 55 | translationX = swipeX.value, 56 | translationY = swipeY.value, 57 | rotationZ = rotationFraction, 58 | ) 59 | .clip(RoundedCornerShape(16.dp)) 60 | ) { 61 | content() 62 | } 63 | } else { 64 | // on swiped 65 | val swipeResult = if (swipeX.value > 0) SwipeResult.ACCEPTED else SwipeResult.REJECTED 66 | onSwiped(swipeResult, item) 67 | } 68 | } 69 | 70 | fun Modifier.dragContent( 71 | swipeX: Animatable, 72 | swipeY: Animatable, 73 | maxX: Float, 74 | onSwiped: (Any, Any) -> Unit 75 | ): Modifier = composed { 76 | val coroutineScope = rememberCoroutineScope() 77 | pointerInput(Unit) { 78 | this.detectDragGestures( 79 | onDragCancel = { 80 | coroutineScope.apply { 81 | launch { swipeX.animateTo(0f) } 82 | launch { swipeY.animateTo(0f) } 83 | } 84 | }, 85 | onDragEnd = { 86 | coroutineScope.apply { 87 | // if it's swiped 1/4th 88 | if (abs(swipeX.targetValue) < abs(maxX) / 4) { 89 | launch { 90 | swipeX.animateTo(0f, tween(400)) 91 | } 92 | launch { 93 | swipeY.animateTo(0f, tween(400)) 94 | } 95 | } else { 96 | launch { 97 | if (swipeX.targetValue > 0) { 98 | swipeX.animateTo(maxX, tween(400)) 99 | } else { 100 | swipeX.animateTo(-maxX, tween(400)) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | ) { change, dragAmount -> 107 | change.consumePositionChange() 108 | coroutineScope.apply { 109 | launch { swipeX.animateTo(swipeX.targetValue + dragAmount.x) } 110 | launch { swipeY.animateTo(swipeY.targetValue + dragAmount.y) } 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/component/EmptyComponent.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.component 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.res.painterResource 16 | import androidx.compose.ui.text.font.FontWeight 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import com.ldlywt.note.R 20 | 21 | @Composable 22 | fun EmptyComponent(modifier: Modifier = Modifier.fillMaxSize()) { 23 | Box( 24 | modifier = modifier, 25 | contentAlignment = Alignment.Center, 26 | ) { 27 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 28 | Image(painter = painterResource(id = R.drawable.ic_empty), contentDescription = null) 29 | Spacer(modifier = Modifier.height(12.dp)) 30 | Text( 31 | text = "NoThing ~", 32 | style = MaterialTheme.typography.bodyLarge.copy(color = Color.Gray.copy(alpha = 0.7f), fontWeight = FontWeight.SemiBold, fontSize = 20.sp) 33 | ) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/component/ImageCard.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.component 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.aspectRatio 6 | import androidx.compose.foundation.layout.fillMaxHeight 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.foundation.lazy.LazyRow 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.layout.ContentScale 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.zIndex 18 | import androidx.navigation.NavHostController 19 | import coil.compose.AsyncImage 20 | import com.ldlywt.note.bean.Note 21 | import com.ldlywt.note.ui.page.router.Screen 22 | 23 | @Composable 24 | fun ImageCard(note: Note, navHostController: NavHostController?) { 25 | 26 | if (note.attachments.size == 1) { 27 | AsyncImage( 28 | model = note.attachments[0].path, 29 | contentDescription = null, 30 | modifier = Modifier 31 | .width(160.dp) 32 | .height(160.dp) 33 | .clip(RoundedCornerShape(8.dp)) 34 | .clickable { 35 | navHostController?.navigate(Screen.PictureDisplay(arrayListOf(note.attachments[0].path), 0)) 36 | }, 37 | contentScale = ContentScale.Crop 38 | ) 39 | } else { 40 | LazyRow( 41 | modifier = Modifier.height(90.dp).padding(end = 15.dp), 42 | horizontalArrangement = Arrangement.spacedBy(6.dp) 43 | ) { 44 | items(count = note.attachments.size, key = { index -> note.attachments[index].path }) { index -> 45 | val path: String = note.attachments[index].path 46 | AsyncImage( 47 | model = path, 48 | contentDescription = null, 49 | modifier = Modifier 50 | .fillMaxHeight() 51 | .aspectRatio(1f) 52 | .zIndex(1f) 53 | .clip(RoundedCornerShape(8.dp)) 54 | .clickable { 55 | navHostController?.navigate(Screen.PictureDisplay(note.attachments.map { it.path }, index)) 56 | }, 57 | contentScale = ContentScale.Crop 58 | ) 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/component/LoadingComponent.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.component 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.DisposableEffect 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | 13 | @Composable 14 | fun LoadingComponent(visible: Boolean) { 15 | val interceptClicks = remember { mutableStateOf(visible) } 16 | 17 | DisposableEffect(interceptClicks.value) { 18 | onDispose { 19 | interceptClicks.value = false 20 | } 21 | } 22 | 23 | val modifier = if (visible) { 24 | Modifier 25 | .fillMaxSize() 26 | // .background(Color.Gray.copy(alpha = 0.3f)) 27 | .clickable(enabled = interceptClicks.value) { /* Do nothing when clicked */ } 28 | } else { 29 | Modifier 30 | } 31 | 32 | Box( 33 | modifier = modifier, 34 | contentAlignment = Alignment.Center 35 | ) { 36 | if (visible) { 37 | FadingCircle() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/component/PIconButton.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.component 2 | 3 | import android.view.HapticFeedbackConstants 4 | import android.view.SoundEffectConstants 5 | import androidx.compose.foundation.layout.offset 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.foundation.shape.CircleShape 8 | import androidx.compose.material3.* 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.vector.ImageVector 14 | import androidx.compose.ui.platform.LocalView 15 | import androidx.compose.ui.unit.dp 16 | import com.moriafly.salt.ui.SaltTheme 17 | 18 | @OptIn(ExperimentalMaterial3Api::class) 19 | @Composable 20 | fun PIconButton( 21 | modifier: Modifier = Modifier, 22 | containerModifier: Modifier = Modifier, 23 | imageVector: ImageVector, 24 | contentDescription: String?, 25 | tint: Color = SaltTheme.colors.text, 26 | showBadge: Boolean = false, 27 | badgeColor: Color = SaltTheme.colors.stroke, 28 | isHaptic: Boolean? = false, 29 | isSound: Boolean? = false, 30 | onClick: () -> Unit = {}, 31 | ) { 32 | val view = LocalView.current 33 | 34 | IconButton( 35 | modifier = containerModifier, 36 | onClick = { 37 | if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) 38 | if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK) 39 | onClick() 40 | }, 41 | ) { 42 | if (showBadge) { 43 | BadgedBox( 44 | badge = { 45 | Badge( 46 | modifier = 47 | Modifier 48 | .size(8.dp) 49 | .offset(x = (-1).dp, y = 0.dp) 50 | .clip(CircleShape), 51 | containerColor = badgeColor, 52 | ) 53 | }, 54 | ) { 55 | Icon( 56 | modifier = modifier, 57 | imageVector = imageVector, 58 | contentDescription = contentDescription, 59 | tint = tint, 60 | ) 61 | } 62 | } else { 63 | Icon( 64 | modifier = modifier, 65 | imageVector = imageVector, 66 | contentDescription = contentDescription, 67 | tint = tint, 68 | ) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/component/RYDialog.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.component 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.Text 5 | import androidx.compose.material3.TextButton 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.res.stringResource 9 | import androidx.compose.ui.window.DialogProperties 10 | import com.ldlywt.note.R 11 | 12 | @Composable 13 | fun RYDialog( 14 | modifier: Modifier = Modifier, 15 | visible: Boolean, 16 | properties: DialogProperties = DialogProperties(), 17 | onDismissRequest: () -> Unit = {}, 18 | icon: @Composable (() -> Unit)? = null, 19 | title: @Composable (() -> Unit)? = null, 20 | text: @Composable (() -> Unit)? = null, 21 | confirmButton: @Composable () -> Unit, 22 | dismissButton: @Composable (() -> Unit)? = null, 23 | ) { 24 | if (visible) { 25 | AlertDialog( 26 | properties = properties, 27 | modifier = modifier, 28 | onDismissRequest = onDismissRequest, 29 | icon = icon, 30 | title = title, 31 | text = text, 32 | confirmButton = confirmButton, 33 | dismissButton = dismissButton, 34 | ) 35 | } 36 | } 37 | 38 | @Composable 39 | fun ConfirmDialog(visible: Boolean, title: String, content: String, onDismissRequest: () -> Unit, onConfirmRequest: () -> Unit) { 40 | RYDialog( 41 | visible = visible, 42 | properties = DialogProperties(), 43 | title = { 44 | Text(text = title) 45 | }, 46 | text = { 47 | Text(text = content) 48 | }, 49 | confirmButton = { 50 | TextButton( 51 | onClick = { 52 | onConfirmRequest() 53 | } 54 | ) { 55 | Text(stringResource(R.string.ok)) 56 | } 57 | }, 58 | dismissButton = { 59 | TextButton( 60 | onClick = { 61 | onDismissRequest() 62 | } 63 | ) { 64 | Text(stringResource(R.string.cancel)) 65 | } 66 | }, 67 | ) 68 | } 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/component/RYScaffold.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.component 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.navigationBarsPadding 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.sharp.ArrowBackIosNew 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.IconButton 13 | import androidx.compose.material3.Scaffold 14 | import androidx.compose.material3.SnackbarHost 15 | import androidx.compose.material3.SnackbarHostState 16 | import androidx.compose.material3.Text 17 | import androidx.compose.material3.TopAppBar 18 | import androidx.compose.material3.TopAppBarDefaults 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.unit.sp 24 | import androidx.navigation.NavHostController 25 | import com.ldlywt.note.R 26 | import com.ldlywt.note.ui.page.router.debouncedPopBackStack 27 | import com.moriafly.salt.ui.SaltTheme 28 | 29 | @OptIn(ExperimentalMaterial3Api::class) 30 | @Composable 31 | fun RYScaffold( 32 | title: String?, 33 | navController: NavHostController?, 34 | containerColor: Color = SaltTheme.colors.background, 35 | actions: @Composable() (RowScope.() -> Unit)? = null, 36 | bottomBar: @Composable() (() -> Unit)? = null, 37 | floatingActionButton: @Composable() (() -> Unit)? = null, 38 | snackBarHostState: SnackbarHostState? = null, 39 | content: @Composable () -> Unit = {}, 40 | ) { 41 | Scaffold( 42 | containerColor = containerColor, 43 | topBar = { 44 | if (title != null) { 45 | TopAppBar( 46 | title = { Text(text = title, style = SaltTheme.textStyles.main.copy(fontSize = 24.sp)) }, 47 | navigationIcon = { 48 | if (navController != null) { 49 | IconButton(onClick = { navController.debouncedPopBackStack() }) { 50 | Icon( 51 | imageVector = Icons.Sharp.ArrowBackIosNew, 52 | contentDescription = stringResource(R.string.back), 53 | tint = SaltTheme.colors.text, 54 | ) 55 | } 56 | 57 | } 58 | }, 59 | actions = { actions?.invoke(this) }, 60 | colors = TopAppBarDefaults.topAppBarColors( 61 | containerColor = Color.Transparent, 62 | ), 63 | ) 64 | } 65 | }, 66 | content = { 67 | Column(modifier = Modifier.navigationBarsPadding()) { 68 | Spacer(modifier = Modifier.height(it.calculateTopPadding())) 69 | content() 70 | } 71 | }, 72 | bottomBar = { bottomBar?.invoke() }, 73 | floatingActionButton = { floatingActionButton?.invoke() }, 74 | snackbarHost = { 75 | snackBarHostState?.let { 76 | SnackbarHost(hostState = it) 77 | } 78 | } 79 | ) 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/component/StateHandler.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.component 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.ldlywt.note.utils.Resource 5 | 6 | @Composable 7 | fun StateHandler( 8 | state: Resource, 9 | onLoading: @Composable (Resource) -> Unit, 10 | onFailure: @Composable (Resource) -> Unit, 11 | onSuccess: @Composable (Resource) -> Unit 12 | ){ 13 | 14 | if(state is Resource.Loading){ 15 | onLoading(state) 16 | } 17 | if(state is Resource.Error){ 18 | onFailure(state) 19 | } 20 | onSuccess(state) 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/db/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.db 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.ldlywt.note.bean.* 7 | import com.ldlywt.note.db.dao.NoteDao 8 | import com.ldlywt.note.db.dao.NoteTagCrossRefDao 9 | import com.ldlywt.note.db.dao.TagDao 10 | import com.ldlywt.note.db.dao.TagNoteDao 11 | 12 | @Database( 13 | entities = [ 14 | Note::class, 15 | Tag::class, 16 | NoteTagCrossRef::class, 17 | Comment::class, 18 | Reminder::class, 19 | ], version = 1, exportSchema = false 20 | ) 21 | @TypeConverters(DatabaseConverters::class) 22 | abstract class AppDatabase : RoomDatabase() { 23 | //创建DAO的抽象类 24 | abstract fun getNoteDao(): NoteDao 25 | abstract fun getTagDao(): TagDao 26 | abstract fun getTagNote(): TagNoteDao 27 | abstract fun getNoteTagCrossRefDao(): NoteTagCrossRefDao 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/db/DatabaseConverters.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.db 2 | 3 | import androidx.room.TypeConverter 4 | import com.ldlywt.note.bean.Attachment 5 | import kotlinx.serialization.decodeFromString 6 | import kotlinx.serialization.encodeToString 7 | import kotlinx.serialization.json.Json 8 | 9 | object DatabaseConverters { 10 | @TypeConverter 11 | fun jsonFromAttachments(attachments: List): String = Json.encodeToString(attachments) 12 | 13 | @TypeConverter 14 | fun attachmentsFromJson(json: String): List = Json.decodeFromString(json) 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/db/dao/NoteDao.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.db.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import androidx.room.Transaction 9 | import androidx.room.Update 10 | import com.ldlywt.note.bean.Note 11 | import com.ldlywt.note.bean.NoteShowBean 12 | import kotlinx.coroutines.flow.Flow 13 | 14 | @Dao 15 | interface NoteDao { 16 | 17 | @Insert(onConflict = OnConflictStrategy.REPLACE) 18 | fun insert(note: Note): Long 19 | 20 | @Insert(onConflict = OnConflictStrategy.REPLACE) 21 | fun insert(list: List) 22 | 23 | @Delete 24 | fun delete(note: Note) 25 | 26 | @Update 27 | fun update(note: Note) 28 | 29 | @Transaction 30 | @Query("SELECT * FROM Note order by update_time desc") 31 | fun queryAll(): Flow> 32 | 33 | @Transaction 34 | @Query("SELECT * FROM Note order by update_time desc") 35 | fun queryAllData(): List 36 | 37 | @Query("select * from Note where note_id =:id") 38 | fun queryById(id: Int): Note 39 | 40 | @Query("select count(*) from Note") 41 | fun getCount(): Int 42 | 43 | @Query("delete from Note") 44 | fun deleteAll() 45 | @Transaction 46 | @Query("SELECT DISTINCT location_info FROM Note WHERE location_info IS NOT NULL AND location_info != ''") 47 | fun getAllLocationInfo(): Flow> 48 | @Transaction 49 | @Query("SELECT * FROM Note WHERE location_info = :targetInfo") 50 | fun getNotesByLocationInfo(targetInfo: String): Flow> 51 | @Transaction 52 | @Query("SELECT * FROM Note WHERE strftime('%Y-%m', create_time/1000) = :yearMonth") 53 | fun queryAllNotesByYearMonth(yearMonth: String): Flow> 54 | 55 | @Query("UPDATE Note SET location_info = NULL WHERE location_info = :locationInfo") 56 | fun clearLocationInfo(locationInfo: String) 57 | 58 | } 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/db/dao/NoteTagCrossRefDao.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.db.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import com.ldlywt.note.bean.NoteTagCrossRef 8 | 9 | @Dao 10 | interface NoteTagCrossRefDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.IGNORE) 13 | fun insertNoteTagCrossRef(entity: NoteTagCrossRef) 14 | 15 | @Delete 16 | fun deleteCrossRef(entity: NoteTagCrossRef) 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/db/dao/TagDao.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.db.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import androidx.room.Transaction 9 | import androidx.room.Update 10 | import com.ldlywt.note.bean.Tag 11 | import kotlinx.coroutines.flow.Flow 12 | 13 | @Dao 14 | interface TagDao { 15 | @Transaction 16 | fun insertOrUpdate(tag: Tag) { 17 | val oldTag = getByName(tag.tag) 18 | if (oldTag == null) { 19 | tag.count = 1 20 | insert(tag) 21 | } else { 22 | tag.count = ++oldTag.count 23 | update(tag) 24 | } 25 | } 26 | 27 | @Transaction 28 | fun deleteOrUpdate(tag: Tag) { 29 | tag.count = --tag.count 30 | if (tag.count <= 0) { 31 | delete(tag) 32 | } else { 33 | update(tag) 34 | } 35 | } 36 | 37 | @Insert(onConflict = OnConflictStrategy.REPLACE) 38 | fun insert(tag: Tag): Long 39 | 40 | @Delete 41 | fun delete(note: Tag) 42 | 43 | @Update 44 | fun update(note: Tag) 45 | 46 | @Transaction 47 | @Query("SELECT * FROM Tag WHERE tag IS NOT NULL AND tag != '' AND is_city_tag IS 0 order by update_time desc") 48 | fun queryAll(): Flow> 49 | 50 | @Transaction 51 | @Query("SELECT * FROM Tag order by update_time desc") 52 | fun queryAllTagList(): List 53 | 54 | @Query("SELECT * FROM Tag WHERE tag =:name LIMIT 1") 55 | fun getByName(name: String): Tag 56 | 57 | @Query("select count(*) from Tag") 58 | fun getCount(): Int 59 | 60 | @Query("delete from Tag") 61 | fun deleteAll() 62 | } 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/db/dao/TagNoteDao.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.db.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import androidx.room.RawQuery 6 | import androidx.room.RewriteQueriesToDropUnusedColumns 7 | import androidx.room.Transaction 8 | import androidx.sqlite.db.SimpleSQLiteQuery 9 | import com.ldlywt.note.bean.Note 10 | import com.ldlywt.note.bean.NoteShowBean 11 | import com.ldlywt.note.bean.NoteTagCrossRef 12 | import com.ldlywt.note.bean.Reminder 13 | import com.ldlywt.note.bean.Tag 14 | import kotlinx.coroutines.flow.Flow 15 | 16 | @Dao 17 | interface TagNoteDao { 18 | @Transaction 19 | @RawQuery( 20 | observedEntities = [ 21 | Note::class, 22 | Tag::class, 23 | Reminder::class, 24 | NoteTagCrossRef::class, 25 | ] 26 | ) 27 | fun rawGetQueryFlow(query: SimpleSQLiteQuery): Flow> 28 | 29 | fun getAll(sortTime: String, order: String): Flow> { 30 | return rawGetQueryFlow( 31 | SimpleSQLiteQuery("SELECT * FROM Note order by $sortTime $order") 32 | ) 33 | } 34 | 35 | @Transaction 36 | @Query("SELECT * FROM Note order by update_time desc") 37 | fun getAllNoteByUpdateTime(): Flow> 38 | 39 | @Transaction 40 | @Query("SELECT * FROM Note order by create_time desc") 41 | fun getAllNoteByCreateTime(): Flow> 42 | 43 | @Transaction 44 | @Query("SELECT * FROM Note order by update_time desc") 45 | fun getAllNoteWithTagList(): List 46 | 47 | @Transaction 48 | @Query("SELECT * FROM Note ORDER BY RANDOM()") 49 | fun getAllRandom(): Flow> 50 | 51 | @Transaction 52 | @Query( 53 | "SELECT COUNT(*) FROM Note n1 join NoteTagCrossRef ncr on " + "n1.note_id=ncr.note_id where ncr.tag=:tagName order" + " by n1.update_time desc" 54 | ) 55 | fun countNoteListWithByTag(tagName: String): Int 56 | 57 | @RewriteQueriesToDropUnusedColumns 58 | @Transaction 59 | @Query( 60 | """ 61 | SELECT * FROM Note 62 | INNER JOIN NoteTagCrossRef ON Note.note_id = NoteTagCrossRef.note_id 63 | WHERE NoteTagCrossRef.tag = :tagName 64 | order by Note.update_time desc 65 | """ 66 | ) 67 | fun getNoteListWithByTag(tagName: String): Flow> 68 | 69 | @Query( 70 | """ 71 | SELECT * FROM Tag 72 | INNER JOIN NoteTagCrossRef ON Tag.tag = NoteTagCrossRef.tag 73 | WHERE NoteTagCrossRef.note_id = :noteId 74 | """ 75 | ) 76 | fun getTagListByNoteId(noteId: Long): Flow> 77 | 78 | @Transaction 79 | @Query("SELECT * FROM Note WHERE note_id = :noteId") 80 | fun getNoteShowBeanById(noteId: Long): NoteShowBean? 81 | 82 | @Transaction 83 | @Query("SELECT * FROM Note WHERE date(create_time / 1000, 'unixepoch') = :selectedDate") 84 | fun getNoteShowOnDate(selectedDate: String): List 85 | 86 | @Transaction 87 | @Query("SELECT DISTINCT strftime('%Y', datetime(create_time/1000, 'unixepoch')) AS year FROM Note ORDER BY year DESC") 88 | suspend fun getAllDistinctYears(): List 89 | 90 | @Transaction 91 | @Query("SELECT * FROM Note WHERE strftime('%Y', datetime(create_time/1000, 'unixepoch')) = :year") 92 | fun getNotesByYear(year: String): Flow> 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/db/repo/TagNoteRepo.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.db.repo 2 | 3 | import androidx.room.Query 4 | import androidx.room.Transaction 5 | import com.ldlywt.note.bean.Note 6 | import com.ldlywt.note.bean.NoteShowBean 7 | import com.ldlywt.note.bean.NoteTagCrossRef 8 | import com.ldlywt.note.bean.Tag 9 | import com.ldlywt.note.db.dao.NoteDao 10 | import com.ldlywt.note.db.dao.NoteTagCrossRefDao 11 | import com.ldlywt.note.db.dao.TagDao 12 | import com.ldlywt.note.db.dao.TagNoteDao 13 | import com.ldlywt.note.utils.CityRegexUtils 14 | import com.ldlywt.note.utils.TopicUtils 15 | import com.ldlywt.note.ui.page.SortTime 16 | import kotlinx.coroutines.flow.Flow 17 | 18 | class TagNoteRepo( 19 | private val noteDao: NoteDao, 20 | private val tagNoteDao: TagNoteDao, 21 | private val tagDao: TagDao, 22 | private val noteTagCrossRefDao: NoteTagCrossRefDao 23 | ) { 24 | 25 | fun queryAllNoteList(): List { 26 | return noteDao.queryAllData() 27 | } 28 | 29 | fun queryAllTagList(): List { 30 | return tagDao.queryAllTagList().filterNot { it.tag.isBlank() } 31 | } 32 | 33 | fun queryAllTagFlow(): Flow> = tagDao.queryAll() 34 | 35 | fun updateTag(tag: Tag) { 36 | tagDao.update(tag) 37 | } 38 | 39 | fun queryAllMemosFlow(sortTime: String): Flow> { 40 | return when (sortTime) { 41 | SortTime.UPDATE_TIME_DESC.name -> { 42 | tagNoteDao.getAll("update_time", "desc") 43 | } 44 | 45 | SortTime.UPDATE_TIME_ASC.name -> { 46 | tagNoteDao.getAll("update_time", "asc") 47 | } 48 | 49 | SortTime.CREATE_TIME_DESC.name -> { 50 | tagNoteDao.getAll("create_time", "desc") 51 | } 52 | 53 | SortTime.CREATE_TIME_ASC.name -> { 54 | tagNoteDao.getAll("create_time", "asc") 55 | } 56 | 57 | else -> { 58 | tagNoteDao.getAll("update_time", "desc") 59 | } 60 | } 61 | } 62 | 63 | fun getAllRandom(): Flow> = tagNoteDao.getAllRandom() 64 | 65 | fun queryAllNoteShowBeanList(): List = tagNoteDao.getAllNoteWithTagList() 66 | 67 | fun countNoteListWithByTag(tagName: String): Int { 68 | return tagNoteDao.countNoteListWithByTag(tagName) 69 | } 70 | 71 | fun getNoteListWithByTag(tagName: String): Flow> { 72 | return tagNoteDao.getNoteListWithByTag(tagName) 73 | } 74 | 75 | fun getTagListByNoteId(noteId: Long): Flow> { 76 | return tagNoteDao.getTagListByNoteId(noteId) 77 | } 78 | 79 | @Query("") 80 | @Transaction 81 | fun insertOrUpdate(card: Note) { 82 | val tagList = TopicUtils.getTopicListByString(card.content) 83 | if (card.locationInfo.isNullOrBlank()) { 84 | val pair = CityRegexUtils.getCityByString(card.content.trim()) 85 | card.locationInfo = pair?.second 86 | if (card.locationInfo != null) { 87 | card.content = pair?.first ?: "" 88 | } 89 | } 90 | val noteId = noteDao.insert(card) 91 | if (tagList.isEmpty()) { 92 | val tempTag = Tag(tag = "") 93 | tagDao.insertOrUpdate(tempTag) 94 | noteTagCrossRefDao.insertNoteTagCrossRef(NoteTagCrossRef(noteId = noteId, tag = tempTag.tag)) 95 | return 96 | } 97 | tagList.forEach { 98 | tagDao.insertOrUpdate(it) 99 | noteTagCrossRefDao.insertNoteTagCrossRef(NoteTagCrossRef(noteId = noteId, tag = it.tag)) 100 | } 101 | } 102 | 103 | @Query("") 104 | @Transaction 105 | fun deleteNote(card: Note, tags: List) { 106 | noteDao.delete(card) 107 | tags.forEach { 108 | tagDao.deleteOrUpdate(it) 109 | noteTagCrossRefDao.deleteCrossRef(NoteTagCrossRef(noteId = card.noteId, tag = it.tag)) 110 | } 111 | } 112 | 113 | fun queryNoteById(noteId: Int): Note = noteDao.queryById(noteId) 114 | 115 | fun getNoteShowBeanById(noteId: Long): NoteShowBean? = tagNoteDao.getNoteShowBeanById(noteId) 116 | 117 | fun getNotesOnSelectedDate(selectedDate: String): List = tagNoteDao.getNoteShowOnDate(selectedDate) 118 | 119 | suspend fun getAllDistinctYears(): List { 120 | return tagNoteDao.getAllDistinctYears() 121 | } 122 | 123 | fun getNotesByYear(year: String): Flow> = tagNoteDao.getNotesByYear(year) 124 | 125 | fun getAllLocationInfo(): Flow> = noteDao.getAllLocationInfo() 126 | 127 | fun getNotesByLocationInfo(targetInfo: String): Flow> = noteDao.getNotesByLocationInfo(targetInfo) 128 | 129 | fun clearLocationInfo(locationInfo: String) { 130 | noteDao.clearLocationInfo(locationInfo) 131 | } 132 | 133 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/hilt/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.hilt 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import androidx.room.RoomDatabase 6 | import androidx.sqlite.db.SupportSQLiteDatabase 7 | import com.ldlywt.note.db.AppDatabase 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.android.qualifiers.ApplicationContext 12 | import dagger.hilt.components.SingletonComponent 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object DatabaseModule { 18 | 19 | @Provides 20 | @Singleton 21 | fun provideRoomDatabase( 22 | @ApplicationContext context: Context, 23 | ): AppDatabase { 24 | return buildDatabase(context) 25 | } 26 | 27 | private const val DATABASE_NAME = "ssndb" 28 | 29 | private fun buildDatabase(context: Context) = 30 | Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DATABASE_NAME).fallbackToDestructiveMigration().addCallback(CALLBACK).build() 31 | 32 | private val CALLBACK = object : RoomDatabase.Callback() { 33 | override fun onCreate(db: SupportSQLiteDatabase) { 34 | super.onCreate(db) 35 | // db.execSQL( 36 | // "CREATE TRIGGER IF NOT EXISTS Trigger_AfterDelete_Note AFTER DELETE on Note FOR EACH ROW \n" + 37 | // "BEGIN \n" + 38 | // "DELETE FROM NoteTagCrossRef WHERE note_id=OLD.note_id;\n" + 39 | // "END" 40 | // ) 41 | db.execSQL( 42 | "CREATE TRIGGER IF NOT EXISTS Trigger_AfterDelete_Note_Tag AFTER DELETE on NoteTagCrossRef WHEN (SELECT count(*) FROM NoteTagCrossRef WHERE tag=OLD.tag)=0 \n" + "BEGIN \n" + "DELETE FROM Tag WHERE tag=OLD.tag;\n" + "END" 43 | ) 44 | 45 | db.execSQL( 46 | "CREATE TRIGGER IF NOT EXISTS Trigger_AfterDelete_Tag_Note AFTER DELETE on NoteTagCrossRef WHEN (SELECT count(*) FROM NoteTagCrossRef WHERE note_id=OLD.note_id)=0 \n" + "BEGIN \n" + "DELETE FROM Note WHERE note_id=OLD.note_id;\n" + "END" 47 | ) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/hilt/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.hilt 2 | 3 | import com.ldlywt.note.db.AppDatabase 4 | import com.ldlywt.note.db.repo.TagNoteRepo 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | class RepositoryModule { 14 | 15 | @Provides 16 | @Singleton 17 | fun provideNoteRepository( 18 | appDatabase: AppDatabase, 19 | ) = TagNoteRepo(appDatabase.getNoteDao(), appDatabase.getTagNote(), appDatabase.getTagDao(), appDatabase.getNoteTagCrossRefDao()) 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/hilt/SyncModule.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.hilt 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.ldlywt.note.backup.SyncManager 6 | import com.ldlywt.note.backup.api.Encryption 7 | import com.ldlywt.note.backup.utils.DefaultEncryption 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.android.qualifiers.ApplicationContext 12 | import dagger.hilt.components.SingletonComponent 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object SyncModule { 18 | 19 | @Provides 20 | @Singleton 21 | fun providesSyncManager( 22 | @ApplicationContext context: Context, 23 | ) = SyncManager(context) 24 | 25 | @Provides 26 | fun provideEncryption(): Encryption { 27 | return DefaultEncryption() 28 | } 29 | 30 | @Provides 31 | @Singleton 32 | fun provideContext(application: Application): Context { 33 | return application.applicationContext 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/state/NoteState.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.state 2 | 3 | import com.ldlywt.note.bean.NoteShowBean 4 | 5 | data class NoteState( 6 | val notes: List = emptyList(), 7 | val title: String = "", 8 | val content: String = "", 9 | val searchQuery: String = "", 10 | val isSearching: Boolean = false, 11 | val editingNote: NoteShowBean? = null, 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/PictureDisplayPage.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.pager.HorizontalPager 8 | import androidx.compose.foundation.pager.rememberPagerState 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.filled.ArrowBack 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.IconButton 14 | import androidx.compose.material3.Surface 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.layout.ContentScale 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.unit.dp 21 | import androidx.navigation.NavHostController 22 | import coil.compose.AsyncImage 23 | import coil.request.ImageRequest 24 | import com.ldlywt.note.ui.page.router.debouncedPopBackStack 25 | import com.ldlywt.note.utils.BlurTransformation 26 | 27 | @OptIn(ExperimentalMaterial3Api::class) 28 | @Composable 29 | fun PictureDisplayPage( 30 | pathList: List, index: Int, 31 | navController: NavHostController 32 | ) { 33 | val pagerState = rememberPagerState(pageCount = { pathList.size }, initialPage = index) // 定义10个页面 34 | 35 | Box { 36 | HorizontalPager(state = pagerState) { page -> 37 | Surface(modifier = Modifier.fillMaxSize()) { 38 | DetailContent( 39 | imgUrl = pathList[page], 40 | requestImage = { 41 | Image(pathList[page]) 42 | } 43 | ) 44 | } 45 | } 46 | IconButton(onClick = { navController.debouncedPopBackStack() }, modifier = Modifier.padding(start = 12.dp, top = 24.dp, end = 0.dp, bottom = 0.dp)) { 47 | Icon( 48 | imageVector = Icons.Filled.ArrowBack, 49 | contentDescription = null, 50 | tint = Color(0xFFFFFFFF) 51 | ) 52 | } 53 | } 54 | } 55 | 56 | @Composable 57 | private fun DetailContent( 58 | imgUrl: String?, 59 | modifier: Modifier = Modifier, 60 | requestImage: @Composable () -> Unit 61 | ) { 62 | Surface { 63 | AsyncImage( 64 | model = ImageRequest.Builder(LocalContext.current) 65 | .data(imgUrl) 66 | .transformations( 67 | BlurTransformation( 68 | LocalContext.current, 69 | radius = 25f, 70 | sampling = 5f 71 | ) 72 | ) 73 | .crossfade(true) 74 | .build(), 75 | contentDescription = null, 76 | modifier = modifier.fillMaxWidth(), 77 | contentScale = ContentScale.Crop, 78 | ) 79 | 80 | requestImage() 81 | } 82 | } 83 | 84 | @Composable 85 | private fun Image(imgUrl: String) { 86 | AsyncImage( 87 | model = ImageRequest.Builder(LocalContext.current) 88 | .data(imgUrl) 89 | .crossfade(true) 90 | .build(), 91 | contentDescription = null, 92 | contentScale = ContentScale.Fit, 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/data/DataManagerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.data 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import androidx.core.content.FileProvider 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.asLiveData 8 | import com.ldlywt.note.App 9 | import com.ldlywt.note.backup.SyncManager 10 | import com.ldlywt.note.backup.model.DavData 11 | import com.ldlywt.note.bean.Note 12 | import com.ldlywt.note.db.repo.TagNoteRepo 13 | import com.ldlywt.note.getAppName 14 | import com.ldlywt.note.utils.BackUp 15 | import com.ldlywt.note.utils.SharedPreferencesUtils 16 | import com.ldlywt.note.utils.backUpFileName 17 | import com.ldlywt.note.utils.withIO 18 | import dagger.hilt.android.lifecycle.HiltViewModel 19 | import kotlinx.coroutines.Dispatchers 20 | import kotlinx.coroutines.withContext 21 | import java.io.File 22 | import javax.inject.Inject 23 | 24 | 25 | @HiltViewModel 26 | class DataManagerViewModel @Inject constructor(private val tagNoteRepo: TagNoteRepo, private val syncManager: SyncManager) : ViewModel() { 27 | 28 | suspend fun restoreForWebdav(): List = withIO { 29 | val dataList = syncManager.listAllFile(getAppName() + "/").filterNotNull().filter { it.name.endsWith(".zip") }.sortedByDescending { it.name } 30 | dataList 31 | } 32 | 33 | suspend fun downloadFileByPath(davData: DavData): String? = withIO { 34 | syncManager.downloadFileByPath(davData.path.substringAfterLast("/dav/"), App.instance.cacheDir.absolutePath) 35 | } 36 | 37 | 38 | val isLogin = SharedPreferencesUtils.davLoginSuccess.asLiveData().value ?: false 39 | 40 | 41 | suspend fun exportToWebdav(context: Context): String = withIO { 42 | val (filename, file, _) = generateZipFile(context, backUpFileName) 43 | val resultStr = syncManager.uploadFile(filename, getAppName(), file) 44 | if (resultStr.startsWith("Success")) { 45 | File(filename).delete() 46 | } 47 | resultStr 48 | } 49 | 50 | private suspend fun generateZipFile(context: Context, fileName: String): Triple = withContext(Dispatchers.IO) { 51 | val file = File(context.cacheDir, fileName) 52 | val uri = FileProvider.getUriForFile(context, "com.ldlywt.note.provider", file) 53 | val fileName = BackUp.exportEncrypted(context, uri) 54 | Triple(fileName, file, uri) 55 | } 56 | 57 | suspend fun fixTag() = withContext(Dispatchers.IO) { 58 | val dataList: List = tagNoteRepo.queryAllNoteList() 59 | dataList.forEach(tagNoteRepo::insertOrUpdate) 60 | tagNoteRepo.queryAllTagList().forEach { tag -> 61 | val count = tagNoteRepo.countNoteListWithByTag(tag.tag) 62 | tag.count = count 63 | tagNoteRepo.updateTag(tag) 64 | } 65 | } 66 | 67 | suspend fun checkConnection(url: String, account: String, pwd: String): Pair = withContext(Dispatchers.IO) { 68 | syncManager.checkConnection(url, account, pwd) 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/input/InputImage.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.input 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.aspectRatio 6 | import androidx.compose.foundation.layout.fillMaxHeight 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.outlined.Delete 10 | import androidx.compose.material3.DropdownMenu 11 | import androidx.compose.material3.DropdownMenuItem 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.rememberCoroutineScope 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.draw.clip 22 | import androidx.compose.ui.layout.ContentScale 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.window.PopupProperties 25 | import androidx.compose.ui.zIndex 26 | import coil.compose.AsyncImage 27 | import com.ldlywt.note.R 28 | import com.ldlywt.note.bean.Attachment 29 | import com.ldlywt.note.utils.str 30 | import kotlinx.coroutines.launch 31 | 32 | @Composable 33 | fun InputImage( 34 | attachment: Attachment, 35 | isEdit: Boolean, 36 | delete: (path: String) -> Unit, 37 | onclick: () -> Unit = {} 38 | ) { 39 | var menuExpanded by remember { mutableStateOf(false) } 40 | val scope = rememberCoroutineScope() 41 | 42 | Box { 43 | AsyncImage( 44 | model = attachment.path, 45 | contentDescription = null, 46 | modifier = Modifier 47 | .fillMaxHeight() 48 | .aspectRatio(1f) 49 | .zIndex(1f) 50 | .clip(RoundedCornerShape(2.dp)) 51 | .clickable { 52 | if (isEdit) { 53 | menuExpanded = true 54 | } else { 55 | onclick() 56 | } 57 | }, 58 | contentScale = ContentScale.Crop 59 | ) 60 | if (isEdit) { 61 | DropdownMenu( 62 | expanded = menuExpanded, 63 | onDismissRequest = { menuExpanded = false }, 64 | properties = PopupProperties(focusable = false) 65 | ) { 66 | DropdownMenuItem( 67 | text = { Text(R.string.delete.str) }, 68 | onClick = { 69 | scope.launch { 70 | delete(attachment.path) 71 | menuExpanded = false 72 | } 73 | }, 74 | leadingIcon = { 75 | Icon( 76 | Icons.Outlined.Delete, 77 | contentDescription = null 78 | ) 79 | }) 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/input/MemoInputViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.input 2 | 3 | import androidx.compose.runtime.mutableStateListOf 4 | import androidx.lifecycle.ViewModel 5 | import com.ldlywt.note.bean.Attachment 6 | import com.ldlywt.note.db.repo.TagNoteRepo 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import javax.inject.Inject 9 | 10 | @HiltViewModel 11 | class MemoInputViewModel @Inject constructor(private val tagNoteRepo: TagNoteRepo) : ViewModel() { 12 | 13 | fun deleteResource(path: String) { 14 | uploadAttachments.remove(uploadAttachments.firstOrNull { it.path == path }) 15 | } 16 | 17 | var uploadAttachments = mutableStateListOf() 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.main 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.activity.enableEdgeToEdge 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.CompositionLocalProvider 9 | import androidx.compose.runtime.collectAsState 10 | import androidx.compose.runtime.getValue 11 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 12 | import androidx.core.view.ViewCompat 13 | import androidx.hilt.navigation.compose.hiltViewModel 14 | import androidx.lifecycle.lifecycleScope 15 | import com.ldlywt.note.biometric.AppBioMetricManager 16 | import com.ldlywt.note.biometric.BiometricAuthListener 17 | import com.ldlywt.note.state.NoteState 18 | import com.ldlywt.note.ui.page.LocalMemosState 19 | import com.ldlywt.note.ui.page.LocalMemosViewModel 20 | import com.ldlywt.note.ui.page.LocalTags 21 | import com.ldlywt.note.ui.page.NoteViewModel 22 | import com.ldlywt.note.ui.page.router.App 23 | import com.ldlywt.note.utils.FirstTimeManager 24 | import com.ldlywt.note.utils.SharedPreferencesUtils 25 | import dagger.hilt.android.AndroidEntryPoint 26 | import kotlinx.coroutines.Dispatchers 27 | import kotlinx.coroutines.flow.firstOrNull 28 | import kotlinx.coroutines.launch 29 | import javax.inject.Inject 30 | 31 | @AndroidEntryPoint 32 | class MainActivity : AppCompatActivity() { 33 | 34 | @Inject 35 | lateinit var firstTimeManager: FirstTimeManager 36 | 37 | @Inject 38 | lateinit var appBioMetricManager: AppBioMetricManager 39 | 40 | override fun onCreate(savedInstanceState: Bundle?) { 41 | super.onCreate(savedInstanceState) 42 | // 设置全局异常捕获处理 43 | // setGlobalExceptionHandler() 44 | 45 | installSplashScreen() 46 | enableEdgeToEdge() 47 | 48 | //https://github.com/android/compose-samples/issues/1256 49 | ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, insets -> insets } 50 | 51 | firstTimeManager.generateIntroduceNoteList() 52 | 53 | lifecycleScope.launch { 54 | handleAuthentication() 55 | } 56 | } 57 | 58 | // 提取公共的 setContent 逻辑 59 | private fun setupContent() { 60 | setContent { 61 | SettingsProvider { 62 | App() 63 | } 64 | } 65 | } 66 | 67 | private suspend fun handleAuthentication() { 68 | val useSafe = SharedPreferencesUtils.useSafe.firstOrNull() ?: false 69 | if (useSafe && appBioMetricManager.canAuthenticate()) { 70 | showBiometricPrompt { 71 | setupContent() 72 | } 73 | } else { 74 | setupContent() 75 | } 76 | } 77 | 78 | 79 | @Composable 80 | fun SettingsProvider( 81 | noteViewModel: NoteViewModel = hiltViewModel(), 82 | content: @Composable () -> Unit 83 | ) { 84 | val state: NoteState by noteViewModel.state.collectAsState(Dispatchers.IO) 85 | val tags by noteViewModel.tags.collectAsState(Dispatchers.IO) 86 | 87 | CompositionLocalProvider( 88 | LocalMemosViewModel provides noteViewModel, 89 | LocalMemosState provides state, 90 | LocalTags provides tags, 91 | ) { 92 | content() 93 | } 94 | } 95 | 96 | 97 | private fun showBiometricPrompt(success: (Boolean) -> Unit) { 98 | appBioMetricManager.initBiometricPrompt(activity = this, listener = object : BiometricAuthListener { 99 | override fun onBiometricAuthSuccess() { 100 | // 验证完成后显示主界面 101 | success(true) 102 | } 103 | 104 | override fun onUserCancelled() { 105 | finish() 106 | } 107 | 108 | override fun onErrorOccurred() { 109 | finish() 110 | } 111 | }) 112 | } 113 | 114 | } 115 | 116 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/main/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.main 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.displayCutoutPadding 7 | import androidx.compose.foundation.layout.fillMaxHeight 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.pager.HorizontalPager 11 | import androidx.compose.foundation.pager.rememberPagerState 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.rememberCoroutineScope 17 | import androidx.compose.runtime.saveable.rememberSaveable 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.platform.LocalConfiguration 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.navigation.NavHostController 23 | import com.ldlywt.note.ui.page.home.AllNotesPage 24 | import com.ldlywt.note.ui.page.home.CalenderPage 25 | import com.ldlywt.note.ui.page.settings.SettingsPage 26 | import com.ldlywt.note.utils.isWideScreen 27 | import kotlinx.coroutines.launch 28 | 29 | @OptIn(ExperimentalFoundationApi::class) 30 | @Composable 31 | fun MainScreen(navController: NavHostController) { 32 | var currentDestination by rememberSaveable { mutableStateOf(NavigationBarPath.AllNote.route) } 33 | val pagerState = rememberPagerState(initialPage = 0) { NavigationBarPath.entries.size } 34 | val scope = rememberCoroutineScope() 35 | 36 | val configuration = LocalConfiguration.current // 屏幕方向改变会触发 recompose 37 | val context = LocalContext.current 38 | var hideNavBar by rememberSaveable { mutableStateOf(false) } 39 | // 在屏幕方向变化时更新 isWideScreen 40 | val isWideScreen = remember(configuration.orientation) { isWideScreen(context) } 41 | 42 | val navigationBar: @Composable () -> Unit = { 43 | AdaptiveNavigationBar( 44 | destinations = NavigationBarPath.entries, 45 | currentDestination = currentDestination, 46 | onNavigateToDestination = { 47 | currentDestination = NavigationBarPath.entries[it].route 48 | scope.launch { pagerState.scrollToPage(it) } 49 | }, 50 | isWideScreen = isWideScreen, 51 | ) 52 | } 53 | 54 | val pagerContent: @Composable (Modifier) -> Unit = { modifier -> 55 | HorizontalPager( 56 | state = pagerState, 57 | userScrollEnabled = false, 58 | modifier = modifier 59 | ) { page -> 60 | when (page) { 61 | 0 -> AllNotesPage(navController = navController) { hide -> 62 | hideNavBar = hide 63 | } 64 | 1 -> CalenderPage(navController = navController) 65 | 2 -> SettingsPage(navController = navController) 66 | } 67 | } 68 | } 69 | 70 | if (isWideScreen) { 71 | Row( 72 | modifier = Modifier 73 | .fillMaxSize() 74 | .displayCutoutPadding() 75 | ) { 76 | if (!hideNavBar) { 77 | navigationBar() 78 | } 79 | pagerContent( 80 | Modifier 81 | .fillMaxHeight() 82 | .weight(1f) 83 | ) 84 | } 85 | } else { 86 | Column(modifier = Modifier.fillMaxSize()) { 87 | pagerContent( 88 | Modifier 89 | .fillMaxWidth() 90 | .weight(1f) 91 | ) 92 | if (!hideNavBar) { 93 | navigationBar() 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/router/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.router 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | @Serializable 7 | sealed class Screen { 8 | 9 | @Serializable 10 | data object Main : Screen() 11 | 12 | @Serializable 13 | data object Explore : Screen() 14 | 15 | @Serializable 16 | data class InputDetail(val id: Long) : Screen() 17 | 18 | @Serializable 19 | object TagList : Screen() 20 | 21 | @Serializable 22 | data class TagDetail(val tag: String) : Screen() 23 | 24 | @Serializable 25 | data class YearDetail(val year: String) : Screen() 26 | 27 | @Serializable 28 | data class LocationDetail(val location: String) : Screen() 29 | 30 | @Serializable 31 | object Search : Screen() 32 | 33 | @Serializable 34 | data class Share(val id: Long) : Screen() 35 | 36 | @Serializable 37 | object DataManager : Screen() 38 | 39 | @Serializable 40 | object DataCloudConfig : Screen() 41 | 42 | @Serializable 43 | object RandomWalk : Screen() 44 | 45 | @Serializable 46 | object Gallery : Screen() 47 | 48 | @Serializable 49 | data class PictureDisplay(val pathList: List, val curIndex: Int) : Screen() 50 | 51 | @Serializable 52 | object MoreInfo : Screen() 53 | 54 | @Serializable 55 | object LocationList : Screen() 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/search/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.search 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.ldlywt.note.bean.NoteShowBean 6 | import com.ldlywt.note.db.repo.TagNoteRepo 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.flow.* 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | @HiltViewModel 14 | class SearchViewModel @Inject constructor(private val tagNoteRepo: TagNoteRepo) : ViewModel() { 15 | 16 | private val _query: MutableStateFlow = MutableStateFlow(value = "") 17 | val query: StateFlow 18 | get() = _query 19 | 20 | 21 | fun clearSearchQuery() { 22 | _query.value = "" 23 | dataFlow.value = emptyList() 24 | } 25 | 26 | fun onQuery(query: String) { 27 | _query.value = query 28 | } 29 | 30 | lateinit var notes: List 31 | 32 | init { 33 | 34 | viewModelScope.launch(Dispatchers.IO) { 35 | notes = tagNoteRepo.queryAllNoteShowBeanList() 36 | } 37 | } 38 | 39 | val dataFlow: MutableStateFlow> = MutableStateFlow(value = emptyList()) 40 | 41 | private fun getSearchResults( 42 | searchKey: String, 43 | notes: List, 44 | ): List = notes.filter { note -> 45 | fun String.matches(): Boolean = contains(searchKey.trim(), true) 46 | note.note.content.matches() || note.note.attachments.any { it.description.matches() } || note.tagList.any { it.tag.matches() } 47 | } 48 | 49 | fun onSearch(str: String) { 50 | dataFlow.value = getSearchResults(str, notes) 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/settings/AboutPage.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.settings 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.text.font.FontWeight 12 | import androidx.compose.ui.unit.dp 13 | import androidx.compose.ui.unit.sp 14 | import com.ldlywt.note.R 15 | import com.moriafly.salt.ui.SaltTheme 16 | 17 | @Composable 18 | fun AboutComposeScreen() { 19 | Column( 20 | modifier = Modifier.fillMaxWidth(), 21 | verticalArrangement = Arrangement.Top, 22 | ) { 23 | Text( 24 | color = SaltTheme.colors.text, 25 | text = stringResource(id = R.string.author), 26 | modifier = Modifier.padding(top = 8.dp), 27 | ) 28 | Text( 29 | color = SaltTheme.colors.text, 30 | text = stringResource(id = R.string.email), 31 | modifier = Modifier.padding(top = 8.dp), 32 | ) 33 | Text( 34 | color = SaltTheme.colors.text, 35 | text = stringResource(id = R.string.about_icon), 36 | modifier = Modifier.padding(top = 24.dp), 37 | fontSize = 18.sp, 38 | fontWeight = FontWeight.Bold 39 | ) 40 | Text( 41 | color = SaltTheme.colors.text, 42 | text = "https://www.iconfont.cn/", 43 | modifier = Modifier.padding(top = 8.dp), 44 | ) 45 | Text( 46 | color = SaltTheme.colors.text, 47 | text = "https://www.flaticon.com/", 48 | modifier = Modifier.padding(top = 8.dp), 49 | ) 50 | Text( 51 | color = SaltTheme.colors.text, 52 | text = "https://iconpark.oceanengine.com/", 53 | modifier = Modifier.padding(top = 8.dp), 54 | ) 55 | Text( 56 | color = SaltTheme.colors.text, 57 | text = "UI", 58 | modifier = Modifier.padding(top = 24.dp), 59 | fontSize = 18.sp, 60 | fontWeight = FontWeight.Bold 61 | ) 62 | Text( 63 | color = SaltTheme.colors.text, 64 | text = "https://github.com/Moriafly/SaltUI", 65 | modifier = Modifier.padding(top = 8.dp), 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/settings/ExplorePage.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.settings 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.LocalConfiguration 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import androidx.navigation.NavHostController 21 | import com.ldlywt.note.R 22 | import com.ldlywt.note.bean.Note 23 | import com.ldlywt.note.component.DraggableCard 24 | import com.ldlywt.note.component.EmptyComponent 25 | import com.ldlywt.note.component.ImageCard 26 | import com.ldlywt.note.component.locationAndTimeText 27 | import com.ldlywt.note.component.showLocationInfoContent 28 | import com.ldlywt.note.ui.page.LocalMemosState 29 | import com.ldlywt.note.ui.page.router.Screen 30 | import com.ldlywt.note.ui.page.router.debouncedPopBackStack 31 | import com.ldlywt.note.utils.orFalse 32 | import com.ldlywt.note.utils.toTime 33 | import com.moriafly.salt.ui.SaltTheme 34 | import com.moriafly.salt.ui.TitleBar 35 | import com.moriafly.salt.ui.UnstableSaltApi 36 | import dev.jeziellago.compose.markdowntext.MarkdownText 37 | 38 | 39 | @OptIn(ExperimentalMaterial3Api::class, UnstableSaltApi::class) 40 | @Composable 41 | fun ExplorePage( 42 | navHostController: NavHostController 43 | ) { 44 | val noteState = LocalMemosState.current 45 | val shuffledList = noteState.notes.shuffled().map { it.note }.map { it.copy() }.take(20).toMutableList() 46 | 47 | Column( 48 | modifier = Modifier 49 | .fillMaxSize() 50 | .background(color = SaltTheme.colors.background) 51 | .padding(top = 30.dp) 52 | ) { 53 | TitleBar( 54 | onBack = { 55 | navHostController.debouncedPopBackStack() 56 | }, 57 | text = stringResource(R.string.random_walk) 58 | ) 59 | 60 | ExploreList(memos = shuffledList, navHostController, onItemClick = { index -> 61 | navHostController.navigate(route = Screen.InputDetail(shuffledList[index].noteId)) 62 | }) 63 | } 64 | 65 | } 66 | 67 | @OptIn(ExperimentalFoundationApi::class) 68 | @Composable 69 | fun ExploreList( 70 | memos: MutableList, navHostController: NavHostController, onItemClick: (index: Int) -> Unit 71 | ) { 72 | 73 | val configuration = LocalConfiguration.current 74 | val screenHeight = configuration.screenHeightDp.dp 75 | val cardHeight = screenHeight - 200.dp 76 | val listEmpty = remember { mutableStateOf(false) } 77 | if (listEmpty.value) { 78 | EmptyComponent() 79 | } 80 | memos.forEachIndexed { index, note -> 81 | DraggableCard( 82 | item = note, 83 | modifier = Modifier 84 | .fillMaxWidth() 85 | .height(cardHeight) 86 | .padding( 87 | top = 16.dp + (index + 2).dp, 88 | bottom = 16.dp, 89 | start = 16.dp, 90 | end = 16.dp 91 | ), 92 | onSwiped = { _, note -> 93 | if (memos.isNotEmpty().orFalse()) { 94 | memos.remove(note) 95 | if (memos.isEmpty().orFalse()) { 96 | listEmpty.value = true 97 | } 98 | } 99 | } 100 | ) { 101 | ExploreMemoCard(note, navHostController) 102 | } 103 | } 104 | } 105 | 106 | @OptIn(ExperimentalFoundationApi::class) 107 | @Composable 108 | fun ExploreMemoCard( 109 | note: Note, navHostController: NavHostController 110 | ) { 111 | 112 | Column( 113 | modifier = Modifier 114 | .fillMaxWidth() 115 | .background(color = SaltTheme.colors.subBackground) 116 | .padding(16.dp) 117 | ) { 118 | Spacer(modifier = Modifier.weight(1f)) 119 | MarkdownText(markdown = note.content, style = SaltTheme.textStyles.paragraph.copy(fontSize = 15.sp, lineHeight = 24.sp), onTagClick = { 120 | navHostController.navigate(Screen.TagDetail(it)) 121 | }) 122 | Spacer(modifier = Modifier.height(12.dp)) 123 | if (note.attachments.isNotEmpty()) { 124 | ImageCard(note, navHostController) 125 | Spacer(modifier = Modifier.height(12.dp)) 126 | } 127 | Spacer(modifier = Modifier.weight(1f)) 128 | locationAndTimeText(note.createTime.toTime(), modifier = Modifier.padding(start = 2.dp)) 129 | showLocationInfoContent(note) 130 | } 131 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/settings/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.settings 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.ldlywt.note.biometric.AppBioMetricManager 7 | import com.ldlywt.note.biometric.BiometricAuthListener 8 | 9 | import com.ldlywt.note.ui.page.main.MainActivity 10 | import com.ldlywt.note.utils.SharedPreferencesUtils 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.StateFlow 15 | import kotlinx.coroutines.flow.first 16 | import kotlinx.coroutines.launch 17 | import javax.inject.Inject 18 | 19 | @HiltViewModel 20 | class SettingsViewModel @Inject constructor( 21 | application: Application, 22 | private val appBioMetricManager: AppBioMetricManager, 23 | ) : AndroidViewModel(application) { 24 | 25 | private val _biometricAuthState = MutableStateFlow(false) 26 | val biometricAuthState: StateFlow = _biometricAuthState 27 | 28 | init { 29 | viewModelScope.launch(Dispatchers.IO) { 30 | _biometricAuthState.value = SharedPreferencesUtils.useSafe.first() 31 | } 32 | } 33 | 34 | fun showBiometricPrompt(activity: MainActivity) { 35 | appBioMetricManager.initBiometricPrompt( 36 | activity = activity, 37 | listener = object : BiometricAuthListener { 38 | override fun onBiometricAuthSuccess() { 39 | viewModelScope.launch { 40 | SharedPreferencesUtils.updateUseSafe(!_biometricAuthState.value) 41 | _biometricAuthState.value = !_biometricAuthState.value 42 | } 43 | 44 | } 45 | 46 | override fun onUserCancelled() { 47 | } 48 | 49 | override fun onErrorOccurred() { 50 | } 51 | } 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/tag/LocationDetailPage.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.tag 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.collectAsState 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import androidx.navigation.NavHostController 12 | import com.ldlywt.note.component.NoteCard 13 | import com.ldlywt.note.component.NoteCardFrom 14 | import com.ldlywt.note.component.RYScaffold 15 | import com.ldlywt.note.ui.page.LocalMemosViewModel 16 | import com.ldlywt.note.utils.SettingsPreferences 17 | 18 | @Composable 19 | fun LocationDetailPage(location: String, navController: NavHostController) { 20 | val noteViewModel = LocalMemosViewModel.current 21 | val list by noteViewModel.getNotesByLocationInfo(location).collectAsState(initial = emptyList()) 22 | val maxLine by SettingsPreferences.cardMaxLine.collectAsState(SettingsPreferences.CardMaxLineMode.MAX_LINE) 23 | 24 | RYScaffold(title = location, navController = navController) { 25 | LazyColumn { 26 | items(count = list.size, key = { it }) { index -> 27 | NoteCard(noteShowBean = list[index], navController, from = NoteCardFrom.TAG_DETAIL, maxLine = maxLine.line) 28 | } 29 | item { 30 | Spacer(modifier = Modifier.height(60.dp)) 31 | } 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/tag/LocationListPage.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.tag 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ExperimentalLayoutApi 6 | import androidx.compose.foundation.layout.FlowRow 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.filled.Close 13 | import androidx.compose.material.icons.filled.LocationOn 14 | import androidx.compose.material3.AssistChipDefaults 15 | import androidx.compose.material3.ElevatedAssistChip 16 | import androidx.compose.material3.ExperimentalMaterial3Api 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.collectAsState 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.compose.ui.unit.dp 28 | import androidx.navigation.NavHostController 29 | import com.ldlywt.note.R 30 | import com.ldlywt.note.ui.page.LocalMemosViewModel 31 | import com.ldlywt.note.ui.page.NoteViewModel 32 | import com.ldlywt.note.ui.page.home.clickable 33 | import com.ldlywt.note.ui.page.router.Screen 34 | import com.ldlywt.note.ui.page.router.debouncedPopBackStack 35 | import com.ldlywt.note.utils.str 36 | import com.moriafly.salt.ui.SaltTheme 37 | import com.moriafly.salt.ui.TitleBar 38 | import com.moriafly.salt.ui.UnstableSaltApi 39 | import com.moriafly.salt.ui.dialog.YesNoDialog 40 | 41 | 42 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, UnstableSaltApi::class) 43 | @Composable 44 | fun LocationListPage(navHostController: NavHostController) { 45 | val noteViewModel: NoteViewModel = LocalMemosViewModel.current 46 | val locationInfoList by noteViewModel.getAllLocationInfo().collectAsState(initial = emptyList()) 47 | var showDialog by remember { mutableStateOf(false) } 48 | var selectedLocationInfo by remember { mutableStateOf("") } 49 | 50 | Column( 51 | modifier = Modifier 52 | .fillMaxSize() 53 | .background(color = SaltTheme.colors.background) 54 | .padding(top = 30.dp) 55 | ) { 56 | TitleBar( 57 | onBack = { 58 | navHostController.debouncedPopBackStack() 59 | }, 60 | text = stringResource(R.string.location_info) 61 | ) 62 | 63 | FlowRow( 64 | modifier = Modifier 65 | .fillMaxWidth() 66 | .padding(start = 12.dp, end = 12.dp), 67 | content = { 68 | repeat(locationInfoList.size) { index -> 69 | ElevatedAssistChip( 70 | onClick = { 71 | navHostController.navigate(Screen.LocationDetail(locationInfoList[index])) 72 | }, 73 | modifier = Modifier.padding(horizontal = 4.dp), 74 | label = { 75 | Text(locationInfoList[index]) 76 | }, 77 | leadingIcon = { 78 | Icon( 79 | Icons.Filled.LocationOn, 80 | contentDescription = "Localized description", 81 | Modifier.size(AssistChipDefaults.IconSize) 82 | ) 83 | }, 84 | trailingIcon = { 85 | Icon( 86 | Icons.Filled.Close, 87 | contentDescription = "Localized description", 88 | Modifier 89 | .size(AssistChipDefaults.IconSize) 90 | .clickable { 91 | selectedLocationInfo = locationInfoList[index] 92 | showDialog = true 93 | } 94 | ) 95 | } 96 | ) 97 | } 98 | } 99 | ) 100 | 101 | if (showDialog) { 102 | YesNoDialog( 103 | title = R.string.warm_reminder.str, 104 | content = stringResource(R.string.delete_location, selectedLocationInfo), 105 | onConfirm = { 106 | noteViewModel.clearLocationInfo(selectedLocationInfo) 107 | showDialog = false 108 | }, 109 | onDismissRequest = { 110 | showDialog = false 111 | } 112 | ) 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/ui/page/tag/TagDetailPage.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.ui.page.tag 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.collectAsState 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.navigation.NavHostController 15 | import com.ldlywt.note.component.NoteCard 16 | import com.ldlywt.note.component.NoteCardFrom 17 | import com.ldlywt.note.ui.page.LocalMemosViewModel 18 | import com.ldlywt.note.ui.page.router.debouncedPopBackStack 19 | import com.ldlywt.note.utils.SettingsPreferences 20 | import com.moriafly.salt.ui.SaltTheme 21 | import com.moriafly.salt.ui.TitleBar 22 | import com.moriafly.salt.ui.UnstableSaltApi 23 | 24 | @OptIn(UnstableSaltApi::class) 25 | @Composable 26 | fun TagDetailPage(tag: String, navController: NavHostController) { 27 | val noteViewModel = LocalMemosViewModel.current 28 | val tagList by noteViewModel.getNoteListByTagFlow(tag).collectAsState(initial = emptyList()) 29 | val maxLine by SettingsPreferences.cardMaxLine.collectAsState(SettingsPreferences.CardMaxLineMode.MAX_LINE) 30 | 31 | Column( 32 | modifier = Modifier 33 | .fillMaxSize() 34 | .background(color = SaltTheme.colors.background) 35 | ) { 36 | 37 | Spacer(modifier = Modifier.height(30.dp)) 38 | 39 | TitleBar( 40 | onBack = { 41 | navController.debouncedPopBackStack() 42 | }, 43 | text = tag 44 | ) 45 | 46 | LazyColumn { 47 | items(count = tagList.size, key = { it }) { index -> 48 | NoteCard(noteShowBean = tagList[index], navController, from = NoteCardFrom.TAG_DETAIL,maxLine = maxLine.line) 49 | } 50 | item { 51 | Spacer(modifier = Modifier.height(60.dp)) 52 | } 53 | } 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/ActivityResultUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.provider.MediaStore 8 | import androidx.activity.result.contract.ActivityResultContract 9 | 10 | 11 | object None 12 | 13 | object ChooseFilesContract : ActivityResultContract>() { 14 | override fun createIntent(context: Context, input: None?): Intent { 15 | return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { 16 | addCategory(Intent.CATEGORY_OPENABLE) 17 | putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*", "audio/*")) 18 | putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) 19 | type = "*/*" 20 | } 21 | } 22 | 23 | override fun parseResult(resultCode: Int, intent: Intent?): List { 24 | if (intent == null || resultCode != Activity.RESULT_OK) return emptyList() 25 | 26 | val clipItemCount = intent.clipData?.itemCount ?: 0 27 | return listOfNotNull(intent.data) + (0 until clipItemCount).mapNotNull { 28 | intent.clipData?.getItemAt(it)?.uri 29 | } 30 | } 31 | } 32 | 33 | object ExportNotesJsonContract : ActivityResultContract() { 34 | override fun createIntent(context: Context, input: None?): Intent { 35 | return Intent(Intent.ACTION_CREATE_DOCUMENT).apply { 36 | addCategory(Intent.CATEGORY_OPENABLE) 37 | type = "application/json" 38 | putExtra(Intent.EXTRA_TITLE, "IdeaMemo.json") 39 | } 40 | } 41 | 42 | override fun parseResult(resultCode: Int, intent: Intent?): Uri? { 43 | return if (intent != null && resultCode == Activity.RESULT_OK) intent.data else null 44 | } 45 | } 46 | 47 | object ChoseFolderContract : ActivityResultContract() { 48 | override fun createIntent(context: Context, input: None?): Intent { 49 | return Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) 50 | } 51 | 52 | override fun parseResult(resultCode: Int, intent: Intent?): Uri? { 53 | return if (intent != null && resultCode == Activity.RESULT_OK) intent.data else null 54 | } 55 | } 56 | 57 | object ExportTextContract : ActivityResultContract() { 58 | override fun createIntent(context: Context, input: None?): Intent { 59 | return Intent(Intent.ACTION_CREATE_DOCUMENT).apply { 60 | addCategory(Intent.CATEGORY_OPENABLE) 61 | type = "text/plain" 62 | putExtra(Intent.EXTRA_TITLE, "IdeaMemo.txt") 63 | } 64 | } 65 | 66 | override fun parseResult(resultCode: Int, intent: Intent?): Uri? { 67 | return if (intent != null && resultCode == Activity.RESULT_OK) intent.data else null 68 | } 69 | } 70 | 71 | class ExportMarkDownContract(val name: String) : ActivityResultContract() { 72 | override fun createIntent(context: Context, input: None?): Intent { 73 | return Intent(Intent.ACTION_CREATE_DOCUMENT).apply { 74 | addCategory(Intent.CATEGORY_OPENABLE) 75 | type = "text/markdown" 76 | putExtra(Intent.EXTRA_TITLE, "$name.md") 77 | } 78 | } 79 | 80 | override fun parseResult(resultCode: Int, intent: Intent?): Uri? { 81 | return if (intent != null && resultCode == Activity.RESULT_OK) intent.data else null 82 | } 83 | } 84 | 85 | object RestoreNotesContract : ActivityResultContract() { 86 | override fun createIntent(context: Context, input: None?): Intent { 87 | return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { 88 | addCategory(Intent.CATEGORY_OPENABLE) 89 | type = "application/zip" 90 | } 91 | } 92 | 93 | override fun parseResult(resultCode: Int, intent: Intent?): Uri? { 94 | return if (intent != null && resultCode == Activity.RESULT_OK) intent.data else null 95 | } 96 | } 97 | 98 | object TakePictureContract : ActivityResultContract() { 99 | override fun createIntent(context: Context, input: Uri): Intent { 100 | return Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { 101 | putExtra(MediaStore.EXTRA_OUTPUT, input) 102 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) 103 | } 104 | } 105 | 106 | override fun parseResult(resultCode: Int, intent: Intent?): Boolean { 107 | return resultCode == Activity.RESULT_OK 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/BioMetricUtil.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import android.content.Context 4 | import com.ldlywt.note.biometric.AppBioMetricManager 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.components.ViewModelComponent 9 | 10 | @Module 11 | @InstallIn(ViewModelComponent::class) 12 | class BioMetricUtil { 13 | 14 | @Provides 15 | fun provideAppBioMetricManager(context: Context): AppBioMetricManager { 16 | return AppBioMetricManager(context) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/BlurTransformation.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Paint 6 | import android.renderscript.Allocation 7 | import android.renderscript.Element 8 | import android.renderscript.RenderScript 9 | import android.renderscript.ScriptIntrinsicBlur 10 | import androidx.core.graphics.applyCanvas 11 | import androidx.core.graphics.createBitmap 12 | import coil.size.Size 13 | import coil.transform.Transformation 14 | 15 | /** 16 | * A [Transformation] that applies a Gaussian blur to an image. 17 | * 18 | * @param context The [Context] used to create a [RenderScript] instance. 19 | * @param radius The radius of the blur. 20 | * @param sampling The sampling multiplier used to scale the image. Values > 1 21 | * will downscale the image. Values between 0 and 1 will upscale the image. 22 | */ 23 | 24 | class BlurTransformation @JvmOverloads constructor( 25 | private val context: Context, 26 | private val radius: Float = DEFAULT_RADIUS, 27 | private val sampling: Float = DEFAULT_SAMPLING 28 | ) : Transformation { 29 | 30 | init { 31 | require(radius in 0.0..25.0) { "radius must be in [0, 25]." } 32 | require(sampling > 0) { "sampling must be > 0." } 33 | } 34 | 35 | override val cacheKey = "${BlurTransformation::class.java.name}-$radius-$sampling" 36 | 37 | override suspend fun transform(input: Bitmap, size: Size): Bitmap { 38 | val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG) 39 | 40 | val scaledWidth = (input.width / sampling).toInt() 41 | val scaledHeight = (input.height / sampling).toInt() 42 | val output = createBitmap(scaledWidth, scaledHeight, input.safeConfig) 43 | //val output = pool.get(scaledWidth, scaledHeight, input.safeConfig) 44 | output.applyCanvas { 45 | scale(1 / sampling, 1 / sampling) 46 | drawBitmap(input, 0f, 0f, paint) 47 | } 48 | 49 | var script: RenderScript? = null 50 | var tmpInt: Allocation? = null 51 | var tmpOut: Allocation? = null 52 | var blur: ScriptIntrinsicBlur? = null 53 | try { 54 | script = RenderScript.create(context) 55 | tmpInt = Allocation.createFromBitmap( 56 | script, 57 | output, 58 | Allocation.MipmapControl.MIPMAP_NONE, 59 | Allocation.USAGE_SCRIPT 60 | ) 61 | tmpOut = Allocation.createTyped(script, tmpInt.type) 62 | blur = ScriptIntrinsicBlur.create(script, Element.U8_4(script)) 63 | blur.setRadius(radius) 64 | blur.setInput(tmpInt) 65 | blur.forEach(tmpOut) 66 | tmpOut.copyTo(output) 67 | } finally { 68 | script?.destroy() 69 | tmpInt?.destroy() 70 | tmpOut?.destroy() 71 | blur?.destroy() 72 | } 73 | 74 | return output 75 | } 76 | 77 | override fun equals(other: Any?): Boolean { 78 | if (this === other) return true 79 | return other is BlurTransformation && 80 | context == other.context && 81 | radius == other.radius && 82 | sampling == other.sampling 83 | } 84 | 85 | override fun hashCode(): Int { 86 | var result = context.hashCode() 87 | result = 31 * result + radius.hashCode() 88 | result = 31 * result + sampling.hashCode() 89 | return result 90 | } 91 | 92 | override fun toString(): String { 93 | return "BlurTransformation(context=$context, radius=$radius, sampling=$sampling)" 94 | } 95 | 96 | private companion object { 97 | private const val DEFAULT_RADIUS = 10f 98 | private const val DEFAULT_SAMPLING = 1f 99 | } 100 | 101 | 102 | } 103 | 104 | internal val Bitmap.safeConfig: Bitmap.Config 105 | get() = config!! -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/Constant.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | 7 | object Constant { 8 | 9 | val USER_AGREEMENT = "https://www.freeprivacypolicy.com/live/77fb28fd-1c21-4a6c-8c8b-1464c314d629" 10 | val PRIVACY_POLICY = "https://www.freeprivacypolicy.com/live/07870fcd-c545-4b1c-9490-4d6de2d8bb5c" 11 | val GITHUB_RELEASE = "https://github.com/ldlywt/IdeaMemo/releases" 12 | val GITHUB_URL = "https://github.com/ldlywt/IdeaMemo" 13 | val GITHUB_FEEDBACK_URL = "https://github.com/ldlywt/IdeaMemo/issues/2" 14 | 15 | const val JIANGUOYUN_URL = "https://dav.jianguoyun.com/dav/" 16 | 17 | val PHOTO_EXTENSIONS = arrayOf(".jpg", ".png", ".jpeg", ".bmp", ".webp", ".heic", ".heif", ".apng", ".avif", ".gif") 18 | val VIDEO_EXTENSIONS = arrayOf(".mp4", ".mkv", ".webm", ".avi", ".3gp", ".mov", ".m4v", ".3gpp") 19 | val AUDIO_EXTENSIONS = arrayOf(".mp3", ".wav", ".wma", ".ogg", ".m4a", ".opus", ".flac", ".aac") 20 | 21 | fun startUserAgreeUrl(context: Context) { 22 | val uri: Uri = Uri.parse(USER_AGREEMENT) 23 | val intent = Intent(Intent.ACTION_VIEW, uri) 24 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 25 | context.startActivity(intent) 26 | } 27 | 28 | fun startPrivacyUrl(context: Context) { 29 | val uri: Uri = Uri.parse(PRIVACY_POLICY) 30 | val intent = Intent(Intent.ACTION_VIEW, uri) 31 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 32 | context.startActivity(intent) 33 | } 34 | 35 | fun startGithubReleaseUrl(context: Context) { 36 | val uri: Uri = Uri.parse(GITHUB_RELEASE) 37 | val intent = Intent(Intent.ACTION_VIEW, uri) 38 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 39 | context.startActivity(intent) 40 | } 41 | 42 | fun startGithubUrl(context: Context) { 43 | val uri: Uri = Uri.parse(GITHUB_URL) 44 | val intent = Intent(Intent.ACTION_VIEW, uri) 45 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 46 | context.startActivity(intent) 47 | } 48 | } 49 | 50 | fun Context.openUrl(url: String) { 51 | val uri: Uri = Uri.parse(url) 52 | val intent = Intent(Intent.ACTION_VIEW, uri) 53 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 54 | startActivity(intent) 55 | 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/CoroutinesHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.async 6 | import kotlinx.coroutines.awaitAll 7 | import kotlinx.coroutines.coroutineScope 8 | import kotlinx.coroutines.launch 9 | import kotlinx.coroutines.withContext 10 | 11 | suspend fun withIO(block: suspend CoroutineScope.() -> T, ): T { 12 | return withContext(Dispatchers.IO, block) 13 | } 14 | 15 | suspend fun withMain(block: suspend CoroutineScope.() -> T, ): T { 16 | return withContext(Dispatchers.Main, block) 17 | } 18 | 19 | 20 | fun lunchIo(runner: suspend CoroutineScope.() -> Unit) = CoroutineScope(Dispatchers.IO).launch { runner.invoke((this)) } 21 | fun lunchMain(runner: suspend CoroutineScope.() -> Unit) = CoroutineScope(Dispatchers.Main).launch { runner.invoke((this)) } 22 | 23 | suspend fun Iterable.pmap(f: suspend (A) -> B): List = coroutineScope { 24 | map { async { f(it) } }.awaitAll() 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/DateUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.Date 5 | import java.util.Locale 6 | 7 | fun Long.toMinute(): String { 8 | val dateTime = Date(this) 9 | val format = SimpleDateFormat("HH:mm", Locale.ENGLISH) 10 | return format.format(dateTime) 11 | } 12 | 13 | fun Long.toDD(): String { 14 | val dateTime = Date(this) 15 | val format = SimpleDateFormat("dd", Locale.ENGLISH) 16 | return format.format(dateTime) 17 | } 18 | 19 | fun Long.toMM(): String { 20 | val dateTime = Date(this) 21 | val format = SimpleDateFormat("MM", Locale.ENGLISH) 22 | return format.format(dateTime) 23 | } 24 | 25 | fun Long.toYYMMDD(): String { 26 | val dateTime = Date(this) 27 | val format = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) 28 | return format.format(dateTime) 29 | } 30 | 31 | fun Long.toDate(): String { 32 | val dateTime = Date(this) 33 | val format = SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH) 34 | return format.format(dateTime) 35 | } 36 | 37 | fun Long.toMYYMM(): String { 38 | val dateTime = Date(this) 39 | val format = SimpleDateFormat("yyyy/MM", Locale.ENGLISH) 40 | return format.format(dateTime) 41 | } 42 | 43 | fun Long.toBackUpFileName(): String { 44 | val dateTime = Date(this) 45 | val format = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH) 46 | return format.format(dateTime) 47 | } 48 | 49 | fun Long.toTime(): String { 50 | val dateTime = Date(this) 51 | val format = SimpleDateFormat("yyyy/MM/dd HH:mm", Locale.ENGLISH) 52 | return format.format(dateTime) 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/DonateUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.widget.Toast 7 | import com.ldlywt.note.App 8 | import com.ldlywt.note.R 9 | 10 | 11 | object DonateUtils { 12 | 13 | /*** 14 | * 支付宝转账 15 | */ 16 | fun openALiPay(activity: Context) { 17 | val url1 = 18 | "intent://platformapi/startapp?saId=10000007&" + "clientVersion=3.7.0.0718&qrcode=https%3A%2F%2Fqr.alipay.com%2Fa6x01470fhxkoehbej8yj77%3F_s" + "%3Dweb-other&_t=1472443966571#Intent;" + "scheme=alipayqr;package=com.eg.android.AlipayGphone;end" 19 | val intent: Intent? 20 | try { 21 | intent = Intent.parseUri(url1, Intent.URI_INTENT_SCHEME) 22 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 23 | activity.startActivity(intent) 24 | } catch (e: Exception) { 25 | e.printStackTrace() 26 | Toast.makeText(App.instance, activity.getString(R.string.failed), Toast.LENGTH_SHORT).show() 27 | } 28 | } 29 | 30 | /** 31 | * 跳转google play 32 | */ 33 | fun openGooglePlay(context: Context) { 34 | val playPackage = "com.android.vending" 35 | try { 36 | val currentPackageName = context.packageName 37 | if (currentPackageName != null) { 38 | val currentPackageUri = Uri.parse("market://details?id=" + context.packageName) 39 | val intent = Intent(Intent.ACTION_VIEW, currentPackageUri) 40 | intent.setPackage(playPackage) 41 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 42 | context.startActivity(intent) 43 | } 44 | } catch (e: java.lang.Exception) { 45 | e.printStackTrace() 46 | val currentPackageUri = Uri.parse("https://play.google.com/store/apps/details?id=" + context.packageName) 47 | val intent = Intent(Intent.ACTION_VIEW, currentPackageUri) 48 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 49 | context.startActivity(intent) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/File.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import java.io.BufferedInputStream 6 | import java.io.BufferedOutputStream 7 | import java.io.File 8 | import java.io.FileOutputStream 9 | import java.io.IOException 10 | 11 | 12 | fun File.newName(): String { 13 | var index = 1 14 | var candidate: String 15 | val split = nameWithoutExtension.split(' ').toMutableList() 16 | val last = split.last() 17 | if ("""^\(\d+\)$""".toRegex().matches(last)) { 18 | split.removeAt(split.lastIndex) 19 | } 20 | val name = split.joinToString(" ") 21 | while (true) { 22 | candidate = if (extension.isEmpty()) "$name ($index)" else "$name ($index).$extension" 23 | if (!File("$parent/$candidate").exists()) { 24 | return candidate 25 | } 26 | index++ 27 | } 28 | } 29 | 30 | fun File.newPath(): String { 31 | return "$parent/" + newName() 32 | } 33 | 34 | fun copyFile(context: Context, pathFrom: Uri, pathTo: String) { 35 | context.contentResolver.openInputStream(pathFrom).use { input -> 36 | var bis: BufferedInputStream? = null 37 | var bos: BufferedOutputStream? = null 38 | 39 | try { 40 | bis = BufferedInputStream(input) 41 | bos = BufferedOutputStream(FileOutputStream(pathTo, false)) 42 | val buf = ByteArray(1024) 43 | bis.read(buf) 44 | do { 45 | bos.write(buf) 46 | } while (bis.read(buf) != -1) 47 | } catch (e: IOException) { 48 | e.printStackTrace() 49 | } finally { 50 | try { 51 | bis?.close() 52 | bos?.close() 53 | } catch (e: IOException) { 54 | e.printStackTrace() 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/FirstTimeManager.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import android.app.Activity 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.material3.AlertDialog 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.unit.dp 15 | import com.ldlywt.note.App 16 | import com.ldlywt.note.R 17 | import com.ldlywt.note.bean.Note 18 | import com.ldlywt.note.db.repo.TagNoteRepo 19 | import com.ldlywt.note.ui.page.home.clickable 20 | import com.moriafly.salt.ui.SaltTheme 21 | import kotlinx.coroutines.flow.first 22 | import javax.inject.Inject 23 | 24 | class FirstTimeManager @Inject constructor() { 25 | 26 | @Inject 27 | lateinit var tagNoteRepo: TagNoteRepo 28 | 29 | fun generateIntroduceNoteList() { 30 | lunchIo { 31 | if (!SettingsPreferences.firstLaunch.first() || tagNoteRepo.queryAllNoteList().isNotEmpty()) { 32 | return@lunchIo 33 | } 34 | if (App.instance.isSystemLanguageEnglish()) { 35 | generateEnglishIntroduceNoteList() 36 | } else { 37 | generateChineseIntroduceNoteList() 38 | } 39 | } 40 | } 41 | 42 | private fun generateChineseIntroduceNoteList() { 43 | val functionNote = Note( 44 | content = "#灵感 \n生活不止眼前的苟且 还有诗和远方。@深圳市", 45 | ) 46 | tagNoteRepo.insertOrUpdate(functionNote) 47 | } 48 | 49 | private fun generateEnglishIntroduceNoteList() { 50 | val functionNote = Note( 51 | content = "#Life \nLess is more.@New York City", 52 | ) 53 | tagNoteRepo.insertOrUpdate(functionNote) 54 | } 55 | 56 | } 57 | 58 | @Composable 59 | fun FirstTimeWarmDialog(block: () -> Unit) { 60 | val context = LocalContext.current 61 | AlertDialog( 62 | containerColor = SaltTheme.colors.subBackground, 63 | onDismissRequest = { }, 64 | title = { Text(stringResource(R.string.welcome), color = SaltTheme.colors.text) }, 65 | text = { 66 | Column { 67 | Text(stringResource(id = R.string.warm_reminder_desc), color = SaltTheme.colors.text) 68 | Spacer(modifier = androidx.compose.ui.Modifier.height(12.dp)) 69 | 70 | Text( 71 | stringResource(id = R.string.browse_tos_tips_service), 72 | style = MaterialTheme.typography.bodyMedium.copy( 73 | color = MaterialTheme.colorScheme.outline, 74 | ), 75 | modifier = androidx.compose.ui.Modifier.clickable { 76 | Constant.startUserAgreeUrl(context) 77 | } 78 | ) 79 | Spacer(modifier = androidx.compose.ui.Modifier.height(6.dp)) 80 | Text( 81 | stringResource(id = R.string.browse_tos_tips_privacy), 82 | style = MaterialTheme.typography.bodyMedium.copy( 83 | color = MaterialTheme.colorScheme.outline, 84 | ), 85 | modifier = androidx.compose.ui.Modifier.clickable { 86 | Constant.startPrivacyUrl(context) 87 | } 88 | ) 89 | } 90 | }, 91 | confirmButton = { 92 | Button(onClick = { 93 | block() 94 | }) { 95 | Text(stringResource(id = R.string.agree)) 96 | } 97 | }, 98 | dismissButton = { 99 | Button(onClick = { 100 | if (context is Activity) { 101 | context.finish() 102 | } 103 | }) { 104 | Text(stringResource(id = R.string.exit)) 105 | } 106 | } 107 | ) 108 | } 109 | 110 | 111 | @Composable 112 | fun TipsDialog(block: () -> Unit) { 113 | AlertDialog( 114 | containerColor = SaltTheme.colors.subBackground, 115 | onDismissRequest = { }, 116 | title = { Text(stringResource(R.string.warm_reminder), color = SaltTheme.colors.text) }, 117 | text = { 118 | Column { 119 | Text(stringResource(id = R.string.warm_reminder_desc), color = SaltTheme.colors.text) 120 | } 121 | }, 122 | confirmButton = { 123 | Button(onClick = { 124 | block() 125 | }) { 126 | Text("OK") 127 | } 128 | } 129 | ) 130 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/ReceiveFileKtx.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import android.net.Uri 4 | import android.os.Environment 5 | import android.provider.OpenableColumns 6 | import android.webkit.MimeTypeMap 7 | import com.ldlywt.note.App 8 | import com.ldlywt.note.bean.Attachment 9 | import top.zibin.luban.Luban 10 | import java.io.File 11 | 12 | suspend fun handlePickFiles( 13 | uris: Set, callback: (list: List) -> Unit 14 | ) { 15 | val items = mutableListOf() 16 | withIO { 17 | uris.forEach { uri -> 18 | val context = App.instance 19 | context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> 20 | cursor.moveToFirst() 21 | var fileName = cursor.getStringValue(OpenableColumns.DISPLAY_NAME) 22 | // val size = cursor.getLongValue(OpenableColumns.SIZE) 23 | val type = context.contentResolver.getType(uri) ?: "" 24 | var extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type) 25 | if (extension.isNullOrEmpty()) { 26 | extension = fileName.getFilenameExtension() 27 | } 28 | if (extension.isNotEmpty()) { 29 | fileName = fileName.getFilenameWithoutExtension() + "." + extension 30 | } 31 | cursor.close() 32 | val fileType: Attachment.Type 33 | try { 34 | val dir = when { 35 | fileName.isVideoFast() -> { 36 | fileType = Attachment.Type.VIDEO 37 | Environment.DIRECTORY_MOVIES 38 | } 39 | 40 | fileName.isImageFast() -> { 41 | fileType = Attachment.Type.IMAGE 42 | Environment.DIRECTORY_PICTURES 43 | } 44 | 45 | fileName.isAudioFast() -> { 46 | fileType = Attachment.Type.AUDIO 47 | Environment.DIRECTORY_MUSIC 48 | } 49 | 50 | else -> { 51 | fileType = Attachment.Type.FILE 52 | Environment.DIRECTORY_DOCUMENTS 53 | } 54 | } 55 | val dst = context.getExternalFilesDir(dir)!!.path + "/$fileName" 56 | val dstFile = File(dst) 57 | if (dstFile.exists()) { 58 | copyFile(context, uri, dstFile.newPath()) 59 | } else { 60 | copyFile(context, uri, dst) 61 | } 62 | Luban.with(context).setTargetDir(context.getExternalFilesDir(dir)!!.path).load(dst).get().forEach { 63 | if (it.exists() && it.path != dst) { 64 | File(dst).delete() 65 | } 66 | items.add(Attachment(path = it.path, fileName = it.name, description = it.name, type = fileType)) 67 | } 68 | } catch (ex: Exception) { 69 | ex.printStackTrace() 70 | } 71 | } 72 | } 73 | callback(items) 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/Resource.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | sealed class Resource( 4 | val data: T? = null, 5 | val error: Throwable? = null 6 | ) { 7 | class Success(data: T) : Resource(data = data) 8 | object Loading : Resource() 9 | class Error(error: Throwable? = null) : Resource(error = error) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/SettingsPreferences.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import android.content.Context 4 | import androidx.annotation.StringRes 5 | import androidx.appcompat.app.AppCompatDelegate 6 | import androidx.datastore.core.DataStore 7 | import androidx.datastore.preferences.core.Preferences 8 | import androidx.datastore.preferences.core.booleanPreferencesKey 9 | import androidx.datastore.preferences.core.edit 10 | import androidx.datastore.preferences.core.stringPreferencesKey 11 | import androidx.datastore.preferences.preferencesDataStore 12 | import com.ldlywt.note.App 13 | import com.ldlywt.note.R 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.map 17 | import kotlinx.coroutines.withContext 18 | 19 | private const val THEME_PREFERENCES = "THEME_PREFERENCES" 20 | private val Context.themePreferences by preferencesDataStore(name = THEME_PREFERENCES) 21 | 22 | 23 | object SettingsPreferences { 24 | enum class ThemeMode(@StringRes val resId: Int) { 25 | LIGHT(R.string.light_mode), DARK(R.string.dark_mode), SYSTEM(R.string.use_device_theme), 26 | } 27 | 28 | enum class CardMaxLineMode(val line: Int) { 29 | TWO_LINE(2), THUR_LINE(4), SIX_LINE(6), EIGHT_LINE(8), MAX_LINE(1000) 30 | } 31 | 32 | private object PreferencesKeys { 33 | val THEME_MODE = stringPreferencesKey("theme_mode") 34 | val DYNAMIC_COLOR = booleanPreferencesKey("dynamic_color") 35 | val FIRST_LAUNCH = booleanPreferencesKey("first_launch") 36 | val CARD_MAX_LINE = stringPreferencesKey("card_max_line") 37 | } 38 | 39 | 40 | private val themePreferences = App.instance.themePreferences 41 | 42 | val themeMode = themePreferences.getEnum(PreferencesKeys.THEME_MODE, ThemeMode.SYSTEM) 43 | val dynamicColor = themePreferences.getBoolean(PreferencesKeys.DYNAMIC_COLOR, false) 44 | val firstLaunch = themePreferences.getBoolean(PreferencesKeys.FIRST_LAUNCH, true) 45 | val cardMaxLine = themePreferences.getEnum(PreferencesKeys.CARD_MAX_LINE, CardMaxLineMode.MAX_LINE) 46 | 47 | private suspend fun updatePreference(key: Preferences.Key, value: T) { 48 | themePreferences.edit { preferences -> 49 | preferences[key] = value 50 | } 51 | } 52 | 53 | fun applyAppCompatThemeMode(themeMode: ThemeMode) { 54 | val appCompatMode = when (themeMode) { 55 | ThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES 56 | ThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO 57 | ThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 58 | } 59 | AppCompatDelegate.setDefaultNightMode(appCompatMode) 60 | } 61 | 62 | suspend fun changeThemeMode(themeMode: ThemeMode) { 63 | updatePreference(PreferencesKeys.THEME_MODE, themeMode.name) 64 | withContext(Dispatchers.Main) { 65 | applyAppCompatThemeMode(themeMode) 66 | } 67 | } 68 | 69 | 70 | suspend fun changeDynamicColor(dynamicTheme: Boolean) { 71 | updatePreference(PreferencesKeys.DYNAMIC_COLOR, dynamicTheme) 72 | } 73 | 74 | suspend fun changeFirstLaunch(isFirst: Boolean) { 75 | updatePreference(PreferencesKeys.FIRST_LAUNCH, isFirst) 76 | } 77 | 78 | suspend fun changeMaxLine(mode: CardMaxLineMode) { 79 | updatePreference(PreferencesKeys.CARD_MAX_LINE, mode.name) 80 | } 81 | } 82 | 83 | 84 | inline fun > DataStore.getEnum(key: Preferences.Key, defaultValue: T): Flow { 85 | return this.data.map { preferences -> 86 | preferences[key]?.let { 87 | try { 88 | enumValueOf(it) 89 | } catch (e: IllegalArgumentException) { 90 | defaultValue 91 | } 92 | } ?: defaultValue 93 | } 94 | } 95 | 96 | 97 | fun DataStore.getBoolean(key: Preferences.Key, defaultValue: Boolean): Flow { 98 | return this.data.map { preferences -> 99 | preferences[key] ?: defaultValue 100 | } 101 | } 102 | 103 | fun DataStore.getInt(key: Preferences.Key, defaultValue: Int): Flow { 104 | return this.data.map { preferences -> 105 | preferences[key] ?: defaultValue 106 | } 107 | } 108 | 109 | fun DataStore.getString(key: Preferences.Key, defaultValue: String?): Flow { 110 | return this.data.map { preferences -> 111 | preferences[key] ?: defaultValue 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/SharedPreferencesUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import android.content.Context 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.booleanPreferencesKey 6 | import androidx.datastore.preferences.core.edit 7 | import androidx.datastore.preferences.core.stringPreferencesKey 8 | import androidx.datastore.preferences.preferencesDataStore 9 | import com.ldlywt.note.App 10 | import com.ldlywt.note.ui.page.SortTime 11 | import kotlinx.coroutines.flow.Flow 12 | 13 | private const val SHARED_PREFERENCES_STORE_NAME = "SHARED_PREFERENCES" 14 | private val Context.sharedDataStore by preferencesDataStore(name = SHARED_PREFERENCES_STORE_NAME) 15 | 16 | object SharedPreferencesUtils { 17 | private object PreferencesKeys { 18 | val SORT_TIME = stringPreferencesKey("sort_time") 19 | val USE_SAFE = booleanPreferencesKey("use_safe") 20 | val LOCAL_AUTO_BACKUP = booleanPreferencesKey("local_auto_backup") 21 | val LOCAL_BACKUP_URI = stringPreferencesKey("local_backup_uri") 22 | val DAV_LOGIN_SUCCESS = booleanPreferencesKey("dav_login_success") 23 | val DAV_SERVER_URL = stringPreferencesKey("dav_server_url") 24 | val DAV_USER_NAME = stringPreferencesKey("dav_user_name") 25 | val DAV_PASSWORD = stringPreferencesKey("dav_password") 26 | } 27 | 28 | private val sharedPreferences = App.instance.sharedDataStore 29 | 30 | 31 | val sortTime: Flow = sharedPreferences.getEnum(PreferencesKeys.SORT_TIME, SortTime.UPDATE_TIME_DESC) 32 | val useSafe: Flow = sharedPreferences.getBoolean(PreferencesKeys.USE_SAFE, false) 33 | 34 | 35 | val localAutoBackup: Flow = sharedPreferences.getBoolean(PreferencesKeys.LOCAL_AUTO_BACKUP, false) 36 | 37 | // content://com.android.externalstorage.documents/tree/primary%3ADocuments 38 | val localBackupUri: Flow = sharedPreferences.getString(PreferencesKeys.LOCAL_BACKUP_URI, null) 39 | val davLoginSuccess: Flow = sharedPreferences.getBoolean(PreferencesKeys.DAV_LOGIN_SUCCESS, false) 40 | val davServerUrl: Flow = sharedPreferences.getString(PreferencesKeys.DAV_SERVER_URL, "https://dav.jianguoyun.com/dav/") 41 | val davUserName: Flow = sharedPreferences.getString(PreferencesKeys.DAV_USER_NAME, null) 42 | val davPassword: Flow = sharedPreferences.getString(PreferencesKeys.DAV_PASSWORD, null) 43 | 44 | 45 | 46 | suspend fun clearDavConfig() { 47 | sharedPreferences.edit { preferences -> 48 | preferences[PreferencesKeys.DAV_LOGIN_SUCCESS] = false 49 | preferences.remove(PreferencesKeys.DAV_SERVER_URL) 50 | preferences.remove(PreferencesKeys.DAV_USER_NAME) 51 | preferences.remove(PreferencesKeys.DAV_PASSWORD) 52 | 53 | } 54 | } 55 | 56 | private suspend fun updatePreference(key: Preferences.Key, value: T?) { 57 | sharedPreferences.edit { preferences -> 58 | if(value!=null) { 59 | preferences[key] = value 60 | }else{ 61 | preferences.remove(key) 62 | } 63 | } 64 | } 65 | 66 | suspend fun updateLocalBackUri(uri: String?){ 67 | updatePreference(PreferencesKeys.LOCAL_BACKUP_URI,uri) 68 | } 69 | suspend fun updateLocalAutoBackup(use: Boolean) { 70 | updatePreference(PreferencesKeys.LOCAL_AUTO_BACKUP, use) 71 | } 72 | suspend fun updateDavLoginSuccess(success: Boolean) { 73 | updatePreference(PreferencesKeys.DAV_LOGIN_SUCCESS, success) 74 | } 75 | 76 | suspend fun updateDavServerUrl(uri: String) { 77 | updatePreference(PreferencesKeys.DAV_SERVER_URL, uri) 78 | } 79 | 80 | suspend fun updateDavUserName(name:String? ) { 81 | updatePreference(PreferencesKeys.DAV_USER_NAME, name) 82 | } 83 | suspend fun updateDavPassword(password:String? ) { 84 | updatePreference(PreferencesKeys.DAV_PASSWORD, password) 85 | } 86 | 87 | suspend fun updateSortTime(sortTime: SortTime) { 88 | updatePreference(PreferencesKeys.SORT_TIME, sortTime.name) 89 | } 90 | suspend fun updateUseSafe(use: Boolean) { 91 | updatePreference(PreferencesKeys.USE_SAFE, use) 92 | } 93 | 94 | 95 | 96 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/String.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | fun String.getFilenameWithoutExtension() = substringBeforeLast(".") 4 | 5 | fun String.getFilenameExtension() = substring(lastIndexOf(".") + 1) 6 | 7 | operator fun String.times(x: Int): String { 8 | val stringBuilder = StringBuilder() 9 | for (i in 1..x) { 10 | stringBuilder.append(this) 11 | } 12 | return stringBuilder.toString() 13 | } 14 | 15 | fun String.isVideoFast() = Constant.VIDEO_EXTENSIONS.any { endsWith(it, true) } 16 | fun String.isImageFast() = Constant.PHOTO_EXTENSIONS.any { endsWith(it, true) } 17 | fun String.isAudioFast() = Constant.AUDIO_EXTENSIONS.any { endsWith(it, true) } -------------------------------------------------------------------------------- /app/src/main/java/com/ldlywt/note/utils/TopicUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ldlywt.note.utils 2 | 3 | import com.ldlywt.note.bean.Tag 4 | import java.util.regex.Matcher 5 | import java.util.regex.Pattern 6 | 7 | object TopicUtils { 8 | 9 | private val inputReg = "(\\#[\u4e00-\u9fa5a-zA-Z]+\\d{0,100})[\\w\\s]" 10 | val pattern: Pattern = Pattern.compile(inputReg) 11 | fun getTopicListByString(text: String): List { 12 | val tagList: MutableList = mutableListOf() 13 | val matcher: Matcher = pattern.matcher(text) 14 | while (matcher.find()) { 15 | val tag = text.substring(matcher.start(), matcher.end()).trim { it <= ' ' } 16 | tagList.add(Tag(tag = tag)) 17 | 18 | } 19 | return tagList 20 | } 21 | } 22 | 23 | object CityRegexUtils { 24 | fun getCityByString(input: String): Pair? { 25 | val lastIndex = input.lastIndexOf('@') 26 | 27 | if (lastIndex != -1 && lastIndex < input.length - 1) { 28 | val beforeLastAt = input.substring(0, lastIndex) 29 | val afterLastAt = input.substring(lastIndex + 1) 30 | return Pair(beforeLastAt, afterLastAt) 31 | } 32 | 33 | return Pair(input, "") 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/dev/jeziellago/compose/markdowntext/AutoSizeConfig.kt: -------------------------------------------------------------------------------- 1 | package dev.jeziellago.compose.markdowntext 2 | 3 | import android.util.TypedValue 4 | 5 | /** 6 | * Requires API Level 26 to apply auto size 7 | * */ 8 | data class AutoSizeConfig( 9 | val autoSizeMinTextSize: Int, 10 | val autoSizeMaxTextSize: Int, 11 | val autoSizeStepGranularity: Int, 12 | val unit: Int = TypedValue.COMPLEX_UNIT_SP, 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/dev/jeziellago/compose/markdowntext/CustomTextView.kt: -------------------------------------------------------------------------------- 1 | package dev.jeziellago.compose.markdowntext 2 | 3 | import android.content.Context 4 | import android.text.Spannable 5 | import android.text.style.ClickableSpan 6 | import android.view.MotionEvent 7 | import androidx.appcompat.widget.AppCompatTextView 8 | 9 | class CustomTextView(context: Context) : AppCompatTextView(context) { 10 | 11 | private var isTextSelectable: Boolean = false 12 | 13 | override fun onTouchEvent(event: MotionEvent): Boolean { 14 | performClick() 15 | if (isTextSelectable) { 16 | return super.onTouchEvent(event) 17 | } else { 18 | if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_DOWN) { 19 | val link = getClickableSpans(event) 20 | 21 | if (link.isNotEmpty()) { 22 | if (event.action == MotionEvent.ACTION_UP) { 23 | link[0].onClick(this) 24 | } 25 | return true 26 | } 27 | } 28 | return false 29 | } 30 | } 31 | 32 | private fun getClickableSpans(event: MotionEvent): Array { 33 | var x = event.x.toInt() 34 | var y = event.y.toInt() 35 | 36 | x -= totalPaddingLeft 37 | y -= totalPaddingTop 38 | 39 | x += scrollX 40 | y += scrollY 41 | 42 | val layout = layout 43 | val line = layout.getLineForVertical(y) 44 | val off = layout.getOffsetForHorizontal(line, x.toFloat()) 45 | 46 | val spannable = text as Spannable 47 | return spannable.getSpans(off, off, ClickableSpan::class.java) 48 | } 49 | 50 | override fun performClick(): Boolean { 51 | return super.performClick() 52 | } 53 | 54 | override fun setTextIsSelectable(selectable: Boolean) { 55 | super.setTextIsSelectable(selectable) 56 | isTextSelectable = selectable 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/dev/jeziellago/compose/markdowntext/MarkdownRender.kt: -------------------------------------------------------------------------------- 1 | package dev.jeziellago.compose.markdowntext 2 | 3 | import android.content.Context 4 | import coil.ImageLoader 5 | import coil.imageLoader 6 | import io.noties.markwon.AbstractMarkwonPlugin 7 | import io.noties.markwon.Markwon 8 | import io.noties.markwon.MarkwonConfiguration 9 | import io.noties.markwon.SoftBreakAddsNewLinePlugin 10 | import io.noties.markwon.ext.strikethrough.StrikethroughPlugin 11 | import io.noties.markwon.ext.tables.TablePlugin 12 | import io.noties.markwon.html.HtmlPlugin 13 | import io.noties.markwon.image.coil.CoilImagesPlugin 14 | import io.noties.markwon.linkify.LinkifyPlugin 15 | 16 | internal object MarkdownRender { 17 | 18 | fun create( 19 | context: Context, 20 | imageLoader: ImageLoader?, 21 | linkifyMask: Int, 22 | enableSoftBreakAddsNewLine: Boolean, 23 | onLinkClicked: ((String) -> Unit)? = null, 24 | ): Markwon { 25 | val coilImageLoader = imageLoader ?: context.imageLoader 26 | return Markwon.builder(context) 27 | .usePlugin(HtmlPlugin.create()) 28 | .usePlugin(CoilImagesPlugin.create(context, coilImageLoader)) 29 | .usePlugin(StrikethroughPlugin.create()) 30 | .usePlugin(TablePlugin.create(context)) 31 | .usePlugin(LinkifyPlugin.create(linkifyMask)) 32 | .apply { 33 | if (enableSoftBreakAddsNewLine) { 34 | usePlugin(SoftBreakAddsNewLinePlugin.create()) 35 | } 36 | } 37 | .usePlugin(object : AbstractMarkwonPlugin() { 38 | override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { 39 | // Setting [MarkwonConfiguration.Builder.linkResolver] overrides 40 | // Markwon's default behaviour - see Markwon's [LinkResolverDef] 41 | // and how it's used in [M 42 | // Only use it if the client explicitly wants to handle link clicks. 43 | onLinkClicked ?: return 44 | builder.linkResolver { _, link -> 45 | // handle individual clicks on Textview link 46 | onLinkClicked.invoke(link) 47 | } 48 | } 49 | }) 50 | .build() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/dev/jeziellago/compose/markdowntext/TextAppearanceExt.kt: -------------------------------------------------------------------------------- 1 | package dev.jeziellago.compose.markdowntext 2 | 3 | import android.graphics.Paint 4 | import android.graphics.Typeface 5 | import android.graphics.text.LineBreaker 6 | import android.os.Build 7 | import android.text.SpannableStringBuilder 8 | import android.text.Spanned 9 | import android.util.TypedValue 10 | import android.view.Gravity 11 | import android.widget.TextView 12 | import androidx.annotation.FontRes 13 | import androidx.compose.ui.text.TextStyle 14 | import androidx.compose.ui.text.font.FontFamily 15 | import androidx.compose.ui.text.font.FontStyle 16 | import androidx.compose.ui.text.font.FontWeight 17 | import androidx.compose.ui.text.font.FontWeight.Companion.Bold 18 | import androidx.compose.ui.text.font.FontWeight.Companion.ExtraBold 19 | import androidx.compose.ui.text.font.FontWeight.Companion.SemiBold 20 | import androidx.compose.ui.text.font.createFontFamilyResolver 21 | import androidx.compose.ui.text.font.resolveAsTypeface 22 | import androidx.compose.ui.text.style.TextAlign 23 | import androidx.compose.ui.text.style.TextDecoration 24 | import androidx.core.content.res.ResourcesCompat 25 | import androidx.core.view.doOnNextLayout 26 | import androidx.core.widget.TextViewCompat 27 | 28 | fun TextView.applyFontWeight(fontWeight: FontWeight) { 29 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 30 | typeface = Typeface.create(typeface, fontWeight.weight, false) 31 | } else { 32 | val weight = when (fontWeight) { 33 | ExtraBold, Bold, SemiBold -> Typeface.BOLD 34 | else -> Typeface.NORMAL 35 | } 36 | setTypeface(typeface, weight) 37 | } 38 | } 39 | 40 | fun TextView.applyFontStyle(fontStyle: FontStyle) { 41 | val type = when (fontStyle) { 42 | FontStyle.Italic -> Typeface.ITALIC 43 | FontStyle.Normal -> Typeface.NORMAL 44 | else -> Typeface.NORMAL 45 | } 46 | setTypeface(typeface, type) 47 | } 48 | 49 | fun TextView.applyFontFamily(fontFamily: FontFamily) { 50 | typeface = createFontFamilyResolver(context).resolveAsTypeface(fontFamily).value 51 | } 52 | 53 | fun TextView.applyFontResource(@FontRes font: Int) { 54 | typeface = ResourcesCompat.getFont(context, font) 55 | } 56 | 57 | fun TextView.applyTextColor(argbColor: Int) { 58 | setTextColor(argbColor) 59 | } 60 | 61 | fun TextView.applyFontSize(textStyle: TextStyle) { 62 | setTextSize(TypedValue.COMPLEX_UNIT_SP, textStyle.fontSize.value) 63 | } 64 | 65 | fun TextView.applyTextDecoration(textStyle: TextStyle) { 66 | if (textStyle.textDecoration == TextDecoration.LineThrough) { 67 | paintFlags = Paint.STRIKE_THRU_TEXT_FLAG 68 | } 69 | } 70 | 71 | fun TextView.applyLineHeight(textStyle: TextStyle) { 72 | if (textStyle.lineHeight.isSp) { 73 | TextViewCompat.setLineHeight( 74 | this, 75 | TypedValue.applyDimension( 76 | TypedValue.COMPLEX_UNIT_SP, 77 | textStyle.lineHeight.value, 78 | context.resources.displayMetrics 79 | ).toInt() 80 | ) 81 | } 82 | } 83 | 84 | fun TextView.applyTextAlign(align: TextAlign) { 85 | gravity = when (align) { 86 | TextAlign.Left, TextAlign.Start -> Gravity.START 87 | TextAlign.Right, TextAlign.End -> Gravity.END 88 | TextAlign.Center -> Gravity.CENTER_HORIZONTAL 89 | else -> Gravity.START 90 | } 91 | 92 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && align == TextAlign.Justify) { 93 | justificationMode = LineBreaker.JUSTIFICATION_MODE_INTER_WORD 94 | } 95 | } 96 | 97 | fun TextView.enableTextOverflow() { 98 | doOnNextLayout { 99 | if (maxLines != -1 && lineCount > maxLines) { 100 | val endOfLastLine = layout.getLineEnd(maxLines - 1) 101 | val startIndex = maxOf(0, endOfLastLine - 3) 102 | val spannedDropLast3Chars = text.subSequence(0, startIndex) as? Spanned 103 | if (spannedDropLast3Chars != null) { 104 | val spannableBuilder = SpannableStringBuilder() 105 | .append(spannedDropLast3Chars) 106 | .append("…") 107 | 108 | text = spannableBuilder 109 | } 110 | } 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /app/src/main/java/top/zibin/luban/CompressionPredicate.java: -------------------------------------------------------------------------------- 1 | package top.zibin.luban; 2 | 3 | /** 4 | * Created on 2018/1/3 19:43 5 | * 6 | * @author andy 7 | * 8 | * A functional interface (callback) that returns true or false for the given input path should be compressed. 9 | */ 10 | 11 | public interface CompressionPredicate { 12 | 13 | /** 14 | * Determine the given input path should be compressed and return a boolean. 15 | * @param path input path 16 | * @return the boolean result 17 | */ 18 | boolean apply(String path); 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/top/zibin/luban/Engine.java: -------------------------------------------------------------------------------- 1 | package top.zibin.luban; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.graphics.Matrix; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.File; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | 12 | /** 13 | * Responsible for starting compress and managing active and cached resources. 14 | */ 15 | class Engine { 16 | private InputStreamProvider srcImg; 17 | private File tagImg; 18 | private int srcWidth; 19 | private int srcHeight; 20 | private boolean focusAlpha; 21 | 22 | Engine(InputStreamProvider srcImg, File tagImg, boolean focusAlpha) throws IOException { 23 | this.tagImg = tagImg; 24 | this.srcImg = srcImg; 25 | this.focusAlpha = focusAlpha; 26 | 27 | BitmapFactory.Options options = new BitmapFactory.Options(); 28 | options.inJustDecodeBounds = true; 29 | options.inSampleSize = 1; 30 | 31 | BitmapFactory.decodeStream(srcImg.open(), null, options); 32 | this.srcWidth = options.outWidth; 33 | this.srcHeight = options.outHeight; 34 | } 35 | 36 | private int computeSize() { 37 | srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth; 38 | srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight; 39 | 40 | int longSide = Math.max(srcWidth, srcHeight); 41 | int shortSide = Math.min(srcWidth, srcHeight); 42 | 43 | float scale = ((float) shortSide / longSide); 44 | if (scale <= 1 && scale > 0.5625) { 45 | if (longSide < 1664) { 46 | return 1; 47 | } else if (longSide < 4990) { 48 | return 2; 49 | } else if (longSide > 4990 && longSide < 10240) { 50 | return 4; 51 | } else { 52 | return longSide / 1280 == 0 ? 1 : longSide / 1280; 53 | } 54 | } else if (scale <= 0.5625 && scale > 0.5) { 55 | return longSide / 1280 == 0 ? 1 : longSide / 1280; 56 | } else { 57 | return (int) Math.ceil(longSide / (1280.0 / scale)); 58 | } 59 | } 60 | 61 | private Bitmap rotatingImage(Bitmap bitmap, int angle) { 62 | Matrix matrix = new Matrix(); 63 | 64 | matrix.postRotate(angle); 65 | 66 | return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); 67 | } 68 | 69 | File compress() throws IOException { 70 | BitmapFactory.Options options = new BitmapFactory.Options(); 71 | options.inSampleSize = computeSize(); 72 | 73 | Bitmap tagBitmap = BitmapFactory.decodeStream(srcImg.open(), null, options); 74 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 75 | 76 | if (Checker.SINGLE.isJPG(srcImg.open())) { 77 | tagBitmap = rotatingImage(tagBitmap, Checker.SINGLE.getOrientation(srcImg.open())); 78 | } 79 | tagBitmap.compress(focusAlpha ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, 60, stream); 80 | tagBitmap.recycle(); 81 | 82 | FileOutputStream fos = new FileOutputStream(tagImg); 83 | fos.write(stream.toByteArray()); 84 | fos.flush(); 85 | fos.close(); 86 | stream.close(); 87 | 88 | return tagImg; 89 | } 90 | } -------------------------------------------------------------------------------- /app/src/main/java/top/zibin/luban/InputStreamAdapter.java: -------------------------------------------------------------------------------- 1 | package top.zibin.luban; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | /** 7 | * Automatically close the previous InputStream when opening a new InputStream, 8 | * and finally need to manually call {@link #close()} to release the resource. 9 | */ 10 | public abstract class InputStreamAdapter implements InputStreamProvider { 11 | 12 | private InputStream inputStream; 13 | 14 | @Override 15 | public InputStream open() throws IOException { 16 | close(); 17 | inputStream = openInternal(); 18 | return inputStream; 19 | } 20 | 21 | public abstract InputStream openInternal() throws IOException; 22 | 23 | @Override 24 | public void close() { 25 | if (inputStream != null) { 26 | try { 27 | inputStream.close(); 28 | } catch (IOException ignore) { 29 | }finally { 30 | inputStream = null; 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/top/zibin/luban/InputStreamProvider.java: -------------------------------------------------------------------------------- 1 | package top.zibin.luban; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | /** 7 | * 通过此接口获取输入流,以兼容文件、FileProvider方式获取到的图片 8 | *

9 | * Get the input stream through this interface, and obtain the picture using compatible files and FileProvider 10 | */ 11 | public interface InputStreamProvider { 12 | 13 | InputStream open() throws IOException; 14 | 15 | void close(); 16 | 17 | String getPath(); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/top/zibin/luban/OnCompressListener.java: -------------------------------------------------------------------------------- 1 | package top.zibin.luban; 2 | 3 | import java.io.File; 4 | 5 | public interface OnCompressListener { 6 | 7 | /** 8 | * Fired when the compression is started, override to handle in your own code 9 | */ 10 | void onStart(); 11 | 12 | /** 13 | * Fired when a compression returns successfully, override to handle in your own code 14 | */ 15 | void onSuccess(File file); 16 | 17 | /** 18 | * Fired when a compression fails to complete, override to handle in your own code 19 | */ 20 | void onError(Throwable e); 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/top/zibin/luban/OnRenameListener.java: -------------------------------------------------------------------------------- 1 | package top.zibin.luban; 2 | 3 | /** 4 | * Author: zibin 5 | * Datetime: 2018/5/18 6 | *

7 | * 提供修改压缩图片命名接口 8 | *

9 | * A functional interface (callback) that used to rename the file after compress. 10 | */ 11 | public interface OnRenameListener { 12 | 13 | /** 14 | * 压缩前调用该方法用于修改压缩后文件名 15 | *

16 | * Call before compression begins. 17 | * 18 | * @param filePath 传入文件路径/ file path 19 | * @return 返回重命名后的字符串/ file name 20 | */ 21 | String rename(String filePath); 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/about.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/agreement.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 20 | 26 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/android.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/app_theme.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/auto_check_update.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/coffee.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/color.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 16 | 20 | 24 | 28 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/complete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dark_color.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/haptic.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/home_screen.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_drop_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_database.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_empty.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 7 | 13 | 17 | 24 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/json_file.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/light_color.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldlywt/IdeaMemo/7efed05aec3672c8c0299b08b0b8302b84a61af7/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldlywt/IdeaMemo/7efed05aec3672c8c0299b08b0b8302b84a61af7/app/src/main/res/mipmap-xxhdpi/ic_start.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/pic_thinking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldlywt/IdeaMemo/7efed05aec3672c8c0299b08b0b8302b84a61af7/app/src/main/res/mipmap-xxhdpi/pic_thinking.png -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 18 | 19 | 26 | 27 | 33 | 34 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network__config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.android.application) apply false 4 | alias(libs.plugins.jetbrains.kotlin.android) apply false 5 | alias(libs.plugins.android.library) apply false 6 | alias(libs.plugins.ksp) apply false 7 | alias(libs.plugins.hilt) apply false 8 | alias(libs.plugins.compose.compiler) apply false 9 | alias(libs.plugins.kotlinx.serialization) apply false 10 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldlywt/IdeaMemo/7efed05aec3672c8c0299b08b0b8302b84a61af7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jan 04 18:25:21 CST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven ( "https://maven.aliyun.com/repository/gradle-plugin/" ) // Gradle 插件镜像 4 | maven("https://www.jitpack.io/") 5 | google() 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | } 10 | dependencyResolutionManagement { 11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 12 | repositories { 13 | maven ( "https://maven.aliyun.com/repository/public/" ) // 阿里云公共仓库 14 | maven ("https://maven.aliyun.com/repository/google/" ) // Google 镜像 15 | maven("https://jitpack.io") 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.name = "Note" 22 | include(":app") 23 | --------------------------------------------------------------------------------