├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── dependabot.yml ├── fdroid.png ├── github.png └── workflows │ ├── debug_build.yml │ ├── nightly_build.yml │ └── release_build.yml ├── .gitignore ├── LICENSE_APACHE_SHEETS_COMPOSE_DIALOGS ├── LICENSE_GPL_EASYNOTES ├── README.md ├── STATUS.md ├── app ├── .gitignore ├── build.gradle.kts ├── debug │ └── output-metadata.json ├── proguard-rules.pro ├── release │ ├── baselineProfiles │ │ ├── 0 │ │ │ └── app-release.dm │ │ └── 1 │ │ │ └── app-release.dm │ └── output-metadata.json └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── com │ │ └── ezpnix │ │ └── writeon │ │ ├── App.kt │ │ ├── core │ │ └── constant │ │ │ ├── Connection.kt │ │ │ └── Database.kt │ │ ├── data │ │ ├── local │ │ │ ├── dao │ │ │ │ └── NotesDao.kt │ │ │ └── database │ │ │ │ ├── NoteDatabase.kt │ │ │ │ └── NoteDatabaseProvider.kt │ │ └── repository │ │ │ ├── ImportExportRepository.kt │ │ │ ├── NoteRepository.kt │ │ │ └── SettingsRepository.kt │ │ ├── di │ │ └── DataModule.kt │ │ ├── domain │ │ ├── model │ │ │ ├── Note.kt │ │ │ └── Settings.kt │ │ ├── repository │ │ │ ├── NoteRepository.kt │ │ │ └── SettingsRepository.kt │ │ └── usecase │ │ │ ├── ImportExportUseCase.kt │ │ │ ├── NoteUseCase.kt │ │ │ └── SettingsUseCase.kt │ │ ├── presentation │ │ ├── MainActivity.kt │ │ ├── components │ │ │ ├── Animations.kt │ │ │ ├── CustomActionButton.kt │ │ │ ├── CustomActions.kt │ │ │ ├── CustomScaffold.kt │ │ │ ├── EncryptionHelper.kt │ │ │ ├── ImageCleaner.kt │ │ │ └── markdown │ │ │ │ ├── Builder.kt │ │ │ │ ├── InlineElements.kt │ │ │ │ ├── LineProccesor.kt │ │ │ │ ├── MarkdownText.kt │ │ │ │ ├── RowElements.kt │ │ │ │ └── WidgetText.kt │ │ ├── navigation │ │ │ ├── AnimatedComposable.kt │ │ │ ├── NavHost.kt │ │ │ └── NavRoutes.kt │ │ ├── screens │ │ │ ├── edit │ │ │ │ ├── EditScreen.kt │ │ │ │ ├── components │ │ │ │ │ ├── CustomIconButton.kt │ │ │ │ │ ├── CustomTextField.kt │ │ │ │ │ └── TextFormattingToolbar.kt │ │ │ │ └── model │ │ │ │ │ └── EditModel.kt │ │ │ ├── home │ │ │ │ ├── HomeScreen.kt │ │ │ │ ├── viewmodel │ │ │ │ │ └── HomeModel.kt │ │ │ │ └── widgets │ │ │ │ │ ├── NoteCard.kt │ │ │ │ │ ├── NoteFilter.kt │ │ │ │ │ ├── NoteGrid.kt │ │ │ │ │ └── Placeholder.kt │ │ │ ├── settings │ │ │ │ ├── AndroidScreen.kt │ │ │ │ ├── BackupWorker.kt │ │ │ │ ├── FlashcardScreen.kt │ │ │ │ ├── ScratchpadScreen.kt │ │ │ │ ├── SettingsScreen.kt │ │ │ │ ├── model │ │ │ │ │ ├── Flashcard.kt │ │ │ │ │ ├── SettingsModel.kt │ │ │ │ │ └── SettingsPreferences.kt │ │ │ │ ├── settings │ │ │ │ │ ├── About.kt │ │ │ │ │ ├── Behaviour.kt │ │ │ │ │ ├── Colors.kt │ │ │ │ │ ├── Guide.kt │ │ │ │ │ ├── Issue.kt │ │ │ │ │ ├── Language.kt │ │ │ │ │ ├── Privacy.kt │ │ │ │ │ └── Tools.kt │ │ │ │ ├── trash │ │ │ │ │ └── Trash.kt │ │ │ │ └── widgets │ │ │ │ │ ├── SettingCategory.kt │ │ │ │ │ ├── SettingsBox.kt │ │ │ │ │ └── SettingsDialog.kt │ │ │ └── terms │ │ │ │ └── TermsScreen.kt │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Schemes.kt │ │ │ └── Theme.kt │ │ └── widget │ │ ├── AddNoteReceiver.kt │ │ ├── AddNoteWidget.kt │ │ ├── NotesWidget.kt │ │ ├── NotesWidgetActivity.kt │ │ ├── NotesWidgetReceiver.kt │ │ └── ui │ │ ├── Note.kt │ │ └── ZeroState.kt │ └── res │ ├── drawable-nodpi │ ├── widget_preview_1.png │ └── widget_preview_2.png │ ├── drawable │ ├── ic_launcher_background.xml │ ├── incognito_fill.xml │ └── splash_icon.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── values-v31 │ ├── colors.xml │ └── themes.xml │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── add_note.xml │ ├── backup_rules.xml │ ├── data_extraction_rules.xml │ ├── locales_config.xml │ └── notes.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── metadata └── en-US │ ├── changelogs │ ├── 1 │ ├── 2 │ ├── 3 │ ├── 4 │ ├── 5 │ ├── 6 │ └── 7 │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ ├── short_description.txt │ └── title.txt └── settings.gradle.kts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: 3zpnix -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | EMAIL: ezpnix@proton.me 2 | 3 | ## Type here 4 | - 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | EMAIL: ezpnix@proton.me 2 | 3 | ## Type here 4 | - 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | EMAIL: ezpnix@proton.me 2 | 3 | ## Type here 4 | - 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | rebase-strategy: "disabled" 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | rebase-strategy: "disabled" -------------------------------------------------------------------------------- /.github/fdroid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/.github/fdroid.png -------------------------------------------------------------------------------- /.github/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/.github/github.png -------------------------------------------------------------------------------- /.github/workflows/debug_build.yml: -------------------------------------------------------------------------------- 1 | name: Debug Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | steps: 21 | - name: Check out repository 22 | uses: actions/checkout@v4 23 | with: 24 | submodules: true 25 | 26 | - name: Set up Java 17 27 | uses: actions/setup-java@v4 28 | with: 29 | java-version: 17 30 | distribution: 'adopt' 31 | cache: gradle 32 | 33 | - name: Grant execution permission to Gradle Wrapper 34 | run: chmod +x gradlew 35 | 36 | - name: Build Debug App 37 | run: ./gradlew assembleDebug 38 | 39 | - name: Upload a Build Artifact 40 | uses: actions/upload-artifact@v4.3.5 41 | with: 42 | name: WriteOn-debug.apk 43 | path: app/build/outputs/apk/debug/app-debug.apk 44 | -------------------------------------------------------------------------------- /.github/workflows/nightly_build.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Build 2 | 3 | on: 4 | #push: 5 | #branches: 6 | #- master 7 | workflow_dispatch: 8 | inputs: 9 | commit: 10 | description: 'Commit hash to build' 11 | required: false 12 | default: '13235d467ccf36691311e14e63e57ac350e3c930' 13 | type: string 14 | 15 | concurrency: 16 | group: ${{ github.workflow }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | packages: write 25 | 26 | steps: 27 | - name: Check out repository 28 | uses: actions/checkout@v4 29 | with: 30 | submodules: true 31 | 32 | - name: Set up Java 17 33 | uses: actions/setup-java@v4 34 | with: 35 | java-version: 17 36 | distribution: 'adopt' 37 | cache: gradle 38 | 39 | - name: Grant execution permission to Gradle Wrapper 40 | run: chmod +x gradlew 41 | 42 | - name: Build Release APK 43 | run: ./gradlew assembleRelease 44 | 45 | - name: Decode the Keystore from Base64 46 | run: echo "${{ secrets.KEY_BASE64 }}" | base64 --decode > signingKey.jks 47 | 48 | - name: Sign app APK 49 | uses: r0adkll/sign-android-release@v1 50 | id: sign_app 51 | with: 52 | releaseDirectory: app/build/outputs/apk/release 53 | signingKeyBase64: ${{ secrets.KEY_BASE64 }} 54 | alias: ${{ secrets.KEY_ALIAS }} 55 | keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} 56 | keyPassword: ${{ secrets.KEYSTORE_PASSWORD }} 57 | env: 58 | BUILD_TOOLS_VERSION: "34.0.0" 59 | 60 | - name: Rename APK 61 | run: mv app/build/outputs/apk/release/app-release-signed.apk WriteOn-nightly.apk 62 | 63 | - name: Upload the APK 64 | uses: actions/upload-artifact@v4.3.5 65 | with: 66 | name: WriteOn-Nightly 67 | path: WriteOn-nightly.apk 68 | 69 | - name: Update nightly release 70 | uses: pyTooling/Actions/releaser@main 71 | with: 72 | tag: Nightly 73 | token: ${{ secrets.TOKEN }} 74 | files: WriteOn-nightly.apk 75 | -------------------------------------------------------------------------------- /.github/workflows/release_build.yml: -------------------------------------------------------------------------------- 1 | name: Release Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | inputs: 9 | commit: 10 | description: 'Commit hash to build' 11 | required: false 12 | default: '13235d467ccf36691311e14e63e57ac350e3c930' 13 | type: string 14 | 15 | concurrency: 16 | group: ${{ github.workflow }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | packages: write 25 | 26 | steps: 27 | - name: Check out repository 28 | uses: actions/checkout@v4 29 | with: 30 | submodules: true 31 | 32 | - name: Set up Java 17 33 | uses: actions/setup-java@v4 34 | with: 35 | java-version: 17 36 | distribution: 'adopt' 37 | cache: gradle 38 | 39 | - name: Grant execution permission to Gradle Wrapper 40 | run: chmod +x gradlew 41 | 42 | - name: Build Release APK 43 | run: ./gradlew assembleRelease 44 | 45 | - name: Decode the Keystore from Base64 46 | run: echo "${{ secrets.KEY_BASE64 }}" | base64 --decode > signingKey.jks 47 | 48 | - name: Sign app APK 49 | uses: r0adkll/sign-android-release@v1 50 | id: sign_app 51 | with: 52 | releaseDirectory: app/build/outputs/apk/release 53 | signingKeyBase64: ${{ secrets.KEY_BASE64 }} 54 | alias: ${{ secrets.KEY_ALIAS }} 55 | keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} 56 | keyPassword: ${{ secrets.KEYSTORE_PASSWORD }} 57 | env: 58 | BUILD_TOOLS_VERSION: "34.0.0" 59 | 60 | - name: Rename APK 61 | run: mv app/build/outputs/apk/release/app-release-signed.apk WriteOn.apk 62 | 63 | - name: Upload the APK 64 | uses: actions/upload-artifact@v4.3.5 65 | with: 66 | name: Write On 67 | path: WriteOn.apk 68 | 69 | - name: Install GitHub CLI 70 | run: sudo apt-get install gh 71 | 72 | - name: Create GitHub Release 73 | run: | 74 | gh release create v1.6 WriteOn.apk \ 75 | --title "v1.6" \ 76 | --notes "Fixed Jun Update~" \ 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific configurations 2 | /.idea 3 | .idea/caches/ 4 | .idea/libraries/ 5 | .idea/shelf/ 6 | .idea/.name 7 | .idea/compiler.xml 8 | .idea/copyright/profiles_settings.xml 9 | .idea/encodings.xml 10 | .idea/misc.xml 11 | .idea/modules.xml 12 | .idea/scopes/scope_settings.xml 13 | .idea/vcs.xml 14 | .idea/jsLibraryMappings.xml 15 | .idea/datasources.xml 16 | .idea/dataSources.ids 17 | .idea/sqlDataSources.xml 18 | .idea/dynamic.xml 19 | .idea/uiDesigner.xml 20 | 21 | # Android Studio 22 | .kotlin/ 23 | /*/build/ 24 | /*/local.properties 25 | /*/out 26 | /*/*/build 27 | /build 28 | /*/*/production 29 | *.ipr 30 | *~ 31 | *.swp 32 | 33 | # IntelliJ IDEA 34 | *.iws 35 | /out/ 36 | 37 | 38 | # Gradle files 39 | .gradle/ 40 | .gradle 41 | build/ 42 | 43 | # Local configuration file (sdk path, etc) 44 | /local.properties 45 | local.properties 46 | 47 | # Log/OS Files 48 | *.log 49 | 50 | # Android Studio generated files and folders 51 | captures/ 52 | .externalNativeBuild/ 53 | .cxx/ 54 | *.apk 55 | output.json 56 | 57 | # IntelliJ 58 | *.iml 59 | .idea/ 60 | misc.xml 61 | deploymentTargetDropDown.xml 62 | render.experimental.xml 63 | 64 | # Keystore files 65 | *.keystore 66 | *.jks 67 | 68 | # Android Profiling 69 | *.hprof -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | App Icon 3 | 4 | # Write On: Simple Notepad ✅ 5 | A clean, intuitive note-taking app with *Material You* design — open source and privacy-respecting. 6 | 7 | [Get it on GitHub](https://github.com/3zpnix/WriteOn/releases) 8 |   9 | [Get it on F-Droid](https://f-droid.org/en/packages/com.ezpnix.writeon/) 10 |
11 | 12 | --- 13 | 14 |
15 | Screenshot 1 16 | Screenshot 2 17 | Screenshot 3 18 |
19 | 20 | --- 21 | 22 | ## 💥 Personal DevLog (^~^) 23 | 24 |
25 | 💥 v1.6 (2025-06-01) 26 | 27 | - [x] New homepage user interface 28 | - [x] More visibility on your notes 29 | - [x] FAB has returned but cleaner now 30 | - [x] Added new Flashcard screen feature 31 | - [x] Column view count is now adjustable 32 | - [x] Improvements to Scratchpad screen 33 | - [x] Quick shortcut button redirects to Styles 34 | - [x] Home greeting placeholder repositioned 35 | - [x] Issue feedback now on the settings screen 36 | - [x] Feedback screen cards now fully clickable 37 | - [x] Added some minor animation improvements 38 | - [x] Get info on your device found in about 39 | - [x] Some icons were replaced to be cleaner 40 | - [x] Bug fixes and optimizations 41 |
42 | 43 |
44 | 💥 v1.5 (2025-05-09) 45 | 46 | - [x] Removed savenote else-function bug 47 | - [x] Homescreen buttons surface lazyrow 48 | - [x] Edit buttons moved to bottom modal 49 | - [x] Calculator supports manual edits 50 | - [x] Listed-section improvements 51 | - [x] Fixed major crash issue 52 |
53 | 54 |
55 | 💥 v1.4 (2025-03-25) 56 | 57 | - [x] Refactored edit/view models 58 | - [x] Row icon buttons replace centered buttons 59 | - [x] Added Help & Feedback section 60 | - [x] Pin/unpin status saves properly 61 | - [x] Fixed calculator parenthesis bug 62 | - [x] New sections in Settings screen 63 | - [x] DPI support improved 64 | - [x] Updated note preview UI 65 | - [x] Alert dialog logic revamped 66 |
67 | 68 |
69 | 💥 v1.3 (2025-02-25) 70 | 71 | - [x] Home UI updated 72 | - [x] Searchbar placeholder added 73 | - [x] Custom dimensions fixed 74 | - [x] Calculator built-in 75 | - [x] Adjustable font size 76 | - [x] More buttons/features 77 | - [x] Calendar bug squashed 78 | - [x] Renamed strings 79 | - [x] Minor bug fixes 80 |
81 | 82 |
83 | 💥 v1.2 (2025-01-10) 84 | 85 | - [x] More markdown support 86 | - [x] TXT export for quick notes 87 | - [x] Improved auto-backup logic 88 | - [x] Translate and share buttons added 89 | - [x] Fixed partial image bug 90 | - [x] UI updates and optimizations 91 |
92 | 93 |
94 | 💥 v1.1 (2024-11-30) 95 | 96 | - [x] UI Changes 97 | - [x] Bug Fixes 98 |
99 | 100 | --- 101 | 102 | ## 📢 Announcements 103 | 104 | - *2025-06-01:* Thankfully, I had time to continue the v1.6 update, had fun with this version, thank you! 105 | - *2025-05-09:* Small v1.5 update released — life’s hectic with job & university, so expect some slow updates. 106 | - *2025-02-25:* Found time for v1.3 update — future updates might be delayed due to part-time job. 107 | - *2025-01-19:* Taking a break to focus on other projects. Still planning v1.3! 108 | - *2024-12-07:* University workload delaying updates — sorry! 109 | - *2024-12-03:* v1.2 will be mostly bug fixes + features. 110 | - *2024-11-19:* Android Studio issues — delays expected. 111 | - *2024-08-26:* Android release coming soon to GitHub and F-Droid! 112 | 113 | --- 114 | 115 | ## 💬 Contact 116 | 117 | - *Email:* ezpnix@proton.me 118 | - *Twitter:* [@3zpnix](https://twitter.com/3zpnix) 119 | 120 | --- 121 | 122 | ## 👋 Features 123 | 124 | **Biometric Auth** • **Backup/Restore** • **Custom Layout** • **Markdown** • 125 | **Built-In Calendar** • **Offline Access** • **Privacy-Friendly** • 126 | **No Bloat Permissions** • **Material You UI** • **Custom Themes** • 127 | **Multiple Export Options** • **Scratchpad** • **Share Text** • 128 | **Flashcard** • **Image Attachments** • **Calculator** • **Fonts** 129 | 130 | --- 131 | 132 | ## ⚠️ License 133 | Write On: Simple Notepad 134 | 135 | Copyright (C)2024 3zpnix 136 | 137 | This software is free to use, modify, and redistribute under 138 | the terms of the GNU General Public License, as published by the 139 | Free Software Foundation. You may choose to use either version 3 of the License 140 | or, at your option, any later version. The software is provided with the hope 141 | that it will be useful, but it comes as is with no warranties, including 142 | implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 143 | For more details, please refer to the GNU General Public License. 144 | 145 | The above copyright notice, this permission notice, and the license must be included in all copies or substantial portions of the software. 146 | 147 | You can find a copy of the GNU General Public License v3 [here](https://www.gnu.org/licenses/)

