├── .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 |

3 |
4 | # Write On: Simple Notepad ✅
5 | A clean, intuitive note-taking app with *Material You* design — open source and privacy-respecting.
6 |
7 | [

](https://github.com/3zpnix/WriteOn/releases)
8 |
9 | [

](https://f-droid.org/en/packages/com.ezpnix.writeon/)
10 |
11 |
12 | ---
13 |
14 |
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 |
5 |
6 |
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 |
5 |
6 |
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 |
--------------------------------------------------------------------------------