148 | -------------------------------------------------------------------------------- /STATUS.md: -------------------------------------------------------------------------------- 1 | # Write On: Simple Notepad ✅ 2 | - Last Updated: 1/10/2025 3 | 4 | ## In Progress: 5 | - [x] More markdown formats 6 | - [x] Quick note export to txt 7 | - [x] Updated auto-backup logic 8 | - [x] Direct to translate button 9 | - [x] Dropdown share button 10 | - [x] Fixed partial image bug 11 | - [x] Updated user interface 12 | - [x] Internet search button 13 | - [x] Updated resource strings 14 | - [x] More optimizations 15 | - [ ] Font Size Changeable 16 | - [ ] Cross Platform Client 17 | 18 | Contact me if you have any suggestions or feedbacks about the current/upcoming changes. For inquiries, email ezpnix@proton.me 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | alias(libs.plugins.ksp) 5 | alias(libs.plugins.compose.compiler) 6 | alias(libs.plugins.hilt) 7 | } 8 | android { 9 | namespace = "com.ezpnix.writeon" 10 | compileSdk = 34 11 | 12 | defaultConfig { 13 | applicationId = "com.ezpnix.writeon" 14 | minSdk = 26 15 | targetSdk = 34 16 | versionCode = 7 17 | versionName = "1.6" 18 | vectorDrawables { 19 | useSupportLibrary = true 20 | } 21 | 22 | // https://developer.android.com/guide/topics/resources/app-languages#gradle-config 23 | resourceConfigurations.plus( 24 | listOf("en") 25 | ) 26 | } 27 | 28 | buildTypes { 29 | release { 30 | isMinifyEnabled = true 31 | isShrinkResources = true 32 | 33 | proguardFiles( 34 | getDefaultProguardFile("proguard-android-optimize.txt"), 35 | "proguard-rules.pro" 36 | ) 37 | signingConfig = signingConfigs.getByName("debug") 38 | } 39 | 40 | debug { 41 | isMinifyEnabled = false 42 | proguardFiles( 43 | getDefaultProguardFile("proguard-android-optimize.txt"), 44 | "proguard-rules.pro" 45 | ) 46 | signingConfig = signingConfigs.getByName("debug") 47 | isDebuggable = true 48 | applicationIdSuffix = ".debug" 49 | } 50 | } 51 | compileOptions { 52 | sourceCompatibility = JavaVersion.VERSION_17 53 | targetCompatibility = JavaVersion.VERSION_17 54 | } 55 | kotlinOptions { 56 | jvmTarget = "17" 57 | } 58 | buildFeatures { 59 | compose = true 60 | buildConfig = true 61 | } 62 | packaging { 63 | resources { 64 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 65 | } 66 | } 67 | buildToolsVersion = "34.0.0" 68 | 69 | dependenciesInfo { 70 | includeInApk = false 71 | includeInBundle = false 72 | } 73 | } 74 | 75 | dependencies { 76 | implementation(libs.androidx.glance) 77 | implementation(libs.coil.compose) 78 | implementation(libs.hilt.navigation.compose) 79 | implementation(libs.androidx.glance.appwidget) 80 | implementation(libs.androidx.biometric.ktx) 81 | ksp(libs.androidx.room.compiler) 82 | ksp(libs.hilt.android.compiler) 83 | ksp(libs.hilt.compile) 84 | implementation(libs.hilt.android) 85 | implementation(libs.androidx.datastore.preferences) 86 | implementation(libs.androidx.room.runtime) 87 | implementation(libs.androidx.compose.material.icons.extended) 88 | implementation(libs.androidx.room.ktx) 89 | implementation(libs.androidx.appcompat) 90 | implementation(libs.androidx.material3) 91 | implementation(libs.androidx.activity.compose) 92 | implementation(libs.androidx.core.ktx) 93 | implementation(libs.androidx.core.splashscreen) 94 | implementation(libs.androidx.navigation.compose) 95 | implementation(libs.compose.calendar) 96 | implementation(libs.compose.core) 97 | implementation(libs.message.bar) 98 | implementation(libs.automatic.backup) 99 | implementation(libs.gson) 100 | } 101 | -------------------------------------------------------------------------------- /app/debug/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.ezpnix.writeon.debug", 8 | "variantName": "debug", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 7, 15 | "versionName": "1.6", 16 | "outputFile": "app-debug.apk" 17 | } 18 | ], 19 | "elementType": "File", 20 | "minSdkVersionForDexing": 26 21 | } -------------------------------------------------------------------------------- /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 | 23 | # === GSON & TypeToken rules (for Flashcard support) === 24 | -keep class com.google.gson.** { *; } 25 | -keep class com.google.gson.reflect.TypeToken 26 | -keepattributes Signature 27 | -keepattributes *Annotation* 28 | 29 | # Keep your app's Flashcard data class used in Gson serialization/deserialization 30 | -keep class com.ezpnix.writeon.presentation.screens.settings.model.Flashcard { *; } 31 | 32 | # Optional: If you use other models with Gson, you can keep them like this: 33 | # -keep class com.ezpnix.writeon.** { *; } 34 | -------------------------------------------------------------------------------- /app/release/baselineProfiles/0/app-release.dm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/release/baselineProfiles/0/app-release.dm -------------------------------------------------------------------------------- /app/release/baselineProfiles/1/app-release.dm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/release/baselineProfiles/1/app-release.dm -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.ezpnix.writeon", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 7, 15 | "versionName": "1.6", 16 | "outputFile": "app-release.apk" 17 | } 18 | ], 19 | "elementType": "File", 20 | "baselineProfiles": [ 21 | { 22 | "minApi": 28, 23 | "maxApi": 30, 24 | "baselineProfiles": [ 25 | "baselineProfiles/1/app-release.dm" 26 | ] 27 | }, 28 | { 29 | "minApi": 31, 30 | "maxApi": 2147483647, 31 | "baselineProfiles": [ 32 | "baselineProfiles/0/app-release.dm" 33 | ] 34 | } 35 | ], 36 | "minSdkVersionForDexing": 26 37 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 11 | 12 | 15 | 16 | 19 | 20 | 21 | 22 | 34 | 36 | 37 | 38 | 39 | 42 | 43 | 45 | 46 | 47 | 48 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/App.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App : Application() -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/core/constant/Connection.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.core.constant 2 | 3 | object ConnectionConst { 4 | 5 | const val SUPPORT_MAIL = "ezpnix@proton.me" 6 | 7 | const val YOUTUBE = "https://youtube.com/@3zpnix/" 8 | 9 | const val GITHUB = "https://github.com/3zpnix/WriteOn/" 10 | 11 | const val KOFI = "https://ko-fi.com/3zpnix" 12 | 13 | const val TERMS_EFFECTIVE_DATE = "2025/09/05" 14 | 15 | val APP_LIST = listOf( 16 | Pair("v1.0", "Sep 16, 2024"), 17 | Pair("v1.1", "Dec 09, 2024"), 18 | Pair("v1.2", "Jan 17, 2025"), 19 | Pair("v1.3", "Mar 01, 2025"), 20 | Pair("v1.4", "Mar 31, 2025"), 21 | Pair("v1.5", "May 18, 2025"), 22 | Pair("v1.6", "Latest Release"), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/core/constant/Database.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.core.constant 2 | 3 | object DatabaseConst { 4 | 5 | const val NOTES_DATABASE_VERSION = 4 6 | 7 | const val NOTES_DATABASE_FILE_NAME = "note-list.db" 8 | 9 | const val NOTES_DATABASE_BACKUP_NAME = "WriteOn" 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/data/local/dao/NotesDao.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.data.local.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.Update 9 | import com.ezpnix.writeon.domain.model.Note 10 | import kotlinx.coroutines.flow.Flow 11 | 12 | @Dao 13 | interface NoteDao { 14 | 15 | @Insert(onConflict = OnConflictStrategy.IGNORE) 16 | suspend fun addNote(note: Note) 17 | 18 | @Query("SELECT * FROM `notes-table`") 19 | fun getAllNotes(): Flow> 20 | 21 | @Update 22 | suspend fun updateNote(note: Note) 23 | 24 | @Delete 25 | suspend fun deleteNote(note: Note) 26 | 27 | @Query("SELECT * FROM `notes-table` WHERE id=:id") 28 | fun getNoteById(id: Int): Flow 29 | 30 | @Query("SELECT id FROM `notes-table` ORDER BY id DESC LIMIT 1") 31 | fun getLastNoteId(): Long? 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/data/local/database/NoteDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.data.local.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.ezpnix.writeon.core.constant.DatabaseConst 6 | import com.ezpnix.writeon.data.local.dao.NoteDao 7 | import com.ezpnix.writeon.domain.model.Note 8 | 9 | @Database( 10 | entities = [Note::class], 11 | version = DatabaseConst.NOTES_DATABASE_VERSION, 12 | exportSchema = false 13 | ) 14 | abstract class NoteDatabase : RoomDatabase() { 15 | 16 | abstract fun noteDao(): NoteDao 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/data/local/database/NoteDatabaseProvider.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.data.local.database 2 | 3 | import android.app.Application 4 | import androidx.room.Room 5 | import androidx.room.migration.Migration 6 | import androidx.sqlite.db.SupportSQLiteDatabase 7 | import com.ezpnix.writeon.core.constant.DatabaseConst 8 | import com.ezpnix.writeon.data.local.dao.NoteDao 9 | 10 | class NoteDatabaseProvider(private val application: Application) { 11 | 12 | @Volatile 13 | private var database: NoteDatabase? = null 14 | 15 | @Synchronized 16 | fun instance(): NoteDatabase { 17 | return database ?: synchronized(this) { 18 | database ?: buildDatabase().also { database = it } 19 | } 20 | } 21 | 22 | private fun buildDatabase(): NoteDatabase { 23 | return Room.databaseBuilder(application.applicationContext, 24 | NoteDatabase::class.java, 25 | DatabaseConst.NOTES_DATABASE_FILE_NAME) 26 | .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_2_4) 27 | .build() 28 | } 29 | 30 | @Synchronized 31 | fun close() { 32 | database?.close() 33 | database = null 34 | } 35 | 36 | fun noteDao(): NoteDao { 37 | return instance().noteDao() 38 | } 39 | } 40 | 41 | private val MIGRATION_1_2 = object : Migration(1, 2) { 42 | override fun migrate(db: SupportSQLiteDatabase) { 43 | db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `created_at` INTEGER NOT NULL DEFAULT ${System.currentTimeMillis()}") 44 | } 45 | } 46 | 47 | private val MIGRATION_2_3 = object : Migration(2, 3) { 48 | override fun migrate(db: SupportSQLiteDatabase) { 49 | db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `pinned` INTEGER NOT NULL DEFAULT 0") 50 | } 51 | } 52 | 53 | private val MIGRATION_3_4 = object : Migration(3, 4) { 54 | override fun migrate(db: SupportSQLiteDatabase) { 55 | db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `encrypted` INTEGER NOT NULL DEFAULT 0") 56 | } 57 | } 58 | 59 | private val MIGRATION_2_4 = object : Migration(2, 4) { 60 | override fun migrate(db: SupportSQLiteDatabase) { 61 | db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `pinned` INTEGER NOT NULL DEFAULT 0") 62 | db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `encrypted` INTEGER NOT NULL DEFAULT 0") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/data/repository/NoteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.data.repository 2 | 3 | import com.ezpnix.writeon.data.local.database.NoteDatabaseProvider 4 | import com.ezpnix.writeon.domain.model.Note 5 | import com.ezpnix.writeon.domain.repository.NoteRepository 6 | import kotlinx.coroutines.flow.Flow 7 | import javax.inject.Inject 8 | 9 | class NoteRepositoryImpl @Inject constructor( 10 | private val provider: NoteDatabaseProvider 11 | ) : NoteRepository { 12 | override fun getAllNotes(): Flow> { 13 | return provider.noteDao().getAllNotes() 14 | } 15 | 16 | override suspend fun addNote(note: Note) { 17 | provider.noteDao().addNote(note) 18 | } 19 | 20 | override suspend fun updateNote(note: Note) { 21 | provider.noteDao().updateNote(note) 22 | } 23 | 24 | override suspend fun deleteNote(note: Note) { 25 | provider.noteDao().deleteNote(note) 26 | } 27 | 28 | override fun getNoteById(id: Int): Flow { 29 | return provider.noteDao().getNoteById(id) 30 | } 31 | 32 | override fun getLastNoteId(): Long? { 33 | return provider.noteDao().getLastNoteId() 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/data/repository/SettingsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.data.repository 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.SharedPreferencesMigration 6 | import androidx.datastore.preferences.core.Preferences 7 | import androidx.datastore.preferences.core.booleanPreferencesKey 8 | import androidx.datastore.preferences.core.edit 9 | import androidx.datastore.preferences.core.floatPreferencesKey 10 | import androidx.datastore.preferences.core.intPreferencesKey 11 | import androidx.datastore.preferences.core.stringPreferencesKey 12 | import androidx.datastore.preferences.preferencesDataStore 13 | import com.ezpnix.writeon.domain.repository.SettingsRepository 14 | import com.ezpnix.writeon.widget.NotesWidgetReceiver 15 | import kotlinx.coroutines.flow.first 16 | 17 | private const val PREFERENCES_NAME = "settings updated" 18 | 19 | private val Context.dataStore: DataStore by preferencesDataStore( 20 | name = PREFERENCES_NAME, 21 | produceMigrations = { context -> listOf(SharedPreferencesMigration(context, PREFERENCES_NAME)) } 22 | ) 23 | 24 | class SettingsRepositoryImpl (private val context: Context) : SettingsRepository { 25 | override suspend fun putString(key: String, value: String) { 26 | val preferencesKey = stringPreferencesKey(key) 27 | context.dataStore.edit { preferences -> 28 | preferences[preferencesKey] = value 29 | } 30 | } 31 | 32 | override suspend fun putInt(key: String, value: Int) { 33 | val preferencesKey = intPreferencesKey(key) 34 | context.dataStore.edit { preferences -> 35 | preferences[preferencesKey] = value 36 | } 37 | } 38 | 39 | override suspend fun getString(key: String): String? { 40 | val preferencesKey = stringPreferencesKey(key) 41 | val preferences = context.dataStore.data.first() 42 | return preferences[preferencesKey] 43 | } 44 | 45 | override suspend fun getInt(key: String): Int? { 46 | val preferencesKey = intPreferencesKey(key) 47 | val preferences = context.dataStore.data.first() 48 | return preferences[preferencesKey] 49 | } 50 | 51 | override suspend fun putBoolean(key: String, value: Boolean) { 52 | val preferencesKey = booleanPreferencesKey(key) 53 | context.dataStore.edit { preferences -> 54 | preferences[preferencesKey] = value 55 | } 56 | } 57 | 58 | override suspend fun getBoolean(key: String): Boolean? { 59 | val preferencesKey = booleanPreferencesKey(key) 60 | val preferences = context.dataStore.data.first() 61 | return preferences[preferencesKey] 62 | } 63 | 64 | override suspend fun putFloat(key: String, value: Float) { 65 | val preferencesKey = floatPreferencesKey(key) 66 | context.dataStore.edit { preferences -> 67 | preferences[preferencesKey] = value 68 | } 69 | } 70 | 71 | override suspend fun getFloat(key: String): Float? { 72 | val preferencesKey = floatPreferencesKey(key) 73 | val preferences = context.dataStore.data.first() 74 | return preferences[preferencesKey] 75 | } 76 | 77 | override suspend fun getEveryNotesWidget(): List> { 78 | val preferences = context.dataStore.data.first() 79 | val widgetPairs = mutableListOf>() 80 | 81 | preferences.asMap().forEach { entry -> 82 | val key = entry.key.name 83 | 84 | if (entry.key.name.startsWith(NotesWidgetReceiver.WIDGET_PREFERENCE)) { 85 | val widgetId = key.substringAfter(NotesWidgetReceiver.WIDGET_PREFERENCE).toIntOrNull() 86 | if (widgetId != null) { 87 | val value = entry.value as? Int ?: 0 88 | widgetPairs.add(widgetId to value) 89 | } 90 | } 91 | } 92 | return widgetPairs 93 | } 94 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/di/DataModule.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.di 2 | 3 | import android.app.Application 4 | import android.os.Handler 5 | import android.content.Context 6 | import android.os.Looper 7 | import com.ezpnix.writeon.data.local.database.NoteDatabaseProvider 8 | import com.ezpnix.writeon.data.repository.ImportExportRepository 9 | import com.ezpnix.writeon.data.repository.NoteRepositoryImpl 10 | import com.ezpnix.writeon.data.repository.SettingsRepositoryImpl 11 | import com.ezpnix.writeon.presentation.components.EncryptionHelper 12 | import dagger.Module 13 | import dagger.Provides 14 | import dagger.hilt.InstallIn 15 | import dagger.hilt.android.qualifiers.ApplicationContext 16 | import dagger.hilt.components.SingletonComponent 17 | import kotlinx.coroutines.CoroutineScope 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.ExecutorCoroutineDispatcher 20 | import kotlinx.coroutines.SupervisorJob 21 | import kotlinx.coroutines.asCoroutineDispatcher 22 | import kotlinx.coroutines.sync.Mutex 23 | import java.util.concurrent.Executors 24 | import javax.inject.Qualifier 25 | import javax.inject.Singleton 26 | 27 | @Qualifier 28 | annotation class WidgetCoroutineScope 29 | 30 | @Module 31 | @InstallIn(SingletonComponent::class) 32 | object ApplicationModule { 33 | 34 | @Provides 35 | @Singleton 36 | @WidgetCoroutineScope 37 | fun providesWidgetCoroutineScope(): CoroutineScope = CoroutineScope( 38 | Executors.newSingleThreadExecutor().asCoroutineDispatcher(), 39 | ) 40 | 41 | @Provides 42 | @Singleton 43 | fun provideNoteDatabaseProvider(application: Application): NoteDatabaseProvider = NoteDatabaseProvider(application) 44 | 45 | @Provides 46 | @Singleton 47 | fun provideCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) 48 | 49 | @Provides 50 | fun provideMutex(): Mutex = Mutex() 51 | 52 | @Provides 53 | @Singleton 54 | fun provideExecutorCoroutineDispatcher(): ExecutorCoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() 55 | 56 | @Provides 57 | @Singleton 58 | fun provideNoteRepository(noteDatabaseProvider: NoteDatabaseProvider): NoteRepositoryImpl { 59 | return NoteRepositoryImpl(noteDatabaseProvider) 60 | } 61 | 62 | @Provides 63 | @Singleton 64 | fun provideSettingsRepository(@ApplicationContext context: Context): SettingsRepositoryImpl { 65 | return SettingsRepositoryImpl(context) 66 | } 67 | 68 | @Provides 69 | @Singleton 70 | fun provideBackupRepository( 71 | noteDatabaseProvider: NoteDatabaseProvider, 72 | application: Application, 73 | mutex: Mutex, 74 | coroutineScope: CoroutineScope, 75 | executorCoroutineDispatcher: ExecutorCoroutineDispatcher, 76 | ): ImportExportRepository { 77 | return ImportExportRepository( 78 | provider = noteDatabaseProvider, 79 | context = application, 80 | mutex = mutex, 81 | scope = coroutineScope, 82 | dispatcher = executorCoroutineDispatcher 83 | ) 84 | } 85 | 86 | @Provides 87 | @Singleton 88 | fun provideMutableVaultPassword(): StringBuilder { 89 | return StringBuilder() 90 | } 91 | 92 | @Provides 93 | @Singleton 94 | fun provideEncryptionHelper(mutableVaultPassword: StringBuilder): EncryptionHelper { 95 | return EncryptionHelper(mutableVaultPassword) 96 | } 97 | 98 | @Provides 99 | fun provideHandler(): Handler { 100 | return Handler(Looper.getMainLooper()) 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/domain/model/Note.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.domain.model 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "notes-table") 8 | data class Note( 9 | @PrimaryKey(autoGenerate = true) 10 | val id: Int = 0, 11 | 12 | @ColumnInfo(name = "note-name") 13 | val name: String, 14 | 15 | @ColumnInfo(name = "note-description") 16 | val description: String, 17 | 18 | @ColumnInfo(name = "pinned") 19 | val pinned: Boolean = false, 20 | 21 | @ColumnInfo(name = "encrypted") 22 | val encrypted: Boolean = false, 23 | 24 | @ColumnInfo(name = "created_at") 25 | val createdAt: Long = System.currentTimeMillis(), 26 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/domain/model/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.domain.model 2 | 3 | data class Settings( 4 | val viewMode: Boolean = true, 5 | val automaticTheme: Boolean = false, 6 | val darkTheme: Boolean = true, 7 | var dynamicTheme: Boolean = false, 8 | var amoledTheme: Boolean = false, 9 | var minimalisticMode: Boolean = false, 10 | var extremeAmoledMode: Boolean = false, 11 | var isMarkdownEnabled: Boolean = true, 12 | var screenProtection: Boolean = false, 13 | var encryptBackup: Boolean = false, 14 | var sortDescending: Boolean = true, 15 | var vaultSettingEnabled: Boolean = false, 16 | var vaultEnabled: Boolean = false, 17 | var editMode: Boolean = false, 18 | var showOnlyTitle: Boolean = false, 19 | var termsOfService: Boolean = false, 20 | val isBiometricEnabled: Boolean = false, 21 | val autoBackupEnabled: Boolean = false, 22 | val fontSize: Float = 16f, 23 | val columnsCount: Int = 2, 24 | 25 | var cornerRadius: Int = 32, 26 | ) 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/domain/repository/NoteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.domain.repository 2 | 3 | import com.ezpnix.writeon.domain.model.Note 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface NoteRepository { 7 | fun getAllNotes(): Flow> 8 | suspend fun addNote(note: Note) 9 | suspend fun updateNote(note: Note) 10 | suspend fun deleteNote(note: Note) 11 | fun getNoteById(id: Int): Flow 12 | fun getLastNoteId(): Long? 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/domain/repository/SettingsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.domain.repository 2 | 3 | interface SettingsRepository { 4 | suspend fun putString(key: String, value: String) 5 | suspend fun getString(key: String): String? 6 | 7 | suspend fun putInt(key: String, value: Int) 8 | suspend fun getInt(key: String): Int? 9 | 10 | suspend fun putBoolean(key: String, value: Boolean) 11 | suspend fun getBoolean(key: String): Boolean? 12 | 13 | suspend fun putFloat(key: String, value: Float) 14 | suspend fun getFloat(key: String): Float? 15 | 16 | suspend fun getEveryNotesWidget(): List> 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/domain/usecase/ImportExportUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.domain.usecase 2 | 3 | import android.net.Uri 4 | import android.util.Log 5 | import com.ezpnix.writeon.data.repository.ImportExportRepository 6 | import com.ezpnix.writeon.data.repository.NoteRepositoryImpl 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.NonCancellable 10 | import kotlinx.coroutines.launch 11 | import kotlinx.coroutines.withContext 12 | import java.io.IOException 13 | import javax.inject.Inject 14 | 15 | data class ImportResult( 16 | val successful: Int, 17 | val total: Int, 18 | ) 19 | 20 | class ImportExportUseCase @Inject constructor( 21 | private val noteRepository: NoteRepositoryImpl, 22 | private val coroutineScope: CoroutineScope, 23 | private val fileRepository: ImportExportRepository, 24 | ) 25 | { 26 | // AA1 27 | fun importNotes(uris: List, onResult: (ImportResult) -> Unit) { 28 | coroutineScope.launch(NonCancellable + Dispatchers.IO) { 29 | var successful = 0 30 | uris.forEach{uri -> 31 | try { 32 | noteRepository.addNote(fileRepository.importFile(uri)) 33 | successful++ 34 | } catch (e: IOException) { 35 | Log.e(ImportExportUseCase::class.simpleName, e.message, e) 36 | } 37 | } 38 | withContext(Dispatchers.Main) { 39 | onResult(ImportResult(successful, uris.size)) 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/domain/usecase/NoteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.domain.usecase 2 | 3 | import android.content.Context 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.glance.appwidget.updateAll 8 | import androidx.lifecycle.viewModelScope 9 | import com.ezpnix.writeon.data.repository.NoteRepositoryImpl 10 | import com.ezpnix.writeon.domain.model.Note 11 | import com.ezpnix.writeon.presentation.components.DecryptionResult 12 | import com.ezpnix.writeon.presentation.components.EncryptionHelper 13 | import com.ezpnix.writeon.widget.NotesWidget 14 | import dagger.hilt.android.qualifiers.ApplicationContext 15 | import kotlinx.coroutines.CoroutineScope 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.Job 18 | import kotlinx.coroutines.NonCancellable 19 | import kotlinx.coroutines.flow.Flow 20 | import kotlinx.coroutines.flow.collectLatest 21 | import kotlinx.coroutines.flow.first 22 | import kotlinx.coroutines.launch 23 | import kotlinx.coroutines.withContext 24 | import javax.inject.Inject 25 | 26 | class NoteUseCase @Inject constructor( 27 | private val noteRepository: NoteRepositoryImpl, 28 | private val coroutineScope: CoroutineScope, 29 | private val encryptionHelper: EncryptionHelper, 30 | @ApplicationContext private val context: Context 31 | ) { 32 | var notes: List by mutableStateOf(emptyList()) 33 | private set 34 | 35 | var decryptionResult: DecryptionResult by mutableStateOf(DecryptionResult.LOADING) 36 | 37 | private var observeKeysJob: Job? = null 38 | 39 | fun observe() { 40 | observeNotes() 41 | } 42 | 43 | private fun observeNotes() { 44 | observeKeysJob?.cancel() 45 | observeKeysJob = coroutineScope.launch { 46 | getAllNotes().collectLatest { notes -> 47 | val hasUnencryptedNotes = notes.any { !it.encrypted } 48 | if (!hasUnencryptedNotes) this@NoteUseCase.decryptionResult = DecryptionResult.EMPTY 49 | val processedNotes = notes.mapNotNull { note -> 50 | if (note.encrypted) { 51 | val (decryptedNote, status) = decryptNote(note) 52 | this@NoteUseCase.decryptionResult = status 53 | if (status == DecryptionResult.SUCCESS) decryptedNote else null 54 | } else { 55 | note 56 | } 57 | } 58 | this@NoteUseCase.notes = processedNotes 59 | NotesWidget().updateAll(context) 60 | } 61 | } 62 | } 63 | 64 | private fun encryptNote(note: Note): Note { 65 | return if (note.encrypted) { 66 | note.copy( 67 | name = encryptionHelper.encrypt(note.name), 68 | description = encryptionHelper.encrypt(note.description), 69 | encrypted = true 70 | ) 71 | } else { 72 | note 73 | } 74 | } 75 | 76 | private fun decryptNote(note: Note): Pair { 77 | val (decryptedName, nameResult) = encryptionHelper.decrypt(note.name) 78 | val (decryptedDescription, descriptionResult) = encryptionHelper.decrypt(note.description) 79 | return if (note.encrypted) { 80 | Pair(note.copy( 81 | name = decryptedName ?: "", 82 | description = decryptedDescription ?: "", 83 | ), descriptionResult) 84 | } else { 85 | Pair(note, DecryptionResult.SUCCESS) 86 | } 87 | } 88 | 89 | private fun getAllNotes(): Flow> { 90 | return noteRepository.getAllNotes() 91 | } 92 | 93 | suspend fun addNote(note: Note) { 94 | val noteToSave = encryptNote(note) 95 | if (note.id == 0) { 96 | noteRepository.addNote(noteToSave) 97 | } else { 98 | noteRepository.updateNote(noteToSave) 99 | } 100 | } 101 | 102 | fun pinNote(note: Note) { 103 | coroutineScope.launch(NonCancellable + Dispatchers.IO) { 104 | addNote(note) 105 | } 106 | } 107 | 108 | fun deleteNoteById(id: Int) { 109 | coroutineScope.launch(NonCancellable + Dispatchers.IO) { 110 | val noteToDelete = noteRepository.getNoteById(id).first() 111 | noteRepository.deleteNote(noteToDelete) 112 | } 113 | } 114 | 115 | fun getNoteById(id: Int): Flow { 116 | return noteRepository.getNoteById(id) 117 | } 118 | 119 | fun getLastNoteId(onResult: (Long?) -> Unit) { 120 | coroutineScope.launch(NonCancellable + Dispatchers.IO) { 121 | val lastNoteId = noteRepository.getLastNoteId() 122 | withContext(Dispatchers.Main) { 123 | onResult(lastNoteId) 124 | } 125 | } 126 | } 127 | 128 | suspend fun updatePinStatus(noteId: Int, pinned: Boolean) { 129 | val noteToUpdate = noteRepository.getNoteById(noteId).first() 130 | val updatedNote = noteToUpdate.copy(pinned = pinned) 131 | noteRepository.updateNote(updatedNote) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/domain/usecase/SettingsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.domain.usecase 2 | 3 | import com.ezpnix.writeon.core.constant.ConnectionConst 4 | import com.ezpnix.writeon.data.repository.SettingsRepositoryImpl 5 | import com.ezpnix.writeon.domain.model.Settings 6 | import javax.inject.Inject 7 | 8 | class SettingsUseCase @Inject constructor( 9 | private val settingsRepository: SettingsRepositoryImpl, 10 | 11 | ) { 12 | 13 | suspend fun loadSettingsFromRepository(): Settings { 14 | return Settings().apply { 15 | Settings::class.java.declaredFields.forEach { field -> 16 | field.isAccessible = true 17 | val settingName = field.name 18 | val defaultValue = field.get(this) 19 | val settingValue = getSettingValue(field.type, settingName, defaultValue) 20 | field.set(this, settingValue) 21 | } 22 | } 23 | } 24 | 25 | private suspend fun getSettingValue(fieldType: Class<*>, settingName: String, defaultValue: Any?): Any? { 26 | return try { 27 | when (fieldType) { 28 | Boolean::class.java -> settingsRepository.getBoolean(settingName) ?: defaultValue 29 | String::class.java -> settingsRepository.getString(settingName) ?: defaultValue 30 | Int::class.java -> settingsRepository.getInt(settingName) ?: defaultValue 31 | Float::class.java -> settingsRepository.getFloat(settingName) ?: defaultValue 32 | else -> throw IllegalArgumentException("Unsupported setting type: $fieldType") 33 | } 34 | } catch (e: ClassCastException) { 35 | handleCorruptedPreference(settingName, e) 36 | defaultValue 37 | } 38 | } 39 | 40 | private fun handleCorruptedPreference(settingName: String, e: ClassCastException) { 41 | println("Corrupted preference. Contact support: ${ConnectionConst.SUPPORT_MAIL}") 42 | println("Invalid Key: $settingName") 43 | println(e.stackTraceToString()) 44 | } 45 | 46 | suspend fun saveSettingsToRepository(settings: Settings) { 47 | Settings::class.java.declaredFields.forEach { field -> 48 | field.isAccessible = true 49 | val settingName = field.name 50 | val settingValue = field.get(settings) 51 | saveSettingValue(settingName, settingValue) 52 | } 53 | } 54 | 55 | private suspend fun saveSettingValue(settingName: String, settingValue: Any?) { 56 | when (settingValue) { 57 | is Boolean -> settingsRepository.putBoolean(settingName, settingValue) 58 | is String -> settingsRepository.putString(settingName, settingValue) 59 | is Int -> settingsRepository.putInt(settingName, settingValue) 60 | is Float -> settingsRepository.putFloat(settingName, settingValue) 61 | else -> throw IllegalArgumentException("Unsupported setting type: ${settingValue?.javaClass}") 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation 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.material3.MaterialTheme 8 | import androidx.compose.material3.Surface 9 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 10 | import androidx.lifecycle.ViewModelProvider 11 | import androidx.navigation.compose.rememberNavController 12 | import com.ezpnix.writeon.data.repository.SettingsRepositoryImpl 13 | import com.ezpnix.writeon.presentation.components.registerGalleryObserver 14 | import com.ezpnix.writeon.presentation.navigation.AppNavHost 15 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 16 | import com.ezpnix.writeon.presentation.theme.LeafNotesTheme 17 | import dagger.hilt.android.AndroidEntryPoint 18 | import javax.inject.Inject 19 | import androidx.biometric.BiometricPrompt 20 | import androidx.core.content.ContextCompat 21 | 22 | @AndroidEntryPoint 23 | class MainActivity : AppCompatActivity() { 24 | 25 | @Inject 26 | lateinit var settingsRepositoryImpl: SettingsRepositoryImpl 27 | 28 | private lateinit var settingsViewModel: SettingsViewModel 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | installSplashScreen() 33 | enableEdgeToEdge() 34 | 35 | settingsViewModel = ViewModelProvider(this).get(SettingsViewModel::class.java) 36 | 37 | if (settingsViewModel.settings.value.isBiometricEnabled) { 38 | showBiometricPrompt() 39 | } 40 | 41 | setContent { 42 | val noteId = intent?.getIntExtra("noteId", -1) ?: -1 43 | val navController = rememberNavController() 44 | 45 | registerGalleryObserver(this) 46 | 47 | LeafNotesTheme(settingsViewModel) { 48 | Surface( 49 | color = MaterialTheme.colorScheme.surfaceContainerLow, 50 | ) { 51 | AppNavHost(settingsViewModel, noteId = noteId, navController = navController) 52 | } 53 | } 54 | } 55 | } 56 | 57 | private fun showBiometricPrompt() { 58 | val biometricPrompt = BiometricPrompt( 59 | this, 60 | ContextCompat.getMainExecutor(this), 61 | object : BiometricPrompt.AuthenticationCallback() { 62 | override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { 63 | super.onAuthenticationSucceeded(result) 64 | } 65 | 66 | override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { 67 | super.onAuthenticationError(errorCode, errString) 68 | finish() 69 | } 70 | 71 | override fun onAuthenticationFailed() { 72 | super.onAuthenticationFailed() 73 | } 74 | } 75 | ) 76 | 77 | val promptInfo = BiometricPrompt.PromptInfo.Builder() 78 | .setTitle("Biometric Authentication") 79 | .setSubtitle("Use fingerprint to unlock the application") 80 | .setNegativeButtonText("Cancel") 81 | .build() 82 | 83 | biometricPrompt.authenticate(promptInfo) 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/components/Animations.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.components 2 | 3 | import androidx.compose.animation.EnterTransition 4 | import androidx.compose.animation.ExitTransition 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.animation.fadeIn 7 | import androidx.compose.animation.fadeOut 8 | import androidx.compose.animation.scaleIn 9 | import androidx.compose.animation.scaleOut 10 | import androidx.compose.animation.slideInHorizontally 11 | import androidx.compose.animation.slideOutHorizontally 12 | 13 | private const val DEFAULT_FADE_DURATION = 300 14 | private const val DEFAULT_SCALE_DURATION = 400 15 | private const val DEFAULT_SLIDE_DURATION = 400 16 | private const val DEFAULT_INITIAL_SCALE = 0.9f 17 | 18 | fun getNoteEnterAnimation(): EnterTransition { 19 | return fadeIn(animationSpec = tween(DEFAULT_FADE_DURATION)) + scaleIn( 20 | initialScale = 0.9f, 21 | animationSpec = tween(DEFAULT_SCALE_DURATION) 22 | ) 23 | } 24 | 25 | fun getNoteExitAnimation(slideDirection: Int): ExitTransition { 26 | return slideOutHorizontally( 27 | targetOffsetX = { slideDirection * it }, 28 | animationSpec = tween(durationMillis = DEFAULT_SLIDE_DURATION) 29 | ) + fadeOut(animationSpec = tween(durationMillis = DEFAULT_FADE_DURATION)) 30 | } 31 | 32 | fun defaultScreenEnterAnimation(): EnterTransition { 33 | return fadeIn(animationSpec = tween(DEFAULT_FADE_DURATION)) + 34 | scaleIn( 35 | initialScale = DEFAULT_INITIAL_SCALE, 36 | animationSpec = tween(DEFAULT_SCALE_DURATION) 37 | ) 38 | } 39 | 40 | fun defaultScreenExitAnimation(): ExitTransition { 41 | return fadeOut(animationSpec = tween(DEFAULT_FADE_DURATION)) + 42 | scaleOut( 43 | targetScale = DEFAULT_INITIAL_SCALE, 44 | animationSpec = tween(DEFAULT_SCALE_DURATION) 45 | ) 46 | } 47 | 48 | fun slideScreenEnterAnimation(): EnterTransition { 49 | return slideInHorizontally( 50 | initialOffsetX = { fullWidth -> fullWidth }, 51 | animationSpec = tween(DEFAULT_SLIDE_DURATION) 52 | ) 53 | } 54 | 55 | fun slideScreenExitAnimation(): ExitTransition { 56 | return slideOutHorizontally( 57 | targetOffsetX = { fullWidth -> fullWidth }, 58 | animationSpec = tween(DEFAULT_SLIDE_DURATION) 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/components/CustomActions.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack 7 | import androidx.compose.material.icons.automirrored.rounded.Redo 8 | import androidx.compose.material.icons.automirrored.rounded.Undo 9 | import androidx.compose.material.icons.outlined.PushPin 10 | import androidx.compose.material.icons.rounded.AccountCircle 11 | import androidx.compose.material.icons.rounded.BubbleChart 12 | import androidx.compose.material.icons.rounded.Close 13 | import androidx.compose.material.icons.rounded.Delete 14 | import androidx.compose.material.icons.rounded.Done 15 | import androidx.compose.material.icons.rounded.PushPin 16 | import androidx.compose.material.icons.rounded.SelectAll 17 | import androidx.compose.material.icons.rounded.Settings 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.IconButton 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.Text 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.graphics.vector.ImageVector 26 | import androidx.compose.ui.res.vectorResource 27 | import com.ezpnix.writeon.R 28 | 29 | @Composable 30 | fun CloseButton( 31 | contentDescription: String = "Close", 32 | onCloseClicked: () -> Unit 33 | ) { 34 | IconButton(onClick = onCloseClicked) { 35 | Icon( 36 | imageVector = Icons.Rounded.Close, 37 | contentDescription = contentDescription, 38 | tint = MaterialTheme.colorScheme.onBackground 39 | ) 40 | } 41 | } 42 | 43 | @Composable 44 | fun MoreButton(onClick: () -> Unit) { 45 | IconButton(onClick = { onClick() }) { 46 | Icon(Icons.Rounded.BubbleChart, contentDescription = "Info") 47 | } 48 | } 49 | 50 | @Composable 51 | fun SaveButton(onSaveClicked: () -> Unit) { 52 | IconButton(onClick = onSaveClicked) { 53 | Icon( 54 | imageVector = Icons.Rounded.Done, 55 | contentDescription = "Done", 56 | tint = MaterialTheme.colorScheme.onBackground 57 | ) 58 | } 59 | } 60 | 61 | @Composable 62 | fun UndoButton(onUndoClicked: () -> Unit) { 63 | IconButton(onClick = onUndoClicked) { 64 | Icon( 65 | imageVector = Icons.AutoMirrored.Rounded.Undo, 66 | contentDescription = "Undo", 67 | tint = MaterialTheme.colorScheme.outlineVariant 68 | ) 69 | } 70 | } 71 | 72 | @Composable 73 | fun RedoButton(onRedoClicked: () -> Unit) { 74 | IconButton(onClick = onRedoClicked) { 75 | Icon( 76 | imageVector = Icons.AutoMirrored.Rounded.Redo, 77 | contentDescription = "Redo", 78 | tint = MaterialTheme.colorScheme.outlineVariant 79 | ) 80 | } 81 | } 82 | 83 | @Composable 84 | fun NavigationIcon(onBackNavClicked: () -> Unit) { 85 | 86 | IconButton(onClick = onBackNavClicked) { 87 | Icon( 88 | imageVector = Icons.AutoMirrored.Rounded.ArrowBack, 89 | contentDescription = "Back", 90 | tint = MaterialTheme.colorScheme.onBackground 91 | ) 92 | } 93 | } 94 | 95 | @Composable 96 | fun SettingsButton(onSettingsClicked: () -> Unit) { 97 | IconButton(onClick = onSettingsClicked) { 98 | Icon( 99 | imageVector = Icons.Rounded.Settings, 100 | contentDescription = "Settings", 101 | tint = MaterialTheme.colorScheme.onBackground 102 | ) 103 | } 104 | } 105 | 106 | @Composable 107 | fun PrivacyButton(onSettingsClicked: () -> Unit) { 108 | IconButton(onClick = onSettingsClicked) { 109 | Icon( 110 | ImageVector.vectorResource(id = R.drawable.incognito_fill), 111 | contentDescription = "Settings", 112 | tint = MaterialTheme.colorScheme.onBackground 113 | ) 114 | } 115 | } 116 | 117 | @Composable 118 | fun TitleText(titleText: String) { 119 | Row( 120 | modifier = Modifier.fillMaxWidth(), 121 | verticalAlignment = Alignment.CenterVertically 122 | ) { 123 | Text( 124 | text = titleText, 125 | modifier = Modifier.weight(1f) 126 | ) 127 | } 128 | } 129 | 130 | @Composable 131 | fun PinButton(isPinned: Boolean, onClick: () -> Unit) { 132 | IconButton(onClick = { onClick() }) { 133 | Icon(if (isPinned) Icons.Rounded.PushPin else Icons.Outlined.PushPin, contentDescription = "Pin") 134 | } 135 | } 136 | 137 | @Composable 138 | fun DeleteButton(onClick: () -> Unit) { 139 | IconButton( 140 | onClick = { onClick() } 141 | ) { 142 | Icon( 143 | imageVector = Icons.Rounded.Delete, 144 | contentDescription = "Delete", 145 | ) 146 | } 147 | } 148 | 149 | @Composable 150 | fun SelectAllButton(enabled: Boolean, onClick: () -> Unit) { 151 | if (enabled) { 152 | IconButton( 153 | onClick = { onClick() } 154 | ) { 155 | Icon( 156 | imageVector = Icons.Rounded.SelectAll, 157 | contentDescription = "Select All", 158 | ) 159 | } 160 | } 161 | } 162 | 163 | @Composable 164 | fun MainButton(onSettingsClicked: () -> Unit) { 165 | IconButton(onClick = onSettingsClicked) { 166 | Icon( 167 | imageVector = Icons.Rounded.BubbleChart, 168 | contentDescription = "Settings", 169 | tint = MaterialTheme.colorScheme.onBackground 170 | ) 171 | } 172 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/components/CustomScaffold.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.consumeWindowInsets 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Scaffold 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | 11 | @Composable 12 | fun NotesScaffold( 13 | topBar : @Composable () -> Unit = {}, 14 | floatingActionButton: @Composable () -> Unit = {}, 15 | content: @Composable () -> Unit 16 | ) { 17 | Scaffold( 18 | containerColor = MaterialTheme.colorScheme.surfaceContainerLow, 19 | topBar = { topBar() }, 20 | floatingActionButton = { floatingActionButton() }, 21 | content = { padding -> 22 | Box(modifier = Modifier.padding(padding).consumeWindowInsets(padding)) { 23 | content() 24 | } 25 | } 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/components/EncryptionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.components 2 | 3 | import android.util.Base64 4 | import java.nio.charset.StandardCharsets 5 | import java.security.MessageDigest 6 | import javax.crypto.Cipher 7 | import javax.crypto.SecretKey 8 | import javax.crypto.spec.GCMParameterSpec 9 | import javax.crypto.spec.SecretKeySpec 10 | 11 | class EncryptionHelper(private val mutableVaultPassword: StringBuilder) { 12 | 13 | fun isPasswordEmpty(): Boolean { 14 | return mutableVaultPassword.isEmpty() 15 | } 16 | 17 | fun removePassword() { 18 | mutableVaultPassword.setLength(0) 19 | } 20 | 21 | fun setPassword(newPassword: String) { 22 | mutableVaultPassword.setLength(0) 23 | mutableVaultPassword.append(newPassword) 24 | } 25 | 26 | fun encrypt(data: String): String { 27 | val secretKey = generateSecretKey(mutableVaultPassword.toString()) 28 | val cipher = Cipher.getInstance("AES/GCM/NoPadding") 29 | cipher.init(Cipher.ENCRYPT_MODE, secretKey) 30 | val ivBytes = cipher.iv 31 | val encryptedBytes = cipher.doFinal(data.toByteArray(StandardCharsets.UTF_8)) 32 | return "${Base64.encodeToString(ivBytes, Base64.DEFAULT)}:${Base64.encodeToString(encryptedBytes, Base64.DEFAULT)}" 33 | } 34 | 35 | fun decrypt(data: String): Pair { 36 | if (mutableVaultPassword.isEmpty()) return Pair(null, DecryptionResult.LOADING) 37 | if (data.isBlank()) return Pair(null, DecryptionResult.BLANK_DATA) 38 | 39 | return try { 40 | val split = data.split(":") 41 | if (split.size != 2) return Pair(null, DecryptionResult.INVALID_DATA) 42 | val ivBytes = Base64.decode(split[0], Base64.DEFAULT) 43 | val encryptedBytes = Base64.decode(split[1], Base64.DEFAULT) 44 | val secretKey = generateSecretKey(mutableVaultPassword.toString()) 45 | val cipher = Cipher.getInstance("AES/GCM/NoPadding") 46 | val spec = GCMParameterSpec(128, ivBytes) 47 | cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) 48 | val decryptedBytes = cipher.doFinal(encryptedBytes) 49 | val decryptedData = String(decryptedBytes, StandardCharsets.UTF_8) 50 | Pair(decryptedData, DecryptionResult.SUCCESS) 51 | } catch (e: Exception) { 52 | Pair(null, DecryptionResult.BAD_PASSWORD) 53 | } 54 | } 55 | 56 | private fun generateSecretKey(password: String): SecretKey { 57 | val keySpec = SecretKeySpec(password.toByteArray(StandardCharsets.UTF_8), "AES") 58 | val sha256 = MessageDigest.getInstance("SHA-256") 59 | val keyBytes = sha256.digest(keySpec.encoded) 60 | return SecretKeySpec(keyBytes, "AES") 61 | } 62 | } 63 | 64 | enum class DecryptionResult { 65 | EMPTY, 66 | SUCCESS, 67 | INVALID_DATA, 68 | BLANK_DATA, 69 | BAD_PASSWORD, 70 | LOADING 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/components/ImageCleaner.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.components 2 | 3 | import android.content.Context 4 | import android.database.ContentObserver 5 | import android.net.Uri 6 | import android.os.Handler 7 | import android.os.Looper 8 | import android.provider.MediaStore 9 | import java.io.File 10 | 11 | class GalleryObserver(handler: Handler, private val context: Context) : ContentObserver(handler) { 12 | override fun onChange(selfChange: Boolean, uri: Uri?) { 13 | super.onChange(selfChange, uri) 14 | uri?.let { 15 | deleteFileIfExists(context, getImageName(uri)) 16 | } 17 | } 18 | } 19 | 20 | fun registerGalleryObserver(context: Context) { 21 | val handler = Handler(Looper.getMainLooper()) 22 | val galleryObserver = GalleryObserver(handler, context) 23 | context.contentResolver.registerContentObserver( 24 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 25 | true, 26 | galleryObserver 27 | ) 28 | } 29 | 30 | fun deleteFileIfExists(context: Context, fileName: String): Boolean { 31 | val appStorageDir = getExternalStorageDir(context) 32 | val file = File(appStorageDir, fileName) 33 | return if (file.exists()) { 34 | file.delete() 35 | } else { 36 | false 37 | } 38 | } 39 | 40 | 41 | fun getExternalStorageDir(context: Context): File { 42 | return context.getExternalFilesDir(null) ?: throw IllegalStateException("External storage directory not found") 43 | } 44 | 45 | fun getImageName(uri: Uri?): String { 46 | return if (uri != null) { 47 | uri.lastPathSegment?.filter { it.isDigit() }?.takeLast(10) ?: (1000..99999).random().toString() 48 | } else { 49 | (1000..99999).random().toString() 50 | } + ".jpg" 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/components/markdown/Builder.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.components.markdown 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import androidx.compose.ui.text.SpanStyle 5 | import androidx.compose.ui.text.buildAnnotatedString 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.text.withStyle 8 | 9 | class MarkdownBuilder(internal val lines: List, private var lineProcessors: List) { 10 | var lineIndex = -1 11 | 12 | internal val content = mutableListOf() 13 | 14 | fun add(element: MarkdownElement) { 15 | content.add(element) 16 | } 17 | 18 | fun parse() { 19 | while (hasNextLine()) { 20 | val line = nextLine() 21 | val processor = lineProcessors.find { it.canProcessLine(line) } 22 | if (processor != null) { 23 | processor.processLine(line, this) 24 | } else { 25 | add(NormalText(line)) 26 | } 27 | } 28 | } 29 | 30 | private fun hasNextLine(): Boolean = lineIndex + 1 < lines.size 31 | 32 | private fun nextLine(): String { 33 | lineIndex++ 34 | return lines[lineIndex] 35 | } 36 | } 37 | 38 | /** 39 | * Splits the input string by the specified delimiter and returns a list of index pairs. 40 | * Each pair represents the start and end indices of segments between delimiters. 41 | */ 42 | fun splitByDelimiter(input: String, delimiter: String): List> { 43 | val segments = mutableListOf>() 44 | var startIndex = 0 45 | var delimiterIndex = input.indexOf(delimiter, startIndex) 46 | 47 | while (delimiterIndex != -1) { 48 | if (startIndex != delimiterIndex) { 49 | segments.add(Pair(startIndex, delimiterIndex)) 50 | } else { 51 | segments.add(Pair(startIndex, startIndex)) 52 | } 53 | startIndex = delimiterIndex + delimiter.length 54 | delimiterIndex = input.indexOf(delimiter, startIndex) 55 | } 56 | 57 | if (startIndex < input.length) { 58 | segments.add(Pair(startIndex, input.length)) 59 | } else if (startIndex == input.length) { 60 | segments.add(Pair(startIndex, startIndex)) 61 | } 62 | 63 | return segments.filterIndexed { index, _ -> index % 2 == 1 } 64 | } 65 | 66 | /** 67 | * Checks if a given index is within any of the provided segments. 68 | */ 69 | fun isInSegments(index: Int, segments: List>): Boolean { 70 | return segments.any { segment -> index in segment.first until segment.second } 71 | } 72 | 73 | 74 | /** 75 | * Builds an AnnotatedString with styles applied based on markdown-like syntax. 76 | */ 77 | fun buildString(input: String, defaultFontWeight: FontWeight = FontWeight.Normal): AnnotatedString { 78 | val textStyleSegments: List = listOf( 79 | BoldSegment(), 80 | ItalicSegment(), 81 | HighlightSegment(), 82 | Strikethrough(), 83 | Underline() 84 | ) 85 | 86 | val allSegments = textStyleSegments.associateWith { splitByDelimiter(input, it.delimiter) } 87 | 88 | /** 89 | * Determines the SpanStyle for a given index based on its presence in style segments. 90 | */ 91 | fun getSpanStyle(index: Int): SpanStyle { 92 | val styles = textStyleSegments.filter { segment -> isInSegments(index, allSegments[segment]!!) } 93 | return styles.fold(SpanStyle(fontWeight = defaultFontWeight)) { acc, segment -> acc.merge(segment.getSpanStyle()) } 94 | } 95 | 96 | return buildAnnotatedString { 97 | input.forEachIndexed { index, letter -> 98 | if (textStyleSegments.none { segment -> segment.delimiter.contains(letter) }) { 99 | withStyle(style = getSpanStyle(index)) { 100 | append(letter) 101 | } 102 | } 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/components/markdown/InlineElements.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.components.markdown 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.text.SpanStyle 5 | import androidx.compose.ui.text.font.FontStyle 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.text.style.TextDecoration 8 | 9 | interface TextStyleSegment { 10 | val delimiter: String 11 | fun getSpanStyle(): SpanStyle 12 | } 13 | 14 | data class BoldSegment(override val delimiter: String = "**") : TextStyleSegment { 15 | override fun getSpanStyle() = SpanStyle(fontWeight = FontWeight.Bold) 16 | } 17 | 18 | data class ItalicSegment(override val delimiter: String = "*") : TextStyleSegment { 19 | override fun getSpanStyle() = SpanStyle(fontStyle = FontStyle.Italic) 20 | } 21 | 22 | data class HighlightSegment(override val delimiter: String = "==") : TextStyleSegment { 23 | override fun getSpanStyle() = SpanStyle(background = Color.Yellow.copy(alpha = 0.2f)) 24 | } 25 | 26 | data class Strikethrough(override val delimiter: String = "~~") : TextStyleSegment { 27 | override fun getSpanStyle() = SpanStyle(textDecoration = TextDecoration.LineThrough) 28 | } 29 | 30 | data class Underline(override val delimiter: String = "_") : TextStyleSegment { 31 | override fun getSpanStyle() = SpanStyle(textDecoration = TextDecoration.Underline) 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/components/markdown/LineProccesor.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.components.markdown 2 | 3 | interface MarkdownLineProcessor { 4 | fun canProcessLine(line: String): Boolean 5 | fun processLine(line: String, builder: MarkdownBuilder) 6 | } 7 | 8 | class CodeBlockProcessor : MarkdownLineProcessor { 9 | override fun canProcessLine(line: String): Boolean { 10 | return line.startsWith("```") 11 | } 12 | 13 | override fun processLine(line: String, builder: MarkdownBuilder) { 14 | val codeBlock = StringBuilder() 15 | var index = builder.lineIndex + 1 16 | var isEnded = false 17 | 18 | while (index < builder.lines.size) { 19 | val nextLine = builder.lines[index] 20 | if (nextLine == "```") { 21 | builder.lineIndex = index 22 | isEnded = true 23 | break 24 | } 25 | codeBlock.appendLine(nextLine) 26 | index++ 27 | } 28 | 29 | builder.add(CodeBlock(codeBlock.toString(), isEnded, line)) 30 | } 31 | } 32 | 33 | class CheckboxProcessor : MarkdownLineProcessor { 34 | override fun canProcessLine(line: String): Boolean = line.matches(Regex("^\\[[ xX]]( .*)?")) 35 | 36 | override fun processLine(line: String, builder: MarkdownBuilder) { 37 | val checked = line.contains(Regex("^\\[[Xx]]")) 38 | val text = line.replace(Regex("^\\[[ xX]] ?"), "").trim() 39 | builder.add(CheckboxItem(text, checked, builder.lineIndex)) 40 | } 41 | } 42 | 43 | 44 | class HeadingProcessor : MarkdownLineProcessor { 45 | override fun canProcessLine(line: String): Boolean = line.startsWith("#") 46 | 47 | override fun processLine(line: String, builder: MarkdownBuilder) { 48 | val level = line.takeWhile { it == '#' }.length 49 | val text = line.drop(level).trim() 50 | builder.add(Heading(level, text)) 51 | } 52 | } 53 | 54 | class QuoteProcessor : MarkdownLineProcessor { 55 | override fun canProcessLine(line: String): Boolean = line.trim().startsWith(">") 56 | 57 | override fun processLine(line: String, builder: MarkdownBuilder) { 58 | val level = line.takeWhile { it == '>' }.length 59 | val text = line.drop(level).trim() 60 | builder.add(Quote(level, text)) 61 | } 62 | } 63 | 64 | class ListItemProcessor : MarkdownLineProcessor { 65 | override fun canProcessLine(line: String): Boolean = line.startsWith("- ") 66 | 67 | override fun processLine(line: String, builder: MarkdownBuilder) { 68 | val text = line.removePrefix("- ").trim() 69 | builder.add(ListItem(text)) 70 | } 71 | } 72 | 73 | class ImageInsertionProcessor : MarkdownLineProcessor { 74 | override fun canProcessLine(line: String): Boolean { 75 | return line.trim().startsWith("!(") && line.trim().endsWith(")") 76 | } 77 | 78 | override fun processLine(line: String, builder: MarkdownBuilder) { 79 | val photoUri = line.substringAfter("!(", "").substringBefore(")") 80 | builder.add(ImageInsertion(photoUri)) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/components/markdown/RowElements.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.components.markdown 2 | 3 | sealed interface MarkdownElement { 4 | fun render(builder: StringBuilder) 5 | } 6 | 7 | data class Heading(val level: Int, val text: String) : MarkdownElement { 8 | override fun render(builder: StringBuilder) { 9 | builder.append("#".repeat(level)).append(" $text\n\n") 10 | } 11 | } 12 | 13 | data class CheckboxItem(val text: String, var checked: Boolean = false, var index: Int) : MarkdownElement { 14 | override fun render(builder: StringBuilder) { 15 | builder.append("[${if (checked) "X" else " "}] $text\n") 16 | } 17 | } 18 | 19 | data class Quote(val level: Int, val text: String) : MarkdownElement { 20 | override fun render(builder: StringBuilder) { 21 | builder.append("> ${text}\n") 22 | } 23 | } 24 | 25 | data class ListItem(val text: String) : MarkdownElement { 26 | override fun render(builder: StringBuilder) { 27 | builder.append("- ${text}\n") 28 | } 29 | } 30 | 31 | data class CodeBlock(val code: String, val isEnded: Boolean = false, val firstLine : String) : MarkdownElement { 32 | override fun render(builder: StringBuilder) { 33 | builder.append("```") 34 | isEnded.let { 35 | builder.append(it) 36 | } 37 | builder.append("\n$code\n```\n") 38 | } 39 | } 40 | 41 | data class NormalText(val text: String) : MarkdownElement { 42 | override fun render(builder: StringBuilder) { 43 | builder.append("$text\n\n") 44 | } 45 | } 46 | 47 | data class ImageInsertion(val photoUri: String) : MarkdownElement { 48 | override fun render(builder: StringBuilder) { 49 | builder.append("!($photoUri)\n\n") 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/navigation/AnimatedComposable.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.navigation 2 | 3 | import androidx.compose.animation.AnimatedVisibilityScope 4 | import androidx.compose.runtime.Composable 5 | import androidx.navigation.NamedNavArgument 6 | import androidx.navigation.NavBackStackEntry 7 | import androidx.navigation.NavDeepLink 8 | import androidx.navigation.NavGraphBuilder 9 | import androidx.navigation.compose.composable 10 | import com.ezpnix.writeon.presentation.components.defaultScreenEnterAnimation 11 | import com.ezpnix.writeon.presentation.components.defaultScreenExitAnimation 12 | import com.ezpnix.writeon.presentation.components.slideScreenEnterAnimation 13 | import com.ezpnix.writeon.presentation.components.slideScreenExitAnimation 14 | 15 | fun NavGraphBuilder.animatedComposable( 16 | route: String, 17 | arguments: List = emptyList(), 18 | deepLinks: List = emptyList(), 19 | content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit 20 | ) = composable( 21 | route = route, 22 | arguments = arguments, 23 | deepLinks = deepLinks, 24 | enterTransition = { defaultScreenEnterAnimation() }, 25 | exitTransition = { defaultScreenExitAnimation() }, 26 | popEnterTransition = { defaultScreenEnterAnimation() }, 27 | popExitTransition = { defaultScreenExitAnimation() }, 28 | content = content 29 | ) 30 | 31 | fun NavGraphBuilder.slideInComposable( 32 | route: String, 33 | arguments: List = emptyList(), 34 | deepLinks: List = emptyList(), 35 | content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit 36 | ) = composable( 37 | route = route, 38 | arguments = arguments, 39 | deepLinks = deepLinks, 40 | enterTransition = { slideScreenEnterAnimation() }, 41 | exitTransition = { defaultScreenExitAnimation() }, 42 | popEnterTransition = { defaultScreenEnterAnimation() }, 43 | popExitTransition = { slideScreenExitAnimation() }, 44 | content = content 45 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/navigation/NavHost.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.navigation 2 | 3 | import android.app.Activity 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.platform.LocalContext 6 | import androidx.navigation.NavHostController 7 | import androidx.navigation.compose.NavHost 8 | import androidx.navigation.compose.rememberNavController 9 | import com.ezpnix.writeon.presentation.screens.edit.EditNoteView 10 | import com.ezpnix.writeon.presentation.screens.home.HomeView 11 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 12 | import com.ezpnix.writeon.presentation.screens.terms.TermsScreen 13 | 14 | @Composable 15 | fun AppNavHost(settingsModel: SettingsViewModel,navController: NavHostController = rememberNavController(), noteId: Int) { 16 | val activity = (LocalContext.current as? Activity) 17 | 18 | NavHost(navController, startDestination = if (!settingsModel.settings.value.termsOfService) NavRoutes.Terms.route else if (noteId == -1) NavRoutes.Home.route else NavRoutes.Edit.route) { 19 | animatedComposable(NavRoutes.Home.route) { 20 | HomeView( 21 | onSettingsClicked = { navController.navigate(NavRoutes.Settings.route) }, 22 | onNoteClicked = { id, encrypted -> navController.navigate(NavRoutes.Edit.createRoute(id, encrypted)) }, 23 | settingsModel = settingsModel, 24 | navController = navController 25 | ) 26 | } 27 | 28 | animatedComposable(NavRoutes.Terms.route) { 29 | TermsScreen( 30 | settingsModel 31 | ) 32 | } 33 | 34 | animatedComposable(NavRoutes.Edit.route) { backStackEntry -> 35 | val id = backStackEntry.arguments?.getString("id")?.toIntOrNull() ?: 0 36 | val encrypted = backStackEntry.arguments?.getString("encrypted").toBoolean() 37 | EditNoteView( 38 | settingsViewModel = settingsModel, 39 | id = if (noteId == -1) id else noteId, 40 | encrypted = encrypted 41 | ) { 42 | if (noteId == -1) { 43 | navController.navigateUp() 44 | } else { 45 | activity?.finish() 46 | } 47 | } 48 | } 49 | 50 | settingScreens.forEach { (route, screen) -> 51 | if (route == NavRoutes.Settings.route) { 52 | slideInComposable(route) { 53 | screen(settingsModel,navController) 54 | } 55 | } else { 56 | animatedComposable(route) { 57 | screen(settingsModel,navController) 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/navigation/NavRoutes.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.navigation.NavController 5 | import com.ezpnix.writeon.presentation.screens.settings.AndroidScreen 6 | import com.ezpnix.writeon.presentation.screens.settings.FlashcardScreen 7 | import com.ezpnix.writeon.presentation.screens.settings.MainSettings 8 | import com.ezpnix.writeon.presentation.screens.settings.ScratchpadScreen 9 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 10 | import com.ezpnix.writeon.presentation.screens.settings.settings.AboutScreen 11 | import com.ezpnix.writeon.presentation.screens.settings.settings.ColorStylesScreen 12 | import com.ezpnix.writeon.presentation.screens.settings.settings.LanguageScreen 13 | import com.ezpnix.writeon.presentation.screens.settings.settings.MarkdownScreen 14 | import com.ezpnix.writeon.presentation.screens.settings.settings.PrivacyScreen 15 | import com.ezpnix.writeon.presentation.screens.settings.settings.IssueScreen 16 | import com.ezpnix.writeon.presentation.screens.settings.settings.ToolsScreen 17 | import com.ezpnix.writeon.presentation.screens.settings.settings.GuideScreen 18 | import com.ezpnix.writeon.presentation.screens.settings.trash.TrashScreen 19 | 20 | sealed class NavRoutes(val route: String) { 21 | data object Home : NavRoutes("home") 22 | data object Edit : NavRoutes("edit/{id}/{encrypted}") { 23 | fun createRoute(id: Int, encrypted : Boolean) = "edit/$id/$encrypted" 24 | } 25 | data object Terms : NavRoutes("terms") 26 | data object Settings : NavRoutes("settings") 27 | data object ColorStyles : NavRoutes("settings/color_styles") 28 | data object Language : NavRoutes("settings/language") 29 | data object Privacy : NavRoutes("settings/privacy") 30 | data object Markdown : NavRoutes("settings/markdown") 31 | data object Tools : NavRoutes("settings/tools") 32 | data object About : NavRoutes("settings/about") 33 | data object Scratchpad: NavRoutes("scratchpad") 34 | data object Issue: NavRoutes("issue") 35 | data object Guide: NavRoutes("guide") 36 | data object Trash: NavRoutes("trash") 37 | data object Flashback: NavRoutes("flashback") 38 | data object Android: NavRoutes("android") 39 | } 40 | 41 | val settingScreens = mapOf Unit>( 42 | NavRoutes.Settings.route to { settings, navController -> MainSettings(settings, navController) }, 43 | NavRoutes.ColorStyles.route to { settings, navController -> ColorStylesScreen(navController,settings) }, 44 | NavRoutes.Language.route to { settings, navController -> LanguageScreen(navController,settings) }, 45 | NavRoutes.Privacy.route to { settings, navController -> PrivacyScreen(navController, settings) }, 46 | NavRoutes.Markdown.route to { settings, navController -> MarkdownScreen(navController,settings) }, 47 | NavRoutes.Tools.route to { settings, navController -> ToolsScreen(navController,settings) }, 48 | NavRoutes.About.route to { settings, navController -> AboutScreen(navController,settings) }, 49 | NavRoutes.Scratchpad.route to { settings, navController -> ScratchpadScreen(navController,settings) }, 50 | NavRoutes.Issue.route to { settings, navController -> IssueScreen(navController,settings) }, 51 | NavRoutes.Guide.route to { settings, navController -> GuideScreen(navController, settings) }, 52 | NavRoutes.Trash.route to { settings, navController -> TrashScreen(navController, settings) }, 53 | NavRoutes.Flashback.route to { settings, navController -> FlashcardScreen(navController, settings) }, 54 | NavRoutes.Android.route to { settings, navController -> AndroidScreen(navController, settings) }, 55 | ) 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/edit/components/CustomIconButton.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.edit.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.CardDefaults 7 | import androidx.compose.material3.ElevatedCard 8 | import androidx.compose.material3.Icon 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.vector.ImageVector 13 | import androidx.compose.ui.unit.Dp 14 | import androidx.compose.ui.unit.dp 15 | 16 | @Composable 17 | fun CustomIconButton( 18 | shape: RoundedCornerShape, 19 | elevation: Dp, 20 | icon: ImageVector, 21 | onClick: () -> Unit 22 | ) { 23 | ElevatedCard( 24 | shape = shape, 25 | elevation = CardDefaults.cardElevation(defaultElevation = elevation), 26 | modifier = Modifier 27 | .clip(shape) 28 | .clickable { onClick() } 29 | ) { 30 | Icon( 31 | imageVector = icon, 32 | contentDescription = "", 33 | modifier = Modifier.padding(16.dp), 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/edit/components/CustomTextField.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.edit.components 2 | 3 | import androidx.compose.foundation.interaction.MutableInteractionSource 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.LocalTextStyle 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TextField 9 | import androidx.compose.material3.TextFieldDefaults 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.text.TextRange 18 | import androidx.compose.ui.text.TextStyle 19 | import androidx.compose.ui.text.input.PasswordVisualTransformation 20 | import androidx.compose.ui.text.input.TextFieldValue 21 | import androidx.compose.ui.text.input.VisualTransformation 22 | import androidx.compose.ui.unit.dp 23 | 24 | @Composable 25 | fun CustomTextField( 26 | value: TextFieldValue, 27 | onValueChange: (TextFieldValue) -> Unit, 28 | placeholder: String, 29 | shape: RoundedCornerShape = RoundedCornerShape(0.dp), 30 | interactionSource: MutableInteractionSource = MutableInteractionSource(), 31 | singleLine: Boolean = false, 32 | modifier: Modifier = Modifier, 33 | enabled: Boolean = true, 34 | hideContent: Boolean = false, 35 | textStyle: TextStyle = LocalTextStyle.current 36 | ) { 37 | val visualTransformation = if (hideContent) { 38 | PasswordVisualTransformation() 39 | } else { 40 | VisualTransformation.None 41 | } 42 | 43 | TextField( 44 | value = value, 45 | visualTransformation = visualTransformation, 46 | onValueChange = onValueChange, 47 | interactionSource = interactionSource, 48 | textStyle = textStyle, 49 | enabled = enabled, 50 | modifier = modifier 51 | .fillMaxWidth() 52 | .clip(shape), 53 | singleLine = singleLine, 54 | colors = TextFieldDefaults.colors( 55 | focusedContainerColor = Color.Transparent, 56 | unfocusedContainerColor = Color.Transparent, 57 | unfocusedIndicatorColor = Color.Transparent, 58 | focusedIndicatorColor = Color.Transparent, 59 | ), 60 | placeholder = { 61 | Text(placeholder) 62 | } 63 | ) 64 | } 65 | 66 | class UndoRedoState { 67 | var input by mutableStateOf(TextFieldValue("")) 68 | private val undoHistory = ArrayDeque() 69 | private val redoHistory = ArrayDeque() 70 | 71 | init { 72 | undoHistory.add(input) 73 | } 74 | 75 | fun onInput(value: TextFieldValue) { 76 | val updatedValue = value.copy(value.text, selection = TextRange(value.text.length)) 77 | undoHistory.add(updatedValue) 78 | redoHistory.clear() 79 | input = updatedValue 80 | } 81 | 82 | fun undo() { 83 | if (undoHistory.size > 1) { 84 | val lastState = undoHistory.removeLastOrNull() 85 | lastState?.let { 86 | redoHistory.add(it) 87 | } 88 | 89 | val previousState = undoHistory.lastOrNull() 90 | previousState?.let { 91 | input = it 92 | } 93 | } 94 | } 95 | 96 | fun redo() { 97 | val redoState = redoHistory.removeLastOrNull() 98 | redoState?.let { 99 | undoHistory.add(it) 100 | input = it 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/home/viewmodel/HomeModel.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.home.viewmodel 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import androidx.compose.runtime.State 6 | import androidx.compose.runtime.mutableStateListOf 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.lifecycle.ViewModel 9 | import com.ezpnix.writeon.R 10 | import com.ezpnix.writeon.domain.model.Note 11 | import com.ezpnix.writeon.domain.usecase.NoteUseCase 12 | import com.ezpnix.writeon.presentation.components.DecryptionResult 13 | import com.ezpnix.writeon.presentation.components.EncryptionHelper 14 | import dagger.hilt.android.lifecycle.HiltViewModel 15 | import dagger.hilt.android.qualifiers.ApplicationContext 16 | import javax.inject.Inject 17 | 18 | @HiltViewModel 19 | class HomeViewModel @Inject constructor( 20 | val encryptionHelper: EncryptionHelper, 21 | val noteUseCase: NoteUseCase, 22 | @ApplicationContext private val context: Context, 23 | ) : ViewModel() { 24 | var selectedNotes = mutableStateListOf() 25 | 26 | private var _isDeleteMode = mutableStateOf(false) 27 | val isDeleteMode: State = _isDeleteMode 28 | 29 | private var _isPasswordPromptVisible = mutableStateOf(false) 30 | val isPasswordPromptVisible: State = _isPasswordPromptVisible 31 | 32 | private var _isVaultMode = mutableStateOf(false) 33 | val isVaultMode: State = _isVaultMode 34 | 35 | private var _searchQuery = mutableStateOf("") 36 | val searchQuery: State = _searchQuery 37 | 38 | init { 39 | noteUseCase.observe() 40 | } 41 | 42 | fun toggleIsDeleteMode(enabled: Boolean) { 43 | _isDeleteMode.value = enabled 44 | } 45 | 46 | fun toggleIsVaultMode(enabled: Boolean) { 47 | _isVaultMode.value = enabled 48 | if (!enabled) { 49 | noteUseCase.decryptionResult = DecryptionResult.LOADING 50 | } 51 | noteUseCase.observe() 52 | } 53 | 54 | fun toggleIsPasswordPromptVisible(enabled: Boolean) { 55 | _isPasswordPromptVisible.value = enabled 56 | } 57 | 58 | fun changeSearchQuery(newValue: String) { 59 | _searchQuery.value = newValue 60 | } 61 | 62 | fun pinOrUnpinNotes() { 63 | if (selectedNotes.all { it.pinned }) { 64 | selectedNotes.forEach { note -> 65 | val updatedNote = note.copy(pinned = false) 66 | noteUseCase.pinNote(updatedNote) 67 | } 68 | } else { 69 | selectedNotes.forEach { note -> 70 | val updatedNote = note.copy(pinned = true) 71 | noteUseCase.pinNote(updatedNote) 72 | } 73 | } 74 | 75 | selectedNotes.clear() 76 | } 77 | 78 | fun getAllNotes(): List { 79 | val allNotes = noteUseCase.notes 80 | val filteredNotes = allNotes.filter { it.encrypted == isVaultMode.value} 81 | when (noteUseCase.decryptionResult) { 82 | DecryptionResult.LOADING -> {} 83 | DecryptionResult.EMPTY -> { 84 | if (!encryptionHelper.isPasswordEmpty()) { 85 | toggleIsVaultMode(true) 86 | } 87 | } 88 | DecryptionResult.BAD_PASSWORD, DecryptionResult.BLANK_DATA, DecryptionResult.INVALID_DATA -> { 89 | toggleIsVaultMode(false) 90 | Toast.makeText(context, context.getString(R.string.invalid_password), Toast.LENGTH_SHORT).show() 91 | encryptionHelper.removePassword() 92 | } 93 | else -> { toggleIsVaultMode(true) } 94 | } 95 | return filteredNotes 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/home/widgets/NoteCard.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.home.widgets 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.combinedClickable 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.heightIn 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.CardDefaults 11 | import androidx.compose.material3.ElevatedCard 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.res.dimensionResource 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import com.ezpnix.writeon.R 22 | import com.ezpnix.writeon.domain.model.Note 23 | import com.ezpnix.writeon.presentation.components.markdown.MarkdownText 24 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 25 | 26 | @OptIn(ExperimentalFoundationApi::class) 27 | @Composable 28 | fun NoteCard( 29 | settingsViewModel: SettingsViewModel, 30 | containerColor: Color, 31 | note: Note, 32 | isBorderEnabled: Boolean, 33 | shape: RoundedCornerShape, 34 | onShortClick: () -> Unit, 35 | onLongClick: () -> Unit, 36 | onNoteUpdate: (Note) -> Unit 37 | ) { 38 | val borderModifier = if (isBorderEnabled) { 39 | Modifier.border( 40 | width = 1.5.dp, 41 | color = MaterialTheme.colorScheme.primary, 42 | shape = shape 43 | ) 44 | } else { 45 | if (containerColor != Color.Black) Modifier else 46 | Modifier.border( 47 | width = 1.5.dp, 48 | color = MaterialTheme.colorScheme.surfaceContainerHighest, 49 | shape = shape 50 | ) 51 | } 52 | 53 | ElevatedCard( 54 | modifier = Modifier 55 | .padding(bottom = 12.dp) 56 | .clip(shape) 57 | .combinedClickable( 58 | onClick = { onShortClick() }, 59 | onLongClick = { onLongClick() } 60 | ) 61 | .then(borderModifier), 62 | elevation = CardDefaults.cardElevation(defaultElevation = if (containerColor != Color.Black) 6.dp else 0.dp), 63 | ) { 64 | Column( 65 | modifier = Modifier.padding(16.dp, 12.dp, 16.dp, 12.dp) 66 | ) { 67 | if (note.name.isNotBlank()) { 68 | MarkdownText( 69 | isPreview = true, 70 | isEnabled = settingsViewModel.settings.value.isMarkdownEnabled, 71 | markdown = note.name, 72 | modifier = Modifier 73 | .heightIn(max = dimensionResource(R.dimen.max_name_height)) 74 | .then( 75 | if (note.description.isNotBlank() && !settingsViewModel.settings.value.showOnlyTitle) { 76 | Modifier.padding(bottom = 9.dp) 77 | } else { 78 | Modifier 79 | } 80 | ), 81 | weight = FontWeight.Bold, 82 | spacing = 0.dp, 83 | onContentChange = { onNoteUpdate(note.copy(name = it)) }, 84 | fontSize = 20.sp, 85 | radius = settingsViewModel.settings.value.cornerRadius 86 | ) 87 | } 88 | if (note.description.isNotBlank() && !settingsViewModel.settings.value.showOnlyTitle) { 89 | MarkdownText( 90 | isPreview = true, 91 | markdown = note.description, 92 | isEnabled = settingsViewModel.settings.value.isMarkdownEnabled, 93 | spacing = 0.dp, 94 | modifier = Modifier 95 | .heightIn(max = dimensionResource(R.dimen.max_description_height)), 96 | onContentChange = { onNoteUpdate(note.copy(description = it)) }, 97 | fontSize = 14.sp, 98 | radius = settingsViewModel.settings.value.cornerRadius 99 | ) 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/home/widgets/NoteGrid.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.home.widgets 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.core.MutableTransitionState 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid 8 | import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells 9 | import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan 10 | import androidx.compose.foundation.lazy.staggeredgrid.items 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.text.TextStyle 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import com.ezpnix.writeon.R 23 | import com.ezpnix.writeon.domain.model.Note 24 | import com.ezpnix.writeon.presentation.components.getNoteEnterAnimation 25 | import com.ezpnix.writeon.presentation.components.getNoteExitAnimation 26 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 27 | 28 | @Composable 29 | fun NotesGrid( 30 | settingsViewModel: SettingsViewModel, 31 | containerColor: Color, 32 | onNoteClicked: (Int) -> Unit, 33 | shape : RoundedCornerShape, 34 | notes: List, 35 | onNoteUpdate: (Note) -> Unit, 36 | selectedNotes: MutableList, 37 | viewMode: Boolean, 38 | isDeleteClicked: Boolean, 39 | animationFinished: (Int) -> Unit 40 | ) { 41 | val (pinnedNotes, otherNotes) = notes.partition { it.pinned } 42 | 43 | @Composable 44 | fun Note(note: Note, notes: List) { 45 | val isAnimationVisible = rememberTransitionState() 46 | AnimatedVisibility( 47 | visibleState = isAnimationVisible, 48 | enter = getNoteEnterAnimation(), 49 | exit = getNoteExitAnimation(calculateSlideDirection(notes, note)) 50 | ) { 51 | NoteCard( 52 | settingsViewModel = settingsViewModel, 53 | containerColor = containerColor, 54 | note = note, 55 | shape = shape, 56 | isBorderEnabled = selectedNotes.contains(note), 57 | onShortClick = { handleShortClick(selectedNotes, note, onNoteClicked) }, 58 | onNoteUpdate = onNoteUpdate, 59 | onLongClick = { handleLongClick(selectedNotes, note) } 60 | ) 61 | if (isDeleteClicked && selectedNotes.contains(note)) { 62 | isAnimationVisible.targetState = false 63 | } 64 | } 65 | handleDeleteAnimation(selectedNotes, note, isAnimationVisible, animationFinished) 66 | } 67 | 68 | LazyVerticalStaggeredGrid( 69 | columns = when(viewMode) { 70 | true -> StaggeredGridCells.Fixed(settingsViewModel.settings.value.columnsCount) 71 | false -> StaggeredGridCells.Fixed(1) 72 | }, 73 | horizontalArrangement = Arrangement.spacedBy(12.dp), 74 | content = { 75 | if (pinnedNotes.isNotEmpty()) { 76 | item(span = StaggeredGridItemSpan.FullLine) { 77 | Text( 78 | modifier = Modifier.padding(bottom = 16.dp), 79 | text = stringResource(id = R.string.pinned).uppercase(), 80 | style = TextStyle(fontSize = 10.sp, color = MaterialTheme.colorScheme.secondary) 81 | ) 82 | } 83 | items(pinnedNotes) { note -> 84 | Note(note, pinnedNotes) 85 | } 86 | if (otherNotes.isNotEmpty()) { 87 | item(span = StaggeredGridItemSpan.FullLine) { 88 | Text( 89 | modifier = Modifier.padding(vertical = 16.dp), 90 | text = stringResource(id = R.string.others).uppercase(), 91 | style = TextStyle(fontSize = 10.sp, color = MaterialTheme.colorScheme.secondary) 92 | ) 93 | } 94 | } 95 | } 96 | items(otherNotes) { note -> 97 | Note(note, otherNotes) 98 | } 99 | }, 100 | modifier = Modifier.padding(horizontal = 12.dp) 101 | ) 102 | } 103 | 104 | 105 | private fun calculateSlideDirection(notes: List, note: Note): Int { 106 | return if (notes.indexOf(note) % 2 == 0) -1 else 1 107 | } 108 | 109 | @Composable 110 | private fun rememberTransitionState(): MutableTransitionState { 111 | return remember { MutableTransitionState(false).apply { targetState = true } } 112 | } 113 | 114 | private fun handleShortClick( 115 | selectedNotes: MutableList, 116 | note: Note, 117 | onNoteClicked: (Int) -> Unit 118 | ) { 119 | if (selectedNotes.isNotEmpty()) { 120 | if (selectedNotes.contains(note)) { 121 | selectedNotes.remove(note) 122 | } else { 123 | selectedNotes.add(note) 124 | } 125 | } else { 126 | onNoteClicked(note.id) 127 | } 128 | } 129 | 130 | private fun handleLongClick(selectedNotes: MutableList, note: Note) { 131 | if (!selectedNotes.contains(note)) { 132 | selectedNotes.add(note) 133 | } 134 | } 135 | 136 | private fun handleDeleteAnimation( 137 | selectedNotes: MutableList, 138 | note: Note, 139 | isAnimationVisible: MutableTransitionState, 140 | animationFinished: (Int) -> Unit 141 | ) { 142 | if (!isAnimationVisible.targetState && !isAnimationVisible.currentState && selectedNotes.contains(note)) { 143 | selectedNotes.remove(note) 144 | isAnimationVisible.targetState = true 145 | animationFinished(note.id) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/home/widgets/Placeholder.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.home.widgets 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.imePadding 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.unit.sp 15 | 16 | @Composable 17 | fun Placeholder( 18 | placeholderIcon: @Composable () -> Unit, 19 | placeholderText: String 20 | ) { 21 | Box( 22 | modifier = Modifier 23 | .imePadding() 24 | .fillMaxSize(), 25 | contentAlignment = Alignment.Center, 26 | ) { 27 | Column( 28 | horizontalAlignment = Alignment.CenterHorizontally, 29 | verticalArrangement = Arrangement.spacedBy(20.dp) 30 | ) { 31 | placeholderIcon() 32 | 33 | Text( 34 | text = placeholderText, 35 | color = MaterialTheme.colorScheme.outline, 36 | fontSize = 14.sp 37 | ) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/settings/AndroidScreen.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.settings 2 | 3 | import android.os.Build 4 | import android.os.Environment 5 | import android.os.StatFs 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.foundation.verticalScroll 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack 12 | import androidx.compose.material3.* 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.text.font.FontWeight 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import androidx.navigation.NavController 20 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 21 | import java.text.DecimalFormat 22 | import java.text.SimpleDateFormat 23 | import java.util.* 24 | 25 | @OptIn(ExperimentalMaterial3Api::class) 26 | @Composable 27 | fun AndroidScreen( 28 | navController: NavController, 29 | settingsViewModel: SettingsViewModel 30 | ) { 31 | val stat = StatFs(Environment.getDataDirectory().absolutePath) 32 | val totalStorage = formatStorage(stat.totalBytes) 33 | val availableStorage = formatStorage(stat.availableBytes) 34 | 35 | val deviceInfo = listOf( 36 | "Manufacturer" to Build.MANUFACTURER, 37 | "Model" to Build.MODEL, 38 | "Brand" to Build.BRAND, 39 | "Device" to Build.DEVICE, 40 | "Board" to Build.BOARD, 41 | "Hardware" to Build.HARDWARE, 42 | "CPU ABI" to Build.SUPPORTED_ABIS.joinToString(), 43 | "Android Version" to Build.VERSION.RELEASE, 44 | "SDK Level" to Build.VERSION.SDK_INT.toString(), 45 | "Security Patch" to (Build.VERSION.SECURITY_PATCH ?: "Unknown"), 46 | "Storage" to "$availableStorage free / $totalStorage total", 47 | 48 | "Display" to "${Build.DISPLAY}", 49 | "Host" to "${Build.HOST}", 50 | "Bootloader" to "${Build.BOOTLOADER}", 51 | "Fingerprint" to "${Build.FINGERPRINT}", 52 | "Build ID" to "${Build.ID}", 53 | "Build Time" to formatBuildTime(Build.TIME), 54 | "Radio Version" to (Build.getRadioVersion() ?: "Unknown") 55 | ) 56 | 57 | 58 | Scaffold( 59 | topBar = { 60 | TopAppBar( 61 | title = { Text("Device Information") }, 62 | navigationIcon = { 63 | IconButton(onClick = { navController.popBackStack() }) { 64 | Icon( 65 | imageVector = Icons.AutoMirrored.Rounded.ArrowBack, 66 | contentDescription = "Back" 67 | ) 68 | } 69 | } 70 | ) 71 | }, 72 | content = { padding -> 73 | Column( 74 | modifier = Modifier 75 | .padding(padding) 76 | .fillMaxSize() 77 | .padding(24.dp) 78 | .verticalScroll(rememberScrollState()), 79 | verticalArrangement = Arrangement.Top, 80 | horizontalAlignment = Alignment.CenterHorizontally 81 | ) { 82 | Card( 83 | modifier = Modifier 84 | .fillMaxWidth() 85 | .wrapContentHeight(), 86 | shape = RoundedCornerShape(24.dp), 87 | colors = CardDefaults.cardColors( 88 | containerColor = MaterialTheme.colorScheme.surfaceVariant 89 | ), 90 | elevation = CardDefaults.cardElevation(8.dp) 91 | ) { 92 | Column( 93 | modifier = Modifier 94 | .padding(24.dp) 95 | .fillMaxWidth(), 96 | horizontalAlignment = Alignment.Start 97 | ) { 98 | deviceInfo.forEach { (label, value) -> 99 | Text( 100 | text = "$label: $value", 101 | fontSize = 14.sp, 102 | fontWeight = FontWeight.Medium, 103 | color = MaterialTheme.colorScheme.onSurface, 104 | modifier = Modifier.padding(vertical = 4.dp) 105 | ) 106 | } 107 | 108 | Spacer(Modifier.height(16.dp)) 109 | HorizontalDivider() 110 | Spacer(Modifier.height(12.dp)) 111 | Text( 112 | text = "✦ App Developed By: @3zpnix", 113 | fontSize = 13.sp, 114 | fontWeight = FontWeight.SemiBold, 115 | color = MaterialTheme.colorScheme.primary, 116 | modifier = Modifier.align(Alignment.CenterHorizontally) 117 | ) 118 | } 119 | } 120 | } 121 | } 122 | ) 123 | } 124 | 125 | fun formatStorage(bytes: Long): String { 126 | val df = DecimalFormat("#.##") 127 | val kb = bytes / 1024.0 128 | val mb = kb / 1024.0 129 | val gb = mb / 1024.0 130 | return when { 131 | gb >= 1 -> "${df.format(gb)} GB" 132 | mb >= 1 -> "${df.format(mb)} MB" 133 | else -> "${df.format(kb)} KB" 134 | } 135 | } 136 | 137 | fun formatBuildTime(time: Long): String { 138 | val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) 139 | return sdf.format(Date(time)) 140 | } 141 | 142 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/settings/BackupWorker.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.settings 2 | 3 | import android.content.Context 4 | import androidx.work.CoroutineWorker 5 | import androidx.work.WorkerParameters 6 | import com.ezpnix.writeon.core.constant.DatabaseConst 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import java.io.File 10 | import java.io.FileInputStream 11 | import java.io.FileOutputStream 12 | import java.util.zip.ZipEntry 13 | import java.util.zip.ZipOutputStream 14 | import javax.crypto.Cipher 15 | import javax.crypto.SecretKey 16 | import javax.crypto.SecretKeyFactory 17 | import javax.crypto.spec.IvParameterSpec 18 | import javax.crypto.spec.PBEKeySpec 19 | import javax.crypto.spec.SecretKeySpec 20 | import java.security.SecureRandom 21 | import java.security.spec.KeySpec 22 | 23 | class BackupWorker( 24 | context: Context, 25 | workerParams: WorkerParameters 26 | ) : CoroutineWorker(context, workerParams) { 27 | 28 | override suspend fun doWork(): Result { 29 | val context = applicationContext 30 | 31 | return try { 32 | val backupFolder = File(context.getExternalFilesDir(null), "BackupFolder") 33 | if (!backupFolder.exists()) { 34 | backupFolder.mkdirs() 35 | } 36 | 37 | val backupFileName = "${DatabaseConst.NOTES_DATABASE_BACKUP_NAME}-${currentDateTime()}.zip" 38 | val backupFile = File(backupFolder, backupFileName) 39 | 40 | // REMINDERS: 41 | // Perform the backup operation 42 | // Replace with actual password handling 43 | // If backup is successful, return Result.success() 44 | // If there is an error, return Result.failure() 45 | backupDatabase(backupFile, password = "your_password") 46 | Result.success() 47 | } catch (e: Exception) { 48 | e.printStackTrace() 49 | Result.failure() 50 | } 51 | } 52 | 53 | private suspend fun backupDatabase(backupFile: File, password: String?) { 54 | withContext(Dispatchers.IO) { 55 | val databaseFile = applicationContext.getDatabasePath(DatabaseConst.NOTES_DATABASE_FILE_NAME) 56 | 57 | val tempZipFile = File.createTempFile("backup", ".zip", applicationContext.cacheDir) 58 | 59 | ZipOutputStream(FileOutputStream(tempZipFile)).use { zipOutputStream -> 60 | FileInputStream(databaseFile).use { inputStream -> 61 | val zipEntry = ZipEntry(databaseFile.name) 62 | zipOutputStream.putNextEntry(zipEntry) 63 | inputStream.copyTo(zipOutputStream) 64 | zipOutputStream.closeEntry() 65 | } 66 | } 67 | 68 | val zipData = tempZipFile.readBytes() 69 | 70 | if (password != null) { 71 | val salt = ByteArray(16).apply { SecureRandom().nextBytes(this) } 72 | val secretKey = generateSecretKey(password, salt) 73 | val encryptedData = encrypt(zipData, secretKey) 74 | backupFile.writeBytes(salt + encryptedData) 75 | } else { 76 | backupFile.writeBytes(zipData) 77 | } 78 | 79 | tempZipFile.delete() 80 | } 81 | } 82 | 83 | private fun generateSecretKey(password: String, salt: ByteArray): SecretKey { 84 | val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") 85 | val spec: KeySpec = PBEKeySpec(password.toCharArray(), salt, 65536, 256) 86 | return SecretKeySpec(factory.generateSecret(spec).encoded, "AES") 87 | } 88 | 89 | private fun encrypt(data: ByteArray, secretKey: SecretKey): ByteArray { 90 | val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") 91 | val iv = ByteArray(16).apply { SecureRandom().nextBytes(this) } 92 | cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv)) 93 | val encryptedData = cipher.doFinal(data) 94 | return iv + encryptedData 95 | } 96 | 97 | private fun currentDateTime(): String { 98 | val format = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault()) 99 | return format.format(java.util.Date()) 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/settings/model/Flashcard.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.settings.model 2 | 3 | data class Flashcard( 4 | val word: String = "", 5 | val meaning: String = "" 6 | ) 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/settings/model/SettingsPreferences.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.settings.model 2 | 3 | import android.content.Context 4 | import androidx.datastore.preferences.core.edit 5 | import androidx.datastore.preferences.core.stringPreferencesKey 6 | import androidx.datastore.preferences.preferencesDataStore 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.map 9 | import javax.inject.Inject 10 | import dagger.hilt.android.qualifiers.ApplicationContext 11 | 12 | private val Context.dataStore by preferencesDataStore("user_settings") 13 | 14 | class SettingsPreferences @Inject constructor( 15 | @ApplicationContext private val context: Context 16 | ) { 17 | companion object { 18 | private val PLACEHOLDER_KEY = stringPreferencesKey("search_placeholder") 19 | } 20 | 21 | val dynamicPlaceholder: Flow = context.dataStore.data 22 | .map { preferences -> 23 | preferences[PLACEHOLDER_KEY] ?: "Simple Notepad" 24 | } 25 | 26 | suspend fun savePlaceholder(placeholder: String) { 27 | context.dataStore.edit { preferences -> 28 | preferences[PLACEHOLDER_KEY] = placeholder 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/settings/settings/Behaviour.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.settings.settings 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.material.icons.Icons 7 | import androidx.compose.material.icons.rounded.Edit 8 | import androidx.compose.material.icons.rounded.Style 9 | import androidx.compose.material.icons.rounded.Title 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.unit.dp 14 | import androidx.navigation.NavController 15 | import com.ezpnix.writeon.R 16 | import com.ezpnix.writeon.presentation.screens.settings.SettingsScaffold 17 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 18 | import com.ezpnix.writeon.presentation.screens.settings.widgets.ActionType 19 | import com.ezpnix.writeon.presentation.screens.settings.widgets.SettingsBox 20 | 21 | @Composable 22 | fun MarkdownScreen(navController: NavController, settingsViewModel: SettingsViewModel) { 23 | SettingsScaffold( 24 | settingsViewModel = settingsViewModel, 25 | title = stringResource(id = R.string.Behavior), 26 | onBackNavClicked = { navController.navigateUp() } 27 | ) { 28 | LazyColumn { 29 | item { 30 | SettingsBox( 31 | title = stringResource(id = R.string.markdown), 32 | description = stringResource(id = R.string.markdown_description), 33 | icon = Icons.Rounded.Style, 34 | actionType = ActionType.SWITCH, 35 | radius = shapeManager(isBoth = true, radius = settingsViewModel.settings.value.cornerRadius), 36 | variable = settingsViewModel.settings.value.isMarkdownEnabled, 37 | switchEnabled = { settingsViewModel.update(settingsViewModel.settings.value.copy(isMarkdownEnabled = it))} 38 | ) 39 | Spacer(modifier = Modifier.height(18.dp)) 40 | } 41 | item { 42 | SettingsBox( 43 | title = stringResource(id = R.string.always_edit), 44 | description = stringResource(id = R.string.always_edit_description), 45 | icon = Icons.Rounded.Edit, 46 | actionType = ActionType.SWITCH, 47 | radius = shapeManager(isBoth = true, radius = settingsViewModel.settings.value.cornerRadius), 48 | variable = settingsViewModel.settings.value.editMode, 49 | switchEnabled = { settingsViewModel.update(settingsViewModel.settings.value.copy(editMode = it))} 50 | ) 51 | } 52 | item { 53 | SettingsBox( 54 | title = stringResource(id = R.string.show_only_title), 55 | description = stringResource(id = R.string.show_only_title_description), 56 | icon = Icons.Rounded.Title, 57 | actionType = ActionType.SWITCH, 58 | radius = shapeManager(isBoth = true, radius = settingsViewModel.settings.value.cornerRadius), 59 | variable = settingsViewModel.settings.value.showOnlyTitle, 60 | switchEnabled = { settingsViewModel.update(settingsViewModel.settings.value.copy(showOnlyTitle = it))} 61 | ) 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/settings/settings/Guide.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.settings.settings 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.itemsIndexed 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack 10 | import androidx.compose.material.icons.rounded.ArrowBack 11 | import androidx.compose.material.icons.rounded.ExpandLess 12 | import androidx.compose.material.icons.rounded.ExpandMore 13 | import androidx.compose.material3.* 14 | import androidx.compose.runtime.* 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.compose.ui.unit.dp 19 | import androidx.navigation.NavController 20 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 21 | 22 | @OptIn(ExperimentalMaterial3Api::class) 23 | @Composable 24 | fun GuideScreen(navController: NavController, settingsViewModel: SettingsViewModel) { 25 | val context = LocalContext.current 26 | 27 | val faqList = listOf( 28 | "What is Write On?" to "Write On is a simple app for taking notes—nothing special. You can add images and many more.", 29 | "How do I delete a note?" to "Long-press a note on your home screen to see the available options. You may also discover other hidden features as well such as the swipe to edit/preview note.", 30 | "Can I recover deleted notes?" to "Currently, there is no recovery feature, so be careful when deleting notes. But, this feature is coming real soon so stay tune.", 31 | "How often are updates released?" to "The developer usually releases updates every one to two months, depending on how busy the developer is with real life and stuff.", 32 | "How do I report an issue or bug?" to "You can visit the official GitHub page or come chat with the Developer on his social media account.", 33 | "Do you accept feature requests?" to "Of course! Any feedback or suggestions for improvement are welcome. The developer usually replies within a day or two.", 34 | "Automatic backup files stored?" to "They are currently located in the Android/data folder, which, unfortunately, is inaccessible to some because of the latest android policy", 35 | "Is this app fully open-source?" to "Yes! You may fork it, modify codes to your liking. There’s nothing shady—no virus, no forced ads—just a clean, open-source app.", 36 | "Who developed this app?" to "@3zpnix, a foreign university student somewhere in the world who got bored and wanted to try on something." 37 | ) 38 | 39 | var expandedStates by remember { mutableStateOf(List(faqList.size) { false }) } 40 | 41 | Scaffold( 42 | topBar = { 43 | TopAppBar( 44 | title = { Text("Guide & FAQ", color = MaterialTheme.colorScheme.onSurface) }, 45 | navigationIcon = { 46 | IconButton(onClick = { navController.navigateUp() }) { 47 | Icon( 48 | imageVector = Icons.AutoMirrored.Rounded.ArrowBack, 49 | contentDescription = "Back", 50 | tint = MaterialTheme.colorScheme.onSurface 51 | ) 52 | } 53 | }, 54 | colors = TopAppBarDefaults.mediumTopAppBarColors(containerColor = MaterialTheme.colorScheme.surface) 55 | ) 56 | } 57 | ) { paddingValues -> 58 | LazyColumn( 59 | modifier = Modifier 60 | .fillMaxSize() 61 | .padding(paddingValues) 62 | .padding(16.dp), 63 | verticalArrangement = Arrangement.spacedBy(8.dp) 64 | ) { 65 | itemsIndexed(faqList) { index, (question, answer) -> 66 | Card( 67 | modifier = Modifier 68 | .fillMaxWidth() 69 | .clickable { 70 | expandedStates = expandedStates.toMutableList().apply { 71 | this[index] = !this[index] 72 | } 73 | }, 74 | elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), 75 | colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) 76 | ) { 77 | Column(modifier = Modifier.padding(16.dp)) { 78 | Row( 79 | verticalAlignment = Alignment.CenterVertically, 80 | modifier = Modifier.fillMaxWidth() 81 | ) { 82 | Text( 83 | text = question, 84 | modifier = Modifier.weight(1f), 85 | style = MaterialTheme.typography.bodyLarge, 86 | color = MaterialTheme.colorScheme.primary 87 | ) 88 | Icon( 89 | imageVector = if (expandedStates[index]) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore, 90 | contentDescription = "Expand/Collapse", 91 | tint = MaterialTheme.colorScheme.onSurface 92 | ) 93 | } 94 | AnimatedVisibility(visible = expandedStates[index]) { 95 | Text( 96 | text = answer, 97 | modifier = Modifier.padding(top = 8.dp), 98 | style = MaterialTheme.typography.bodyMedium, 99 | color = MaterialTheme.colorScheme.secondary 100 | ) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/settings/settings/Issue.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.settings.settings 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.lazy.grid.GridCells 6 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material3.* 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | import androidx.navigation.NavController 17 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 18 | 19 | @OptIn(ExperimentalMaterial3Api::class) 20 | @Composable 21 | fun IssueScreen(navController: NavController, settingsViewModel: SettingsViewModel) { 22 | val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6") 23 | 24 | Scaffold( 25 | topBar = { 26 | TopAppBar(title = { Text("Select an Item") }) 27 | } 28 | ) { paddingValues -> 29 | LazyVerticalGrid( 30 | columns = GridCells.Fixed(2), 31 | modifier = Modifier 32 | .fillMaxSize() 33 | .padding(paddingValues) 34 | .padding(16.dp) 35 | ) { 36 | items(items.size) { index -> 37 | GridItem(itemName = items[index], onClick = { 38 | }) 39 | } 40 | } 41 | } 42 | } 43 | 44 | @Composable 45 | fun GridItem(itemName: String, onClick: () -> Unit) { 46 | Card( 47 | modifier = Modifier 48 | .fillMaxWidth() 49 | .padding(8.dp) 50 | .clickable { onClick() }, 51 | shape = RoundedCornerShape(12.dp), 52 | colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primary) 53 | ) { 54 | Box( 55 | contentAlignment = Alignment.Center, 56 | modifier = Modifier 57 | .padding(16.dp) 58 | .height(100.dp) 59 | ) { 60 | Text( 61 | text = itemName, 62 | fontSize = 18.sp, 63 | fontWeight = FontWeight.Bold, 64 | color = Color.White 65 | ) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/settings/settings/Language.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.settings.settings 2 | 3 | import androidx.appcompat.app.AppCompatDelegate 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.rounded.Translate 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.platform.LocalContext 9 | import androidx.compose.ui.res.stringResource 10 | import androidx.core.os.LocaleListCompat 11 | import androidx.navigation.NavController 12 | import com.ezpnix.writeon.R 13 | import com.ezpnix.writeon.presentation.screens.settings.SettingsScaffold 14 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 15 | import com.ezpnix.writeon.presentation.screens.settings.widgets.ActionType 16 | import com.ezpnix.writeon.presentation.screens.settings.widgets.ListDialog 17 | import com.ezpnix.writeon.presentation.screens.settings.widgets.SettingsBox 18 | 19 | @Composable 20 | fun LanguageScreen(navController: NavController, settingsViewModel: SettingsViewModel) { 21 | SettingsScaffold( 22 | settingsViewModel = settingsViewModel, 23 | title = stringResource(id = R.string.language), 24 | onBackNavClicked = { navController.navigateUp() } 25 | ) { 26 | LazyColumn { 27 | item { 28 | SettingsBox( 29 | title = stringResource(id = R.string.language), 30 | description = stringResource(id = R.string.language_description), 31 | icon = Icons.Rounded.Translate, 32 | radius = shapeManager(radius = settingsViewModel.settings.value.cornerRadius, isBoth = true), 33 | actionType = ActionType.CUSTOM, 34 | customAction = { onExit -> OnLanguageClicked(settingsViewModel) { onExit() } 35 | } 36 | ) 37 | } 38 | } 39 | } 40 | } 41 | 42 | @Composable 43 | private fun OnLanguageClicked(settingsViewModel: SettingsViewModel, onExit: () -> Unit) { 44 | val context = LocalContext.current 45 | val languages = settingsViewModel.getSupportedLanguages(context).toList() 46 | ListDialog( 47 | text = stringResource(R.string.language), 48 | list = languages, 49 | settingsViewModel = settingsViewModel, 50 | onExit = onExit, 51 | extractDisplayData = { it }, 52 | initialItem = Pair(context.getString(R.string.system_language), second = ""), 53 | setting = { isFirstItem, isLastItem, displayData -> 54 | SettingsBox( 55 | isBig = false, 56 | title = displayData.first, 57 | radius = shapeManager(isFirst = isFirstItem, isLast = isLastItem, radius = settingsViewModel.settings.value.cornerRadius), 58 | actionType = ActionType.RADIOBUTTON, 59 | variable = if (displayData.second.isNotBlank()) { 60 | AppCompatDelegate.getApplicationLocales()[0]?.language == displayData.second 61 | } else { 62 | AppCompatDelegate.getApplicationLocales().isEmpty 63 | }, 64 | switchEnabled = { if (displayData.second.isNotBlank()) { 65 | AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(displayData.second)) 66 | } else { 67 | AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()) 68 | } 69 | } 70 | ) 71 | } 72 | ) 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/settings/trash/Trash.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.settings.trash 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack 7 | import androidx.compose.material.icons.rounded.ArrowBack 8 | import androidx.compose.material.icons.rounded.Delete 9 | import androidx.compose.material.icons.rounded.DeleteForever 10 | import androidx.compose.material.icons.rounded.Info 11 | import androidx.compose.material.icons.rounded.Restore 12 | import androidx.compose.material.icons.rounded.SelectAll 13 | import androidx.compose.material3.* 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.setValue 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.compose.ui.text.TextStyle 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.window.Popup 25 | import androidx.navigation.NavController 26 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 27 | 28 | @OptIn(ExperimentalMaterial3Api::class) 29 | @Composable 30 | fun TrashScreen(navController: NavController, settings: SettingsViewModel) { 31 | val context = LocalContext.current 32 | var showInfoPopup by remember { mutableStateOf(false) } 33 | val iconColor = MaterialTheme.colorScheme.primary 34 | 35 | Scaffold( 36 | topBar = { 37 | TopAppBar( 38 | title = { 39 | Row( 40 | verticalAlignment = Alignment.CenterVertically, 41 | horizontalArrangement = Arrangement.spacedBy(2.dp) 42 | ) { 43 | Text("Trash") 44 | IconButton(onClick = { showInfoPopup = !showInfoPopup }) { 45 | Icon(imageVector = Icons.Rounded.Info, contentDescription = "Info", tint = iconColor) 46 | } 47 | } 48 | }, 49 | navigationIcon = { 50 | IconButton(onClick = { navController.navigateUp() }) { 51 | Icon(imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back") 52 | } 53 | }, 54 | actions = { 55 | IconButton(onClick = { 56 | Toast.makeText(context, "Coming Soon!", Toast.LENGTH_SHORT).show() 57 | }) { 58 | Icon(imageVector = Icons.Rounded.Restore, contentDescription = "Restore", tint = iconColor) 59 | } 60 | IconButton(onClick = { 61 | Toast.makeText(context, "Coming Soon!", Toast.LENGTH_SHORT).show() 62 | }) { 63 | Icon(imageVector = Icons.Rounded.SelectAll, contentDescription = "Select All", tint = iconColor) 64 | } 65 | IconButton(onClick = { 66 | Toast.makeText(context, "Coming Soon!", Toast.LENGTH_SHORT).show() 67 | }) { 68 | Icon(imageVector = Icons.Rounded.DeleteForever, contentDescription = "Delete Forever", tint = iconColor) 69 | } 70 | } 71 | ) 72 | }, 73 | content = { paddingValues -> 74 | Box( 75 | modifier = Modifier 76 | .fillMaxSize() 77 | .padding(paddingValues), 78 | contentAlignment = Alignment.Center 79 | ) { 80 | Column( 81 | horizontalAlignment = Alignment.CenterHorizontally 82 | ) { 83 | Icon( 84 | imageVector = Icons.Rounded.Delete, 85 | contentDescription = "Trash Icon", 86 | modifier = Modifier.size(64.dp) 87 | ) 88 | Spacer(modifier = Modifier.height(16.dp)) 89 | Text( 90 | text = "Coming Soon!", 91 | style = TextStyle() 92 | ) 93 | } 94 | if (showInfoPopup) { 95 | Popup(alignment = Alignment.TopCenter) { 96 | Card( 97 | modifier = Modifier 98 | .padding(16.dp) 99 | .fillMaxWidth(), 100 | elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) 101 | ) { 102 | Text( 103 | text = "About: Feature coming soon from developer.", 104 | modifier = Modifier.padding(16.dp) 105 | ) 106 | } 107 | } 108 | } 109 | } 110 | } 111 | ) 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/settings/widgets/SettingCategory.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.settings.widgets 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material3.CardDefaults 14 | import androidx.compose.material3.ElevatedCard 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.draw.clip 26 | import androidx.compose.ui.draw.scale 27 | import androidx.compose.ui.graphics.vector.ImageVector 28 | import androidx.compose.ui.text.font.FontWeight 29 | import androidx.compose.ui.unit.dp 30 | import androidx.compose.ui.unit.sp 31 | 32 | enum class SettingActionType { 33 | NAVIGATE, 34 | LINK 35 | } 36 | 37 | @Composable 38 | fun SettingCategory( 39 | title: String, 40 | subTitle: String = "", 41 | icon: ImageVector, 42 | shape: RoundedCornerShape, 43 | isLast: Boolean = false, 44 | smallSetting: Boolean = false, 45 | 46 | actionType: SettingActionType = SettingActionType.NAVIGATE, 47 | linkClicked: () -> Unit = {}, 48 | 49 | action: () -> Unit = {}, 50 | composableAction: @Composable (() -> Unit) -> Unit = {}, 51 | ) { 52 | var showCustomAction by remember { mutableStateOf(false) } 53 | if (showCustomAction) composableAction { showCustomAction = !showCustomAction } 54 | 55 | ElevatedCard( 56 | shape = shape, 57 | modifier = Modifier 58 | .clip(shape) 59 | .clickable { 60 | showCustomAction = !showCustomAction 61 | when (actionType) { 62 | SettingActionType.LINK -> linkClicked() 63 | SettingActionType.NAVIGATE -> action() 64 | } 65 | }, 66 | colors = if (smallSetting) 67 | CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.primary) 68 | else 69 | CardDefaults.elevatedCardColors(), 70 | elevation = CardDefaults.elevatedCardElevation(defaultElevation = 6.dp), 71 | ) { 72 | Row( 73 | modifier = Modifier 74 | .clip(shape) 75 | .fillMaxSize() 76 | .padding( 77 | horizontal = 24.dp, 78 | vertical = if (smallSetting) 11.dp else 16.dp 79 | ), 80 | ) { 81 | if (smallSetting) { 82 | Row(verticalAlignment = Alignment.CenterVertically) { 83 | RenderCategoryTitle(title = title) 84 | Spacer(modifier = Modifier.weight(1f)) 85 | RenderCategoryDescription(subTitle = subTitle, smallSetting = true) 86 | RenderCategoryIcon(icon, true) 87 | } 88 | } else { 89 | Column { 90 | RenderCategoryTitle(title = title) 91 | RenderCategoryDescription(subTitle = subTitle, smallSetting = false) 92 | } 93 | Spacer(modifier = Modifier.weight(1f)) 94 | RenderCategoryIcon(icon, false) 95 | } 96 | } 97 | } 98 | Spacer(modifier = Modifier.height(if (isLast) 26.dp else 2.dp)) 99 | } 100 | 101 | @Composable 102 | private fun RenderCategoryTitle(title: String) { 103 | Text( 104 | text = title, 105 | fontSize = 16.sp, 106 | fontWeight = FontWeight.Bold 107 | ) 108 | } 109 | 110 | @Composable 111 | private fun RenderCategoryDescription(subTitle: String, smallSetting: Boolean) { 112 | if (subTitle.isNotBlank()) { 113 | Text( 114 | color = if (smallSetting) MaterialTheme.colorScheme.surfaceContainerHigh else MaterialTheme.colorScheme.primary, 115 | text = subTitle, 116 | fontSize = if (smallSetting) 13.sp else 10.sp, 117 | modifier = Modifier.padding(if (smallSetting) 7.dp else 0.dp) 118 | ) 119 | } 120 | } 121 | 122 | @Composable 123 | private fun RenderCategoryIcon(icon: ImageVector, reverseColors: Boolean) { 124 | Box( 125 | modifier = Modifier 126 | .background( 127 | color = if (reverseColors) MaterialTheme.colorScheme.surfaceContainerHighest else MaterialTheme.colorScheme.primary, 128 | shape = RoundedCornerShape(50) 129 | ), 130 | ) { 131 | Icon( 132 | imageVector = icon, 133 | contentDescription = null, 134 | tint = if (reverseColors) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.surfaceContainerHigh, 135 | modifier = Modifier 136 | .scale(if (reverseColors) 0.8f else 1f) 137 | .padding(9.dp) 138 | ) 139 | } 140 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/settings/widgets/SettingsDialog.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.settings.widgets 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.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.foundation.lazy.itemsIndexed 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.text.font.FontWeight 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import androidx.compose.ui.window.Dialog 21 | import androidx.compose.ui.window.DialogProperties 22 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 23 | import com.ezpnix.writeon.presentation.screens.settings.settings.shapeManager 24 | 25 | @Composable 26 | fun ListDialog( 27 | text: String, 28 | list: List, 29 | initialItem: T? = null, 30 | settingsViewModel: SettingsViewModel, 31 | onExit: () -> Unit, 32 | extractDisplayData: (T) -> Pair, 33 | setting: @Composable (Boolean, Boolean, Pair) -> Unit 34 | ) { 35 | Dialog( 36 | onDismissRequest = { onExit() }, 37 | properties = DialogProperties(usePlatformDefaultWidth = false), 38 | ) { 39 | Column( 40 | modifier = Modifier 41 | .background( 42 | color = MaterialTheme.colorScheme.surfaceContainerLow, 43 | shape = shapeManager(isBoth = true, radius = settingsViewModel.settings.value.cornerRadius) 44 | ) 45 | .padding(22.dp, 0.dp, 22.dp, 0.dp) 46 | .fillMaxSize(.8f) 47 | ) { 48 | Text( 49 | text = text, 50 | textAlign = TextAlign.Center, 51 | fontWeight = FontWeight.Bold, 52 | modifier = Modifier 53 | .fillMaxWidth() 54 | .padding(20.dp), 55 | fontSize = 20.sp, 56 | ) 57 | LazyColumn { 58 | initialItem?.let { initial -> 59 | item { 60 | val displayData = extractDisplayData(initial) 61 | setting(true, list.size == 1, displayData) 62 | } 63 | } 64 | itemsIndexed(list) { index, content -> 65 | val isFirstItem = (initialItem == null && index == 0) 66 | val isLastItem = index == list.lastIndex 67 | val displayData = extractDisplayData(content) 68 | setting(isFirstItem, isLastItem, displayData) 69 | if (isLastItem) Spacer(modifier = Modifier.height(20.dp)) 70 | } 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/screens/terms/TermsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.screens.terms 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | import com.ezpnix.writeon.R 17 | import com.ezpnix.writeon.core.constant.ConnectionConst 18 | import com.ezpnix.writeon.presentation.components.AgreeButton 19 | import com.ezpnix.writeon.presentation.components.NotesScaffold 20 | import com.ezpnix.writeon.presentation.components.markdown.MarkdownText 21 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 22 | import com.ezpnix.writeon.presentation.screens.settings.settings.shapeManager 23 | 24 | @Composable 25 | fun TermsScreen( 26 | settingsViewModel: SettingsViewModel 27 | ) { 28 | NotesScaffold( 29 | floatingActionButton = { 30 | AgreeButton(text = stringResource(id = R.string.agree)) { 31 | settingsViewModel.update(settingsViewModel.settings.value.copy(termsOfService = true)) 32 | } 33 | }, 34 | content = { 35 | Column( 36 | modifier = Modifier.padding(16.dp) 37 | ) { 38 | Text( 39 | text = stringResource(id = R.string.terms_of_service), 40 | style = MaterialTheme.typography.headlineLarge, 41 | modifier = Modifier.padding(0.dp,16.dp,16.dp,16.dp) 42 | ) 43 | Box( 44 | modifier = Modifier 45 | .fillMaxSize() 46 | .clip( 47 | shapeManager( 48 | isBoth = true, 49 | radius = settingsViewModel.settings.value.cornerRadius 50 | ) 51 | ) 52 | .background( 53 | color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.5f) 54 | ) 55 | .padding(1.dp) 56 | ) { 57 | Column(modifier = Modifier.padding(16.dp)) { 58 | MarkdownText( 59 | fontSize = 12.sp, 60 | radius = settingsViewModel.settings.value.cornerRadius, 61 | markdown = getTermsOfService(), 62 | isEnabled = true 63 | ) 64 | } 65 | } 66 | } 67 | } 68 | ) 69 | } 70 | 71 | @Composable 72 | fun getTermsOfService(): String { 73 | return buildString { 74 | append("### ${stringResource(R.string.terms_acceptance_title)}\n") 75 | append("${stringResource(R.string.terms_acceptance_body)}\n\n") 76 | 77 | append("### ${stringResource(R.string.terms_license_title)}\n") 78 | append("${stringResource(R.string.terms_license_body)}\n\n") 79 | 80 | append("### ${stringResource(R.string.terms_responsibilities_title)}\n") 81 | append("${stringResource(R.string.terms_responsibilities_body)}\n\n") 82 | 83 | append("### ${stringResource(R.string.terms_liabilities_title)}\n") 84 | append("${stringResource(R.string.terms_liabilities_body)}\n\n") 85 | 86 | append("### ${stringResource(R.string.terms_warranties_title)}\n") 87 | append("${stringResource(R.string.terms_warranties_body)}\n\n") 88 | 89 | append("### ${stringResource(R.string.terms_changes_title)}\n") 90 | append("${stringResource(R.string.terms_changes_body)}\n\n") 91 | 92 | append("### ${stringResource(R.string.terms_privacy_title)}\n") 93 | append("${stringResource(R.string.terms_privacy_body)} **https://github.com/3zpnix/WriteOn/**.\n\n") 94 | 95 | append("### ${stringResource(R.string.terms_contact_title)}\n") 96 | append("${stringResource(R.string.terms_contact_body)} ${ConnectionConst.SUPPORT_MAIL}.\n\n") 97 | 98 | append("*${stringResource(R.string.terms_effective_date)}: ${ConnectionConst.TERMS_EFFECTIVE_DATE}\n") 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val primaryLight = Color(0xFF004D40) // Dark teal 6 | val primaryContainerLight = Color(0xFFE8F5E9) // Light green background 7 | val onPrimaryContainerLight = Color(0xFF388E3C) // Medium green 8 | val secondaryLight = Color(0xFF64B5F6) // Light blue 9 | val onSecondaryLight = Color(0xFFFFFFFF) // White for contrast 10 | val secondaryContainerLight = Color(0xFFE3F2FD) // Light blue background 11 | val onSecondaryContainerLight = Color(0xFF1E88E5) // Medium blue 12 | val tertiaryLight = Color(0xFFFFD54F) // Soft yellow 13 | val onTertiaryLight = Color(0xFF000000) // Black for contrast 14 | val tertiaryContainerLight = Color(0xFFFFF8E1) // Light yellow background 15 | val onTertiaryContainerLight = Color(0xFFF57F17) // Dark yellow 16 | val errorLight = Color(0xFFD32F2F) // Vivid red 17 | val onErrorLight = Color(0xFFFFFFFF) // White for contrast 18 | val errorContainerLight = Color(0xFFFFCDD2) // Light red background 19 | val onErrorContainerLight = Color(0xFFB00020) // Dark red 20 | val backgroundLight = Color(0xFFFFFFFF) // Pure white 21 | val onBackgroundLight = Color(0xFF000000) // Black for readability 22 | val surfaceLight = Color(0xFFFFFFFF) // Pure white 23 | val onSurfaceLight = Color(0xFF000000) // Black for readability 24 | val surfaceVariantLight = Color(0xFFF5F5F5) // Very light gray 25 | val onSurfaceVariantLight = Color(0xFF757575) // Medium gray 26 | val outlineLight = Color(0xFFBDBDBD) // Light gray 27 | val outlineVariantLight = Color(0xFFE0E0E0) // Very light gray 28 | val scrimLight = Color(0xFF000000) // Black for overlays 29 | val inverseSurfaceLight = Color(0xFF121212) // Very dark gray 30 | val inverseOnSurfaceLight = Color(0xFFFFFFFF) // White for contrast 31 | val inversePrimaryLight = Color(0xFF81C784) // Match primaryLight 32 | val surfaceDimLight = Color(0xFFF5F5F5) // Very light gray 33 | val surfaceBrightLight = Color(0xFFFFFFFF) // Pure white 34 | val surfaceContainerLowestLight = Color(0xFFFFFFFF) // Pure white 35 | val surfaceContainerLowLight = Color(0xFFF5F5F5) // Very light gray 36 | val surfaceContainerLight = Color(0xFFFAFAFA) // Slightly darker light gray 37 | val surfaceContainerHighLight = Color(0xFFF0F0F0) // Light gray 38 | val surfaceContainerHighestLight = Color(0xFFE0E0E0) // Light gray 39 | 40 | 41 | val primaryDark = Color(0xFFB9F6CA) // Soft mint green 42 | val onPrimaryDark = Color(0xFF004D40) // Dark teal 43 | val primaryContainerDark = Color(0xFF004D40) // Medium teal 44 | val onPrimaryContainerDark = Color(0xFFE0F2F1) // Light teal background 45 | val secondaryDark = Color(0xFFBBDEFB) // Light sky blue 46 | val onSecondaryDark = Color(0xFF0D47A1) // Dark blue 47 | val secondaryContainerDark = Color(0xFF1E88E5) // Medium blue 48 | val onSecondaryContainerDark = Color(0xFFE3F2FD) // Light blue background 49 | val tertiaryDark = Color(0xFFFFF59D) // Soft yellow 50 | val onTertiaryDark = Color(0xFFF57F17) // Dark yellow 51 | val tertiaryContainerDark = Color(0xFFFFE082) // Medium yellow 52 | val onTertiaryContainerDark = Color(0xFFFFF8E1) // Light yellow background 53 | val errorDark = Color(0xFFFF8A80) // Light red 54 | val onErrorDark = Color(0xFFD32F2F) // Vivid red 55 | val errorContainerDark = Color(0xFFB00020) // Dark red 56 | val onErrorContainerDark = Color(0xFFFFCDD2) // Light red background 57 | val backgroundDark = Color(0xFF121212) // Very dark gray 58 | val onBackgroundDark = Color(0xFFE0E0E0) // Light gray for readability 59 | val surfaceDark = Color(0xFF121212) // Very dark gray 60 | val onSurfaceDark = Color(0xFFE0E0E0) // Light gray for readability 61 | val surfaceVariantDark = Color(0xFF2C2C2C) // Dark gray 62 | val onSurfaceVariantDark = Color(0xFFBDBDBD) // Light gray 63 | val outlineDark = Color(0xFF8A8A8A) // Medium gray 64 | val outlineVariantDark = Color(0xFF2C2C2C) // Dark gray 65 | val scrimDark = Color(0xFF000000) // Black for overlays 66 | val inverseSurfaceDark = Color(0xFFE0E0E0) // Light gray 67 | val inverseOnSurfaceDark = Color(0xFF121212) // Very dark gray 68 | val inversePrimaryDark = Color(0xFFB9F6CA) // Match primaryDark 69 | val surfaceDimDark = Color(0xFF121212) // Very dark gray 70 | val surfaceBrightDark = Color(0xFF1E1E1E) // Dark gray 71 | val surfaceContainerLowestDark = Color(0xFF0A0A0A) // Nearly black 72 | val surfaceContainerLowDark = Color(0xFF1A1A1A) // Dark gray 73 | val surfaceContainerDark = Color(0xFF222222) // Slightly lighter dark gray 74 | val surfaceContainerHighDark = Color(0xFF2C2C2C) // Dark gray 75 | val surfaceContainerHighestDark = Color(0xFF333333) // Lightest dark gray -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/theme/Schemes.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.theme 2 | 3 | import androidx.compose.material3.darkColorScheme 4 | import androidx.compose.material3.lightColorScheme 5 | 6 | val lightScheme = lightColorScheme( 7 | primary = primaryLight, 8 | onPrimary = surfaceDimLight, 9 | primaryContainer = primaryContainerLight, 10 | onPrimaryContainer = onPrimaryContainerLight, 11 | secondary = secondaryLight, 12 | onSecondary = onSecondaryLight, 13 | secondaryContainer = secondaryContainerLight, 14 | onSecondaryContainer = onSecondaryContainerLight, 15 | tertiary = tertiaryLight, 16 | onTertiary = onTertiaryLight, 17 | tertiaryContainer = tertiaryContainerLight, 18 | onTertiaryContainer = onTertiaryContainerLight, 19 | error = errorLight, 20 | onError = onErrorLight, 21 | errorContainer = errorContainerLight, 22 | onErrorContainer = onErrorContainerLight, 23 | background = backgroundLight, 24 | onBackground = onBackgroundLight, 25 | surface = surfaceLight, 26 | onSurface = onSurfaceLight, 27 | surfaceVariant = surfaceVariantLight, 28 | onSurfaceVariant = onSurfaceVariantLight, 29 | outline = outlineLight, 30 | outlineVariant = outlineVariantLight, 31 | scrim = scrimLight, 32 | inverseSurface = inverseSurfaceLight, 33 | inverseOnSurface = inverseOnSurfaceLight, 34 | inversePrimary = inversePrimaryLight, 35 | surfaceDim = surfaceDimLight, 36 | surfaceBright = surfaceBrightLight, 37 | surfaceContainerLowest = surfaceContainerLowestLight, 38 | surfaceContainerLow = surfaceContainerLowLight, 39 | surfaceContainer = surfaceContainerLight, 40 | surfaceContainerHigh = surfaceContainerHighLight, 41 | surfaceContainerHighest = surfaceContainerHighestLight, 42 | ) 43 | 44 | val darkScheme = darkColorScheme( 45 | primary = primaryDark, 46 | onPrimary = onPrimaryDark, 47 | primaryContainer = primaryContainerDark, 48 | onPrimaryContainer = onPrimaryContainerDark, 49 | secondary = secondaryDark, 50 | onSecondary = onSecondaryDark, 51 | secondaryContainer = secondaryContainerDark, 52 | onSecondaryContainer = onSecondaryContainerDark, 53 | tertiary = tertiaryDark, 54 | onTertiary = onTertiaryDark, 55 | tertiaryContainer = tertiaryContainerDark, 56 | onTertiaryContainer = onTertiaryContainerDark, 57 | error = errorDark, 58 | onError = onErrorDark, 59 | errorContainer = errorContainerDark, 60 | onErrorContainer = onErrorContainerDark, 61 | background = backgroundDark, 62 | onBackground = onBackgroundDark, 63 | surface = surfaceDark, 64 | onSurface = onSurfaceDark, 65 | surfaceVariant = surfaceVariantDark, 66 | onSurfaceVariant = onSurfaceVariantDark, 67 | outline = outlineDark, 68 | outlineVariant = outlineVariantDark, 69 | scrim = scrimDark, 70 | inverseSurface = inverseSurfaceDark, 71 | inverseOnSurface = inverseOnSurfaceDark, 72 | inversePrimary = inversePrimaryDark, 73 | surfaceDim = surfaceDimDark, 74 | surfaceBright = surfaceBrightDark, 75 | surfaceContainerLowest = surfaceContainerLowestDark, 76 | surfaceContainerLow = surfaceContainerLowDark, 77 | surfaceContainer = surfaceContainerDark, 78 | surfaceContainerHigh = surfaceContainerHighDark, 79 | surfaceContainerHighest = surfaceContainerHighestDark, 80 | ) 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/presentation/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.presentation.theme 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.os.Build 6 | import android.view.WindowManager 7 | import androidx.compose.foundation.isSystemInDarkTheme 8 | import androidx.compose.material3.ColorScheme 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Typography 11 | import androidx.compose.material3.dynamicDarkColorScheme 12 | import androidx.compose.material3.dynamicLightColorScheme 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.compose.ui.platform.LocalView 17 | import androidx.core.view.WindowCompat 18 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 19 | 20 | private fun getColorScheme(context: Context, isDarkTheme: Boolean, isDynamicTheme: Boolean, isAmoledTheme: Boolean): ColorScheme { 21 | if (isDynamicTheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 22 | if (isDarkTheme) { 23 | if (isAmoledTheme) { 24 | return dynamicDarkColorScheme(context).copy(surfaceContainerLow = Color.Black, surface = Color.Black) 25 | } 26 | 27 | return dynamicDarkColorScheme(context) 28 | } else { 29 | return dynamicLightColorScheme(context) 30 | } 31 | } else if (isDarkTheme) { 32 | if (isAmoledTheme) { 33 | return darkScheme.copy(surfaceContainerLow = Color.Black, surface = Color.Black) 34 | } 35 | 36 | return darkScheme 37 | } else { 38 | return lightScheme 39 | } 40 | } 41 | 42 | @Composable 43 | fun LeafNotesTheme( 44 | settingsModel: SettingsViewModel, 45 | content: @Composable () -> Unit 46 | ) { 47 | if (settingsModel.settings.value.automaticTheme) { 48 | settingsModel.update(settingsModel.settings.value.copy(dynamicTheme = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)) 49 | settingsModel.update(settingsModel.settings.value.copy(darkTheme = isSystemInDarkTheme())) 50 | } 51 | 52 | val context = LocalContext.current 53 | val activity = LocalView.current.context as Activity 54 | WindowCompat.getInsetsController(activity.window, activity.window.decorView).apply { 55 | isAppearanceLightStatusBars = !settingsModel.settings.value.darkTheme 56 | } 57 | if (settingsModel.settings.value.screenProtection) { 58 | activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) 59 | } else { 60 | activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) 61 | } 62 | 63 | MaterialTheme( 64 | colorScheme = getColorScheme(context, settingsModel.settings.value.darkTheme, settingsModel.settings.value.dynamicTheme, settingsModel.settings.value.amoledTheme), 65 | typography = Typography(), 66 | content = content 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/widget/AddNoteReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.widget 2 | 3 | import androidx.glance.appwidget.GlanceAppWidget 4 | import androidx.glance.appwidget.GlanceAppWidgetReceiver 5 | 6 | class AddNoteReceiver : GlanceAppWidgetReceiver() { 7 | override val glanceAppWidget: GlanceAppWidget = AddNoteWidget() 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/widget/AddNoteWidget.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.widget 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.compose.ui.unit.dp 6 | import androidx.compose.ui.unit.sp 7 | import androidx.glance.GlanceId 8 | import androidx.glance.GlanceModifier 9 | import androidx.glance.GlanceTheme 10 | import androidx.glance.action.clickable 11 | import androidx.glance.appwidget.GlanceAppWidget 12 | import androidx.glance.appwidget.cornerRadius 13 | import androidx.glance.appwidget.provideContent 14 | import androidx.glance.background 15 | import androidx.glance.layout.Alignment 16 | import androidx.glance.layout.Row 17 | import androidx.glance.layout.fillMaxSize 18 | import androidx.glance.layout.height 19 | import androidx.glance.layout.width 20 | import androidx.glance.text.Text 21 | import androidx.glance.text.TextStyle 22 | import com.ezpnix.writeon.R 23 | import com.ezpnix.writeon.presentation.MainActivity 24 | 25 | class AddNoteWidget : GlanceAppWidget() { 26 | override suspend fun provideGlance(context: Context, id: GlanceId) { 27 | provideContent { 28 | GlanceTheme { 29 | val intent = Intent(context, MainActivity::class.java).apply { 30 | putExtra("noteId", 0) 31 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 32 | } 33 | Row( 34 | modifier = GlanceModifier 35 | .fillMaxSize() 36 | .height(950.dp) 37 | .width(100.dp) 38 | .background(GlanceTheme.colors.primary) 39 | .clickable { context.startActivity(intent) } 40 | .cornerRadius(90.dp), 41 | verticalAlignment = Alignment.CenterVertically, 42 | horizontalAlignment = Alignment.CenterHorizontally 43 | ) { 44 | Text( 45 | text = context.getString(R.string.new_note), 46 | style = TextStyle( 47 | fontSize = 16.sp, 48 | color = GlanceTheme.colors.background 49 | ), 50 | ) 51 | } 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/widget/NotesWidget.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.widget 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.glance.GlanceId 6 | import androidx.glance.GlanceTheme 7 | import androidx.glance.appwidget.GlanceAppWidget 8 | import androidx.glance.appwidget.GlanceAppWidgetManager 9 | import androidx.glance.appwidget.provideContent 10 | import androidx.glance.currentState 11 | import androidx.glance.state.GlanceStateDefinition 12 | import com.ezpnix.writeon.domain.usecase.NoteUseCase 13 | import com.ezpnix.writeon.widget.ui.SelectedNote 14 | import com.ezpnix.writeon.widget.ui.ZeroState 15 | import dagger.hilt.EntryPoint 16 | import dagger.hilt.EntryPoints 17 | import dagger.hilt.InstallIn 18 | import dagger.hilt.components.SingletonComponent 19 | import java.io.File 20 | 21 | @EntryPoint 22 | @InstallIn(SingletonComponent::class) 23 | interface WidgetModelRepositoryEntryPoint { 24 | fun noteUseCase() : NoteUseCase 25 | } 26 | 27 | class NotesWidget : GlanceAppWidget() { 28 | override val stateDefinition: GlanceStateDefinition>> 29 | get() = object: GlanceStateDefinition>> { 30 | override suspend fun getDataStore( 31 | context: Context, 32 | fileKey: String 33 | ): DataStore>> { 34 | return NotesDataStore(context) 35 | } 36 | override fun getLocation(context: Context, fileKey: String): File { 37 | throw NotImplementedError("Not implemented") 38 | } 39 | } 40 | 41 | override suspend fun provideGlance(context: Context, id: GlanceId) { 42 | val noteUseCase = getNoteUseCase(context) 43 | val widgetId = GlanceAppWidgetManager(context).getAppWidgetId(id) 44 | 45 | provideContent { 46 | GlanceTheme { 47 | currentState>>().firstOrNull { it.first == widgetId }?.second.let { noteId -> 48 | noteUseCase.observe() 49 | val selectedNote = noteUseCase.notes.filter { it.id == noteId } 50 | when { 51 | selectedNote.isEmpty() -> ZeroState(widgetId = widgetId) 52 | else -> SelectedNote(selectedNote.first(), noteUseCase, widgetId = widgetId) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | fun getNoteUseCase(applicationContext: Context): NoteUseCase { 61 | var widgetModelRepositoryEntrypoint: WidgetModelRepositoryEntryPoint = EntryPoints.get( 62 | applicationContext, 63 | WidgetModelRepositoryEntryPoint::class.java, 64 | ) 65 | return widgetModelRepositoryEntrypoint.noteUseCase() 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/widget/NotesWidgetActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.widget 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.activity.enableEdgeToEdge 9 | import androidx.compose.ui.res.stringResource 10 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 11 | import androidx.glance.appwidget.updateAll 12 | import androidx.hilt.navigation.compose.hiltViewModel 13 | import com.ezpnix.writeon.R 14 | import com.ezpnix.writeon.data.repository.SettingsRepositoryImpl 15 | import com.ezpnix.writeon.domain.usecase.NoteUseCase 16 | import com.ezpnix.writeon.presentation.components.NotesScaffold 17 | import com.ezpnix.writeon.presentation.screens.home.getContainerColor 18 | import com.ezpnix.writeon.presentation.screens.home.sorter 19 | import com.ezpnix.writeon.presentation.screens.home.widgets.NoteFilter 20 | import com.ezpnix.writeon.presentation.screens.settings.TopBar 21 | import com.ezpnix.writeon.presentation.screens.settings.model.SettingsViewModel 22 | import com.ezpnix.writeon.presentation.screens.settings.settings.shapeManager 23 | import com.ezpnix.writeon.presentation.theme.LeafNotesTheme 24 | import dagger.hilt.android.AndroidEntryPoint 25 | import kotlinx.coroutines.runBlocking 26 | import javax.inject.Inject 27 | 28 | @AndroidEntryPoint 29 | class NotesWidgetActivity : ComponentActivity() { 30 | @Inject 31 | lateinit var noteUseCase: NoteUseCase 32 | 33 | @Inject 34 | lateinit var settingsRepository: SettingsRepositoryImpl 35 | 36 | override fun onCreate(savedInstanceState: Bundle?) { 37 | super.onCreate(savedInstanceState) 38 | installSplashScreen() 39 | enableEdgeToEdge() 40 | 41 | val appWidgetId = intent?.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID,) ?: AppWidgetManager.INVALID_APPWIDGET_ID 42 | setContent { 43 | val settings = hiltViewModel() 44 | noteUseCase.observe() 45 | 46 | LeafNotesTheme(settingsModel = settings) { 47 | NotesScaffold( 48 | topBar = { 49 | TopBar( 50 | title = stringResource(id = R.string.select_note), 51 | onBackNavClicked = { finish() } 52 | ) 53 | }, 54 | content = { 55 | NoteFilter( 56 | settingsViewModel = settings, 57 | containerColor = getContainerColor(settings), 58 | shape = shapeManager(radius = settings.settings.value.cornerRadius / 2, isBoth = true), 59 | onNoteClicked = { id -> 60 | runBlocking { 61 | settingsRepository.putInt("${NotesWidgetReceiver.WIDGET_PREFERENCE}${appWidgetId}", id) 62 | NotesWidget().updateAll(this@NotesWidgetActivity) 63 | val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) 64 | setResult(RESULT_OK, resultValue) 65 | finish() 66 | } 67 | }, 68 | notes = noteUseCase.notes.sortedWith(sorter(settings.settings.value.sortDescending)), 69 | viewMode = false, 70 | ) 71 | } 72 | ) 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/widget/NotesWidgetReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.widget 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.glance.appwidget.GlanceAppWidget 6 | import androidx.glance.appwidget.GlanceAppWidgetReceiver 7 | import com.ezpnix.writeon.data.repository.SettingsRepositoryImpl 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.flow 10 | 11 | class NotesWidgetReceiver : GlanceAppWidgetReceiver() { 12 | override val glanceAppWidget: GlanceAppWidget = NotesWidget() 13 | 14 | companion object { 15 | const val WIDGET_PREFERENCE = "widgetNote_" 16 | } 17 | } 18 | 19 | 20 | class NotesDataStore(private val context: Context): DataStore>> { 21 | override val data: Flow>> 22 | get() { 23 | val settingsRepository = SettingsRepositoryImpl(context) 24 | return flow { emit(settingsRepository.getEveryNotesWidget()) } 25 | } 26 | 27 | override suspend fun updateData(transform: suspend (t: List>) -> List>): List> { 28 | throw NotImplementedError("Not implemented") 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/widget/ui/Note.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.widget.ui 2 | 3 | import android.content.Intent 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.unit.dp 6 | import androidx.compose.ui.unit.sp 7 | import androidx.glance.GlanceModifier 8 | import androidx.glance.GlanceTheme 9 | import androidx.glance.LocalContext 10 | import androidx.glance.action.clickable 11 | import androidx.glance.background 12 | import androidx.glance.layout.Column 13 | import androidx.glance.layout.fillMaxSize 14 | import androidx.glance.layout.fillMaxWidth 15 | import androidx.glance.layout.padding 16 | import androidx.glance.text.FontWeight 17 | import com.ezpnix.writeon.domain.model.Note 18 | import com.ezpnix.writeon.domain.usecase.NoteUseCase 19 | import com.ezpnix.writeon.presentation.MainActivity 20 | import com.ezpnix.writeon.presentation.components.markdown.WidgetText 21 | import kotlinx.coroutines.CoroutineScope 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.launch 24 | 25 | @Composable 26 | fun SelectedNote(note: Note, noteUseCase: NoteUseCase, widgetId: Int) { 27 | val context = LocalContext.current 28 | val glanceModifier = GlanceModifier.clickable { 29 | val intent = Intent(context, MainActivity::class.java).apply { 30 | putExtra("noteId", note.id) 31 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 32 | } 33 | context.startActivity(intent) 34 | } 35 | 36 | Column( 37 | modifier = glanceModifier 38 | .background(GlanceTheme.colors.background) 39 | .fillMaxSize() 40 | .padding(6.dp) 41 | ) { 42 | if(note.name.isNotBlank()) { 43 | WidgetText( 44 | modifier = glanceModifier, 45 | markdown = note.name, 46 | weight = FontWeight.Bold, 47 | fontSize = 24.sp, 48 | color = GlanceTheme.colors.primary, 49 | onContentChange = { 50 | CoroutineScope(Dispatchers.IO).launch { 51 | noteUseCase.addNote(note.copy(name = it)) 52 | noteUseCase.observe() 53 | } 54 | } 55 | ) 56 | } 57 | if(note.description.isNotBlank()) { 58 | WidgetText( 59 | modifier = glanceModifier.fillMaxWidth(), 60 | markdown = note.description, 61 | weight = FontWeight.Normal, 62 | fontSize = 12.sp, 63 | color = GlanceTheme.colors.primary, 64 | onContentChange = { 65 | CoroutineScope(Dispatchers.IO).launch { 66 | noteUseCase.addNote(note.copy(description = it)) 67 | noteUseCase.observe() 68 | } 69 | } 70 | ) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/ezpnix/writeon/widget/ui/ZeroState.kt: -------------------------------------------------------------------------------- 1 | package com.ezpnix.writeon.widget.ui 2 | 3 | import android.appwidget.AppWidgetManager 4 | import androidx.compose.runtime.Composable 5 | import androidx.glance.Button 6 | import androidx.glance.GlanceModifier 7 | import androidx.glance.GlanceTheme 8 | import androidx.glance.LocalContext 9 | import androidx.glance.action.ActionParameters 10 | import androidx.glance.action.actionParametersOf 11 | import androidx.glance.action.actionStartActivity 12 | import androidx.glance.background 13 | import androidx.glance.layout.Alignment 14 | import androidx.glance.layout.Box 15 | import androidx.glance.layout.fillMaxSize 16 | import com.ezpnix.writeon.R 17 | import com.ezpnix.writeon.widget.NotesWidgetActivity 18 | 19 | @Composable 20 | fun ZeroState(widgetId: Int) { 21 | val widgetIdKey = ActionParameters.Key(AppWidgetManager.EXTRA_APPWIDGET_ID) 22 | val context = LocalContext.current 23 | 24 | Box( 25 | modifier = GlanceModifier 26 | .background(GlanceTheme.colors.background) 27 | .fillMaxSize(), 28 | contentAlignment = Alignment.Center) { 29 | Button( 30 | text = context.getString(R.string.select_note), 31 | onClick = actionStartActivity( 32 | parameters = actionParametersOf(widgetIdKey to widgetId), 33 | ), 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/widget_preview_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/drawable-nodpi/widget_preview_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/widget_preview_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/drawable-nodpi/widget_preview_2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 13 | 15 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 31 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 73 | 75 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/incognito_fill.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-v31/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #010101 4 | @android:color/system_accent1_200 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-v31/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #010101 4 | #fddbd6 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 150.dp 4 | 75.dp 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2F2D2D 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/add_note.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/locales_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/xml/notes.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /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.hilt) apply false 4 | alias(libs.plugins.android.application) apply false 5 | alias(libs.plugins.jetbrains.kotlin.android) apply false 6 | alias(libs.plugins.ksp) apply false 7 | alias(libs.plugins.compose.compiler) apply false 8 | } 9 | 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. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-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 24 | org.gradle.configuration-cache=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.5.2" 3 | datastorePreferences = "1.1.1" 4 | glance = "1.1.0" 5 | hilt = "2.52" 6 | hiltCompiler = "1.2.0" 7 | hiltAndroidCompiler = "2.52" 8 | coilCompose = "2.7.0" 9 | hiltNavigationCompose = "1.2.0" 10 | kotlin = "2.0.0" 11 | ksp = "2.0.0-1.0.24" 12 | room = "2.6.1" 13 | activityCompose = "1.9.1" 14 | coreKtx = "1.13.1" 15 | coreSplashscreen = "1.0.1" 16 | compose = "1.6.8" 17 | material3 = "1.2.1" 18 | navigationCompose = "2.7.7" 19 | appcompat = "1.7.0" 20 | glanceAppwidget = "1.1.0" 21 | mkcompose = "1.3.0" 22 | mkcalendar = "1.2.1" 23 | messageBar = "1.0.5" 24 | biometricktx = "1.2.0-alpha05" 25 | runtimeKtx = "2.9.1" 26 | preferenceKtx = "1.2.1" 27 | gson = "2.10.1" 28 | 29 | [libraries] 30 | # Room 31 | androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } 32 | androidx-glance = { module = "androidx.glance:glance", version.ref = "glance" } 33 | androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } 34 | androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } 35 | androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } 36 | 37 | # AndroidX 38 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } 39 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } 40 | androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } 41 | 42 | # Compose 43 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } 44 | androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "compose" } 45 | androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } 46 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } 47 | 48 | #Hilt 49 | hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroidCompiler" } 50 | hilt-compile = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltCompiler"} 51 | hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" } 52 | hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } 53 | 54 | # Coil 55 | coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } 56 | 57 | # Glance 58 | androidx-glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glanceAppwidget" } 59 | 60 | # Calendar 61 | compose-calendar = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "calendar", version.ref = "mkcalendar" } 62 | compose-core = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "core", version.ref = "mkcompose" } 63 | 64 | # MessageBar 65 | message-bar = { group = "com.stevdza-san", name = "messagebarkmp", version.ref = "messageBar" } 66 | 67 | # FingerPrint 68 | androidx-biometric-ktx = { group = "androidx.biometric", name = "biometric", version.ref = "biometricktx"} 69 | 70 | #AutomaticBackup 71 | automatic-backup = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "runtimeKtx"} 72 | androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" } 73 | 74 | #Gson 75 | gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } 76 | 77 | [plugins] 78 | hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } 79 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 80 | android-application = { id = "com.android.application", version.ref = "agp" } 81 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 82 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 83 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d -------------------------------------------------------------------------------- /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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/1: -------------------------------------------------------------------------------- 1 | • Released First App 2 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/2: -------------------------------------------------------------------------------- 1 | • Bug fixes 2 | • Typos fixed 3 | • Optimizations 4 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/3: -------------------------------------------------------------------------------- 1 | • More markdown formats 2 | • Quick note export to txt 3 | • Updated auto-backup logic 4 | • Direct to translate button 5 | • Dropdown share button 6 | • Fixed partial image bug 7 | • Updated user interface 8 | • Internet search button 9 | • Updated resource strings 10 | • More optimizations -------------------------------------------------------------------------------- /metadata/en-US/changelogs/4: -------------------------------------------------------------------------------- 1 | • Updated home user interface 2 | • Searchbar placeholder feature 3 | • Fixed custom size dimensions 4 | • Directly calculate within the app 5 | • Ability to change font size 6 | • Added more featured buttons 7 | • Calendar date issue fixed 8 | • Renamed some strings 9 | • Squished some bugs -------------------------------------------------------------------------------- /metadata/en-US/changelogs/5: -------------------------------------------------------------------------------- 1 | • Updated home user interface 2 | • Fixed some underlying issues with the edit model and view model 3 | • Centered home screen buttons have been replaced with a set of row icon buttons 4 | • Added Help & Feedback section for all questions and answers 5 | • Pin/unpin status changes can now be saved independently 6 | • Calculator parenthesis typo issue has been fixed 7 | • Settings screen has now two new section content 8 | • Added app stability for custom dpi dimensions 9 | • Modified note preview screen user interface 10 | • Revamped the alert dialog logic pop back -------------------------------------------------------------------------------- /metadata/en-US/changelogs/6: -------------------------------------------------------------------------------- 1 | • Homescreen buttons to surface lazyrow 2 | • Removed savenote else function issue 3 | • Modified edit buttons to bottom modal 4 | • Calculator manual edits now functional 5 | • Replaced ann-section with sources 6 | • Results are now visible down below 7 | • Adjusted position for most of the buttons 8 | • Fixed the crashing issue from the previous version -------------------------------------------------------------------------------- /metadata/en-US/changelogs/7: -------------------------------------------------------------------------------- 1 | • New homepage user interface 2 | • More visibility on your notes 3 | • FAB has returned but cleaner now 4 | • Added new Flashcard screen feature 5 | • Column view count is now adjustable 6 | • Improvements to Scratchpad screen 7 | • Quick shortcut button redirects to Styles 8 | • Home greeting placeholder repositioned 9 | • Issue feedback now on the settings screen 10 | • Feedback screen cards now fully clickable 11 | • Added some minor animation improvements 12 | • Get info on your device found in about 13 | • Some icons were replaced to be cleaner 14 | • Bug fixes and optimizations -------------------------------------------------------------------------------- /metadata/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | A simple note-taking app with a Material You design to create a visually comforting interface and distraction-free environment. With a focus on simplicity and usability, offering several useful features. 2 | 3 | 🔐 Key features include: 4 | 5 | ★ Biometric Authentication ★ Backup/Restore ★ Screen Protection ★ Markdown Support ★ Customizable Themes ★ Offline Functionality ★ Data Privacy ★ No Unnecessary Permissions ★ Intuitive UI ★ Built-In Calendar ★ Quick note export to txt ★ Direct to translate button ★ Dropdown share button ★ Internet search button ★ Attach Images ★ Calculator ★ 6 | 7 | Whether you're jotting down quick thoughts or crafting detailed notes. Write On is the perfect companion for staying organized and manageable. Attach photos, codes, words, quotes, works, anything that your brain needs to write on. 8 | 9 | Open-Source Code: https://github.com/3zpnix/WriteOn 10 | -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/metadata/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/metadata/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/metadata/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3zpnix/WriteOn/3360c6c095bd61fd6a31fa025c84ce0f462bb000/metadata/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /metadata/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | A clean function-based intuitive note-taking app with Material You design. 2 | -------------------------------------------------------------------------------- /metadata/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Write On: Simple Notepad 2 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | 13 | } 14 | } 15 | dependencyResolutionManagement { 16 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 17 | repositories { 18 | google() 19 | mavenCentral() 20 | maven(url = "https://jitpack.io") 21 | } 22 | } 23 | 24 | rootProject.name = "WriteOn" 25 | include(":app") 26 | --------------------------------------------------------------------------------