mirror of
https://github.com/PhilKes/NotallyX.git
synced 2025-06-29 20:59:54 +00:00
Compare commits
150 commits
Author | SHA1 | Date | |
---|---|---|---|
|
d00300fa0e | ||
|
f13e8227ca | ||
|
11e472cd30 | ||
|
4cc957ccd4 | ||
|
86b74762c5 | ||
|
118285545a | ||
|
402baf8056 | ||
|
de27d40880 | ||
|
66ce623e85 | ||
|
3c2400c7e6 | ||
|
c64a7b2ed7 | ||
|
fb687856f1 | ||
|
23d678c8a3 | ||
|
ade08b52ed | ||
|
62a35132e0 | ||
|
9fbe5a6b94 | ||
|
cf7f6f9dda | ||
|
01ac48f930 | ||
|
015f43e94b | ||
|
830fb6a75c | ||
|
c34ee3633e | ||
|
628bd9d564 | ||
|
3e889879fb | ||
|
b191618a46 | ||
|
5cbc62bdf7 | ||
|
d1e5770180 | ||
|
29cee8faf4 | ||
|
4f993af93f | ||
|
0f0eb80e9b | ||
|
1314ab4437 | ||
|
39022edfab | ||
|
157ecb1b13 | ||
|
fb35ffdac4 | ||
|
0fee25f022 | ||
|
2341c30586 | ||
|
e553e78efb | ||
|
724d08507a | ||
|
771546a0cb | ||
|
1a6d4083e4 | ||
|
3ac63349d8 | ||
|
06c48ab8d9 | ||
|
8d20f26eae | ||
|
66f0d33cd4 | ||
|
6d81b6f7c0 | ||
|
2c1f5d5338 | ||
|
adb981d76c | ||
|
73a0345fe4 | ||
|
0407f2bdc4 | ||
|
212c354072 | ||
|
07ff5691e2 | ||
|
20cf84ab69 | ||
|
6bfa013a6c | ||
|
66a7b02c69 | ||
|
62035091f5 | ||
|
209e19d690 | ||
|
899f2c0f9a | ||
|
3a0b699c82 | ||
|
18947835f1 | ||
|
5c0ea100ee | ||
|
392a060329 | ||
|
1aa5e2c9e7 | ||
|
0d86d7aad8 | ||
|
772a43de31 | ||
|
ad9b410d45 | ||
|
6cf2a8ce1f | ||
|
c33b639a07 | ||
|
61112a18d2 | ||
|
30f889f3db | ||
|
b95782e53a | ||
|
19df4b817f | ||
|
333b57c29d | ||
|
87124c32f4 | ||
|
f4a7074811 | ||
|
1f6afb03d4 | ||
|
56683d5255 | ||
|
e24f630acf | ||
|
6b3fec40eb | ||
|
205116ac60 | ||
|
ffeecdf1ca | ||
|
8d6b318e3b | ||
|
8d477e4366 | ||
|
79eacb079d | ||
|
c122c2cc48 | ||
|
f97f99ded0 | ||
|
506bc5c362 | ||
|
8784145b83 | ||
|
7b1aa83fca | ||
|
f9ea26f1fa | ||
|
54d835e40b | ||
|
9fd735ba95 | ||
|
b5e13ce73a | ||
|
c586eab072 | ||
|
860db3e6bb | ||
|
883998e27f | ||
|
4924ee46ec | ||
|
ad5ad25e11 | ||
|
93119098bc | ||
|
764c562859 | ||
|
b1bf6bc12c | ||
|
44d19341b1 | ||
|
be734e080c | ||
|
8a4e2f9a92 | ||
|
48c07dfe0f | ||
|
e09b0f44b7 | ||
|
155d4c1cd9 | ||
|
8e3110f077 | ||
|
17a3eda124 | ||
|
7edbcccbe4 | ||
|
f4bfa7ccb8 | ||
|
ae8bc8e859 | ||
|
8af22e1e88 | ||
|
a57628dc7a | ||
|
d239f20e6f | ||
|
2ddfd5adb9 | ||
|
b7b0b48c62 | ||
|
cc64bad689 | ||
|
f4de4133ed | ||
|
21707dfe08 | ||
|
d2ba38a20e | ||
|
fe6d6eca8b | ||
|
c5cfa7a6c9 | ||
|
5711830b0f | ||
|
dae19f07e0 | ||
|
36f3cc284f | ||
|
f38f813af6 | ||
|
6b80b37714 | ||
|
6c78204111 | ||
|
900defa670 | ||
|
a925712e1f | ||
|
11d1d1fcc5 | ||
|
67e2a35c8f | ||
|
11eeb0d5bc | ||
|
259e223637 | ||
|
d7ad549878 | ||
|
86998a84a0 | ||
|
ac2b87bba1 | ||
|
13e7b5ac1e | ||
|
5ac794885f | ||
|
320c9048a5 | ||
|
0cb3fa92df | ||
|
2bd5c575fe | ||
|
dbe1b0726b | ||
|
dad74ace96 | ||
|
467f877dcc | ||
|
3e4acd9355 | ||
|
715d1aba1d | ||
|
000bfe7466 | ||
|
b0d3cde257 | ||
|
d330f93f00 | ||
|
f64267c226 |
151 changed files with 301107 additions and 1024 deletions
56
.github/workflows/deploy.yaml
vendored
Normal file
56
.github/workflows/deploy.yaml
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- documentation/**
|
||||
# Review gh actions docs if you want to further define triggers, paths, etc
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docusaurus
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache-dependency-path: documentation/yarn.lock
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: documentation
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build website
|
||||
working-directory: documentation
|
||||
run: yarn build
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: documentation/build
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
needs: build
|
||||
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
|
||||
# Deploy to the github-pages environment
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
82
CHANGELOG.md
82
CHANGELOG.md
|
@ -1,5 +1,87 @@
|
|||
# Changelog
|
||||
|
||||
## [v7.4.0](https://github.com/PhilKes/NotallyX/tree/v7.4.0) (2025-04-18)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.3.1...v7.4.0)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Don't force capitalization when adding a label [\#532](https://github.com/PhilKes/NotallyX/issues/532)
|
||||
- Add a screen protection against screenshot attempts [\#386](https://github.com/PhilKes/NotallyX/issues/386)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Share pure text note error [\#544](https://github.com/PhilKes/NotallyX/issues/544)
|
||||
- Crash when deleting checked items in a list [\#539](https://github.com/PhilKes/NotallyX/issues/539)
|
||||
- Keyboard don't open after closing it on Android 7 [\#537](https://github.com/PhilKes/NotallyX/issues/537)
|
||||
- Unable to open links before changing view mode [\#527](https://github.com/PhilKes/NotallyX/issues/527)
|
||||
- Reminder popup cut on small screens [\#522](https://github.com/PhilKes/NotallyX/issues/522)
|
||||
- Auto Backup failed [\#514](https://github.com/PhilKes/NotallyX/issues/514)
|
||||
|
||||
## [v7.3.1](https://github.com/PhilKes/NotallyX/tree/v7.3.1) (2025-04-08)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.3.0...v7.3.1)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Button to close note search doesn't work [\#519](https://github.com/PhilKes/NotallyX/issues/519)
|
||||
- app crashes when pressing label [\#517](https://github.com/PhilKes/NotallyX/issues/517)
|
||||
|
||||
## [v7.3.0](https://github.com/PhilKes/NotallyX/tree/v7.3.0) (2025-04-07)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.2.1...v7.3.0)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Persist viewMode of each note individually [\#497](https://github.com/PhilKes/NotallyX/issues/497)
|
||||
- Read-only mode by default and new notes [\#495](https://github.com/PhilKes/NotallyX/issues/495)
|
||||
- Hide notes based on labels [\#401](https://github.com/PhilKes/NotallyX/issues/401)
|
||||
- An archived note should be visible in its label's folder. [\#398](https://github.com/PhilKes/NotallyX/issues/398)
|
||||
- Sharing notes from the app [\#380](https://github.com/PhilKes/NotallyX/issues/380)
|
||||
- Add support for json notes import [\#377](https://github.com/PhilKes/NotallyX/issues/377)
|
||||
- Sharing images to the app [\#281](https://github.com/PhilKes/NotallyX/issues/281)
|
||||
- Strikethrough checked items lists [\#250](https://github.com/PhilKes/NotallyX/issues/250)
|
||||
- Click on list element to check it [\#248](https://github.com/PhilKes/NotallyX/issues/248)
|
||||
- Add long press actions to undo/redo buttons [\#244](https://github.com/PhilKes/NotallyX/issues/244)
|
||||
- Convert Note \<=\> List [\#190](https://github.com/PhilKes/NotallyX/issues/190)
|
||||
- Edit labels inside notes [\#180](https://github.com/PhilKes/NotallyX/issues/180)
|
||||
- Support Wallpaper color themes [\#175](https://github.com/PhilKes/NotallyX/issues/175)
|
||||
- View Mode [\#76](https://github.com/PhilKes/NotallyX/issues/76)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Android 7.0 Navigation bar color issue [\#515](https://github.com/PhilKes/NotallyX/issues/515)
|
||||
- Search Mode loop for Android \< 9.0 [\#508](https://github.com/PhilKes/NotallyX/issues/508)
|
||||
- BaseNote.viewMode database migration error [\#505](https://github.com/PhilKes/NotallyX/issues/505)
|
||||
- New list items can't be added with linebreak/enter [\#496](https://github.com/PhilKes/NotallyX/issues/496)
|
||||
- Undo changes more than the last changed character [\#472](https://github.com/PhilKes/NotallyX/issues/472)
|
||||
- Auto Backup failed [\#468](https://github.com/PhilKes/NotallyX/issues/468)
|
||||
- Amount of backups to keep in periodic backups are not respected if nextcloud mount is used. [\#133](https://github.com/PhilKes/NotallyX/issues/133)
|
||||
|
||||
## [v7.2.1](https://github.com/PhilKes/NotallyX/tree/v7.2.1) (2025-03-18)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.2.0...v7.2.1)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Note not automatically saved when App is killed by system [\#446](https://github.com/PhilKes/NotallyX/issues/446)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Auto Backup failed [\#456](https://github.com/PhilKes/NotallyX/issues/456)
|
||||
|
||||
## [v7.2.1](https://github.com/PhilKes/NotallyX/tree/v7.2.1) (2025-03-18)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.2.0...v7.2.1)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Note not automatically saved when App is killed by system [\#446](https://github.com/PhilKes/NotallyX/issues/446)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Auto Backup failed [\#456](https://github.com/PhilKes/NotallyX/issues/456)
|
||||
|
||||
## [v7.2.0](https://github.com/PhilKes/NotallyX/tree/v7.2.0) (2025-03-08)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.1.0...v7.2.0)
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
<b>NotallyX | Minimalistic note taking app</b>
|
||||
<p>
|
||||
<center>
|
||||
<a href='https://play.google.com/store/apps/details?id=com.philkes.notallyx&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' height='80'/></a>
|
||||
<a href="https://f-droid.org/en/packages/com.philkes.notallyx"><img alt='IzzyOnDroid' height='80' src='https://fdroid.gitlab.io/artwork/badge/get-it-on.png' /></a>
|
||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.philkes.notallyx"><img alt='F-Droid' height='80' src='https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png' /></a>
|
||||
<a href="https://github.com/PhilKes/NotallyX/issues/120"><img alt="JoinTesters" height="80" src="fastlane/join-testers.png" /></a>
|
||||
</center>
|
||||
</p>
|
||||
</h2>
|
||||
|
|
|
@ -190,7 +190,7 @@ dependencies {
|
|||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
implementation("androidx.sqlite:sqlite-ktx:2.4.0")
|
||||
implementation("androidx.work:work-runtime:2.9.1")
|
||||
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
implementation("cat.ereza:customactivityoncrash:2.4.0")
|
||||
implementation("com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0")
|
||||
implementation("com.github.bumptech.glide:glide:4.15.1")
|
||||
|
|
271494
app/obfuscation/mapping.txt
Normal file
271494
app/obfuscation/mapping.txt
Normal file
File diff suppressed because it is too large
Load diff
12
app/proguard-rules.pro
vendored
12
app/proguard-rules.pro
vendored
|
@ -11,14 +11,12 @@
|
|||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
-keepattributes LineNumberTable,SourceFile
|
||||
-renamesourcefileattribute SourceFile
|
||||
-dontobfuscate
|
||||
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable
|
||||
-printmapping obfuscation/mapping.txt
|
||||
|
||||
-keep class ** extends androidx.navigation.Navigator
|
||||
-keep class ** implements org.ocpsoft.prettytime.TimeUnit
|
||||
|
||||
|
|
164
app/schemas/com.philkes.notallyx.data.NotallyDatabase/9.json
Normal file
164
app/schemas/com.philkes.notallyx.data.NotallyDatabase/9.json
Normal file
|
@ -0,0 +1,164 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 9,
|
||||
"identityHash": "042b20b5b4cfc8415e6cf6348196e869",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "BaseNote",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `folder` TEXT NOT NULL, `color` TEXT NOT NULL, `title` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `modifiedTimestamp` INTEGER NOT NULL, `labels` TEXT NOT NULL, `body` TEXT NOT NULL, `spans` TEXT NOT NULL, `items` TEXT NOT NULL, `images` TEXT NOT NULL, `files` TEXT NOT NULL, `audios` TEXT NOT NULL, `reminders` TEXT NOT NULL, `viewMode` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "folder",
|
||||
"columnName": "folder",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "modifiedTimestamp",
|
||||
"columnName": "modifiedTimestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "labels",
|
||||
"columnName": "labels",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "body",
|
||||
"columnName": "body",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spans",
|
||||
"columnName": "spans",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "items",
|
||||
"columnName": "items",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "images",
|
||||
"columnName": "images",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "files",
|
||||
"columnName": "files",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "audios",
|
||||
"columnName": "audios",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "reminders",
|
||||
"columnName": "reminders",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "viewMode",
|
||||
"columnName": "viewMode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_BaseNote_id_folder_pinned_timestamp_labels",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"folder",
|
||||
"pinned",
|
||||
"timestamp",
|
||||
"labels"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_BaseNote_id_folder_pinned_timestamp_labels` ON `${TABLE_NAME}` (`id`, `folder`, `pinned`, `timestamp`, `labels`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Label",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, PRIMARY KEY(`value`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"value"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '042b20b5b4cfc8415e6cf6348196e869')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -6,18 +6,15 @@
|
|||
<uses-permission
|
||||
android:name="android.permission.ACCESS_NETWORK_STATE"
|
||||
tools:node="remove" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
||||
<application
|
||||
|
@ -71,9 +68,30 @@
|
|||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="file" android:mimeType="text/*" />
|
||||
<data android:scheme="content" android:mimeType="text/*" />
|
||||
|
||||
<data android:scheme="file" android:mimeType="application/json" />
|
||||
<data android:scheme="content" android:mimeType="application/json" />
|
||||
|
||||
<data android:scheme="file" android:mimeType="application/xml" />
|
||||
<data android:scheme="content" android:mimeType="application/xml" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".presentation.activity.note.ViewImageActivity" />
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
package com.philkes.notallyx
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.philkes.notallyx.presentation.setEnabledSecureFlag
|
||||
import com.philkes.notallyx.presentation.view.misc.NotNullLiveData
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.BiometricLock
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences
|
||||
|
@ -32,7 +36,7 @@ import kotlinx.coroutines.MainScope
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class NotallyXApplication : Application() {
|
||||
class NotallyXApplication : Application(), Application.ActivityLifecycleCallbacks {
|
||||
|
||||
private lateinit var biometricLockObserver: Observer<BiometricLock>
|
||||
private lateinit var preferences: NotallyXPreferences
|
||||
|
@ -42,10 +46,16 @@ class NotallyXApplication : Application() {
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
registerActivityLifecycleCallbacks(this)
|
||||
if (isTestRunner()) return
|
||||
|
||||
preferences = NotallyXPreferences.getInstance(this)
|
||||
if (preferences.useDynamicColors.value) {
|
||||
if (DynamicColors.isDynamicColorAvailable()) {
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
} else {
|
||||
setTheme(R.style.AppTheme)
|
||||
}
|
||||
preferences.theme.observeForeverWithPrevious { (oldTheme, theme) ->
|
||||
when (theme) {
|
||||
Theme.DARK ->
|
||||
|
@ -60,7 +70,7 @@ class NotallyXApplication : Application() {
|
|||
)
|
||||
}
|
||||
if (oldTheme != null) {
|
||||
WidgetProvider.updateWidgets(this)
|
||||
WidgetProvider.updateWidgets(this, locked = locked.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,4 +172,20 @@ class NotallyXApplication : Application() {
|
|||
return Build.FINGERPRINT.equals("robolectric", ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
activity.setEnabledSecureFlag(preferences.secureFlag.value)
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import com.philkes.notallyx.data.model.BaseNote
|
|||
import com.philkes.notallyx.data.model.Color
|
||||
import com.philkes.notallyx.data.model.Converters
|
||||
import com.philkes.notallyx.data.model.Label
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.data.model.toColorString
|
||||
import com.philkes.notallyx.presentation.view.misc.NotNullLiveData
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.BiometricLock
|
||||
|
@ -32,7 +33,7 @@ import net.sqlcipher.database.SQLiteDatabase
|
|||
import net.sqlcipher.database.SupportFactory
|
||||
|
||||
@TypeConverters(Converters::class)
|
||||
@Database(entities = [BaseNote::class, Label::class], version = 8)
|
||||
@Database(entities = [BaseNote::class, Label::class], version = 9)
|
||||
abstract class NotallyDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun getLabelDao(): LabelDao
|
||||
|
@ -131,6 +132,7 @@ abstract class NotallyDatabase : RoomDatabase() {
|
|||
Migration6,
|
||||
Migration7,
|
||||
Migration8,
|
||||
Migration9,
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
SQLiteDatabase.loadLibs(context)
|
||||
|
@ -254,17 +256,21 @@ abstract class NotallyDatabase : RoomDatabase() {
|
|||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow("id"))
|
||||
val colorString = cursor.getString(cursor.getColumnIndexOrThrow("color"))
|
||||
val color =
|
||||
try {
|
||||
Color.valueOf(colorString)
|
||||
} catch (e: Exception) {
|
||||
Color.DEFAULT
|
||||
}
|
||||
val color = Color.valueOfOrDefault(colorString)
|
||||
val hexColor = color.toColorString()
|
||||
db.execSQL("UPDATE BaseNote SET color = ? WHERE id = ?", arrayOf(hexColor, id))
|
||||
}
|
||||
cursor.close()
|
||||
}
|
||||
}
|
||||
|
||||
object Migration9 : Migration(8, 9) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"ALTER TABLE `BaseNote` ADD COLUMN `viewMode` TEXT NOT NULL DEFAULT '${NoteViewMode.EDIT.name}'"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ interface BaseNoteDao {
|
|||
@Query("SELECT id, reminders FROM BaseNote WHERE reminders IS NOT NULL AND reminders != '[]'")
|
||||
suspend fun getAllReminders(): List<NoteIdReminder>
|
||||
|
||||
@Query("SELECT color FROM BaseNote WHERE id = :id ") fun getColorOfNote(id: Long): String
|
||||
@Query("SELECT color FROM BaseNote WHERE id = :id ") fun getColorOfNote(id: Long): String?
|
||||
|
||||
@Query(
|
||||
"SELECT id, title, type, reminders FROM BaseNote WHERE reminders IS NOT NULL AND reminders != '[]'"
|
||||
|
@ -163,14 +163,14 @@ interface BaseNoteDao {
|
|||
* directly on the LiveData to filter the results accordingly.
|
||||
*/
|
||||
fun getBaseNotesByLabel(label: String): LiveData<List<BaseNote>> {
|
||||
val result = getBaseNotesByLabel(label, Folder.NOTES)
|
||||
val result = getBaseNotesByLabel(label, setOf(Folder.NOTES, Folder.ARCHIVED))
|
||||
return result.map { list -> list.filter { baseNote -> baseNote.labels.contains(label) } }
|
||||
}
|
||||
|
||||
@Query(
|
||||
"SELECT * FROM BaseNote WHERE folder = :folder AND labels LIKE '%' || :label || '%' ORDER BY pinned DESC, timestamp DESC"
|
||||
"SELECT * FROM BaseNote WHERE folder IN (:folders) AND labels LIKE '%' || :label || '%' ORDER BY folder DESC, pinned DESC, timestamp DESC"
|
||||
)
|
||||
fun getBaseNotesByLabel(label: String, folder: Folder): LiveData<List<BaseNote>>
|
||||
fun getBaseNotesByLabel(label: String, folders: Collection<Folder>): LiveData<List<BaseNote>>
|
||||
|
||||
@Query(
|
||||
"SELECT * FROM BaseNote WHERE folder = :folder AND labels == '[]' ORDER BY pinned DESC, timestamp DESC"
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.philkes.notallyx.R
|
|||
import com.philkes.notallyx.data.NotallyDatabase
|
||||
import com.philkes.notallyx.data.imports.evernote.EvernoteImporter
|
||||
import com.philkes.notallyx.data.imports.google.GoogleKeepImporter
|
||||
import com.philkes.notallyx.data.imports.txt.JsonImporter
|
||||
import com.philkes.notallyx.data.imports.txt.PlainTextImporter
|
||||
import com.philkes.notallyx.data.model.Audio
|
||||
import com.philkes.notallyx.data.model.FileAttachment
|
||||
|
@ -39,6 +40,7 @@ class NotesImporter(private val app: Application, private val database: NotallyD
|
|||
ImportSource.GOOGLE_KEEP -> GoogleKeepImporter()
|
||||
ImportSource.EVERNOTE -> EvernoteImporter()
|
||||
ImportSource.PLAIN_TEXT -> PlainTextImporter()
|
||||
ImportSource.JSON -> JsonImporter()
|
||||
}.import(app, uri, tempDir, progress)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "import: failed", e)
|
||||
|
@ -153,6 +155,13 @@ enum class ImportSource(
|
|||
null,
|
||||
R.drawable.text_file,
|
||||
),
|
||||
JSON(
|
||||
R.string.json_files,
|
||||
FOLDER_OR_FILE_MIMETYPE,
|
||||
R.string.json_files_help,
|
||||
null,
|
||||
R.drawable.file_json,
|
||||
),
|
||||
}
|
||||
|
||||
const val FOLDER_OR_FILE_MIMETYPE = "FOLDER_OR_FILE"
|
||||
|
|
|
@ -17,6 +17,7 @@ import com.philkes.notallyx.data.model.BaseNote
|
|||
import com.philkes.notallyx.data.model.FileAttachment
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.utils.log
|
||||
import com.philkes.notallyx.utils.startsWithAnyOf
|
||||
|
@ -155,6 +156,7 @@ fun EvernoteNote.mapToBaseNote(): BaseNote {
|
|||
files = files,
|
||||
audios = audios,
|
||||
reminders = mutableListOf(),
|
||||
NoteViewMode.EDIT,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import com.philkes.notallyx.data.model.BaseNote
|
|||
import com.philkes.notallyx.data.model.FileAttachment
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.utils.listFilesRecursive
|
||||
import com.philkes.notallyx.utils.log
|
||||
|
@ -162,6 +163,7 @@ class GoogleKeepImporter : ExternalImporter {
|
|||
files = files,
|
||||
audios = audios,
|
||||
reminders = mutableListOf(),
|
||||
NoteViewMode.EDIT,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package com.philkes.notallyx.data.imports.txt
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.philkes.notallyx.data.imports.ExternalImporter
|
||||
import com.philkes.notallyx.data.imports.ImportProgress
|
||||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import com.philkes.notallyx.data.model.toBaseNote
|
||||
import com.philkes.notallyx.utils.MIME_TYPE_JSON
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
|
||||
class JsonImporter : ExternalImporter {
|
||||
|
||||
override fun import(
|
||||
app: Application,
|
||||
source: Uri,
|
||||
destination: File,
|
||||
progress: MutableLiveData<ImportProgress>?,
|
||||
): Pair<List<BaseNote>, File?> {
|
||||
val notes = mutableListOf<BaseNote>()
|
||||
fun readJsonFiles(file: DocumentFile) {
|
||||
when {
|
||||
file.isDirectory -> {
|
||||
file.listFiles().forEach { readJsonFiles(it) }
|
||||
}
|
||||
file.isFile -> {
|
||||
if (file.type != MIME_TYPE_JSON) {
|
||||
return
|
||||
}
|
||||
val fileNameWithoutExtension = file.name?.substringBeforeLast(".") ?: ""
|
||||
val content =
|
||||
app.contentResolver.openInputStream(file.uri)?.use { inputStream ->
|
||||
BufferedReader(InputStreamReader(inputStream)).use { reader ->
|
||||
reader.readText()
|
||||
}
|
||||
} ?: ""
|
||||
notes.add(content.toBaseNote().copy(id = 0L, title = fileNameWithoutExtension))
|
||||
}
|
||||
}
|
||||
}
|
||||
val file =
|
||||
if (source.pathSegments.firstOrNull() == "tree") {
|
||||
DocumentFile.fromTreeUri(app, source)
|
||||
} else DocumentFile.fromSingleUri(app, source)
|
||||
file?.let { readJsonFiles(it) }
|
||||
return Pair(notes, null)
|
||||
}
|
||||
}
|
|
@ -9,11 +9,11 @@ import com.philkes.notallyx.data.imports.ImportProgress
|
|||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.utils.MIME_TYPE_JSON
|
||||
import java.io.BufferedReader
|
||||
import com.philkes.notallyx.utils.readFileContents
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
|
||||
class PlainTextImporter : ExternalImporter {
|
||||
|
||||
|
@ -35,12 +35,7 @@ class PlainTextImporter : ExternalImporter {
|
|||
return
|
||||
}
|
||||
val fileNameWithoutExtension = file.name?.substringBeforeLast(".") ?: ""
|
||||
var content =
|
||||
app.contentResolver.openInputStream(file.uri)?.use { inputStream ->
|
||||
BufferedReader(InputStreamReader(inputStream)).use { reader ->
|
||||
reader.readText()
|
||||
}
|
||||
} ?: ""
|
||||
var content = app.contentResolver.readFileContents(file.uri)
|
||||
val listItems = mutableListOf<ListItem>()
|
||||
content.findListSyntaxRegex()?.let { listSyntaxRegex ->
|
||||
listItems.addAll(content.extractListItems(listSyntaxRegex))
|
||||
|
@ -65,6 +60,7 @@ class PlainTextImporter : ExternalImporter {
|
|||
files = listOf(),
|
||||
audios = listOf(),
|
||||
reminders = listOf(),
|
||||
NoteViewMode.EDIT,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,12 +25,59 @@ data class BaseNote(
|
|||
val files: List<FileAttachment>,
|
||||
val audios: List<Audio>,
|
||||
val reminders: List<Reminder>,
|
||||
val viewMode: NoteViewMode,
|
||||
) : Item {
|
||||
|
||||
companion object {
|
||||
const val COLOR_DEFAULT = "DEFAULT"
|
||||
const val COLOR_NEW = "NEW"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BaseNote
|
||||
|
||||
if (id != other.id) return false
|
||||
if (type != other.type) return false
|
||||
if (folder != other.folder) return false
|
||||
if (color != other.color) return false
|
||||
if (title != other.title) return false
|
||||
if (pinned != other.pinned) return false
|
||||
if (timestamp != other.timestamp) return false
|
||||
if (labels != other.labels) return false
|
||||
if (body != other.body) return false
|
||||
if (spans != other.spans) return false
|
||||
if (items != other.items) return false
|
||||
if (images != other.images) return false
|
||||
if (files != other.files) return false
|
||||
if (audios != other.audios) return false
|
||||
if (reminders != other.reminders) return false
|
||||
if (viewMode != other.viewMode) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + type.hashCode()
|
||||
result = 31 * result + folder.hashCode()
|
||||
result = 31 * result + color.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + pinned.hashCode()
|
||||
result = 31 * result + timestamp.hashCode()
|
||||
result = 31 * result + labels.hashCode()
|
||||
result = 31 * result + body.hashCode()
|
||||
result = 31 * result + spans.hashCode()
|
||||
result = 31 * result + items.hashCode()
|
||||
result = 31 * result + images.hashCode()
|
||||
result = 31 * result + files.hashCode()
|
||||
result = 31 * result + audios.hashCode()
|
||||
result = 31 * result + reminders.hashCode()
|
||||
result = 31 * result + viewMode.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseNote.deepCopy(): BaseNote {
|
||||
|
|
|
@ -16,6 +16,13 @@ enum class Color {
|
|||
|
||||
companion object {
|
||||
fun allColorStrings() = entries.map { it.toColorString() }.toList()
|
||||
|
||||
fun valueOfOrDefault(value: String) =
|
||||
try {
|
||||
Color.valueOf(value)
|
||||
} catch (e: Exception) {
|
||||
DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,9 @@ object Converters {
|
|||
|
||||
@TypeConverter fun labelsToJson(labels: List<String>) = JSONArray(labels).toString()
|
||||
|
||||
@TypeConverter fun jsonToLabels(json: String) = JSONArray(json).iterable<String>().toList()
|
||||
@TypeConverter fun jsonToLabels(json: String) = jsonToLabels(JSONArray(json))
|
||||
|
||||
fun jsonToLabels(jsonArray: JSONArray) = jsonArray.iterable<String>().toList()
|
||||
|
||||
@TypeConverter
|
||||
fun filesToJson(files: List<FileAttachment>): String {
|
||||
|
@ -24,10 +26,10 @@ object Converters {
|
|||
return JSONArray(objects).toString()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToFiles(json: String): List<FileAttachment> {
|
||||
val iterable = JSONArray(json).iterable<JSONObject>()
|
||||
return iterable.map { jsonObject ->
|
||||
@TypeConverter fun jsonToFiles(json: String) = jsonToFiles(JSONArray(json))
|
||||
|
||||
fun jsonToFiles(jsonArray: JSONArray): List<FileAttachment> {
|
||||
return jsonArray.iterable<JSONObject>().map { jsonObject ->
|
||||
val localName = getSafeLocalName(jsonObject)
|
||||
val originalName = getSafeOriginalName(jsonObject)
|
||||
val mimeType = jsonObject.getString("mimeType")
|
||||
|
@ -47,10 +49,10 @@ object Converters {
|
|||
return JSONArray(objects).toString()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAudios(json: String): List<Audio> {
|
||||
val iterable = JSONArray(json).iterable<JSONObject>()
|
||||
return iterable.map { jsonObject ->
|
||||
@TypeConverter fun jsonToAudios(json: String) = jsonToAudios(JSONArray(json))
|
||||
|
||||
fun jsonToAudios(json: JSONArray): List<Audio> {
|
||||
return json.iterable<JSONObject>().map { jsonObject ->
|
||||
val name = jsonObject.getString("name")
|
||||
val duration = jsonObject.getSafeLong("duration")
|
||||
val timestamp = jsonObject.getLong("timestamp")
|
||||
|
@ -58,31 +60,63 @@ object Converters {
|
|||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToSpans(json: String): List<SpanRepresentation> {
|
||||
val iterable = JSONArray(json).iterable<JSONObject>()
|
||||
return iterable.map { jsonObject ->
|
||||
val bold = jsonObject.getSafeBoolean("bold")
|
||||
val link = jsonObject.getSafeBoolean("link")
|
||||
val linkData = jsonObject.getSafeString("linkData")
|
||||
val italic = jsonObject.getSafeBoolean("italic")
|
||||
val monospace = jsonObject.getSafeBoolean("monospace")
|
||||
val strikethrough = jsonObject.getSafeBoolean("strikethrough")
|
||||
val start = jsonObject.getInt("start")
|
||||
val end = jsonObject.getInt("end")
|
||||
SpanRepresentation(start, end, bold, link, linkData, italic, monospace, strikethrough)
|
||||
}
|
||||
@TypeConverter fun jsonToSpans(json: String) = jsonToSpans(JSONArray(json))
|
||||
|
||||
fun jsonToSpans(jsonArray: JSONArray): List<SpanRepresentation> {
|
||||
return jsonArray
|
||||
.iterable<JSONObject>()
|
||||
.map { jsonObject ->
|
||||
val bold = jsonObject.getSafeBoolean("bold")
|
||||
val link = jsonObject.getSafeBoolean("link")
|
||||
val linkData = jsonObject.getSafeString("linkData")
|
||||
val italic = jsonObject.getSafeBoolean("italic")
|
||||
val monospace = jsonObject.getSafeBoolean("monospace")
|
||||
val strikethrough = jsonObject.getSafeBoolean("strikethrough")
|
||||
try {
|
||||
val start = jsonObject.getInt("start")
|
||||
val end = jsonObject.getInt("end")
|
||||
SpanRepresentation(
|
||||
start,
|
||||
end,
|
||||
bold,
|
||||
link,
|
||||
linkData,
|
||||
italic,
|
||||
monospace,
|
||||
strikethrough,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun spansToJson(list: List<SpanRepresentation>) = spansToJSONArray(list).toString()
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToItems(json: String): List<ListItem> {
|
||||
val iterable = JSONArray(json).iterable<JSONObject>()
|
||||
return iterable.map { jsonObject ->
|
||||
val body = jsonObject.getString("body")
|
||||
val checked = jsonObject.getBoolean("checked")
|
||||
fun spansToJSONArray(list: List<SpanRepresentation>): JSONArray {
|
||||
val objects =
|
||||
list.map { representation ->
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("bold", representation.bold)
|
||||
jsonObject.put("link", representation.link)
|
||||
jsonObject.put("linkData", representation.linkData)
|
||||
jsonObject.put("italic", representation.italic)
|
||||
jsonObject.put("monospace", representation.monospace)
|
||||
jsonObject.put("strikethrough", representation.strikethrough)
|
||||
jsonObject.put("start", representation.start)
|
||||
jsonObject.put("end", representation.end)
|
||||
}
|
||||
return JSONArray(objects)
|
||||
}
|
||||
|
||||
@TypeConverter fun jsonToItems(json: String) = jsonToItems(JSONArray(json))
|
||||
|
||||
fun jsonToItems(json: JSONArray): List<ListItem> {
|
||||
return json.iterable<JSONObject>().map { jsonObject ->
|
||||
val body = jsonObject.getSafeString("body") ?: ""
|
||||
val checked = jsonObject.getSafeBoolean("checked")
|
||||
val isChild = jsonObject.getSafeBoolean("isChild")
|
||||
val order = jsonObject.getSafeInt("order")
|
||||
ListItem(body, checked, isChild, order, mutableListOf())
|
||||
|
@ -103,39 +137,25 @@ object Converters {
|
|||
return JSONArray(objects)
|
||||
}
|
||||
|
||||
fun spansToJSONArray(list: List<SpanRepresentation>): JSONArray {
|
||||
val objects =
|
||||
list.map { representation ->
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("bold", representation.bold)
|
||||
jsonObject.put("link", representation.link)
|
||||
jsonObject.put("linkData", representation.linkData)
|
||||
jsonObject.put("italic", representation.italic)
|
||||
jsonObject.put("monospace", representation.monospace)
|
||||
jsonObject.put("strikethrough", representation.strikethrough)
|
||||
jsonObject.put("start", representation.start)
|
||||
jsonObject.put("end", representation.end)
|
||||
}
|
||||
return JSONArray(objects)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun remindersToJson(reminders: List<Reminder>): String {
|
||||
fun remindersToJson(reminders: List<Reminder>) = remindersToJSONArray(reminders).toString()
|
||||
|
||||
fun remindersToJSONArray(reminders: List<Reminder>): JSONArray {
|
||||
val objects =
|
||||
reminders.map { reminder ->
|
||||
JSONObject().apply {
|
||||
put("id", reminder.id) // Store date as long timestamp
|
||||
put("dateTime", reminder.dateTime.time) // Store date as long timestamp
|
||||
put("repetition", reminder.repetition?.let { repetitionToJson(it) })
|
||||
put("repetition", reminder.repetition?.let { repetitionToJsonObject(it) })
|
||||
}
|
||||
}
|
||||
return JSONArray(objects).toString()
|
||||
return JSONArray(objects)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToReminders(json: String): List<Reminder> {
|
||||
val iterable = JSONArray(json).iterable<JSONObject>()
|
||||
return iterable.map { jsonObject ->
|
||||
@TypeConverter fun jsonToReminders(json: String) = jsonToReminders(JSONArray(json))
|
||||
|
||||
fun jsonToReminders(jsonArray: JSONArray): List<Reminder> {
|
||||
return jsonArray.iterable<JSONObject>().map { jsonObject ->
|
||||
val id = jsonObject.getLong("id")
|
||||
val dateTime = Date(jsonObject.getLong("dateTime"))
|
||||
val repetition = jsonObject.getSafeString("repetition")?.let { jsonToRepetition(it) }
|
||||
|
@ -145,10 +165,14 @@ object Converters {
|
|||
|
||||
@TypeConverter
|
||||
fun repetitionToJson(repetition: Repetition): String {
|
||||
return repetitionToJsonObject(repetition).toString()
|
||||
}
|
||||
|
||||
fun repetitionToJsonObject(repetition: Repetition): JSONObject {
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("value", repetition.value)
|
||||
jsonObject.put("unit", repetition.unit.name) // Store the TimeUnit as a string
|
||||
return jsonObject.toString()
|
||||
return jsonObject
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
|
|
|
@ -5,5 +5,14 @@ import java.io.Serializable
|
|||
enum class Folder : Serializable {
|
||||
NOTES,
|
||||
DELETED,
|
||||
ARCHIVED,
|
||||
ARCHIVED;
|
||||
|
||||
companion object {
|
||||
fun valueOfOrDefault(value: String) =
|
||||
try {
|
||||
valueOf(value)
|
||||
} catch (e: Exception) {
|
||||
NOTES
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,8 +34,7 @@ data class ListItem(
|
|||
if (other !is ListItem) {
|
||||
return false
|
||||
}
|
||||
return (this.id == other.id &&
|
||||
this.body == other.body &&
|
||||
return (this.body == other.body &&
|
||||
this.order == other.order &&
|
||||
this.checked == other.checked &&
|
||||
this.isChild == other.isChild)
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.text.Html
|
|||
import androidx.core.text.toHtml
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.dao.NoteIdReminder
|
||||
import com.philkes.notallyx.data.model.BaseNote.Companion.COLOR_DEFAULT
|
||||
import com.philkes.notallyx.presentation.applySpans
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
|
@ -12,6 +13,7 @@ import java.util.Calendar
|
|||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val NOTE_URL_PREFIX = "note://"
|
||||
|
@ -41,7 +43,15 @@ fun String.getNoteTypeFromUrl(): Type {
|
|||
|
||||
val FileAttachment.isImage: Boolean
|
||||
get() {
|
||||
return mimeType.startsWith("image/")
|
||||
return mimeType.isImageMimeType
|
||||
}
|
||||
val String.isImageMimeType: Boolean
|
||||
get() {
|
||||
return startsWith("image/")
|
||||
}
|
||||
val String.isAudioMimeType: Boolean
|
||||
get() {
|
||||
return startsWith("audio/")
|
||||
}
|
||||
|
||||
fun BaseNote.toTxt(includeTitle: Boolean = true, includeCreationDate: Boolean = true) =
|
||||
|
@ -70,7 +80,8 @@ fun BaseNote.toJson(): String {
|
|||
.put("color", color)
|
||||
.put("title", title)
|
||||
.put("pinned", pinned)
|
||||
.put("date-created", timestamp)
|
||||
.put("timestamp", timestamp)
|
||||
.put("modifiedTimestamp", modifiedTimestamp)
|
||||
.put("labels", JSONArray(labels))
|
||||
|
||||
when (type) {
|
||||
|
@ -83,10 +94,85 @@ fun BaseNote.toJson(): String {
|
|||
jsonObject.put("items", Converters.itemsToJSONArray(items))
|
||||
}
|
||||
}
|
||||
|
||||
jsonObject.put("reminders", Converters.remindersToJSONArray(reminders))
|
||||
jsonObject.put("viewMode", viewMode.name)
|
||||
return jsonObject.toString(2)
|
||||
}
|
||||
|
||||
fun String.toBaseNote(): BaseNote {
|
||||
val jsonObject = JSONObject(this)
|
||||
val id = jsonObject.getLongOrDefault("id", -1L)
|
||||
val type = Type.valueOfOrDefault(jsonObject.getStringOrDefault("type", Type.NOTE.name))
|
||||
val folder = Folder.valueOfOrDefault(jsonObject.getStringOrDefault("folder", Folder.NOTES.name))
|
||||
val color =
|
||||
jsonObject.getStringOrDefault("color", COLOR_DEFAULT).takeIf { it.isValid() }
|
||||
?: COLOR_DEFAULT
|
||||
val title = jsonObject.getStringOrDefault("title", "")
|
||||
val pinned = jsonObject.getBooleanOrDefault("pinned", false)
|
||||
val timestamp = jsonObject.getLongOrDefault("timestamp", System.currentTimeMillis())
|
||||
val modifiedTimestamp = jsonObject.getLongOrDefault("modifiedTimestamp", timestamp)
|
||||
val labels = Converters.jsonToLabels(jsonObject.getArrayOrEmpty("labels"))
|
||||
val body = jsonObject.getStringOrDefault("body", "")
|
||||
val spans = Converters.jsonToSpans(jsonObject.getArrayOrEmpty("spans"))
|
||||
val items = Converters.jsonToItems(jsonObject.getArrayOrEmpty("items"))
|
||||
val images = Converters.jsonToFiles(jsonObject.getArrayOrEmpty("images"))
|
||||
val files = Converters.jsonToFiles(jsonObject.getArrayOrEmpty("files"))
|
||||
val audios = Converters.jsonToAudios(jsonObject.getArrayOrEmpty("audios"))
|
||||
val reminders = Converters.jsonToReminders(jsonObject.getArrayOrEmpty("reminders"))
|
||||
val viewMode = NoteViewMode.valueOfOrDefault(jsonObject.getStringOrDefault("viewMode", ""))
|
||||
return BaseNote(
|
||||
id,
|
||||
type,
|
||||
folder,
|
||||
color,
|
||||
title,
|
||||
pinned,
|
||||
timestamp,
|
||||
modifiedTimestamp,
|
||||
labels,
|
||||
body,
|
||||
spans,
|
||||
items,
|
||||
images,
|
||||
files,
|
||||
audios,
|
||||
reminders,
|
||||
viewMode,
|
||||
)
|
||||
}
|
||||
|
||||
private fun JSONObject.getStringOrDefault(key: String, defaultValue: String): String {
|
||||
return try {
|
||||
getString(key)
|
||||
} catch (exception: JSONException) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.getArrayOrEmpty(key: String): JSONArray {
|
||||
return try {
|
||||
getJSONArray(key)
|
||||
} catch (exception: JSONException) {
|
||||
JSONArray("[]")
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.getBooleanOrDefault(key: String, defaultValue: Boolean): Boolean {
|
||||
return try {
|
||||
getBoolean(key)
|
||||
} catch (exception: JSONException) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.getLongOrDefault(key: String, defaultValue: Long): Long {
|
||||
return try {
|
||||
getLong(key)
|
||||
} catch (exception: JSONException) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseNote.toHtml(showDateCreated: Boolean) = buildString {
|
||||
val date = DateFormat.getDateInstance(DateFormat.FULL).format(timestamp)
|
||||
val title = Html.escapeHtml(title)
|
||||
|
@ -222,3 +308,15 @@ fun List<ListItem>.toText() = buildString {
|
|||
}
|
||||
|
||||
fun Collection<ListItem>.deepCopy() = map { it.copy(children = mutableListOf()) }
|
||||
|
||||
fun ColorString.isValid() =
|
||||
when (this) {
|
||||
COLOR_DEFAULT -> true
|
||||
else ->
|
||||
try {
|
||||
android.graphics.Color.parseColor(this)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.philkes.notallyx.data.model
|
||||
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.StaticTextProvider
|
||||
|
||||
enum class NoteViewMode(override val textResId: Int) : StaticTextProvider {
|
||||
READ_ONLY(R.string.read_only),
|
||||
EDIT(R.string.edit);
|
||||
|
||||
companion object {
|
||||
fun valueOfOrDefault(value: String) =
|
||||
try {
|
||||
NoteViewMode.valueOf(value)
|
||||
} catch (e: Exception) {
|
||||
EDIT
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,5 +2,14 @@ package com.philkes.notallyx.data.model
|
|||
|
||||
enum class Type {
|
||||
NOTE,
|
||||
LIST,
|
||||
LIST;
|
||||
|
||||
companion object {
|
||||
fun valueOfOrDefault(value: String) =
|
||||
try {
|
||||
Type.valueOf(value)
|
||||
} catch (e: Exception) {
|
||||
NOTE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import android.text.TextWatcher
|
|||
import android.text.style.CharacterStyle
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.SuggestionSpan
|
||||
import android.text.style.TypefaceSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.util.TypedValue
|
||||
|
@ -55,13 +56,16 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.marginTop
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
|
@ -87,12 +91,16 @@ import com.philkes.notallyx.data.imports.ImportStage
|
|||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.SpanRepresentation
|
||||
import com.philkes.notallyx.databinding.DialogInputBinding
|
||||
import com.philkes.notallyx.databinding.DialogProgressBinding
|
||||
import com.philkes.notallyx.databinding.LabelBinding
|
||||
import com.philkes.notallyx.presentation.activity.main.MainActivity
|
||||
import com.philkes.notallyx.presentation.view.misc.NotNullLiveData
|
||||
import com.philkes.notallyx.presentation.view.misc.Progress
|
||||
import com.philkes.notallyx.presentation.view.misc.StylableEditTextWithHistory
|
||||
import com.philkes.notallyx.presentation.view.note.listitem.ListManager
|
||||
import com.philkes.notallyx.presentation.view.note.listitem.adapter.ListItemVH
|
||||
import com.philkes.notallyx.presentation.viewmodel.BaseNoteModel
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.DateFormat
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.TextSize
|
||||
import com.philkes.notallyx.utils.changehistory.ChangeHistory
|
||||
|
@ -114,7 +122,7 @@ fun String.applySpans(representations: List<SpanRepresentation>): Editable {
|
|||
->
|
||||
try {
|
||||
if (bold) {
|
||||
editable.setSpan(StyleSpan(Typeface.BOLD), start, end)
|
||||
editable.setSpan(createBoldSpan(), start, end)
|
||||
}
|
||||
if (italic) {
|
||||
editable.setSpan(StyleSpan(Typeface.ITALIC), start, end)
|
||||
|
@ -136,6 +144,13 @@ fun String.applySpans(representations: List<SpanRepresentation>): Editable {
|
|||
return editable
|
||||
}
|
||||
|
||||
fun createBoldSpan() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
StyleSpan(Typeface.BOLD, 700)
|
||||
} else {
|
||||
StyleSpan(Typeface.BOLD)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts or removes spans based on the selection range.
|
||||
*
|
||||
|
@ -226,14 +241,23 @@ fun ViewGroup.addIconButton(
|
|||
title: Int,
|
||||
drawable: Int,
|
||||
marginStart: Int = 10,
|
||||
onClick: ((item: View) -> Unit)? = null,
|
||||
): View {
|
||||
onLongClick: View.OnLongClickListener? = null,
|
||||
onClick: View.OnClickListener? = null,
|
||||
): ImageButton {
|
||||
val view =
|
||||
ImageButton(ContextThemeWrapper(context, R.style.AppTheme)).apply {
|
||||
setImageResource(drawable)
|
||||
contentDescription = context.getString(title)
|
||||
setBackgroundResource(R.color.Transparent)
|
||||
val titleText = context.getString(title)
|
||||
contentDescription = titleText
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
tooltipText = titleText
|
||||
}
|
||||
val outValue = TypedValue()
|
||||
context.theme.resolveAttribute(android.R.attr.actionBarItemBackground, outValue, true)
|
||||
setBackgroundResource(outValue.resourceId)
|
||||
setOnLongClickListener(onLongClick)
|
||||
setOnClickListener(onClick)
|
||||
|
||||
scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
adjustViewBounds = true
|
||||
layoutParams =
|
||||
|
@ -281,11 +305,11 @@ fun EditText.createListTextWatcherWithHistory(
|
|||
onTextChanged: ((text: CharSequence, start: Int, count: Int) -> Boolean)? = null,
|
||||
) =
|
||||
object : TextWatcher {
|
||||
private lateinit var stateBefore: EditTextState
|
||||
private var ignoreOriginalChange: Boolean = false
|
||||
private lateinit var textBefore: Editable
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
stateBefore = EditTextState(getText()!!.clone(), selectionStart)
|
||||
textBefore = text.clone()
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
|
@ -293,13 +317,14 @@ fun EditText.createListTextWatcherWithHistory(
|
|||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val textAfter = s!!.clone()
|
||||
if (textAfter.hasNotChanged(textBefore)) {
|
||||
return
|
||||
}
|
||||
if (!ignoreOriginalChange) {
|
||||
listManager.changeText(
|
||||
positionGetter.invoke(),
|
||||
EditTextState(getText()!!.clone(), selectionStart),
|
||||
before = stateBefore,
|
||||
editText = this@createListTextWatcherWithHistory,
|
||||
listener = this,
|
||||
EditTextState(textAfter, selectionStart),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -323,6 +348,9 @@ fun StylableEditTextWithHistory.createTextWatcherWithHistory(
|
|||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val textAfter = requireNotNull(s).clone()
|
||||
if (textAfter.hasNotChanged(stateBefore.text)) {
|
||||
return
|
||||
}
|
||||
updateModel.invoke(textAfter)
|
||||
changeHistory.push(
|
||||
EditTextWithHistoryChange(
|
||||
|
@ -335,6 +363,10 @@ fun StylableEditTextWithHistory.createTextWatcherWithHistory(
|
|||
}
|
||||
}
|
||||
|
||||
fun Editable.hasNotChanged(before: Editable): Boolean {
|
||||
return toString() == before.toString() && getSpans<SuggestionSpan>().isNotEmpty()
|
||||
}
|
||||
|
||||
fun Editable.clone(): Editable = Editable.Factory.getInstance().newEditable(this)
|
||||
|
||||
fun View.getString(id: Int, vararg formatArgs: String): String {
|
||||
|
@ -357,12 +389,12 @@ fun RadioGroup.checkedTag(): Any {
|
|||
return this.findViewById<RadioButton?>(this.checkedRadioButtonId).tag
|
||||
}
|
||||
|
||||
fun Activity.showKeyboard(view: View) {
|
||||
fun Context.showKeyboard(view: View) {
|
||||
ContextCompat.getSystemService(this, InputMethodManager::class.java)
|
||||
?.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
fun Activity.hideKeyboard(view: View) {
|
||||
fun Context.hideKeyboard(view: View) {
|
||||
ContextCompat.getSystemService(this, InputMethodManager::class.java)
|
||||
?.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
}
|
||||
|
@ -506,6 +538,64 @@ fun Activity.checkAlarmPermission(
|
|||
} else onSuccess()
|
||||
}
|
||||
|
||||
fun Activity.setEnabledSecureFlag(enabled: Boolean) {
|
||||
if (enabled) {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
fun Fragment.displayEditLabelDialog(
|
||||
oldValue: String,
|
||||
model: BaseNoteModel,
|
||||
onUpdateLabel: ((oldLabel: String, newLabel: String) -> Unit)? = null,
|
||||
) {
|
||||
requireContext().displayEditLabelDialog(oldValue, model, layoutInflater, onUpdateLabel)
|
||||
}
|
||||
|
||||
fun Activity.displayEditLabelDialog(
|
||||
oldValue: String,
|
||||
model: BaseNoteModel,
|
||||
onUpdateLabel: ((oldLabel: String, newLabel: String) -> Unit)? = null,
|
||||
) {
|
||||
displayEditLabelDialog(oldValue, model, layoutInflater, onUpdateLabel)
|
||||
}
|
||||
|
||||
fun Context.displayEditLabelDialog(
|
||||
oldValue: String,
|
||||
model: BaseNoteModel,
|
||||
layoutInflater: LayoutInflater,
|
||||
onUpdateLabel: ((oldLabel: String, newLabel: String) -> Unit)? = null,
|
||||
) {
|
||||
val dialogBinding = DialogInputBinding.inflate(layoutInflater)
|
||||
dialogBinding.EditText.setText(oldValue)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(dialogBinding.root)
|
||||
.setTitle(R.string.edit_label)
|
||||
.setCancelButton()
|
||||
.setPositiveButton(R.string.save) { dialog, _ ->
|
||||
val value = dialogBinding.EditText.text.toString().trim()
|
||||
if (value.isNotEmpty()) {
|
||||
model.updateLabel(oldValue, value) { success ->
|
||||
if (success) {
|
||||
onUpdateLabel?.invoke(oldValue, value)
|
||||
dialog.dismiss()
|
||||
} else showToast(R.string.label_exists)
|
||||
}
|
||||
}
|
||||
}
|
||||
.showAndFocus(dialogBinding.EditText, allowFullSize = true) { positiveButton ->
|
||||
dialogBinding.EditText.doAfterTextChanged { text ->
|
||||
positiveButton.isEnabled = !text.isNullOrEmpty()
|
||||
}
|
||||
positiveButton.isEnabled = oldValue.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTimestamp(timestamp: Long, dateFormat: DateFormat): String {
|
||||
val date = Date(timestamp)
|
||||
return when (dateFormat) {
|
||||
|
@ -566,11 +656,16 @@ fun @receiver:ColorInt Int.withAlpha(alpha: Float): Int {
|
|||
fun Context.getColorFromAttr(@AttrRes attr: Int): Int {
|
||||
val typedValue = TypedValue()
|
||||
val resolved = theme.resolveAttribute(attr, typedValue, true)
|
||||
if (resolved) {
|
||||
return typedValue.data // Returns the color as an Int
|
||||
} else {
|
||||
if (!resolved) {
|
||||
throw IllegalArgumentException("Attribute not found in current theme")
|
||||
}
|
||||
return if (typedValue.resourceId != 0) {
|
||||
// It's a reference (@color/something), resolve it properly
|
||||
ContextCompat.getColor(this, typedValue.resourceId)
|
||||
} else {
|
||||
// It's a direct color value
|
||||
typedValue.data
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setControlsContrastColorForAllViews(
|
||||
|
@ -740,16 +835,7 @@ fun Context.getContrastFontColor(@ColorInt backgroundColor: Int): Int {
|
|||
else ContextCompat.getColor(this, R.color.TextLight)
|
||||
}
|
||||
|
||||
fun @receiver:ColorInt Int.isLightColor(): Boolean {
|
||||
val red = android.graphics.Color.red(this) / 255.0
|
||||
val green = android.graphics.Color.green(this) / 255.0
|
||||
val blue = android.graphics.Color.blue(this) / 255.0
|
||||
|
||||
// Calculate relative luminance
|
||||
val luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue
|
||||
|
||||
return luminance > 0.5
|
||||
}
|
||||
fun @receiver:ColorInt Int.isLightColor() = ColorUtils.calculateLuminance(this) > 0.5
|
||||
|
||||
fun MaterialAlertDialogBuilder.setCancelButton(listener: DialogInterface.OnClickListener? = null) =
|
||||
setNegativeButton(R.string.cancel, listener)
|
||||
|
@ -786,12 +872,20 @@ fun Context.showToast(message: CharSequence) =
|
|||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
fun ViewGroup.addFastScroll(context: Context) {
|
||||
FastScrollerBuilder(this)
|
||||
.useMd2Style()
|
||||
.setTrackDrawable(ContextCompat.getDrawable(context, R.drawable.scroll_track)!!)
|
||||
.setPadding(0, 0, 2.dp, 0)
|
||||
.build()
|
||||
fun Context.restartApplication(
|
||||
fragmentIdToOpen: Int? = null,
|
||||
extra: Pair<String, Boolean>? = null,
|
||||
) {
|
||||
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
||||
val componentName = intent!!.component
|
||||
val mainIntent =
|
||||
Intent.makeRestartActivityTask(componentName).apply {
|
||||
fragmentIdToOpen?.let { putExtra(MainActivity.EXTRA_FRAGMENT_TO_OPEN, it) }
|
||||
extra?.let { (key, value) -> putExtra(key, value) }
|
||||
}
|
||||
mainIntent.setPackage(packageName)
|
||||
startActivity(mainIntent)
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
|
@ -802,6 +896,14 @@ fun Context.extractColor(color: String): Int {
|
|||
}
|
||||
}
|
||||
|
||||
fun ViewGroup.addFastScroll(context: Context) {
|
||||
FastScrollerBuilder(this)
|
||||
.useMd2Style()
|
||||
.setTrackDrawable(ContextCompat.getDrawable(context, R.drawable.scroll_track)!!)
|
||||
.setPadding(0, 0, 2.dp, 0)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun Window.setLightStatusAndNavBar(value: Boolean, view: View = decorView) {
|
||||
val windowInsetsControllerCompat = WindowInsetsControllerCompat(this, view)
|
||||
windowInsetsControllerCompat.isAppearanceLightStatusBars = value
|
||||
|
@ -813,6 +915,8 @@ fun ChipGroup.bindLabels(
|
|||
textSize: TextSize,
|
||||
paddingTop: Boolean,
|
||||
color: Int? = null,
|
||||
onClick: ((label: String) -> Unit)? = null,
|
||||
onLongClick: ((label: String) -> Unit)? = null,
|
||||
) {
|
||||
if (labels.isEmpty()) {
|
||||
visibility = View.GONE
|
||||
|
@ -830,6 +934,13 @@ fun ChipGroup.bindLabels(
|
|||
setTextSize(TypedValue.COMPLEX_UNIT_SP, labelSize)
|
||||
text = label
|
||||
color?.let { setControlsContrastColorForAllViews(it) }
|
||||
onClick?.let { setOnClickListener { it(label) } }
|
||||
onLongClick?.let {
|
||||
setOnLongClickListener {
|
||||
it(label)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -856,6 +967,29 @@ fun RecyclerView.initListView(context: Context) {
|
|||
setPadding(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
val RecyclerView.focusedViewHolder
|
||||
get() =
|
||||
focusedChild?.let { view ->
|
||||
val position = getChildAdapterPosition(view)
|
||||
if (position == RecyclerView.NO_POSITION) {
|
||||
null
|
||||
} else {
|
||||
findViewHolderForAdapterPosition(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun RecyclerView.showKeyboardOnFocusedItem() {
|
||||
(focusedViewHolder as? ListItemVH)?.let {
|
||||
it.binding.root.context?.showKeyboard(it.binding.EditText)
|
||||
}
|
||||
}
|
||||
|
||||
fun RecyclerView.hideKeyboardOnFocusedItem() {
|
||||
(focusedViewHolder as? ListItemVH)?.let {
|
||||
it.binding.root.context?.hideKeyboard(it.binding.EditText)
|
||||
}
|
||||
}
|
||||
|
||||
fun MaterialAutoCompleteTextView.select(value: CharSequence) {
|
||||
setText(value, false)
|
||||
}
|
||||
|
|
|
@ -64,7 +64,10 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
|
|||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (preferences.biometricLock.value == BiometricLock.ENABLED) {
|
||||
if (
|
||||
preferences.biometricLock.value == BiometricLock.ENABLED &&
|
||||
notallyXApplication.locked.value
|
||||
) {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,8 +31,6 @@ import com.philkes.notallyx.R
|
|||
import com.philkes.notallyx.data.NotallyDatabase
|
||||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.data.model.toText
|
||||
import com.philkes.notallyx.databinding.ActivityMainBinding
|
||||
import com.philkes.notallyx.presentation.activity.LockedActivity
|
||||
import com.philkes.notallyx.presentation.activity.main.fragment.DisplayLabelFragment.Companion.EXTRA_DISPLAYED_LABEL
|
||||
|
@ -40,7 +38,6 @@ import com.philkes.notallyx.presentation.activity.main.fragment.NotallyFragment
|
|||
import com.philkes.notallyx.presentation.activity.note.EditListActivity
|
||||
import com.philkes.notallyx.presentation.activity.note.EditNoteActivity
|
||||
import com.philkes.notallyx.presentation.add
|
||||
import com.philkes.notallyx.presentation.applySpans
|
||||
import com.philkes.notallyx.presentation.getQuantityString
|
||||
import com.philkes.notallyx.presentation.movedToResId
|
||||
import com.philkes.notallyx.presentation.setCancelButton
|
||||
|
@ -78,6 +75,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
|
|||
var getCurrentFragmentNotes: (() -> Collection<BaseNote>?)? = null
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
baseModel.keyword = ""
|
||||
return navController.navigateUp(configuration)
|
||||
}
|
||||
|
||||
|
@ -95,7 +93,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
|
|||
|
||||
val fragmentIdToLoad = intent.getIntExtra(EXTRA_FRAGMENT_TO_OPEN, -1)
|
||||
if (fragmentIdToLoad != -1) {
|
||||
navController.navigate(fragmentIdToLoad, Bundle())
|
||||
navController.navigate(fragmentIdToLoad, intent.extras)
|
||||
} else if (savedInstanceState == null) {
|
||||
navigateToStartView()
|
||||
}
|
||||
|
@ -107,7 +105,10 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
|
|||
if (baseModel.actionMode.enabled.value) {
|
||||
return
|
||||
}
|
||||
if (!isStartViewFragment) {
|
||||
if (
|
||||
!isStartViewFragment &&
|
||||
!intent.getBooleanExtra(EXTRA_SKIP_START_VIEW_ON_BACK, false)
|
||||
) {
|
||||
navigateToStartView()
|
||||
} else {
|
||||
finish()
|
||||
|
@ -192,7 +193,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
|
|||
.setCheckable(true)
|
||||
.setIcon(R.drawable.settings)
|
||||
}
|
||||
baseModel.preferences.labelsHiddenInNavigation.observe(this) { hiddenLabels ->
|
||||
baseModel.preferences.labelsHidden.observe(this) { hiddenLabels ->
|
||||
hideLabelsInNavigation(hiddenLabels, baseModel.preferences.maxLabels.value)
|
||||
}
|
||||
baseModel.preferences.maxLabels.observe(this) { maxLabels ->
|
||||
|
@ -241,10 +242,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
|
|||
} else null
|
||||
configuration = AppBarConfiguration(binding.NavigationView.menu, binding.DrawerLayout)
|
||||
setupActionBarWithNavController(navController, configuration)
|
||||
hideLabelsInNavigation(
|
||||
baseModel.preferences.labelsHiddenInNavigation.value,
|
||||
maxLabelsToDisplay,
|
||||
)
|
||||
hideLabelsInNavigation(baseModel.preferences.labelsHidden.value, maxLabelsToDisplay)
|
||||
}
|
||||
|
||||
private fun navigateToLabel(label: String) {
|
||||
|
@ -321,12 +319,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
|
|||
|
||||
private fun share() {
|
||||
val baseNote = baseModel.actionMode.getFirstNote()
|
||||
val body =
|
||||
when (baseNote.type) {
|
||||
Type.NOTE -> baseNote.body.applySpans(baseNote.spans)
|
||||
Type.LIST -> baseNote.items.toText()
|
||||
}
|
||||
this.shareNote(baseNote.title, body)
|
||||
this.shareNote(baseNote)
|
||||
}
|
||||
|
||||
private fun deleteForever() {
|
||||
|
@ -689,5 +682,6 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
|
|||
|
||||
companion object {
|
||||
const val EXTRA_FRAGMENT_TO_OPEN = "notallyx.intent.extra.FRAGMENT_TO_OPEN"
|
||||
const val EXTRA_SKIP_START_VIEW_ON_BACK = "notallyx.intent.extra.SKIP_START_VIEW_ON_BACK"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.view.Menu
|
|||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
|
@ -19,6 +18,7 @@ import com.philkes.notallyx.databinding.DialogInputBinding
|
|||
import com.philkes.notallyx.databinding.FragmentNotesBinding
|
||||
import com.philkes.notallyx.presentation.activity.main.fragment.DisplayLabelFragment.Companion.EXTRA_DISPLAYED_LABEL
|
||||
import com.philkes.notallyx.presentation.add
|
||||
import com.philkes.notallyx.presentation.displayEditLabelDialog
|
||||
import com.philkes.notallyx.presentation.initListView
|
||||
import com.philkes.notallyx.presentation.setCancelButton
|
||||
import com.philkes.notallyx.presentation.showAndFocus
|
||||
|
@ -77,7 +77,7 @@ class LabelsFragment : Fragment(), LabelListener {
|
|||
|
||||
override fun onEdit(position: Int) {
|
||||
labelAdapter?.currentList?.get(position)?.let { (label, _) ->
|
||||
displayEditLabelDialog(label)
|
||||
displayEditLabelDialog(label, model)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,13 +87,13 @@ class LabelsFragment : Fragment(), LabelListener {
|
|||
|
||||
override fun onToggleVisibility(position: Int) {
|
||||
labelAdapter?.currentList?.get(position)?.let { value ->
|
||||
val hiddenLabels = model.preferences.labelsHiddenInNavigation.value.toMutableSet()
|
||||
val hiddenLabels = model.preferences.labelsHidden.value.toMutableSet()
|
||||
if (value.visibleInNavigation) {
|
||||
hiddenLabels.add(value.label)
|
||||
} else {
|
||||
hiddenLabels.remove(value.label)
|
||||
}
|
||||
model.savePreference(model.preferences.labelsHiddenInNavigation, hiddenLabels)
|
||||
model.savePreference(model.preferences.labelsHidden, hiddenLabels)
|
||||
|
||||
val currentList = labelAdapter!!.currentList.toMutableList()
|
||||
currentList[position] =
|
||||
|
@ -104,7 +104,7 @@ class LabelsFragment : Fragment(), LabelListener {
|
|||
|
||||
private fun setupObserver() {
|
||||
model.labels.observe(viewLifecycleOwner) { labels ->
|
||||
val hiddenLabels = model.preferences.labelsHiddenInNavigation.value
|
||||
val hiddenLabels = model.preferences.labelsHidden.value
|
||||
val labelsData = labels.map { label -> LabelData(label, !hiddenLabels.contains(label)) }
|
||||
labelAdapter?.submitList(labelsData)
|
||||
binding?.ImageView?.isVisible = labels.isEmpty()
|
||||
|
@ -148,37 +148,4 @@ class LabelsFragment : Fragment(), LabelListener {
|
|||
.setCancelButton()
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun displayEditLabelDialog(oldValue: String) {
|
||||
val dialogBinding = DialogInputBinding.inflate(layoutInflater)
|
||||
|
||||
dialogBinding.EditText.setText(oldValue)
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(dialogBinding.root)
|
||||
.setTitle(R.string.edit_label)
|
||||
.setCancelButton()
|
||||
.setPositiveButton(R.string.save) { dialog, _ ->
|
||||
val value = dialogBinding.EditText.text.toString().trim()
|
||||
if (value.isNotEmpty()) {
|
||||
model.updateLabel(oldValue, value) { success ->
|
||||
if (success) {
|
||||
dialog.dismiss()
|
||||
} else
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.label_exists,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.showAndFocus(dialogBinding.EditText, allowFullSize = true) { positiveButton ->
|
||||
dialogBinding.EditText.doAfterTextChanged { text ->
|
||||
positiveButton.isEnabled = !text.isNullOrEmpty()
|
||||
}
|
||||
positiveButton.isEnabled = oldValue.isNotEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -195,27 +195,20 @@ abstract class NotallyFragment : Fragment(), ItemListener {
|
|||
}
|
||||
}
|
||||
doAfterTextChanged { text ->
|
||||
model.keyword = requireNotNull(text).trim().toString()
|
||||
if (
|
||||
model.keyword.isNotEmpty() &&
|
||||
navController.currentDestination?.id != R.id.Search
|
||||
) {
|
||||
val bundle =
|
||||
Bundle().apply {
|
||||
putSerializable(EXTRA_INITIAL_FOLDER, model.folder.value)
|
||||
putSerializable(EXTRA_INITIAL_LABEL, model.currentLabel)
|
||||
}
|
||||
navController.navigate(R.id.Search, bundle)
|
||||
val isSearchFragment = navController.currentDestination?.id == R.id.Search
|
||||
if (isSearchFragment) {
|
||||
model.keyword = requireNotNull(text).trim().toString()
|
||||
}
|
||||
}
|
||||
setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus && navController.currentDestination?.id != R.id.Search) {
|
||||
val bundle =
|
||||
if (text?.isNotEmpty() == true && !isSearchFragment) {
|
||||
setText("")
|
||||
model.keyword = text.trim().toString()
|
||||
navController.navigate(
|
||||
R.id.Search,
|
||||
Bundle().apply {
|
||||
putSerializable(EXTRA_INITIAL_FOLDER, model.folder.value)
|
||||
putSerializable(EXTRA_INITIAL_LABEL, model.currentLabel)
|
||||
}
|
||||
navController.navigate(R.id.Search, bundle)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -243,7 +236,8 @@ abstract class NotallyFragment : Fragment(), ItemListener {
|
|||
maxItems.value,
|
||||
maxLines.value,
|
||||
maxTitle.value,
|
||||
labelsHiddenInOverview.value,
|
||||
labelTagsHiddenInOverview.value,
|
||||
imagesHiddenInOverview.value,
|
||||
),
|
||||
model.imageRoot,
|
||||
this@NotallyFragment,
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.view.View
|
|||
import androidx.core.os.BundleCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
|
||||
class SearchFragment : NotallyFragment() {
|
||||
|
@ -44,6 +45,9 @@ class SearchFragment : NotallyFragment() {
|
|||
isVisible = true
|
||||
}
|
||||
} else binding?.ChipGroup?.isVisible = false
|
||||
getObservable().observe(viewLifecycleOwner) { items ->
|
||||
model.actionMode.updateSelected(items?.filterIsInstance<BaseNote>()?.map { it.id })
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBackground() = R.drawable.search
|
||||
|
|
|
@ -6,15 +6,17 @@ import android.net.Uri
|
|||
import android.text.method.PasswordTransformationMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.databinding.ChoiceItemBinding
|
||||
import com.philkes.notallyx.databinding.DialogDateFormatBinding
|
||||
import com.philkes.notallyx.databinding.DialogNotesSortBinding
|
||||
import com.philkes.notallyx.databinding.DialogPreferenceBooleanBinding
|
||||
import com.philkes.notallyx.databinding.DialogPreferenceEnumWithToggleBinding
|
||||
import com.philkes.notallyx.databinding.DialogSelectionBoxBinding
|
||||
import com.philkes.notallyx.databinding.DialogTextInputBinding
|
||||
import com.philkes.notallyx.databinding.PreferenceBinding
|
||||
|
@ -41,6 +43,7 @@ import com.philkes.notallyx.presentation.viewmodel.preference.NotesSortPreferenc
|
|||
import com.philkes.notallyx.presentation.viewmodel.preference.SortDirection
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.StringPreference
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.TextProvider
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.Theme
|
||||
import com.philkes.notallyx.utils.canAuthenticateWithBiometrics
|
||||
import com.philkes.notallyx.utils.toReadablePath
|
||||
|
||||
|
@ -198,28 +201,35 @@ fun PreferenceBinding.setup(
|
|||
Value.text = dateFormatValue.getText(context)
|
||||
|
||||
root.setOnClickListener {
|
||||
val layout = DialogDateFormatBinding.inflate(layoutInflater, null, false)
|
||||
val layout = DialogPreferenceEnumWithToggleBinding.inflate(layoutInflater, null, false)
|
||||
layout.EnumHint.apply {
|
||||
setText(R.string.date_format_hint)
|
||||
isVisible = true
|
||||
}
|
||||
DateFormat.entries.forEachIndexed { idx, dateFormat ->
|
||||
ChoiceItemBinding.inflate(layoutInflater).root.apply {
|
||||
id = idx
|
||||
text = dateFormat.getText(context)
|
||||
tag = dateFormat
|
||||
layout.DateFormatRadioGroup.addView(this)
|
||||
layout.EnumRadioGroup.addView(this)
|
||||
if (dateFormat == dateFormatValue) {
|
||||
layout.DateFormatRadioGroup.check(this.id)
|
||||
layout.EnumRadioGroup.check(this.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layout.ApplyToNoteView.isChecked = applyToNoteViewValue
|
||||
layout.Toggle.apply {
|
||||
setText(R.string.date_format_apply_in_note_view)
|
||||
isChecked = applyToNoteViewValue
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(dateFormatPreference.titleResId)
|
||||
.setView(layout.root)
|
||||
.setPositiveButton(R.string.save) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
val dateFormat = layout.DateFormatRadioGroup.checkedTag() as DateFormat
|
||||
val applyToNoteView = layout.ApplyToNoteView.isChecked
|
||||
val dateFormat = layout.EnumRadioGroup.checkedTag() as DateFormat
|
||||
val applyToNoteView = layout.Toggle.isChecked
|
||||
onSave(dateFormat, applyToNoteView)
|
||||
}
|
||||
.setCancelButton()
|
||||
|
@ -227,6 +237,52 @@ fun PreferenceBinding.setup(
|
|||
}
|
||||
}
|
||||
|
||||
fun PreferenceBinding.setup(
|
||||
themePreference: EnumPreference<Theme>,
|
||||
themeValue: Theme,
|
||||
useDynamicColorsValue: Boolean,
|
||||
context: Context,
|
||||
layoutInflater: LayoutInflater,
|
||||
onSave: (theme: Theme, useDynamicColors: Boolean) -> Unit,
|
||||
) {
|
||||
Title.setText(themePreference.titleResId!!)
|
||||
|
||||
Value.text = themeValue.getText(context)
|
||||
|
||||
root.setOnClickListener {
|
||||
val layout = DialogPreferenceEnumWithToggleBinding.inflate(layoutInflater, null, false)
|
||||
Theme.entries.forEachIndexed { idx, theme ->
|
||||
ChoiceItemBinding.inflate(layoutInflater).root.apply {
|
||||
id = idx
|
||||
text = theme.getText(context)
|
||||
tag = theme
|
||||
layout.EnumRadioGroup.addView(this)
|
||||
if (theme == themeValue) {
|
||||
layout.EnumRadioGroup.check(this.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layout.Toggle.apply {
|
||||
isVisible = DynamicColors.isDynamicColorAvailable()
|
||||
setText(R.string.theme_use_dynamic_colors)
|
||||
isChecked = useDynamicColorsValue
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(themePreference.titleResId)
|
||||
.setView(layout.root)
|
||||
.setPositiveButton(R.string.save) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
val theme = layout.EnumRadioGroup.checkedTag() as Theme
|
||||
val useDynamicColors = layout.Toggle.isChecked
|
||||
onSave(theme, useDynamicColors)
|
||||
}
|
||||
.setCancelButton()
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun PreferenceBinding.setup(
|
||||
preference: BooleanPreference,
|
||||
value: Boolean,
|
||||
|
@ -434,6 +490,29 @@ fun PreferenceSeekbarBinding.setup(
|
|||
}
|
||||
}
|
||||
|
||||
fun PreferenceSeekbarBinding.setupAutoSaveIdleTime(
|
||||
preference: IntPreference,
|
||||
context: Context,
|
||||
value: Int = preference.value,
|
||||
onChange: (newValue: Int) -> Unit,
|
||||
) {
|
||||
Slider.apply {
|
||||
setLabelFormatter { sliderValue ->
|
||||
if (sliderValue == -1f) {
|
||||
context.getString(R.string.disabled)
|
||||
} else "${sliderValue.toInt()}s"
|
||||
}
|
||||
addOnChangeListener { _, value, _ ->
|
||||
if (value == -1f) {
|
||||
setAlpha(0.6f) // Reduce opacity to make it look disabled
|
||||
} else {
|
||||
setAlpha(1f) // Restore normal appearance
|
||||
}
|
||||
}
|
||||
}
|
||||
setup(preference, context, value, onChange)
|
||||
}
|
||||
|
||||
fun PreferenceBinding.setupStartView(
|
||||
preference: StringPreference,
|
||||
value: String,
|
||||
|
|
|
@ -31,7 +31,9 @@ import com.philkes.notallyx.data.imports.txt.APPLICATION_TEXT_MIME_TYPES
|
|||
import com.philkes.notallyx.data.model.toText
|
||||
import com.philkes.notallyx.databinding.DialogTextInputBinding
|
||||
import com.philkes.notallyx.databinding.FragmentSettingsBinding
|
||||
import com.philkes.notallyx.presentation.activity.main.MainActivity
|
||||
import com.philkes.notallyx.presentation.setCancelButton
|
||||
import com.philkes.notallyx.presentation.setEnabledSecureFlag
|
||||
import com.philkes.notallyx.presentation.setupImportProgressDialog
|
||||
import com.philkes.notallyx.presentation.setupProgressDialog
|
||||
import com.philkes.notallyx.presentation.showAndFocus
|
||||
|
@ -51,6 +53,7 @@ import com.philkes.notallyx.utils.MIME_TYPE_JSON
|
|||
import com.philkes.notallyx.utils.MIME_TYPE_ZIP
|
||||
import com.philkes.notallyx.utils.backup.exportPreferences
|
||||
import com.philkes.notallyx.utils.catchNoBrowserInstalled
|
||||
import com.philkes.notallyx.utils.getExtraBooleanFromBundleOrIntent
|
||||
import com.philkes.notallyx.utils.getLastExceptionLog
|
||||
import com.philkes.notallyx.utils.getLogFile
|
||||
import com.philkes.notallyx.utils.getUriForFile
|
||||
|
@ -95,7 +98,13 @@ class SettingsFragment : Fragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupActivityResultLaunchers()
|
||||
savedInstanceState?.getBoolean(EXTRA_SHOW_IMPORT_BACKUPS_FOLDER, false)?.let {
|
||||
val showImportBackupsFolder =
|
||||
getExtraBooleanFromBundleOrIntent(
|
||||
savedInstanceState,
|
||||
EXTRA_SHOW_IMPORT_BACKUPS_FOLDER,
|
||||
false,
|
||||
)
|
||||
showImportBackupsFolder.let {
|
||||
if (it) {
|
||||
model.refreshBackupsFolder(
|
||||
requireContext(),
|
||||
|
@ -238,9 +247,27 @@ class SettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
theme.observe(viewLifecycleOwner) { value ->
|
||||
binding.Theme.setup(theme, value, requireContext()) { newValue ->
|
||||
model.savePreference(theme, newValue)
|
||||
theme.merge(useDynamicColors).observe(viewLifecycleOwner) {
|
||||
(themeValue, useDynamicColorsValue) ->
|
||||
binding.Theme.setup(
|
||||
theme,
|
||||
themeValue,
|
||||
useDynamicColorsValue,
|
||||
requireContext(),
|
||||
layoutInflater,
|
||||
) { newThemeValue, newUseDynamicColorsValue ->
|
||||
model.savePreference(theme, newThemeValue)
|
||||
model.savePreference(useDynamicColors, newUseDynamicColorsValue)
|
||||
val packageManager = requireContext().packageManager
|
||||
val intent = packageManager.getLaunchIntentForPackage(requireContext().packageName)
|
||||
val componentName = intent!!.component
|
||||
val mainIntent =
|
||||
Intent.makeRestartActivityTask(componentName).apply {
|
||||
putExtra(MainActivity.EXTRA_FRAGMENT_TO_OPEN, R.id.Settings)
|
||||
}
|
||||
mainIntent.setPackage(requireContext().packageName)
|
||||
requireContext().startActivity(mainIntent)
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -313,15 +340,26 @@ class SettingsFragment : Fragment() {
|
|||
MaxLabels.setup(maxLabels, requireContext()) { newValue ->
|
||||
model.savePreference(maxLabels, newValue)
|
||||
}
|
||||
labelsHiddenInOverview.observe(viewLifecycleOwner) { value ->
|
||||
labelTagsHiddenInOverview.observe(viewLifecycleOwner) { value ->
|
||||
binding.LabelsHiddenInOverview.setup(
|
||||
labelsHiddenInOverview,
|
||||
labelTagsHiddenInOverview,
|
||||
value,
|
||||
requireContext(),
|
||||
layoutInflater,
|
||||
R.string.labels_hidden_in_overview,
|
||||
) { enabled ->
|
||||
model.savePreference(labelsHiddenInOverview, enabled)
|
||||
model.savePreference(labelTagsHiddenInOverview, enabled)
|
||||
}
|
||||
}
|
||||
imagesHiddenInOverview.observe(viewLifecycleOwner) { value ->
|
||||
binding.ImagesHiddenInOverview.setup(
|
||||
imagesHiddenInOverview,
|
||||
value,
|
||||
requireContext(),
|
||||
layoutInflater,
|
||||
R.string.images_hidden_in_overview,
|
||||
) { enabled ->
|
||||
model.savePreference(imagesHiddenInOverview, enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -414,7 +452,7 @@ class SettingsFragment : Fragment() {
|
|||
when (selectedImportSource.mimeType) {
|
||||
FOLDER_OR_FILE_MIMETYPE ->
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.plain_text_files)
|
||||
.setTitle(selectedImportSource.displayNameResId)
|
||||
.setItems(
|
||||
arrayOf(
|
||||
getString(R.string.folder),
|
||||
|
@ -566,6 +604,14 @@ class SettingsFragment : Fragment() {
|
|||
model.savePreference(backupPassword, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
secureFlag.observe(viewLifecycleOwner) { value ->
|
||||
binding.SecureFlag.setup(secureFlag, value, requireContext(), layoutInflater) { newValue
|
||||
->
|
||||
model.savePreference(secureFlag, newValue)
|
||||
activity?.setEnabledSecureFlag(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun NotallyXPreferences.setupSettings(binding: FragmentSettingsBinding) {
|
||||
|
@ -616,6 +662,11 @@ class SettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
AutoSaveAfterIdle.setupAutoSaveIdleTime(autoSaveAfterIdleTime, requireContext()) {
|
||||
newValue ->
|
||||
model.savePreference(autoSaveAfterIdleTime, newValue)
|
||||
}
|
||||
|
||||
ClearData.setOnClickListener {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.clear_data_message)
|
||||
|
@ -689,8 +740,10 @@ class SettingsFragment : Fragment() {
|
|||
.setCancelButton()
|
||||
.show()
|
||||
}
|
||||
Rate.setOnClickListener {
|
||||
openLink("https://play.google.com/store/apps/details?id=com.philkes.notallyx")
|
||||
}
|
||||
SourceCode.setOnClickListener { openLink("https://github.com/PhilKes/NotallyX") }
|
||||
// TODO: add ColorPickerView
|
||||
Libraries.setOnClickListener {
|
||||
val libraries =
|
||||
arrayOf(
|
||||
|
|
|
@ -8,15 +8,20 @@ import android.graphics.drawable.Drawable
|
|||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.text.Editable
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.view.ViewGroup.VISIBLE
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageButton
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
|
@ -36,10 +41,14 @@ import com.philkes.notallyx.data.NotallyDatabase
|
|||
import com.philkes.notallyx.data.model.Audio
|
||||
import com.philkes.notallyx.data.model.FileAttachment
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.data.model.toText
|
||||
import com.philkes.notallyx.data.model.isImageMimeType
|
||||
import com.philkes.notallyx.databinding.ActivityEditBinding
|
||||
import com.philkes.notallyx.presentation.activity.LockedActivity
|
||||
import com.philkes.notallyx.presentation.activity.main.MainActivity
|
||||
import com.philkes.notallyx.presentation.activity.main.MainActivity.Companion.EXTRA_FRAGMENT_TO_OPEN
|
||||
import com.philkes.notallyx.presentation.activity.main.MainActivity.Companion.EXTRA_SKIP_START_VIEW_ON_BACK
|
||||
import com.philkes.notallyx.presentation.activity.main.fragment.DisplayLabelFragment.Companion.EXTRA_DISPLAYED_LABEL
|
||||
import com.philkes.notallyx.presentation.activity.note.SelectLabelsActivity.Companion.EXTRA_SELECTED_LABELS
|
||||
import com.philkes.notallyx.presentation.activity.note.reminders.RemindersActivity
|
||||
|
@ -47,9 +56,11 @@ import com.philkes.notallyx.presentation.add
|
|||
import com.philkes.notallyx.presentation.addFastScroll
|
||||
import com.philkes.notallyx.presentation.addIconButton
|
||||
import com.philkes.notallyx.presentation.bindLabels
|
||||
import com.philkes.notallyx.presentation.displayEditLabelDialog
|
||||
import com.philkes.notallyx.presentation.displayFormattedTimestamp
|
||||
import com.philkes.notallyx.presentation.extractColor
|
||||
import com.philkes.notallyx.presentation.getQuantityString
|
||||
import com.philkes.notallyx.presentation.hideKeyboard
|
||||
import com.philkes.notallyx.presentation.isLightColor
|
||||
import com.philkes.notallyx.presentation.setCancelButton
|
||||
import com.philkes.notallyx.presentation.setControlsContrastColorForAllViews
|
||||
|
@ -76,6 +87,7 @@ import com.philkes.notallyx.presentation.widget.WidgetProvider
|
|||
import com.philkes.notallyx.utils.FileError
|
||||
import com.philkes.notallyx.utils.backup.exportNotes
|
||||
import com.philkes.notallyx.utils.changehistory.ChangeHistory
|
||||
import com.philkes.notallyx.utils.getMimeType
|
||||
import com.philkes.notallyx.utils.getUriForFile
|
||||
import com.philkes.notallyx.utils.log
|
||||
import com.philkes.notallyx.utils.mergeSkipFirst
|
||||
|
@ -114,12 +126,29 @@ abstract class EditActivity(private val type: Type) :
|
|||
protected var colorInt: Int = -1
|
||||
protected var inputMethodManager: InputMethodManager? = null
|
||||
|
||||
protected lateinit var toggleViewMode: ImageButton
|
||||
protected val canEdit
|
||||
get() = notallyModel.viewMode.value == NoteViewMode.EDIT
|
||||
|
||||
private val autoSaveHandler = Handler(Looper.getMainLooper())
|
||||
private val autoSaveRunnable = Runnable {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
updateModel()
|
||||
if (notallyModel.isModified()) {
|
||||
Log.d(TAG, "Auto-saving note...")
|
||||
saveNote(checkAutoSave = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
if (notallyModel.isEmpty()) {
|
||||
notallyModel.deleteBaseNote(checkAutoSave = false)
|
||||
} else if (notallyModel.isModified()) {
|
||||
saveNote()
|
||||
} else {
|
||||
notallyModel.checkBackupOnSave()
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
|
@ -137,9 +166,9 @@ abstract class EditActivity(private val type: Type) :
|
|||
}
|
||||
}
|
||||
|
||||
open suspend fun saveNote() {
|
||||
open suspend fun saveNote(checkAutoSave: Boolean = true) {
|
||||
updateModel()
|
||||
notallyModel.saveNote()
|
||||
notallyModel.saveNote(checkAutoSave)
|
||||
WidgetProvider.sendBroadcast(application, longArrayOf(notallyModel.id))
|
||||
}
|
||||
|
||||
|
@ -159,11 +188,15 @@ abstract class EditActivity(private val type: Type) :
|
|||
if (persistedId == null || notallyModel.originalNote == null) {
|
||||
notallyModel.setState(id)
|
||||
}
|
||||
if (notallyModel.isNewNote && intent.action == Intent.ACTION_SEND) {
|
||||
handleSharedNote()
|
||||
} else if (notallyModel.isNewNote) {
|
||||
intent.getStringExtra(EXTRA_DISPLAYED_LABEL)?.let {
|
||||
notallyModel.setLabels(listOf(it))
|
||||
if (notallyModel.isNewNote) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SEND,
|
||||
Intent.ACTION_SEND_MULTIPLE -> handleSharedNote()
|
||||
Intent.ACTION_VIEW -> handleViewNote()
|
||||
else ->
|
||||
intent.getStringExtra(EXTRA_DISPLAYED_LABEL)?.let {
|
||||
notallyModel.setLabels(listOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,6 +226,31 @@ abstract class EditActivity(private val type: Type) :
|
|||
}
|
||||
}
|
||||
|
||||
open fun toggleCanEdit(mode: NoteViewMode) {
|
||||
binding.EnterTitle.apply {
|
||||
if (isFocused) {
|
||||
when {
|
||||
mode == NoteViewMode.EDIT -> showKeyboard(this)
|
||||
else -> hideKeyboard(this)
|
||||
}
|
||||
}
|
||||
setCanEdit(mode == NoteViewMode.EDIT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
autoSaveHandler.removeCallbacks(autoSaveRunnable)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
protected fun resetIdleTimer() {
|
||||
autoSaveHandler.removeCallbacks(autoSaveRunnable)
|
||||
val idleTime = preferences.autoSaveAfterIdleTime.value
|
||||
if (idleTime > -1) {
|
||||
autoSaveHandler.postDelayed(autoSaveRunnable, idleTime.toLong() * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupActivityResultLaunchers() {
|
||||
recordAudioActivityResultLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
|
@ -243,6 +301,7 @@ abstract class EditActivity(private val type: Type) :
|
|||
paddingTop = true,
|
||||
colorInt,
|
||||
)
|
||||
resetIdleTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -317,6 +376,7 @@ abstract class EditActivity(private val type: Type) :
|
|||
ChangeHistory().apply {
|
||||
canUndo.observe(this@EditActivity) { canUndo -> undo?.isEnabled = canUndo }
|
||||
canRedo.observe(this@EditActivity) { canRedo -> redo?.isEnabled = canRedo }
|
||||
stackPointer.observe(this@EditActivity) { _ -> resetIdleTimer() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -445,7 +505,19 @@ abstract class EditActivity(private val type: Type) :
|
|||
binding.BottomAppBarCenter.apply {
|
||||
removeAllViews()
|
||||
undo =
|
||||
addIconButton(R.string.undo, R.drawable.undo, marginStart = 2) {
|
||||
addIconButton(
|
||||
R.string.undo,
|
||||
R.drawable.undo,
|
||||
marginStart = 2,
|
||||
onLongClick = {
|
||||
try {
|
||||
changeHistory.undoAll()
|
||||
} catch (e: ChangeHistory.ChangeHistoryException) {
|
||||
application.log(TAG, throwable = e)
|
||||
}
|
||||
true
|
||||
},
|
||||
) {
|
||||
try {
|
||||
changeHistory.undo()
|
||||
} catch (e: ChangeHistory.ChangeHistoryException) {
|
||||
|
@ -455,7 +527,19 @@ abstract class EditActivity(private val type: Type) :
|
|||
.apply { isEnabled = changeHistory.canUndo.value }
|
||||
|
||||
redo =
|
||||
addIconButton(R.string.redo, R.drawable.redo, marginStart = 2) {
|
||||
addIconButton(
|
||||
R.string.redo,
|
||||
R.drawable.redo,
|
||||
marginStart = 2,
|
||||
onLongClick = {
|
||||
try {
|
||||
changeHistory.redoAll()
|
||||
} catch (e: ChangeHistory.ChangeHistoryException) {
|
||||
application.log(TAG, throwable = e)
|
||||
}
|
||||
true
|
||||
},
|
||||
) {
|
||||
try {
|
||||
changeHistory.redo()
|
||||
} catch (e: ChangeHistory.ChangeHistoryException) {
|
||||
|
@ -466,14 +550,31 @@ abstract class EditActivity(private val type: Type) :
|
|||
}
|
||||
binding.BottomAppBarRight.apply {
|
||||
removeAllViews()
|
||||
addIconButton(R.string.more, R.drawable.more_vert, marginStart = 0) {
|
||||
MoreNoteBottomSheet(this@EditActivity, createFolderActions(), colorInt)
|
||||
|
||||
addToggleViewMode()
|
||||
addIconButton(R.string.tap_for_more_options, R.drawable.more_vert, marginStart = 0) {
|
||||
MoreNoteBottomSheet(
|
||||
this@EditActivity,
|
||||
createNoteTypeActions() + createFolderActions(),
|
||||
colorInt,
|
||||
)
|
||||
.show(supportFragmentManager, MoreNoteBottomSheet.TAG)
|
||||
}
|
||||
}
|
||||
setBottomAppBarColor(colorInt)
|
||||
}
|
||||
|
||||
protected fun ViewGroup.addToggleViewMode() {
|
||||
toggleViewMode =
|
||||
addIconButton(R.string.edit, R.drawable.visibility) {
|
||||
notallyModel.viewMode.value =
|
||||
when (notallyModel.viewMode.value) {
|
||||
NoteViewMode.EDIT -> NoteViewMode.READ_ONLY
|
||||
NoteViewMode.READ_ONLY -> NoteViewMode.EDIT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun createFolderActions() =
|
||||
when (notallyModel.folder) {
|
||||
Folder.NOTES ->
|
||||
|
@ -513,12 +614,68 @@ abstract class EditActivity(private val type: Type) :
|
|||
)
|
||||
}
|
||||
|
||||
protected fun createNoteTypeActions() =
|
||||
when (notallyModel.type) {
|
||||
Type.NOTE ->
|
||||
listOf(
|
||||
Action(R.string.convert_to_list_note, R.drawable.convert_to_text) { _ ->
|
||||
convertTo(Type.LIST)
|
||||
true
|
||||
}
|
||||
)
|
||||
Type.LIST ->
|
||||
listOf(
|
||||
Action(R.string.convert_to_text_note, R.drawable.convert_to_text) { _ ->
|
||||
convertTo(Type.NOTE)
|
||||
true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertTo(type: Type) {
|
||||
updateModel()
|
||||
lifecycleScope.launch {
|
||||
notallyModel.convertTo(type)
|
||||
val intent =
|
||||
Intent(
|
||||
this@EditActivity,
|
||||
when (type) {
|
||||
Type.NOTE -> EditNoteActivity::class.java
|
||||
Type.LIST -> EditListActivity::class.java
|
||||
},
|
||||
)
|
||||
intent.putExtra(EXTRA_SELECTED_BASE_NOTE, notallyModel.id)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun configureUI()
|
||||
|
||||
open fun setupListeners() {
|
||||
binding.EnterTitle.initHistory(changeHistory) { text ->
|
||||
notallyModel.title = text.trim().toString()
|
||||
}
|
||||
notallyModel.viewMode.observe(this) { value ->
|
||||
toggleViewMode.apply {
|
||||
setImageResource(
|
||||
when (value) {
|
||||
NoteViewMode.READ_ONLY -> R.drawable.edit
|
||||
else -> R.drawable.visibility
|
||||
}
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
tooltipText =
|
||||
getString(
|
||||
when (value) {
|
||||
NoteViewMode.READ_ONLY -> R.string.edit
|
||||
else -> R.string.read_only
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
value?.let { toggleCanEdit(it) }
|
||||
}
|
||||
}
|
||||
|
||||
open fun setStateFromModel(savedInstanceState: Bundle?) {
|
||||
|
@ -535,26 +692,90 @@ abstract class EditActivity(private val type: Type) :
|
|||
} else DateFormat.ABSOLUTE
|
||||
binding.Date.displayFormattedTimestamp(date, dateFormat, datePrefixResId)
|
||||
binding.EnterTitle.setText(notallyModel.title)
|
||||
bindLabels()
|
||||
setColor()
|
||||
}
|
||||
|
||||
private fun bindLabels() {
|
||||
binding.LabelGroup.bindLabels(
|
||||
notallyModel.labels,
|
||||
notallyModel.textSize,
|
||||
paddingTop = true,
|
||||
colorInt,
|
||||
onClick = { label ->
|
||||
val bundle = Bundle()
|
||||
bundle.putString(EXTRA_DISPLAYED_LABEL, label)
|
||||
startActivity(
|
||||
Intent(this, MainActivity::class.java).apply {
|
||||
putExtra(EXTRA_FRAGMENT_TO_OPEN, R.id.DisplayLabel)
|
||||
putExtra(EXTRA_DISPLAYED_LABEL, label)
|
||||
putExtra(EXTRA_SKIP_START_VIEW_ON_BACK, true)
|
||||
}
|
||||
)
|
||||
},
|
||||
onLongClick = { label ->
|
||||
displayEditLabelDialog(label, baseModel) { oldLabel, newLabel ->
|
||||
notallyModel.labels.apply {
|
||||
remove(oldLabel)
|
||||
add(newLabel)
|
||||
}
|
||||
bindLabels()
|
||||
}
|
||||
},
|
||||
)
|
||||
setColor()
|
||||
}
|
||||
|
||||
private fun handleSharedNote() {
|
||||
val title = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||
|
||||
val string = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
|
||||
val files =
|
||||
IntentCompat.getParcelableArrayListExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)
|
||||
?: IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)
|
||||
?.let { listOf(it) }
|
||||
if (string != null) {
|
||||
notallyModel.body = Editable.Factory.getInstance().newEditable(string)
|
||||
}
|
||||
if (title != null) {
|
||||
notallyModel.title = title
|
||||
}
|
||||
files?.let {
|
||||
val filesByType =
|
||||
it.groupBy { uri ->
|
||||
getMimeType(uri)?.let { mimeType ->
|
||||
if (mimeType.isImageMimeType) {
|
||||
NotallyModel.FileType.IMAGE
|
||||
} else {
|
||||
NotallyModel.FileType.ANY
|
||||
}
|
||||
} ?: NotallyModel.FileType.ANY
|
||||
}
|
||||
filesByType[NotallyModel.FileType.IMAGE]?.let { images ->
|
||||
notallyModel.addImages(images.toTypedArray())
|
||||
}
|
||||
filesByType[NotallyModel.FileType.ANY]?.let { otherFiles ->
|
||||
notallyModel.addFiles(otherFiles.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleViewNote() {
|
||||
val text =
|
||||
intent.data?.let { uri ->
|
||||
contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
inputStream.bufferedReader().readText()
|
||||
}
|
||||
?: run {
|
||||
showToast(R.string.cant_load_file)
|
||||
null
|
||||
}
|
||||
} ?: intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
val title = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||
if (text != null) {
|
||||
notallyModel.body = Editable.Factory.getInstance().newEditable(text)
|
||||
}
|
||||
if (title != null) {
|
||||
notallyModel.title = title
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(24)
|
||||
|
@ -645,6 +866,7 @@ abstract class EditActivity(private val type: Type) :
|
|||
}
|
||||
notallyModel.color = selectedColor
|
||||
setColor()
|
||||
resetIdleTimer()
|
||||
},
|
||||
) { colorToDelete, newColor ->
|
||||
baseModel.changeColor(colorToDelete, newColor)
|
||||
|
@ -652,6 +874,7 @@ abstract class EditActivity(private val type: Type) :
|
|||
notallyModel.color = newColor
|
||||
setColor()
|
||||
}
|
||||
resetIdleTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -669,12 +892,7 @@ abstract class EditActivity(private val type: Type) :
|
|||
}
|
||||
|
||||
override fun share() {
|
||||
val body =
|
||||
when (type) {
|
||||
Type.NOTE -> notallyModel.body
|
||||
Type.LIST -> notallyModel.items.toMutableList().toText()
|
||||
}
|
||||
this.shareNote(notallyModel.title, body)
|
||||
this.shareNote(notallyModel.getBaseNote())
|
||||
}
|
||||
|
||||
override fun export(mimeType: ExportMimeType) {
|
||||
|
@ -877,7 +1095,9 @@ abstract class EditActivity(private val type: Type) :
|
|||
colorInt = extractColor(notallyModel.color)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
window.statusBarColor = colorInt
|
||||
window.navigationBarColor = colorInt
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
window.navigationBarColor = colorInt
|
||||
}
|
||||
window.setLightStatusAndNavBar(colorInt.isLightColor())
|
||||
}
|
||||
binding.apply {
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
package com.philkes.notallyx.presentation.activity.note
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.presentation.addIconButton
|
||||
import com.philkes.notallyx.presentation.hideKeyboardOnFocusedItem
|
||||
import com.philkes.notallyx.presentation.setOnNextAction
|
||||
import com.philkes.notallyx.presentation.showKeyboardOnFocusedItem
|
||||
import com.philkes.notallyx.presentation.view.note.action.MoreListActions
|
||||
import com.philkes.notallyx.presentation.view.note.action.MoreListBottomSheet
|
||||
import com.philkes.notallyx.presentation.view.note.listitem.HighlightText
|
||||
|
@ -69,6 +73,21 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
|
|||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun toggleCanEdit(mode: NoteViewMode) {
|
||||
super.toggleCanEdit(mode)
|
||||
when (mode) {
|
||||
NoteViewMode.EDIT -> binding.MainListView.showKeyboardOnFocusedItem()
|
||||
NoteViewMode.READ_ONLY -> binding.MainListView.hideKeyboardOnFocusedItem()
|
||||
}
|
||||
adapter?.viewMode = mode
|
||||
adapterChecked?.viewMode = mode
|
||||
binding.AddItem.visibility =
|
||||
when (mode) {
|
||||
NoteViewMode.EDIT -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteChecked() {
|
||||
listManager.deleteCheckedItems()
|
||||
}
|
||||
|
@ -85,8 +104,13 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
|
|||
super.initBottomMenu()
|
||||
binding.BottomAppBarRight.apply {
|
||||
removeAllViews()
|
||||
addIconButton(R.string.more, R.drawable.more_vert, marginStart = 0) {
|
||||
MoreListBottomSheet(this@EditListActivity, createFolderActions(), colorInt)
|
||||
addToggleViewMode()
|
||||
addIconButton(R.string.tap_for_more_options, R.drawable.more_vert, marginStart = 0) {
|
||||
MoreListBottomSheet(
|
||||
this@EditListActivity,
|
||||
createNoteTypeActions() + createFolderActions(),
|
||||
colorInt,
|
||||
)
|
||||
.show(supportFragmentManager, MoreListBottomSheet.TAG)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,10 +21,12 @@ import android.widget.LinearLayout
|
|||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.data.model.createNoteUrl
|
||||
import com.philkes.notallyx.data.model.getNoteIdFromUrl
|
||||
|
@ -38,7 +40,9 @@ import com.philkes.notallyx.presentation.activity.note.PickNoteActivity.Companio
|
|||
import com.philkes.notallyx.presentation.activity.note.PickNoteActivity.Companion.EXTRA_PICKED_NOTE_TYPE
|
||||
import com.philkes.notallyx.presentation.add
|
||||
import com.philkes.notallyx.presentation.addIconButton
|
||||
import com.philkes.notallyx.presentation.createBoldSpan
|
||||
import com.philkes.notallyx.presentation.dp
|
||||
import com.philkes.notallyx.presentation.hideKeyboard
|
||||
import com.philkes.notallyx.presentation.setControlsContrastColorForAllViews
|
||||
import com.philkes.notallyx.presentation.setOnNextAction
|
||||
import com.philkes.notallyx.presentation.showKeyboard
|
||||
|
@ -65,8 +69,6 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
|
|||
override fun configureUI() {
|
||||
binding.EnterTitle.setOnNextAction { binding.EnterBody.requestFocus() }
|
||||
|
||||
setupEditor()
|
||||
|
||||
if (notallyModel.isNewNote) {
|
||||
binding.EnterBody.requestFocus()
|
||||
}
|
||||
|
@ -78,6 +80,17 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
|
|||
setupActivityResultLaunchers()
|
||||
}
|
||||
|
||||
override fun toggleCanEdit(mode: NoteViewMode) {
|
||||
super.toggleCanEdit(mode)
|
||||
textFormatMenu.isVisible = mode == NoteViewMode.EDIT
|
||||
when {
|
||||
mode == NoteViewMode.EDIT -> showKeyboard(binding.EnterBody)
|
||||
binding.EnterBody.isFocused -> hideKeyboard(binding.EnterBody)
|
||||
}
|
||||
binding.EnterBody.setCanEdit(mode == NoteViewMode.EDIT)
|
||||
setupEditor()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.apply {
|
||||
|
@ -172,78 +185,8 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
|
|||
|
||||
private fun setupEditor() {
|
||||
setupMovementMethod()
|
||||
|
||||
binding.EnterBody.customSelectionActionModeCallback =
|
||||
object : ActionMode.Callback {
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?) = false
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
binding.EnterBody.isActionModeOn = true
|
||||
// Try block is there because this will crash on MiUI as Xiaomi has a broken
|
||||
// ActionMode implementation
|
||||
try {
|
||||
menu?.apply {
|
||||
add(R.string.link, 0, showAsAction = MenuItem.SHOW_AS_ACTION_NEVER) {
|
||||
binding.EnterBody.showAddLinkDialog(
|
||||
this@EditNoteActivity,
|
||||
mode = mode,
|
||||
)
|
||||
}
|
||||
add(R.string.bold, 0, showAsAction = MenuItem.SHOW_AS_ACTION_NEVER) {
|
||||
binding.EnterBody.applySpan(StyleSpan(Typeface.BOLD))
|
||||
mode?.finish()
|
||||
}
|
||||
add(R.string.italic, 0, showAsAction = MenuItem.SHOW_AS_ACTION_NEVER) {
|
||||
binding.EnterBody.applySpan(StyleSpan(Typeface.ITALIC))
|
||||
mode?.finish()
|
||||
}
|
||||
add(
|
||||
R.string.monospace,
|
||||
0,
|
||||
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER,
|
||||
) {
|
||||
binding.EnterBody.applySpan(TypefaceSpan("monospace"))
|
||||
mode?.finish()
|
||||
}
|
||||
add(
|
||||
R.string.strikethrough,
|
||||
0,
|
||||
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER,
|
||||
) {
|
||||
binding.EnterBody.applySpan(StrikethroughSpan())
|
||||
mode?.finish()
|
||||
}
|
||||
add(
|
||||
R.string.clear_formatting,
|
||||
0,
|
||||
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER,
|
||||
) {
|
||||
binding.EnterBody.clearFormatting()
|
||||
mode?.finish()
|
||||
}
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
binding.EnterBody.isActionModeOn = false
|
||||
}
|
||||
}
|
||||
|
||||
binding.ContentLayout.setOnClickListener {
|
||||
binding.EnterBody.apply {
|
||||
requestFocus()
|
||||
setSelection(length())
|
||||
showKeyboard(this)
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.EnterBody.customInsertionActionModeCallback =
|
||||
if (canEdit) {
|
||||
object : ActionMode.Callback {
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false
|
||||
|
||||
|
@ -255,8 +198,54 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
|
|||
// ActionMode implementation
|
||||
try {
|
||||
menu?.apply {
|
||||
add(R.string.link_note, 0, order = Menu.CATEGORY_CONTAINER + 1) {
|
||||
linkNote(pickNoteNewActivityResultLauncher)
|
||||
add(
|
||||
R.string.link,
|
||||
0,
|
||||
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER,
|
||||
) {
|
||||
binding.EnterBody.showAddLinkDialog(
|
||||
this@EditNoteActivity,
|
||||
mode = mode,
|
||||
)
|
||||
}
|
||||
add(
|
||||
R.string.bold,
|
||||
0,
|
||||
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER,
|
||||
) {
|
||||
binding.EnterBody.applySpan(createBoldSpan())
|
||||
mode?.finish()
|
||||
}
|
||||
add(
|
||||
R.string.italic,
|
||||
0,
|
||||
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER,
|
||||
) {
|
||||
binding.EnterBody.applySpan(StyleSpan(Typeface.ITALIC))
|
||||
mode?.finish()
|
||||
}
|
||||
add(
|
||||
R.string.monospace,
|
||||
0,
|
||||
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER,
|
||||
) {
|
||||
binding.EnterBody.applySpan(TypefaceSpan("monospace"))
|
||||
mode?.finish()
|
||||
}
|
||||
add(
|
||||
R.string.strikethrough,
|
||||
0,
|
||||
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER,
|
||||
) {
|
||||
binding.EnterBody.applySpan(StrikethroughSpan())
|
||||
mode?.finish()
|
||||
}
|
||||
add(
|
||||
R.string.clear_formatting,
|
||||
0,
|
||||
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER,
|
||||
) {
|
||||
binding.EnterBody.clearFormatting()
|
||||
mode?.finish()
|
||||
}
|
||||
}
|
||||
|
@ -270,26 +259,69 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
|
|||
binding.EnterBody.isActionModeOn = false
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.EnterBody.customInsertionActionModeCallback =
|
||||
if (canEdit) {
|
||||
object : ActionMode.Callback {
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?) = false
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
binding.EnterBody.isActionModeOn = true
|
||||
// Try block is there because this will crash on MiUI as Xiaomi has a
|
||||
// broken
|
||||
// ActionMode implementation
|
||||
try {
|
||||
menu?.apply {
|
||||
add(
|
||||
R.string.link_note,
|
||||
0,
|
||||
order = Menu.CATEGORY_CONTAINER + 1,
|
||||
) {
|
||||
linkNote(pickNoteNewActivityResultLauncher)
|
||||
mode?.finish()
|
||||
}
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
binding.EnterBody.isActionModeOn = false
|
||||
}
|
||||
}
|
||||
} else null
|
||||
}
|
||||
binding.EnterBody.setOnSelectionChange { selStart, selEnd ->
|
||||
if (selEnd - selStart > 0) {
|
||||
if (!textFormatMenu.isEnabled) {
|
||||
initBottomTextFormattingMenu()
|
||||
if (canEdit) {
|
||||
binding.EnterBody.setOnSelectionChange { selStart, selEnd ->
|
||||
if (selEnd - selStart > 0) {
|
||||
if (!textFormatMenu.isEnabled) {
|
||||
initBottomTextFormattingMenu()
|
||||
}
|
||||
textFormatMenu.isEnabled = true
|
||||
textFormattingAdapter?.updateTextFormattingToggles(selStart, selEnd)
|
||||
} else {
|
||||
if (textFormatMenu.isEnabled) {
|
||||
initBottomMenu()
|
||||
}
|
||||
textFormatMenu.isEnabled = false
|
||||
}
|
||||
textFormatMenu.isEnabled = true
|
||||
textFormattingAdapter?.updateTextFormattingToggles(selStart, selEnd)
|
||||
} else {
|
||||
if (textFormatMenu.isEnabled) {
|
||||
initBottomMenu()
|
||||
}
|
||||
textFormatMenu.isEnabled = false
|
||||
}
|
||||
} else {
|
||||
binding.EnterBody.setOnSelectionChange { _, _ -> }
|
||||
}
|
||||
binding.ContentLayout.setOnClickListener {
|
||||
binding.EnterBody.apply {
|
||||
requestFocus()
|
||||
setSelection(length())
|
||||
showKeyboard(this)
|
||||
if (canEdit) {
|
||||
setSelection(length())
|
||||
showKeyboard(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -366,19 +398,23 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
|
|||
val movementMethod = LinkMovementMethod { span ->
|
||||
val items =
|
||||
if (span.url.isNoteUrl()) {
|
||||
arrayOf(
|
||||
getString(R.string.remove_link),
|
||||
getString(R.string.change_note),
|
||||
getString(R.string.edit),
|
||||
getString(R.string.open_note),
|
||||
)
|
||||
if (canEdit) {
|
||||
arrayOf(
|
||||
getString(R.string.open_note),
|
||||
getString(R.string.remove_link),
|
||||
getString(R.string.change_note),
|
||||
getString(R.string.edit),
|
||||
)
|
||||
} else arrayOf(getString(R.string.open_note))
|
||||
} else {
|
||||
arrayOf(
|
||||
getString(R.string.remove_link),
|
||||
getString(R.string.copy),
|
||||
getString(R.string.edit),
|
||||
getString(R.string.open_link),
|
||||
)
|
||||
if (canEdit) {
|
||||
arrayOf(
|
||||
getString(R.string.open_link),
|
||||
getString(R.string.copy),
|
||||
getString(R.string.remove_link),
|
||||
getString(R.string.edit),
|
||||
)
|
||||
} else arrayOf(getString(R.string.open_link), getString(R.string.copy))
|
||||
}
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(
|
||||
|
@ -390,35 +426,16 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
|
|||
)
|
||||
.setItems(items) { _, which ->
|
||||
when (which) {
|
||||
0 -> {
|
||||
binding.EnterBody.removeSpanWithHistory(
|
||||
span,
|
||||
span.url.isNoteUrl() ||
|
||||
span.url == binding.EnterBody.getSpanText(span),
|
||||
)
|
||||
}
|
||||
0 -> openLink(span)
|
||||
1 ->
|
||||
if (span.url.isNoteUrl()) {
|
||||
selectedSpan = span
|
||||
linkNote(pickNoteUpdateActivityResultLauncher)
|
||||
} else {
|
||||
copyToClipBoard(span.url)
|
||||
showToast(R.string.copied_link)
|
||||
}
|
||||
|
||||
2 -> {
|
||||
binding.EnterBody.showEditDialog(span)
|
||||
}
|
||||
|
||||
3 -> {
|
||||
span.url?.let {
|
||||
if (it.isNoteUrl()) {
|
||||
span.navigateToNote()
|
||||
} else {
|
||||
openLink(span.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
removeLink(span)
|
||||
} else copyLink(span)
|
||||
2 ->
|
||||
if (span.url.isNoteUrl()) {
|
||||
changeNoteLink(span)
|
||||
} else removeLink(span)
|
||||
3 -> editLink(span)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
@ -426,6 +443,37 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
|
|||
binding.EnterBody.movementMethod = movementMethod
|
||||
}
|
||||
|
||||
private fun openLink(span: URLSpan) {
|
||||
span.url?.let {
|
||||
if (it.isNoteUrl()) {
|
||||
span.navigateToNote()
|
||||
} else {
|
||||
openLink(span.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun editLink(span: URLSpan) {
|
||||
binding.EnterBody.showEditDialog(span)
|
||||
}
|
||||
|
||||
private fun changeNoteLink(span: URLSpan) {
|
||||
selectedSpan = span
|
||||
linkNote(pickNoteUpdateActivityResultLauncher)
|
||||
}
|
||||
|
||||
private fun copyLink(span: URLSpan) {
|
||||
copyToClipBoard(span.url)
|
||||
showToast(R.string.copied_link)
|
||||
}
|
||||
|
||||
private fun removeLink(span: URLSpan) {
|
||||
binding.EnterBody.removeSpanWithHistory(
|
||||
span,
|
||||
span.url.isNoteUrl() || span.url == binding.EnterBody.getSpanText(span),
|
||||
)
|
||||
}
|
||||
|
||||
private fun openLink(url: String) {
|
||||
val uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri).wrapWithChooser(this)
|
||||
|
|
|
@ -51,7 +51,8 @@ open class PickNoteActivity : LockedActivity<ActivityPickNoteBinding>(), ItemLis
|
|||
maxItems.value,
|
||||
maxLines.value,
|
||||
maxTitle.value,
|
||||
labelsHiddenInOverview.value,
|
||||
labelTagsHiddenInOverview.value,
|
||||
imagesHiddenInOverview.value,
|
||||
),
|
||||
application.getExternalImagesDirectory(),
|
||||
this@PickNoteActivity,
|
||||
|
@ -71,6 +72,7 @@ open class PickNoteActivity : LockedActivity<ActivityPickNoteBinding>(), ItemLis
|
|||
|
||||
val pinned = Header(getString(R.string.pinned))
|
||||
val others = Header(getString(R.string.others))
|
||||
val archived = Header(getString(R.string.archived))
|
||||
|
||||
database.observe(this) {
|
||||
lifecycleScope.launch {
|
||||
|
@ -78,7 +80,7 @@ open class PickNoteActivity : LockedActivity<ActivityPickNoteBinding>(), ItemLis
|
|||
withContext(Dispatchers.IO) {
|
||||
val raw =
|
||||
it.getBaseNoteDao().getAllNotes().filter { it.id != excludedNoteId }
|
||||
BaseNoteModel.transform(raw, pinned, others)
|
||||
BaseNoteModel.transform(raw, pinned, others, archived)
|
||||
}
|
||||
adapter.submitList(notes)
|
||||
binding.EmptyView.visibility =
|
||||
|
|
|
@ -31,7 +31,6 @@ import com.philkes.notallyx.presentation.bindLabels
|
|||
import com.philkes.notallyx.presentation.displayFormattedTimestamp
|
||||
import com.philkes.notallyx.presentation.dp
|
||||
import com.philkes.notallyx.presentation.extractColor
|
||||
import com.philkes.notallyx.presentation.getColorFromAttr
|
||||
import com.philkes.notallyx.presentation.getQuantityString
|
||||
import com.philkes.notallyx.presentation.setControlsContrastColorForAllViews
|
||||
import com.philkes.notallyx.presentation.view.misc.ItemListener
|
||||
|
@ -46,6 +45,7 @@ data class BaseNoteVHPreferences(
|
|||
val maxLines: Int,
|
||||
val maxTitleLines: Int,
|
||||
val hideLabels: Boolean,
|
||||
val hideImages: Boolean,
|
||||
)
|
||||
|
||||
class BaseNoteVH(
|
||||
|
@ -203,21 +203,15 @@ class BaseNoteVH(
|
|||
|
||||
private fun setColor(color: String) {
|
||||
binding.root.apply {
|
||||
if (color == BaseNote.COLOR_DEFAULT) {
|
||||
setCardBackgroundColor(0)
|
||||
setControlsContrastColorForAllViews(context.getColorFromAttr(R.attr.colorSurface))
|
||||
} else {
|
||||
val colorInt = context.extractColor(color)
|
||||
setCardBackgroundColor(colorInt)
|
||||
setControlsContrastColorForAllViews(colorInt)
|
||||
}
|
||||
val colorInt = context.extractColor(color)
|
||||
setCardBackgroundColor(colorInt)
|
||||
setControlsContrastColorForAllViews(colorInt)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setImages(images: List<FileAttachment>, mediaRoot: File?) {
|
||||
|
||||
binding.apply {
|
||||
if (images.isNotEmpty()) {
|
||||
if (images.isNotEmpty() && !preferences.hideImages) {
|
||||
ImageView.visibility = VISIBLE
|
||||
Message.visibility = GONE
|
||||
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
package com.philkes.notallyx.presentation.view.misc
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.text.method.KeyListener
|
||||
import android.util.AttributeSet
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import com.philkes.notallyx.presentation.clone
|
||||
import com.philkes.notallyx.presentation.showKeyboard
|
||||
|
||||
open class EditTextWithWatcher(context: Context, attrs: AttributeSet) :
|
||||
AppCompatEditText(context, attrs) {
|
||||
var textWatcher: TextWatcher? = null
|
||||
private var onSelectionChange: ((selStart: Int, selEnd: Int) -> Unit)? = null
|
||||
private var keyListenerInstance: KeyListener? = null
|
||||
|
||||
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
||||
super.onSelectionChanged(selStart, selEnd)
|
||||
|
@ -30,33 +34,60 @@ open class EditTextWithWatcher(context: Context, attrs: AttributeSet) :
|
|||
super.setText(text, BufferType.EDITABLE)
|
||||
}
|
||||
|
||||
fun setCanEdit(value: Boolean) {
|
||||
if (!value) {
|
||||
clearFocus()
|
||||
}
|
||||
keyListener?.let { keyListenerInstance = it }
|
||||
keyListener = if (value) keyListenerInstance else null // Disables text editing
|
||||
isCursorVisible = true
|
||||
isFocusable = value
|
||||
isFocusableInTouchMode = value
|
||||
setTextIsSelectable(true)
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) {
|
||||
setOnClickListener {
|
||||
if (value) {
|
||||
context.showKeyboard(this)
|
||||
}
|
||||
}
|
||||
setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus && value) {
|
||||
context.showKeyboard(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"You should not access text Editable directly, use other member functions to edit/read text properties.",
|
||||
replaceWith = ReplaceWith("changeText/applyWithoutTextWatcher/..."),
|
||||
)
|
||||
override fun getText(): Editable? {
|
||||
return super.getText()
|
||||
return getTextSafe()
|
||||
}
|
||||
|
||||
fun getTextClone(): Editable {
|
||||
return super.getText()!!.clone()
|
||||
return getTextSafe().clone()
|
||||
}
|
||||
|
||||
fun applyWithoutTextWatcher(
|
||||
callback: EditTextWithWatcher.() -> Unit
|
||||
): Pair<Editable, Editable> {
|
||||
val textBefore = super.getText()!!.clone()
|
||||
val textBefore = getTextClone()
|
||||
val editTextWatcher = textWatcher
|
||||
editTextWatcher?.let { removeTextChangedListener(it) }
|
||||
callback()
|
||||
editTextWatcher?.let { addTextChangedListener(it) }
|
||||
return Pair(textBefore, super.getText()!!.clone())
|
||||
return Pair(textBefore, getTextClone())
|
||||
}
|
||||
|
||||
fun changeText(callback: (text: Editable) -> Unit): Pair<Editable, Editable> {
|
||||
return applyWithoutTextWatcher { callback(super.getText()!!) }
|
||||
return applyWithoutTextWatcher { callback(getTextSafe()!!) }
|
||||
}
|
||||
|
||||
private fun getTextSafe() = super.getText() ?: Editable.Factory.getInstance().newEditable("")
|
||||
|
||||
fun focusAndSelect(
|
||||
start: Int = selectionStart,
|
||||
end: Int = selectionEnd,
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.text.style.TypefaceSpan
|
|||
import android.text.style.URLSpan
|
||||
import androidx.annotation.ColorInt
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.presentation.createBoldSpan
|
||||
import com.philkes.notallyx.presentation.view.misc.StylableEditTextWithHistory
|
||||
|
||||
class TextFormattingAdapter(
|
||||
|
@ -35,7 +36,7 @@ class TextFormattingAdapter(
|
|||
private val bold: Toggle =
|
||||
Toggle(R.string.bold, R.drawable.format_bold, false) {
|
||||
if (!it.checked) {
|
||||
editText.applySpan(StyleSpan(Typeface.BOLD))
|
||||
editText.applySpan(createBoldSpan())
|
||||
} else {
|
||||
editText.clearFormatting(type = StylableEditTextWithHistory.TextStyleType.BOLD)
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ class MoreNoteBottomSheet(
|
|||
setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
|
||||
setOnClickListener {
|
||||
callbacks.export(mimeType)
|
||||
fragment.hide()
|
||||
fragment.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import com.philkes.notallyx.data.model.ListItem
|
|||
import com.philkes.notallyx.data.model.deepCopy
|
||||
import com.philkes.notallyx.data.model.findChild
|
||||
import com.philkes.notallyx.data.model.plus
|
||||
import com.philkes.notallyx.data.model.shouldParentBeChecked
|
||||
import com.philkes.notallyx.data.model.shouldParentBeUnchecked
|
||||
import com.philkes.notallyx.utils.filter
|
||||
import com.philkes.notallyx.utils.forEach
|
||||
import com.philkes.notallyx.utils.indices
|
||||
|
@ -149,9 +151,20 @@ fun <R> List<R>.getOrNull(index: Int) = if (lastIndex >= index) this[index] else
|
|||
fun Collection<ListItem>.init(resetIds: Boolean = true): List<ListItem> {
|
||||
val initializedItems = deepCopy()
|
||||
initList(initializedItems, resetIds)
|
||||
checkBrokenList(initializedItems)
|
||||
return initializedItems
|
||||
}
|
||||
|
||||
private fun checkBrokenList(list: List<ListItem>) {
|
||||
list.forEach { listItem ->
|
||||
if (listItem.shouldParentBeChecked()) {
|
||||
listItem.checked = true
|
||||
} else if (listItem.shouldParentBeUnchecked()) {
|
||||
listItem.checked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initList(list: List<ListItem>, resetIds: Boolean) {
|
||||
if (resetIds) {
|
||||
list.forEachIndexed { index, item -> item.id = index }
|
||||
|
@ -250,7 +263,7 @@ fun SortedList<ListItem>.findParentsByChecked(checked: Boolean): List<ListItem>
|
|||
fun SortedList<ListItem>.deleteCheckedItems() {
|
||||
mapIndexed { index, listItem -> Pair(index, listItem) }
|
||||
.filter { it.second.checked }
|
||||
.sortedBy { it.second.isChild }
|
||||
.sortedBy { !it.second.isChild }
|
||||
.forEach { remove(it.second) }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.philkes.notallyx.presentation.view.note.listitem
|
||||
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
|
@ -84,28 +83,28 @@ class ListManager(
|
|||
|
||||
internal fun setState(state: ListState) {
|
||||
adapter.submitList(state.items) {
|
||||
// Focus item's EditText and set cursor position
|
||||
state.focusedItemPos?.let { itemPos ->
|
||||
recyclerView.post {
|
||||
if (itemPos in 0..items.size) {
|
||||
recyclerView.smoothScrollToPosition(itemPos)
|
||||
(recyclerView.findViewHolderForAdapterPosition(itemPos) as? ListItemVH?)
|
||||
?.let { viewHolder ->
|
||||
inputMethodManager?.let { inputManager ->
|
||||
val maxCursorPos = viewHolder.binding.EditText.length()
|
||||
viewHolder.focusEditText(
|
||||
selectionStart =
|
||||
state.cursorPos?.coerceIn(0, maxCursorPos)
|
||||
?: maxCursorPos,
|
||||
inputMethodManager = inputManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
state.focusedItemPos?.let { itemPos -> focusItem(itemPos, state.cursorPos) }
|
||||
}
|
||||
this.itemsChecked?.setItems(state.checkedItems!!)
|
||||
}
|
||||
|
||||
private fun focusItem(itemPos: Int, cursorPos: Int?) {
|
||||
// Focus item's EditText and set cursor position
|
||||
recyclerView.post {
|
||||
if (itemPos in 0..items.size) {
|
||||
recyclerView.smoothScrollToPosition(itemPos)
|
||||
(recyclerView.findViewHolderForAdapterPosition(itemPos) as? ListItemVH?)?.let {
|
||||
viewHolder ->
|
||||
inputMethodManager?.let { inputManager ->
|
||||
val maxCursorPos = viewHolder.binding.EditText.length()
|
||||
viewHolder.focusEditText(
|
||||
selectionStart = cursorPos?.coerceIn(0, maxCursorPos) ?: maxCursorPos,
|
||||
inputMethodManager = inputManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.itemsChecked?.setItems(state.checkedItems!!)
|
||||
}
|
||||
|
||||
fun add(
|
||||
|
@ -282,23 +281,15 @@ class ListManager(
|
|||
}
|
||||
}
|
||||
|
||||
fun changeText(
|
||||
position: Int,
|
||||
value: EditTextState,
|
||||
before: EditTextState? = null,
|
||||
pushChange: Boolean = true,
|
||||
editText: EditText?,
|
||||
listener: TextWatcher?,
|
||||
) {
|
||||
fun changeText(position: Int, value: EditTextState, pushChange: Boolean = true) {
|
||||
val stateBefore = getState()
|
||||
// if(!pushChange) {
|
||||
endSearch?.invoke()
|
||||
// }
|
||||
val item = items[position]
|
||||
item.body = value.text.toString()
|
||||
if (pushChange) {
|
||||
changeHistory.push(
|
||||
ListEditTextChange(editText!!, position, before!!, value, listener!!, this)
|
||||
)
|
||||
changeHistory.push(ListEditTextChange(stateBefore, getState(), this))
|
||||
// TODO: fix focus change
|
||||
// refreshSearch?.invoke(editText)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.core.widget.NestedScrollView
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.presentation.view.note.listitem.HighlightText
|
||||
import com.philkes.notallyx.presentation.view.note.listitem.ListManager
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences
|
||||
|
@ -38,6 +39,12 @@ class CheckedListItemAdapter(
|
|||
override fun getItem(position: Int): ListItem = list[position]
|
||||
}
|
||||
|
||||
var viewMode: NoteViewMode = NoteViewMode.EDIT
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
internal fun setList(list: SortedList<ListItem>) {
|
||||
this.list = list
|
||||
}
|
||||
|
@ -51,7 +58,7 @@ class CheckedListItemAdapter(
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ListItemVH, position: Int) {
|
||||
itemAdapterBase.onBindViewHolder(holder, position)
|
||||
itemAdapterBase.onBindViewHolder(holder, position, viewMode)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.recyclerview.widget.DiffUtil
|
|||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.presentation.view.note.listitem.HighlightText
|
||||
import com.philkes.notallyx.presentation.view.note.listitem.ListManager
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences
|
||||
|
@ -37,6 +38,12 @@ class ListItemAdapter(
|
|||
override fun getItem(position: Int): ListItem = this@ListItemAdapter.getItem(position)
|
||||
}
|
||||
|
||||
var viewMode: NoteViewMode = NoteViewMode.EDIT
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
lateinit var items: MutableList<ListItem>
|
||||
private set
|
||||
|
||||
|
@ -55,7 +62,7 @@ class ListItemAdapter(
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ListItemVH, position: Int) {
|
||||
itemAdapterBase.onBindViewHolder(holder, position)
|
||||
itemAdapterBase.onBindViewHolder(holder, position, viewMode)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.core.widget.NestedScrollView
|
|||
import androidx.recyclerview.widget.NestedScrollViewItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.databinding.RecyclerListItemBinding
|
||||
import com.philkes.notallyx.presentation.view.note.listitem.ListItemDragCallback
|
||||
import com.philkes.notallyx.presentation.view.note.listitem.ListManager
|
||||
|
@ -40,7 +41,7 @@ abstract class ListItemAdapterBase(
|
|||
touchHelper.attachToRecyclerView(recyclerView)
|
||||
}
|
||||
|
||||
fun onBindViewHolder(holder: ListItemVH, position: Int) {
|
||||
fun onBindViewHolder(holder: ListItemVH, position: Int, viewMode: NoteViewMode) {
|
||||
val item = getItem(position)
|
||||
holder.bind(
|
||||
backgroundColor,
|
||||
|
@ -48,6 +49,7 @@ abstract class ListItemAdapterBase(
|
|||
position,
|
||||
highlights[position],
|
||||
preferences.listItemSorting.value,
|
||||
viewMode,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package com.philkes.notallyx.presentation.view.note.listitem.adapter
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.util.TypedValue
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View.GONE
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener
|
||||
import android.widget.EditText
|
||||
|
@ -18,6 +21,7 @@ import cn.leaqi.drawer.SwipeDrawer.STATE_OPEN
|
|||
import com.philkes.notallyx.data.imports.txt.extractListItems
|
||||
import com.philkes.notallyx.data.imports.txt.findListSyntaxRegex
|
||||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.databinding.RecyclerListItemBinding
|
||||
import com.philkes.notallyx.presentation.clone
|
||||
import com.philkes.notallyx.presentation.createListTextWatcherWithHistory
|
||||
|
@ -29,6 +33,7 @@ import com.philkes.notallyx.presentation.view.note.listitem.firstBodyOrEmptyStri
|
|||
import com.philkes.notallyx.presentation.viewmodel.preference.ListItemSort
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.TextSize
|
||||
import com.philkes.notallyx.utils.changehistory.EditTextState
|
||||
import com.philkes.notallyx.utils.copyToClipBoard
|
||||
|
||||
class ListItemVH(
|
||||
val binding: RecyclerListItemBinding,
|
||||
|
@ -45,11 +50,6 @@ class ListItemVH(
|
|||
binding.EditText.apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, body)
|
||||
|
||||
setOnNextAction {
|
||||
val position = bindingAdapterPosition + 1
|
||||
listManager.add(position)
|
||||
}
|
||||
|
||||
textWatcher =
|
||||
createListTextWatcherWithHistory(
|
||||
listManager,
|
||||
|
@ -61,10 +61,6 @@ class ListItemVH(
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
setOnFocusChangeListener { _, hasFocus ->
|
||||
binding.Delete.visibility = if (hasFocus) VISIBLE else INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
binding.DragHandle.setOnTouchListener { _, event ->
|
||||
|
@ -95,20 +91,21 @@ class ListItemVH(
|
|||
position: Int,
|
||||
highlights: List<ListItemHighlight>?,
|
||||
autoSort: ListItemSort,
|
||||
viewMode: NoteViewMode,
|
||||
) {
|
||||
updateEditText(item, position)
|
||||
updateEditText(item, position, viewMode)
|
||||
|
||||
updateCheckBox(item, position)
|
||||
|
||||
updateDeleteButton(item, position)
|
||||
updateDeleteButton(item, position, viewMode)
|
||||
|
||||
updateSwipe(item.isChild, position != 0 && !item.checked)
|
||||
updateSwipe(item.isChild, viewMode == NoteViewMode.EDIT && position != 0 && !item.checked)
|
||||
binding.DragHandle.apply {
|
||||
visibility =
|
||||
if (item.checked && autoSort == ListItemSort.AUTO_SORT_BY_CHECKED) {
|
||||
INVISIBLE
|
||||
} else {
|
||||
VISIBLE
|
||||
when {
|
||||
viewMode != NoteViewMode.EDIT -> GONE
|
||||
item.checked && autoSort == ListItemSort.AUTO_SORT_BY_CHECKED -> INVISIBLE
|
||||
else -> VISIBLE
|
||||
}
|
||||
contentDescription = "Drag$position"
|
||||
}
|
||||
|
@ -130,9 +127,14 @@ class ListItemVH(
|
|||
binding.EditText.focusAndSelect(selectionStart, selectionEnd, inputMethodManager)
|
||||
}
|
||||
|
||||
private fun updateDeleteButton(item: ListItem, position: Int) {
|
||||
private fun updateDeleteButton(item: ListItem, position: Int, viewMode: NoteViewMode) {
|
||||
binding.Delete.apply {
|
||||
visibility = if (item.checked) VISIBLE else INVISIBLE
|
||||
visibility =
|
||||
when {
|
||||
viewMode != NoteViewMode.EDIT -> GONE
|
||||
item.checked -> VISIBLE
|
||||
else -> INVISIBLE
|
||||
}
|
||||
setOnClickListener {
|
||||
listManager.delete(absoluteAdapterPosition, inCheckedList = inCheckedList)
|
||||
}
|
||||
|
@ -140,10 +142,50 @@ class ListItemVH(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateEditText(item: ListItem, position: Int) {
|
||||
private fun updateEditText(item: ListItem, position: Int, viewMode: NoteViewMode) {
|
||||
binding.EditText.apply {
|
||||
setText(item.body)
|
||||
isEnabled = !item.checked
|
||||
paintFlags =
|
||||
if (item.checked) {
|
||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||
} else {
|
||||
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
|
||||
}
|
||||
alpha = if (item.checked) 0.5f else 1.0f
|
||||
contentDescription = "EditText$position"
|
||||
if (viewMode == NoteViewMode.EDIT) {
|
||||
setOnFocusChangeListener { _, hasFocus ->
|
||||
binding.Delete.visibility = if (hasFocus) VISIBLE else INVISIBLE
|
||||
}
|
||||
binding.Content.descendantFocusability = ViewGroup.FOCUS_BEFORE_DESCENDANTS
|
||||
} else {
|
||||
onFocusChangeListener = null
|
||||
binding.Content.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
|
||||
}
|
||||
setCanEdit(viewMode == NoteViewMode.EDIT)
|
||||
isFocusable = !item.checked
|
||||
when (viewMode) {
|
||||
NoteViewMode.EDIT -> {
|
||||
setOnClickListener(null)
|
||||
setOnLongClickListener(null)
|
||||
}
|
||||
NoteViewMode.READ_ONLY -> {
|
||||
setOnClickListener {
|
||||
if (absoluteAdapterPosition != NO_POSITION) {
|
||||
listManager.changeChecked(
|
||||
absoluteAdapterPosition,
|
||||
!item.checked,
|
||||
inCheckedList,
|
||||
)
|
||||
}
|
||||
}
|
||||
setOnLongClickListener {
|
||||
context?.copyToClipBoard(item.body)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
setOnNextAction { listManager.add(bindingAdapterPosition + 1) }
|
||||
setOnKeyListener { _, keyCode, event ->
|
||||
if (
|
||||
event.action == KeyEvent.ACTION_DOWN &&
|
||||
|
@ -162,7 +204,6 @@ class ListItemVH(
|
|||
false
|
||||
}
|
||||
}
|
||||
contentDescription = "EditText$position"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,12 +211,12 @@ class ListItemVH(
|
|||
|
||||
private fun updateCheckBox(item: ListItem, position: Int) {
|
||||
if (checkBoxListener == null) {
|
||||
checkBoxListener = OnCheckedChangeListener { buttonView, isChecked ->
|
||||
buttonView!!.setOnCheckedChangeListener(null)
|
||||
checkBoxListener = OnCheckedChangeListener { _, isChecked ->
|
||||
binding.CheckBox.setOnCheckedChangeListener(null)
|
||||
if (absoluteAdapterPosition != NO_POSITION) {
|
||||
listManager.changeChecked(absoluteAdapterPosition, isChecked, inCheckedList)
|
||||
}
|
||||
buttonView.setOnCheckedChangeListener(checkBoxListener)
|
||||
binding.CheckBox.setOnCheckedChangeListener(checkBoxListener)
|
||||
}
|
||||
}
|
||||
binding.CheckBox.apply {
|
||||
|
@ -235,13 +276,7 @@ class ListItemVH(
|
|||
private fun EditText.changeText(position: Int, after: CharSequence) {
|
||||
setText(after)
|
||||
val stateAfter = EditTextState(editableText.clone(), selectionStart)
|
||||
listManager.changeText(
|
||||
position,
|
||||
stateAfter,
|
||||
pushChange = false,
|
||||
editText = null,
|
||||
listener = null,
|
||||
)
|
||||
listManager.changeText(position, stateAfter, pushChange = false)
|
||||
}
|
||||
|
||||
fun getSelection() = with(binding.EditText) { Pair(selectionStart, selectionEnd) }
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
|
@ -13,6 +12,7 @@ import androidx.lifecycle.AndroidViewModel
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.room.withTransaction
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
@ -38,7 +38,9 @@ import com.philkes.notallyx.data.model.Item
|
|||
import com.philkes.notallyx.data.model.Label
|
||||
import com.philkes.notallyx.data.model.SearchResult
|
||||
import com.philkes.notallyx.data.model.toNoteIdReminders
|
||||
import com.philkes.notallyx.presentation.activity.main.fragment.settings.SettingsFragment.Companion.EXTRA_SHOW_IMPORT_BACKUPS_FOLDER
|
||||
import com.philkes.notallyx.presentation.getQuantityString
|
||||
import com.philkes.notallyx.presentation.restartApplication
|
||||
import com.philkes.notallyx.presentation.setCancelButton
|
||||
import com.philkes.notallyx.presentation.showToast
|
||||
import com.philkes.notallyx.presentation.view.misc.NotNullLiveData
|
||||
|
@ -114,6 +116,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
|
||||
private val pinned = Header(app.getString(R.string.pinned))
|
||||
private val others = Header(app.getString(R.string.others))
|
||||
private val archived = Header(app.getString(R.string.archived))
|
||||
|
||||
val preferences = NotallyXPreferences.getInstance(app)
|
||||
|
||||
|
@ -126,6 +129,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
val actionMode = ActionMode()
|
||||
|
||||
internal var showRefreshBackupsFolderAfterThemeChange = false
|
||||
private var labelsHiddenObserver: Observer<Set<String>>? = null
|
||||
|
||||
init {
|
||||
NotallyDatabase.getDatabase(app).observeForever(::init)
|
||||
|
@ -149,11 +153,12 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
allNotes = baseNoteDao.getAllAsync()
|
||||
allNotes!!.observeForever(allNotesObserver!!)
|
||||
|
||||
if (baseNotes == null) {
|
||||
baseNotes = Content(baseNoteDao.getFrom(Folder.NOTES), ::transform)
|
||||
} else {
|
||||
baseNotes!!.setObserver(baseNoteDao.getFrom(Folder.NOTES))
|
||||
labelsHiddenObserver?.let { preferences.labelsHidden.removeObserver(it) }
|
||||
labelsHiddenObserver = Observer { labelsHidden ->
|
||||
baseNotes = null
|
||||
initBaseNotes(labelsHidden)
|
||||
}
|
||||
preferences.labelsHidden.observeForever(labelsHiddenObserver!!)
|
||||
|
||||
if (deletedNotes == null) {
|
||||
deletedNotes = Content(baseNoteDao.getFrom(Folder.DELETED), ::transform)
|
||||
|
@ -187,6 +192,18 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun initBaseNotes(labelsHidden: Set<String>) {
|
||||
val overviewNotes =
|
||||
baseNoteDao.getFrom(Folder.NOTES).map { list ->
|
||||
list.filter { baseNote -> baseNote.labels.none { labelsHidden.contains(it) } }
|
||||
}
|
||||
if (baseNotes == null) {
|
||||
baseNotes = Content(overviewNotes, ::transform)
|
||||
} else {
|
||||
baseNotes!!.setObserver(overviewNotes)
|
||||
}
|
||||
}
|
||||
|
||||
fun getNotesByLabel(label: String): Content {
|
||||
if (labelCache[label] == null) {
|
||||
labelCache[label] = Content(baseNoteDao.getBaseNotesByLabel(label), ::transform)
|
||||
|
@ -198,7 +215,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
return Content(baseNoteDao.getBaseNotesWithoutLabel(Folder.NOTES), ::transform)
|
||||
}
|
||||
|
||||
private fun transform(list: List<BaseNote>) = transform(list, pinned, others)
|
||||
private fun transform(list: List<BaseNote>) = transform(list, pinned, others, archived)
|
||||
|
||||
fun disableBackups() {
|
||||
val value = preferences.backupsFolder.value
|
||||
|
@ -318,7 +335,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
fun importZipBackup(uri: Uri, password: String) {
|
||||
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
app.log(TAG, throwable = throwable)
|
||||
app.showToast(R.string.invalid_backup)
|
||||
app.showToast("${app.getString(R.string.invalid_backup)}: ${throwable.message}")
|
||||
}
|
||||
|
||||
val backupDir = app.getBackupDir()
|
||||
|
@ -330,7 +347,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
fun importXmlBackup(uri: Uri) {
|
||||
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
app.log(TAG, throwable = throwable)
|
||||
app.showToast(R.string.invalid_backup)
|
||||
app.showToast("${app.getString(R.string.invalid_backup)}: ${throwable.message}")
|
||||
}
|
||||
|
||||
viewModelScope.launch(exceptionHandler) {
|
||||
|
@ -348,18 +365,15 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
|
||||
fun importFromOtherApp(uri: Uri, importSource: ImportSource) {
|
||||
val database = NotallyDatabase.getDatabase(app, observePreferences = false).value
|
||||
|
||||
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Toast.makeText(
|
||||
app,
|
||||
if (throwable is ImportException) {
|
||||
throwable.textResId
|
||||
} else R.string.invalid_backup,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
app.log(TAG, throwable = throwable)
|
||||
if (throwable is ImportException) {
|
||||
app.showToast(throwable.textResId)
|
||||
} else {
|
||||
app.showToast("${app.getString(R.string.invalid_backup)}: ${throwable.message}")
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(exceptionHandler) {
|
||||
val importedNotes =
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -536,7 +550,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
|
||||
fun deleteLabel(value: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) { commonDao.deleteLabel(value) }
|
||||
val labelsHiddenPreference = preferences.labelsHiddenInNavigation
|
||||
val labelsHiddenPreference = preferences.labelsHidden
|
||||
val labelsHidden = labelsHiddenPreference.value.toMutableSet()
|
||||
if (labelsHidden.contains(value)) {
|
||||
labelsHidden.remove(value)
|
||||
|
@ -552,7 +566,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
|
||||
fun updateLabel(oldValue: String, newValue: String, onComplete: (success: Boolean) -> Unit) {
|
||||
executeAsyncWithCallback({ commonDao.updateLabel(oldValue, newValue) }, onComplete)
|
||||
val labelsHiddenPreference = preferences.labelsHiddenInNavigation
|
||||
val labelsHiddenPreference = preferences.labelsHidden
|
||||
val labelsHidden = labelsHiddenPreference.value.toMutableSet()
|
||||
if (labelsHidden.contains(oldValue)) {
|
||||
labelsHidden.remove(oldValue)
|
||||
|
@ -595,6 +609,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
clearPersistedUriPermissions(backupsFolder)
|
||||
}
|
||||
callback()
|
||||
app.restartApplication(R.id.Settings)
|
||||
}
|
||||
|
||||
fun importPreferences(
|
||||
|
@ -607,6 +622,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
val oldBackupsFolder = preferences.backupsFolder.value
|
||||
val dataInPublicFolderBefore = preferences.dataInPublicFolder.value
|
||||
val themeBefore = preferences.theme.value
|
||||
val useDynamicColorsBefore = preferences.useDynamicColors.value
|
||||
val oldStartView = preferences.startView.value
|
||||
|
||||
val success = preferences.import(context, uri)
|
||||
|
@ -618,6 +634,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
finishImportPreferences(
|
||||
oldBackupsFolder,
|
||||
themeBefore,
|
||||
useDynamicColorsBefore,
|
||||
oldStartView,
|
||||
context,
|
||||
askForUriPermissions,
|
||||
|
@ -631,6 +648,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
finishImportPreferences(
|
||||
oldBackupsFolder,
|
||||
themeBefore,
|
||||
useDynamicColorsBefore,
|
||||
oldStartView,
|
||||
context,
|
||||
askForUriPermissions,
|
||||
|
@ -644,15 +662,18 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
private fun finishImportPreferences(
|
||||
oldBackupsFolder: String,
|
||||
themeBefore: Theme,
|
||||
useDynamicColorsBefore: Boolean,
|
||||
oldStartView: String,
|
||||
context: Context,
|
||||
askForUriPermissions: (uri: Uri) -> Unit,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
val backupFolder = preferences.backupsFolder.getFreshValue()
|
||||
val hasUseDynamicColorsChange =
|
||||
useDynamicColorsBefore != preferences.useDynamicColors.getFreshValue()
|
||||
if (oldBackupsFolder != backupFolder) {
|
||||
showRefreshBackupsFolderAfterThemeChange = true
|
||||
if (themeBefore == preferences.theme.getFreshValue()) {
|
||||
if (themeBefore == preferences.theme.getFreshValue() && !hasUseDynamicColorsChange) {
|
||||
refreshBackupsFolder(context, backupFolder, askForUriPermissions)
|
||||
}
|
||||
} else {
|
||||
|
@ -664,6 +685,9 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
}
|
||||
preferences.theme.refresh()
|
||||
callback()
|
||||
if (showRefreshBackupsFolderAfterThemeChange) {
|
||||
app.restartApplication(R.id.Settings, EXTRA_SHOW_IMPORT_BACKUPS_FOLDER to true)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshBackupsFolder(
|
||||
|
@ -716,24 +740,35 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
|
|||
const val CURRENT_LABEL_EMPTY = ""
|
||||
val CURRENT_LABEL_NONE: String? = null
|
||||
|
||||
fun transform(list: List<BaseNote>, pinned: Header, others: Header): List<Item> {
|
||||
fun transform(
|
||||
list: List<BaseNote>,
|
||||
pinned: Header,
|
||||
others: Header,
|
||||
archived: Header,
|
||||
): List<Item> {
|
||||
if (list.isEmpty()) {
|
||||
return list
|
||||
} else {
|
||||
val firstNote = list[0]
|
||||
return if (firstNote.pinned) {
|
||||
val newList = ArrayList<Item>(list.size + 2)
|
||||
newList.add(pinned)
|
||||
|
||||
val firstUnpinnedNote = list.indexOfFirst { baseNote -> !baseNote.pinned }
|
||||
list.forEachIndexed { index, baseNote ->
|
||||
if (index == firstUnpinnedNote) {
|
||||
newList.add(others)
|
||||
}
|
||||
newList.add(baseNote)
|
||||
val firstPinnedNote = list.indexOfFirst { baseNote -> baseNote.pinned }
|
||||
val firstUnpinnedNote =
|
||||
list.indexOfFirst { baseNote ->
|
||||
!baseNote.pinned && baseNote.folder != Folder.ARCHIVED
|
||||
}
|
||||
newList
|
||||
} else list
|
||||
val mutableList: MutableList<Item> = list.toMutableList()
|
||||
if (firstPinnedNote != -1) {
|
||||
mutableList.add(firstPinnedNote, pinned)
|
||||
if (firstUnpinnedNote != -1) {
|
||||
mutableList.add(firstUnpinnedNote + 1, others)
|
||||
}
|
||||
}
|
||||
val firstArchivedNote =
|
||||
mutableList.indexOfFirst { item ->
|
||||
item is BaseNote && item.folder == Folder.ARCHIVED
|
||||
}
|
||||
if (firstArchivedNote != -1) {
|
||||
mutableList.add(firstArchivedNote, archived)
|
||||
}
|
||||
return mutableList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,14 @@ import com.philkes.notallyx.R
|
|||
import com.philkes.notallyx.data.NotallyDatabase
|
||||
import com.philkes.notallyx.data.dao.BaseNoteDao
|
||||
import com.philkes.notallyx.data.dao.NoteIdReminder
|
||||
import com.philkes.notallyx.data.imports.txt.extractListItems
|
||||
import com.philkes.notallyx.data.imports.txt.findListSyntaxRegex
|
||||
import com.philkes.notallyx.data.model.Audio
|
||||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import com.philkes.notallyx.data.model.FileAttachment
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.data.model.Reminder
|
||||
import com.philkes.notallyx.data.model.SpanRepresentation
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
|
@ -40,7 +43,7 @@ import com.philkes.notallyx.presentation.widget.WidgetProvider
|
|||
import com.philkes.notallyx.utils.Cache
|
||||
import com.philkes.notallyx.utils.Event
|
||||
import com.philkes.notallyx.utils.FileError
|
||||
import com.philkes.notallyx.utils.backup.checkAutoSave
|
||||
import com.philkes.notallyx.utils.backup.checkBackupOnSave
|
||||
import com.philkes.notallyx.utils.backup.importAudio
|
||||
import com.philkes.notallyx.utils.backup.importFile
|
||||
import com.philkes.notallyx.utils.cancelNoteReminders
|
||||
|
@ -84,10 +87,13 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) {
|
|||
var body: Editable = SpannableStringBuilder()
|
||||
|
||||
val items = ArrayList<ListItem>()
|
||||
|
||||
val images = NotNullLiveData<List<FileAttachment>>(emptyList())
|
||||
val files = NotNullLiveData<List<FileAttachment>>(emptyList())
|
||||
val audios = NotNullLiveData<List<Audio>>(emptyList())
|
||||
|
||||
val reminders = NotNullLiveData<List<Reminder>>(emptyList())
|
||||
val viewMode = NotNullLiveData(NoteViewMode.EDIT)
|
||||
|
||||
val addingFiles = MutableLiveData<Progress>()
|
||||
val eventBus = MutableLiveData<Event<List<FileError>>>()
|
||||
|
@ -246,6 +252,7 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) {
|
|||
files.value = baseNote.files
|
||||
audios.value = baseNote.audios
|
||||
reminders.value = baseNote.reminders
|
||||
viewMode.value = baseNote.viewMode
|
||||
} else {
|
||||
originalNote = createBaseNote()
|
||||
app.showToast(R.string.cant_find_note)
|
||||
|
@ -268,7 +275,7 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) {
|
|||
withContext(Dispatchers.IO) { app.deleteAttachments(attachments) }
|
||||
}
|
||||
if (checkAutoSave) {
|
||||
app.checkAutoSave(preferences, forceFullBackup = true)
|
||||
app.checkBackupOnSave(preferences, forceFullBackup = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,19 +284,26 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) {
|
|||
this.items.addAll(items)
|
||||
}
|
||||
|
||||
suspend fun saveNote(): Long {
|
||||
suspend fun saveNote(checkBackupOnSave: Boolean = true): Long {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val note = getBaseNote()
|
||||
val id = baseNoteDao.insert(note)
|
||||
app.checkAutoSave(
|
||||
preferences,
|
||||
note = note,
|
||||
forceFullBackup = originalNote?.attachmentsDifferFrom(note) == true,
|
||||
)
|
||||
if (checkBackupOnSave) {
|
||||
checkBackupOnSave(note)
|
||||
}
|
||||
originalNote = note.deepCopy()
|
||||
return@withContext id
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkBackupOnSave(note: BaseNote = getBaseNote()) {
|
||||
app.checkBackupOnSave(
|
||||
preferences,
|
||||
note = note,
|
||||
forceFullBackup = originalNote?.attachmentsDifferFrom(note) == true,
|
||||
)
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return title.isEmpty() &&
|
||||
body.isEmpty() &&
|
||||
|
@ -336,6 +350,7 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) {
|
|||
files.value,
|
||||
audios.value,
|
||||
reminders.value,
|
||||
viewMode.value,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -434,6 +449,34 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) {
|
|||
withContext(Dispatchers.IO) { baseNoteDao.updateReminders(id, updatedReminders) }
|
||||
}
|
||||
|
||||
suspend fun convertTo(noteType: Type) {
|
||||
when (noteType) {
|
||||
Type.NOTE -> {
|
||||
body = SpannableStringBuilder(items.joinToString(separator = "\n") { it.body })
|
||||
type = Type.NOTE
|
||||
setItems(ArrayList())
|
||||
}
|
||||
Type.LIST -> {
|
||||
val text = body.toString()
|
||||
val listSyntaxRegex =
|
||||
text.findListSyntaxRegex(checkContains = true, plainNewLineAllowed = true)
|
||||
if (listSyntaxRegex != null) {
|
||||
setItems(text.extractListItems(listSyntaxRegex))
|
||||
} else {
|
||||
setItems(
|
||||
text.lines().mapIndexed { idx, itemText ->
|
||||
ListItem(itemText, false, false, idx, mutableListOf())
|
||||
}
|
||||
)
|
||||
}
|
||||
type = Type.LIST
|
||||
body = SpannableStringBuilder()
|
||||
}
|
||||
}
|
||||
Cache.list = ArrayList()
|
||||
saveNote(checkBackupOnSave = false)
|
||||
}
|
||||
|
||||
enum class FileType {
|
||||
IMAGE,
|
||||
ANY,
|
||||
|
|
|
@ -29,6 +29,7 @@ class NotallyXPreferences private constructor(private val context: Context) {
|
|||
}
|
||||
|
||||
val theme = createEnumPreference(preferences, "theme", Theme.FOLLOW_SYSTEM, R.string.theme)
|
||||
val useDynamicColors = BooleanPreference("useDynamicColors", preferences, false)
|
||||
val textSize =
|
||||
createEnumPreference(preferences, "textSize", TextSize.MEDIUM, R.string.text_size)
|
||||
val dateFormat =
|
||||
|
@ -75,15 +76,21 @@ class NotallyXPreferences private constructor(private val context: Context) {
|
|||
10,
|
||||
R.string.max_lines_to_display_title,
|
||||
)
|
||||
val labelsHiddenInNavigation =
|
||||
StringSetPreference("labelsHiddenInNavigation", preferences, setOf())
|
||||
val labelsHiddenInOverview =
|
||||
val labelsHidden = StringSetPreference("labelsHiddenInNavigation", preferences, setOf())
|
||||
val labelTagsHiddenInOverview =
|
||||
BooleanPreference(
|
||||
"labelsHiddenInOverview",
|
||||
preferences,
|
||||
false,
|
||||
R.string.labels_hidden_in_overview_title,
|
||||
)
|
||||
val imagesHiddenInOverview =
|
||||
BooleanPreference(
|
||||
"imagesHiddenInOverview",
|
||||
preferences,
|
||||
false,
|
||||
R.string.images_hidden_in_overview_title,
|
||||
)
|
||||
val maxLabels =
|
||||
IntPreference(
|
||||
"maxLabelsInNavigation",
|
||||
|
@ -111,6 +118,16 @@ class NotallyXPreferences private constructor(private val context: Context) {
|
|||
)
|
||||
}
|
||||
|
||||
val autoSaveAfterIdleTime =
|
||||
IntPreference(
|
||||
"autoSaveAfterIdleTime",
|
||||
preferences,
|
||||
5,
|
||||
-1,
|
||||
20,
|
||||
R.string.auto_save_after_idle_time,
|
||||
)
|
||||
|
||||
val biometricLock =
|
||||
createEnumPreference(
|
||||
preferences,
|
||||
|
@ -125,6 +142,8 @@ class NotallyXPreferences private constructor(private val context: Context) {
|
|||
val fallbackDatabaseEncryptionKey by lazy {
|
||||
ByteArrayPreference("fallback_database_encryption_key", encryptedPreferences, ByteArray(0))
|
||||
}
|
||||
val secureFlag =
|
||||
BooleanPreference("secureFlag", preferences, false, R.string.disallow_screenshots)
|
||||
|
||||
val dataInPublicFolder =
|
||||
BooleanPreference("dataOnExternalStorage", preferences, false, R.string.data_in_public)
|
||||
|
@ -193,7 +212,7 @@ class NotallyXPreferences private constructor(private val context: Context) {
|
|||
context.importPreferences(uri, preferences.edit()).also { reload() }
|
||||
|
||||
fun reset() {
|
||||
preferences.edit().clear().apply()
|
||||
preferences.edit().clear().commit()
|
||||
encryptedPreferences.edit().clear().apply()
|
||||
backupsFolder.refresh()
|
||||
dataInPublicFolder.refresh()
|
||||
|
@ -213,12 +232,15 @@ class NotallyXPreferences private constructor(private val context: Context) {
|
|||
maxItems,
|
||||
maxLines,
|
||||
maxTitle,
|
||||
labelsHiddenInNavigation,
|
||||
labelsHiddenInOverview,
|
||||
secureFlag,
|
||||
labelsHidden,
|
||||
labelTagsHiddenInOverview,
|
||||
maxLabels,
|
||||
periodicBackups,
|
||||
backupPassword,
|
||||
backupOnSave,
|
||||
autoSaveAfterIdleTime,
|
||||
imagesHiddenInOverview,
|
||||
)
|
||||
.forEach { it.refresh() }
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.philkes.notallyx.NotallyXApplication
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.NotallyDatabase
|
||||
import com.philkes.notallyx.data.dao.BaseNoteDao
|
||||
|
@ -42,9 +43,10 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
super.onReceive(context, intent)
|
||||
when (intent.action) {
|
||||
ACTION_NOTES_MODIFIED -> {
|
||||
val app = context.applicationContext as NotallyXApplication
|
||||
val noteIds = intent.getLongArrayExtra(EXTRA_MODIFIED_NOTES)
|
||||
if (noteIds != null) {
|
||||
updateWidgets(context, noteIds)
|
||||
updateWidgets(context, noteIds, locked = app.locked.value)
|
||||
}
|
||||
}
|
||||
ACTION_OPEN_NOTE -> openActivity(context, intent, EditNoteActivity::class.java)
|
||||
|
@ -85,7 +87,8 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
baseNoteDao.updateChecked(noteId, childrenPositions + position, checked!!)
|
||||
}
|
||||
} finally {
|
||||
updateWidgets(context, longArrayOf(noteId))
|
||||
val app = context.applicationContext as NotallyXApplication
|
||||
updateWidgets(context, longArrayOf(noteId), locked = app.locked.value)
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
|
@ -135,19 +138,19 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
val app = context.applicationContext as Application
|
||||
val app = context.applicationContext as NotallyXApplication
|
||||
val preferences = NotallyXPreferences.getInstance(app)
|
||||
|
||||
appWidgetIds.forEach { id ->
|
||||
val noteId = preferences.getWidgetData(id)
|
||||
val noteType = preferences.getWidgetNoteType(id) ?: return
|
||||
updateWidget(app, appWidgetManager, id, noteId, noteType)
|
||||
updateWidget(app, appWidgetManager, id, noteId, noteType, locked = app.locked.value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun updateWidgets(context: Context, noteIds: LongArray? = null, locked: Boolean = false) {
|
||||
fun updateWidgets(context: Context, noteIds: LongArray? = null, locked: Boolean) {
|
||||
val app = context.applicationContext as Application
|
||||
val preferences = NotallyXPreferences.getInstance(app)
|
||||
|
||||
|
@ -181,65 +184,90 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
|
||||
intent.embedIntentExtras()
|
||||
|
||||
if (!locked) {
|
||||
MainScope().launch {
|
||||
val database = NotallyDatabase.getDatabase(context).value
|
||||
MainScope().launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val color = database.getBaseNoteDao().getColorOfNote(noteId)
|
||||
val preferences = NotallyXPreferences.getInstance(context)
|
||||
val (backgroundColor, _) = context.extractWidgetColors(color, preferences)
|
||||
val view =
|
||||
RemoteViews(context.packageName, R.layout.widget).apply {
|
||||
setRemoteAdapter(R.id.ListView, intent)
|
||||
setEmptyView(R.id.ListView, R.id.Empty)
|
||||
setOnClickPendingIntent(
|
||||
R.id.Empty,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.apply {
|
||||
action = ACTION_SELECT_NOTE
|
||||
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
|
||||
}
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
setPendingIntentTemplate(
|
||||
R.id.ListView,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
|
||||
noteType?.let {
|
||||
setOnClickPendingIntent(
|
||||
R.id.Layout,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.setOpenNoteIntent(noteType, noteId)
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
}
|
||||
|
||||
setInt(R.id.Layout, "setBackgroundColor", backgroundColor)
|
||||
}
|
||||
manager.updateAppWidget(id, view)
|
||||
manager.notifyAppWidgetViewDataChanged(id, R.id.ListView)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val view =
|
||||
RemoteViews(context.packageName, R.layout.widget_locked).apply {
|
||||
noteType?.let {
|
||||
val lockedPendingIntent =
|
||||
context.getOpenNotePendingIntent(noteId, noteType)
|
||||
setOnClickPendingIntent(R.id.Layout, lockedPendingIntent)
|
||||
setOnClickPendingIntent(R.id.Text, lockedPendingIntent)
|
||||
val color =
|
||||
withContext(Dispatchers.IO) { database.getBaseNoteDao().getColorOfNote(noteId) }
|
||||
if (color == null) {
|
||||
val app = context.applicationContext as Application
|
||||
val preferences = NotallyXPreferences.getInstance(app)
|
||||
preferences.deleteWidget(id)
|
||||
val view =
|
||||
RemoteViews(context.packageName, R.layout.widget).apply {
|
||||
setRemoteAdapter(R.id.ListView, intent)
|
||||
setEmptyView(R.id.ListView, R.id.Empty)
|
||||
setOnClickPendingIntent(
|
||||
R.id.Empty,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.apply {
|
||||
action = ACTION_SELECT_NOTE
|
||||
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
|
||||
}
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
setPendingIntentTemplate(
|
||||
R.id.ListView,
|
||||
Intent(context, WidgetProvider::class.java).asPendingIntent(context),
|
||||
)
|
||||
}
|
||||
setTextViewCompoundDrawablesRelative(
|
||||
R.id.Text,
|
||||
0,
|
||||
R.drawable.lock_big,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
manager.updateAppWidget(id, view)
|
||||
|
||||
manager.updateAppWidget(id, view)
|
||||
manager.notifyAppWidgetViewDataChanged(id, R.id.ListView)
|
||||
return@launch
|
||||
}
|
||||
if (!locked) {
|
||||
val view =
|
||||
RemoteViews(context.packageName, R.layout.widget).apply {
|
||||
setRemoteAdapter(R.id.ListView, intent)
|
||||
setEmptyView(R.id.ListView, R.id.Empty)
|
||||
setOnClickPendingIntent(
|
||||
R.id.Empty,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.apply {
|
||||
action = ACTION_SELECT_NOTE
|
||||
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
|
||||
}
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
setPendingIntentTemplate(
|
||||
R.id.ListView,
|
||||
Intent(context, WidgetProvider::class.java).asPendingIntent(context),
|
||||
)
|
||||
|
||||
val preferences = NotallyXPreferences.getInstance(context)
|
||||
val (backgroundColor, _) =
|
||||
context.extractWidgetColors(color, preferences)
|
||||
noteType?.let {
|
||||
setOnClickPendingIntent(
|
||||
R.id.Layout,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.setOpenNoteIntent(noteType, noteId)
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
}
|
||||
setInt(R.id.Layout, "setBackgroundColor", backgroundColor)
|
||||
}
|
||||
manager.updateAppWidget(id, view)
|
||||
manager.notifyAppWidgetViewDataChanged(id, R.id.ListView)
|
||||
} else {
|
||||
val view =
|
||||
RemoteViews(context.packageName, R.layout.widget_locked).apply {
|
||||
noteType?.let {
|
||||
val lockedPendingIntent =
|
||||
context.getOpenNotePendingIntent(noteId, noteType)
|
||||
setOnClickPendingIntent(R.id.Layout, lockedPendingIntent)
|
||||
setOnClickPendingIntent(R.id.Text, lockedPendingIntent)
|
||||
}
|
||||
setTextViewCompoundDrawablesRelative(
|
||||
R.id.Text,
|
||||
0,
|
||||
R.drawable.lock_big,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
manager.updateAppWidget(id, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,13 @@ class ActionMode {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateSelected(availableItemIds: List<Long>?) {
|
||||
selectedNotes.keys
|
||||
.filter { availableItemIds?.contains(it) == false }
|
||||
.forEach { selectedNotes.remove(it) }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun isEnabled() = enabled.value
|
||||
|
||||
// We assume selectedNotes.size is 1
|
||||
|
|
|
@ -80,7 +80,7 @@ private fun Context.createReminderAlarmIntent(noteId: Long, reminderId: Long): P
|
|||
intent.putExtra(ReminderReceiver.EXTRA_NOTE_ID, noteId)
|
||||
return PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
(noteId.toString() + reminderId.toString()).toInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
|
||||
)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package com.philkes.notallyx.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
|
@ -12,11 +11,11 @@ import android.content.ContentResolver
|
|||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.hardware.biometrics.BiometricManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
|
@ -33,16 +32,19 @@ import com.philkes.notallyx.BuildConfig
|
|||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.data.model.toText
|
||||
import com.philkes.notallyx.presentation.activity.note.EditActivity.Companion.EXTRA_SELECTED_BASE_NOTE
|
||||
import com.philkes.notallyx.presentation.activity.note.EditListActivity
|
||||
import com.philkes.notallyx.presentation.activity.note.EditNoteActivity
|
||||
import com.philkes.notallyx.presentation.showToast
|
||||
import com.philkes.notallyx.presentation.view.misc.NotNullLiveData
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStreamWriter
|
||||
import java.io.PrintWriter
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
@ -96,7 +98,7 @@ fun ClipboardManager.getLatestText(): CharSequence? {
|
|||
return primaryClip?.let { if (it.itemCount > 0) it.getItemAt(0)!!.text else null }
|
||||
}
|
||||
|
||||
fun Activity.copyToClipBoard(text: CharSequence) {
|
||||
fun Context.copyToClipBoard(text: CharSequence) {
|
||||
ContextCompat.getSystemService(this, ClipboardManager::class.java)?.let {
|
||||
val clip = ClipData.newPlainText("label", text)
|
||||
it.setPrimaryClip(clip)
|
||||
|
@ -121,30 +123,15 @@ fun Context.getFileName(uri: Uri): String? =
|
|||
}
|
||||
|
||||
fun Context.canAuthenticateWithBiometrics(): Int {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
val keyguardManager = ContextCompat.getSystemService(this, KeyguardManager::class.java)
|
||||
val packageManager: PackageManager = this.packageManager
|
||||
if (!packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
|
||||
return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
}
|
||||
if (keyguardManager?.isKeyguardSecure == false) {
|
||||
return BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
val biometricManager: BiometricManager =
|
||||
this.getSystemService(BiometricManager::class.java)
|
||||
return biometricManager.canAuthenticate()
|
||||
val biometricManager = androidx.biometric.BiometricManager.from(this)
|
||||
val authenticators =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
} else {
|
||||
val biometricManager: BiometricManager =
|
||||
this.getSystemService(BiometricManager::class.java)
|
||||
return biometricManager.canAuthenticate(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
)
|
||||
androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||
}
|
||||
}
|
||||
return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
return biometricManager.canAuthenticate(authenticators)
|
||||
}
|
||||
|
||||
fun Context.getUriForFile(file: File): Uri =
|
||||
|
@ -152,6 +139,8 @@ fun Context.getUriForFile(file: File): Uri =
|
|||
|
||||
private val LOG_DATE_FORMATTER = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
|
||||
|
||||
fun Context.getMimeType(uri: Uri) = contentResolver.getType(uri)
|
||||
|
||||
fun ContextWrapper.log(
|
||||
tag: String,
|
||||
msg: String? = null,
|
||||
|
@ -166,8 +155,7 @@ fun ContextWrapper.log(
|
|||
fun ContextWrapper.getLastExceptionLog(): String? {
|
||||
val logFile = getLogFile()
|
||||
if (logFile.exists()) {
|
||||
val logContents = logFile.readText().substringAfterLast("[Start]")
|
||||
return URLEncoder.encode(logContents, StandardCharsets.UTF_8.toString())
|
||||
return logFile.readText().substringAfterLast("[Start]")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
@ -197,16 +185,24 @@ fun Context.logToFile(
|
|||
val logFile =
|
||||
folder.findFile(fileName).let {
|
||||
if (it == null || !it.exists()) {
|
||||
folder.createFile("text/plain", fileName)
|
||||
folder.createFile("text/plain", fileName.removeSuffix(".txt"))
|
||||
} else if (it.isLargerThanKb(MAX_LOGS_FILE_SIZE_KB)) {
|
||||
it.delete()
|
||||
folder.createFile("text/plain", fileName)
|
||||
folder.createFile("text/plain", fileName.removeSuffix(".txt"))
|
||||
} else it
|
||||
}
|
||||
|
||||
logFile?.let { file ->
|
||||
val contentResolver = contentResolver
|
||||
val outputStream = contentResolver.openOutputStream(file.uri, "wa")
|
||||
val (outputStream, logFileContents) =
|
||||
try {
|
||||
Pair(contentResolver.openOutputStream(file.uri, "wa"), null)
|
||||
} catch (e: UnsupportedOperationException) {
|
||||
Pair(
|
||||
contentResolver.openOutputStream(file.uri, "w"),
|
||||
contentResolver.readFileContents(file.uri),
|
||||
)
|
||||
}
|
||||
|
||||
outputStream?.use { output ->
|
||||
val writer = PrintWriter(OutputStreamWriter(output, Charsets.UTF_8))
|
||||
|
@ -214,6 +210,7 @@ fun Context.logToFile(
|
|||
val formatter = DateFormat.getDateTimeInstance()
|
||||
val time = formatter.format(System.currentTimeMillis())
|
||||
|
||||
logFileContents?.let { writer.println(it) }
|
||||
if (throwable != null || stackTrace != null) {
|
||||
writer.println("[Start]")
|
||||
}
|
||||
|
@ -243,6 +240,17 @@ fun Fragment.reportBug(stackTrace: String?) {
|
|||
}
|
||||
}
|
||||
|
||||
fun Fragment.getExtraBooleanFromBundleOrIntent(
|
||||
bundle: Bundle?,
|
||||
key: String,
|
||||
defaultValue: Boolean,
|
||||
): Boolean {
|
||||
return bundle.getExtraBooleanOrDefault(
|
||||
key,
|
||||
activity?.intent?.getBooleanExtra(key, defaultValue) ?: defaultValue,
|
||||
)
|
||||
}
|
||||
|
||||
fun Context.reportBug(stackTrace: String?) {
|
||||
catchNoBrowserInstalled { startActivity(createReportBugIntent(stackTrace)) }
|
||||
}
|
||||
|
@ -279,16 +287,34 @@ fun Context.createReportBugIntent(
|
|||
.wrapWithChooser(this)
|
||||
}
|
||||
|
||||
fun Context.shareNote(title: String, body: CharSequence) {
|
||||
val text = body.truncate(150_000)
|
||||
fun ContextWrapper.shareNote(note: BaseNote) {
|
||||
val body =
|
||||
when (note.type) {
|
||||
Type.NOTE -> note.body
|
||||
Type.LIST -> note.items.toMutableList().toText()
|
||||
}
|
||||
val filesUris =
|
||||
note.images
|
||||
.map { File(getExternalImagesDirectory(), it.localName) }
|
||||
.map { getUriForFile(it) }
|
||||
shareNote(note.title, body, filesUris)
|
||||
}
|
||||
|
||||
private fun Context.shareNote(title: String, body: CharSequence, imageUris: List<Uri>) {
|
||||
val text = body.truncate(150_000)
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SEND)
|
||||
Intent(if (imageUris.size > 1) Intent.ACTION_SEND_MULTIPLE else Intent.ACTION_SEND)
|
||||
.apply {
|
||||
type = "text/plain"
|
||||
type = if (imageUris.isEmpty()) "text/*" else "image/*"
|
||||
putExtra(Intent.EXTRA_TEXT, text.toString())
|
||||
putExtra(Intent.EXTRA_TITLE, title)
|
||||
putExtra(Intent.EXTRA_SUBJECT, title)
|
||||
if (imageUris.size > 1) {
|
||||
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(imageUris))
|
||||
} else if (imageUris.isNotEmpty()) {
|
||||
putExtra(Intent.EXTRA_STREAM, imageUris.first())
|
||||
}
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
.wrapWithChooser(this)
|
||||
startActivity(intent)
|
||||
|
@ -400,3 +426,12 @@ fun Activity.resetApplication() {
|
|||
startActivity(resetApplicationIntent)
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
}
|
||||
|
||||
fun Bundle?.getExtraBooleanOrDefault(key: String, defaultValue: Boolean): Boolean {
|
||||
return this?.getBoolean(key, defaultValue) ?: defaultValue
|
||||
}
|
||||
|
||||
fun ContentResolver.readFileContents(uri: Uri) =
|
||||
openInputStream(uri)?.use { inputStream ->
|
||||
BufferedReader(InputStreamReader(inputStream)).use { reader -> reader.readText() }
|
||||
} ?: ""
|
||||
|
|
|
@ -149,7 +149,7 @@ fun Context.getExportedPath() = getEmptyFolder("exported")
|
|||
|
||||
fun ContextWrapper.getLogsDir() = File(filesDir, "logs").also { it.mkdir() }
|
||||
|
||||
const val APP_LOG_FILE_NAME = "Log.v1.txt"
|
||||
const val APP_LOG_FILE_NAME = "notallyx-logs.txt"
|
||||
|
||||
fun ContextWrapper.getLogFile(): File {
|
||||
return File(getLogsDir(), APP_LOG_FILE_NAME)
|
||||
|
|
|
@ -81,6 +81,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import net.lingala.zip4j.exception.ZipException
|
||||
import net.lingala.zip4j.model.ZipParameters
|
||||
import net.lingala.zip4j.model.enums.CompressionLevel
|
||||
import net.lingala.zip4j.model.enums.EncryptionMethod
|
||||
|
@ -123,8 +124,8 @@ fun ContextWrapper.createBackup(): Result {
|
|||
try {
|
||||
val formatter = SimpleDateFormat("yyyyMMdd-HHmmssSSS", Locale.ENGLISH)
|
||||
val backupFilePrefix = PERIODIC_BACKUP_FILE_PREFIX
|
||||
val name = "$backupFilePrefix${formatter.format(System.currentTimeMillis())}"
|
||||
log(msg = "Creating '$uri/$name.zip'...")
|
||||
val name = "$backupFilePrefix${formatter.format(System.currentTimeMillis())}.zip"
|
||||
log(msg = "Creating '$uri/$name'...")
|
||||
val zipUri = requireNotNull(folder.createFile(MIME_TYPE_ZIP, name)).uri
|
||||
val exportedNotes = app.exportAsZip(zipUri, password = preferences.backupPassword.value)
|
||||
log(msg = "Exported $exportedNotes notes")
|
||||
|
@ -164,13 +165,16 @@ fun ContextWrapper.autoBackupOnSave(backupPath: String, password: String, savedN
|
|||
backupPath,
|
||||
"Auto backup on note save (${savedNote?.let { "id: '${savedNote.id}, title: '${savedNote.title}'" }}) failed, because auto-backup path '$backupPath' is invalid",
|
||||
) ?: return
|
||||
fun log(msg: String? = null, throwable: Throwable? = null) {
|
||||
logToFile(TAG, folder, NOTALLYX_BACKUP_LOGS_FILE, msg = msg, throwable = throwable)
|
||||
}
|
||||
try {
|
||||
var backupFile = folder.findFile(ON_SAVE_BACKUP_FILE)
|
||||
if (savedNote == null || backupFile == null || !backupFile.exists()) {
|
||||
backupFile = folder.createFile(MIME_TYPE_ZIP, ON_SAVE_BACKUP_FILE)
|
||||
exportAsZip(backupFile!!.uri, password = password)
|
||||
} else {
|
||||
NotallyDatabase.getDatabase(this, observePreferences = false).value.checkpoint()
|
||||
val (_, file) = copyDatabase()
|
||||
val files =
|
||||
with(savedNote) {
|
||||
images.map {
|
||||
|
@ -188,21 +192,23 @@ fun ContextWrapper.autoBackupOnSave(backupPath: String, password: String, savedN
|
|||
audios.map {
|
||||
BackupFile(SUBFOLDER_AUDIOS, File(getExternalAudioDirectory(), it.name))
|
||||
} +
|
||||
BackupFile(
|
||||
null,
|
||||
NotallyDatabase.getCurrentDatabaseFile(this@autoBackupOnSave),
|
||||
)
|
||||
BackupFile(null, file)
|
||||
}
|
||||
exportToZip(backupFile.uri, files, password)
|
||||
try {
|
||||
exportToZip(backupFile.uri, files, password)
|
||||
} catch (e: ZipException) {
|
||||
log(
|
||||
msg =
|
||||
"Re-creating full backup since existing auto backup ZIP is corrupt: ${e.message}"
|
||||
)
|
||||
backupFile.delete()
|
||||
autoBackupOnSave(backupPath, password, savedNote)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logToFile(
|
||||
TAG,
|
||||
folder,
|
||||
NOTALLYX_BACKUP_LOGS_FILE,
|
||||
msg =
|
||||
"Auto backup on note save (${savedNote?.let { "id: '${savedNote.id}, title: '${savedNote.title}'" }}) failed",
|
||||
throwable = e,
|
||||
log(
|
||||
"Auto backup on note save (${savedNote?.let { "id: '${savedNote.id}, title: '${savedNote.title}'" }}) failed",
|
||||
e,
|
||||
)
|
||||
tryPostErrorNotification(e)
|
||||
}
|
||||
|
@ -224,7 +230,7 @@ private fun ContextWrapper.requireBackupFolder(path: String, msg: String): Docum
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun ContextWrapper.checkAutoSave(
|
||||
suspend fun ContextWrapper.checkBackupOnSave(
|
||||
preferences: NotallyXPreferences,
|
||||
note: BaseNote? = null,
|
||||
forceFullBackup: Boolean = false,
|
||||
|
@ -400,6 +406,7 @@ fun ContextWrapper.copyDatabase(): Pair<NotallyDatabase, File> {
|
|||
val database = NotallyDatabase.getDatabase(this, observePreferences = false).value
|
||||
database.checkpoint()
|
||||
val preferences = NotallyXPreferences.getInstance(this)
|
||||
val databaseFile = NotallyDatabase.getCurrentDatabaseFile(this)
|
||||
return if (
|
||||
preferences.biometricLock.value == BiometricLock.ENABLED &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
|
@ -407,16 +414,11 @@ fun ContextWrapper.copyDatabase(): Pair<NotallyDatabase, File> {
|
|||
val cipher = getInitializedCipherForDecryption(iv = preferences.iv.value!!)
|
||||
val passphrase = cipher.doFinal(preferences.databaseEncryptionKey.value)
|
||||
val decryptedFile = File(cacheDir, DATABASE_NAME)
|
||||
decryptDatabase(
|
||||
this,
|
||||
passphrase,
|
||||
decryptedFile,
|
||||
NotallyDatabase.getExternalDatabaseFile(this),
|
||||
)
|
||||
decryptDatabase(this, passphrase, databaseFile, decryptedFile)
|
||||
Pair(database, decryptedFile)
|
||||
} else {
|
||||
val dbFile = File(cacheDir, DATABASE_NAME)
|
||||
NotallyDatabase.getCurrentDatabaseFile(this).copyTo(dbFile, overwrite = true)
|
||||
databaseFile.copyTo(dbFile, overwrite = true)
|
||||
Pair(database, dbFile)
|
||||
}
|
||||
}
|
||||
|
@ -512,13 +514,14 @@ fun exportPdfFile(
|
|||
total: Int? = null,
|
||||
duplicateFileCount: Int = 1,
|
||||
) {
|
||||
val filePath = "$fileName.${ExportMimeType.PDF.fileExtension}"
|
||||
val validFileName = fileName.ifBlank { app.getString(R.string.note) }
|
||||
val filePath = "$validFileName.${ExportMimeType.PDF.fileExtension}"
|
||||
if (folder.findFile(filePath)?.exists() == true) {
|
||||
return exportPdfFile(
|
||||
app,
|
||||
note,
|
||||
folder,
|
||||
"${fileName.removeTrailingParentheses()} ($duplicateFileCount)",
|
||||
"${validFileName.removeTrailingParentheses()} ($duplicateFileCount)",
|
||||
pdfPrintListener,
|
||||
progress,
|
||||
counter,
|
||||
|
@ -576,8 +579,9 @@ suspend fun exportPlainTextFile(
|
|||
)
|
||||
}
|
||||
return withContext(Dispatchers.IO) {
|
||||
val validFileName = fileName.takeIf { it.isNotBlank() } ?: app.getString(R.string.note)
|
||||
val file =
|
||||
folder.createFile(exportType.mimeType, fileName)?.let {
|
||||
folder.createFile(exportType.mimeType, validFileName)?.let {
|
||||
app.contentResolver.openOutputStream(it.uri)?.use { stream ->
|
||||
OutputStreamWriter(stream).use { writer ->
|
||||
writer.write(
|
||||
|
|
|
@ -22,11 +22,13 @@ import com.philkes.notallyx.data.model.Converters
|
|||
import com.philkes.notallyx.data.model.FileAttachment
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.Label
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.data.model.parseToColorString
|
||||
import com.philkes.notallyx.presentation.getQuantityString
|
||||
import com.philkes.notallyx.presentation.showToast
|
||||
import com.philkes.notallyx.presentation.viewmodel.NotallyModel.FileType
|
||||
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences
|
||||
import com.philkes.notallyx.utils.FileError
|
||||
import com.philkes.notallyx.utils.SUBFOLDER_AUDIOS
|
||||
import com.philkes.notallyx.utils.SUBFOLDER_FILES
|
||||
|
@ -43,6 +45,8 @@ import com.philkes.notallyx.utils.log
|
|||
import com.philkes.notallyx.utils.mimeTypeToFileExtension
|
||||
import com.philkes.notallyx.utils.rename
|
||||
import com.philkes.notallyx.utils.scheduleNoteReminders
|
||||
import com.philkes.notallyx.utils.security.SQLCipherUtils
|
||||
import com.philkes.notallyx.utils.security.decryptDatabase
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
|
@ -85,12 +89,31 @@ suspend fun ContextWrapper.importZip(
|
|||
NotallyDatabase.DATABASE_NAME,
|
||||
)
|
||||
|
||||
var dbFile = File(databaseFolder, NotallyDatabase.DATABASE_NAME)
|
||||
val state = SQLCipherUtils.getDatabaseState(dbFile)
|
||||
if (state == SQLCipherUtils.State.ENCRYPTED) {
|
||||
val fallbackEncryptionKey =
|
||||
NotallyXPreferences.getInstance(this@importZip)
|
||||
.fallbackDatabaseEncryptionKey
|
||||
.value
|
||||
if (fallbackEncryptionKey != null) {
|
||||
val dbFileDecrypted =
|
||||
File(databaseFolder, "${NotallyDatabase.DATABASE_NAME}-decrypted")
|
||||
decryptDatabase(
|
||||
this@importZip,
|
||||
fallbackEncryptionKey,
|
||||
dbFile,
|
||||
dbFileDecrypted,
|
||||
)
|
||||
dbFile = dbFileDecrypted
|
||||
} else {
|
||||
throw IllegalArgumentException(
|
||||
"Backup contains encrypted database and 'fallbackDatabaseEncryptionKey' has no value!"
|
||||
)
|
||||
}
|
||||
}
|
||||
val database =
|
||||
SQLiteDatabase.openDatabase(
|
||||
File(databaseFolder, NotallyDatabase.DATABASE_NAME).path,
|
||||
null,
|
||||
SQLiteDatabase.OPEN_READONLY,
|
||||
)
|
||||
SQLiteDatabase.openDatabase(dbFile.path, null, SQLiteDatabase.OPEN_READONLY)
|
||||
|
||||
val labelCursor = database.query("Label", null, null, null, null, null, null)
|
||||
val baseNoteCursor = database.query("BaseNote", null, null, null, null, null, null)
|
||||
|
@ -176,14 +199,11 @@ suspend fun ContextWrapper.importZip(
|
|||
showToast(message)
|
||||
} catch (e: ZipException) {
|
||||
if (e.type == ZipException.Type.WRONG_PASSWORD) {
|
||||
log(TAG, throwable = e)
|
||||
showToast(R.string.wrong_password)
|
||||
} else {
|
||||
log(TAG, throwable = e)
|
||||
showToast(R.string.invalid_backup)
|
||||
throw e
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showToast(R.string.invalid_backup)
|
||||
log(TAG, throwable = e)
|
||||
} finally {
|
||||
importingBackup?.value = ImportProgress(inProgress = false)
|
||||
}
|
||||
|
@ -249,8 +269,8 @@ private fun Cursor.toBaseNote(): BaseNote {
|
|||
else -> throw IllegalArgumentException("pinned must be 0 or 1")
|
||||
}
|
||||
|
||||
val type = Type.valueOf(typeTmp)
|
||||
val folder = Folder.valueOf(folderTmp)
|
||||
val type = Type.valueOfOrDefault(typeTmp)
|
||||
val folder = Folder.valueOfOrDefault(folderTmp)
|
||||
|
||||
val labels = Converters.jsonToLabels(labelsTmp)
|
||||
val spans = Converters.jsonToSpans(spansTmp)
|
||||
|
@ -280,6 +300,11 @@ private fun Cursor.toBaseNote(): BaseNote {
|
|||
Converters.jsonToReminders(getString(remindersIndex))
|
||||
} else emptyList()
|
||||
|
||||
val viewModeIndex = getColumnIndex("viewMode")
|
||||
val viewMode =
|
||||
if (viewModeIndex != -1) {
|
||||
NoteViewMode.valueOfOrDefault(getString(viewModeIndex))
|
||||
} else NoteViewMode.EDIT
|
||||
return BaseNote(
|
||||
0,
|
||||
type,
|
||||
|
@ -297,6 +322,7 @@ private fun Cursor.toBaseNote(): BaseNote {
|
|||
files,
|
||||
audios,
|
||||
reminders,
|
||||
viewMode,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -330,8 +356,7 @@ fun Context.importPreferences(jsonFile: Uri, to: SharedPreferences.Editor): Bool
|
|||
else -> to.putString(key, value.toString())
|
||||
}
|
||||
}
|
||||
to.apply()
|
||||
return true
|
||||
return to.commit()
|
||||
} catch (e: Exception) {
|
||||
if (this is ContextWrapper) {
|
||||
log(TAG, "Import preferences from '$jsonFile' failed", throwable = e)
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.philkes.notallyx.data.model.BaseNote
|
|||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.Label
|
||||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.data.model.SpanRepresentation
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
import com.philkes.notallyx.data.model.parseToColorString
|
||||
|
@ -113,6 +114,7 @@ private fun XmlPullParser.parseBaseNote(rootTag: String, folder: Folder): BaseNo
|
|||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
NoteViewMode.EDIT,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import kotlin.IllegalStateException
|
|||
|
||||
class ChangeHistory {
|
||||
private val changeStack = ArrayList<Change>()
|
||||
private var stackPointer = NotNullLiveData(-1)
|
||||
var stackPointer = NotNullLiveData(-1)
|
||||
|
||||
internal val canUndo = NotNullLiveData(false)
|
||||
internal val canRedo = NotNullLiveData(false)
|
||||
|
@ -35,6 +35,12 @@ class ChangeHistory {
|
|||
makeListAction.redo()
|
||||
}
|
||||
|
||||
fun redoAll() {
|
||||
while (stackPointer.value < changeStack.lastIndex) {
|
||||
redo()
|
||||
}
|
||||
}
|
||||
|
||||
fun undo() {
|
||||
if (stackPointer.value < 0) {
|
||||
throw ChangeHistoryException("There is no Change to undo!}")
|
||||
|
@ -45,6 +51,12 @@ class ChangeHistory {
|
|||
stackPointer.value -= 1
|
||||
}
|
||||
|
||||
fun undoAll() {
|
||||
while (stackPointer.value >= 0) {
|
||||
undo()
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
stackPointer.value = -1
|
||||
changeStack.clear()
|
||||
|
|
|
@ -1,33 +1,7 @@
|
|||
package com.philkes.notallyx.utils.changehistory
|
||||
|
||||
import android.text.TextWatcher
|
||||
import android.text.style.CharacterStyle
|
||||
import android.widget.EditText
|
||||
import com.philkes.notallyx.presentation.view.note.listitem.ListManager
|
||||
import com.philkes.notallyx.presentation.view.note.listitem.ListState
|
||||
|
||||
open class ListEditTextChange(
|
||||
private val editText: EditText,
|
||||
position: Int,
|
||||
before: EditTextState,
|
||||
after: EditTextState,
|
||||
private val listener: TextWatcher,
|
||||
private val listManager: ListManager,
|
||||
) : ListPositionValueChange<EditTextState>(after, before, position) {
|
||||
|
||||
override fun update(position: Int, value: EditTextState, isUndo: Boolean) {
|
||||
listManager.changeText(
|
||||
position,
|
||||
value,
|
||||
pushChange = false,
|
||||
editText = editText,
|
||||
listener = listener,
|
||||
)
|
||||
editText.apply {
|
||||
removeTextChangedListener(listener)
|
||||
text = value.text.withoutSpans<CharacterStyle>()
|
||||
requestFocus()
|
||||
setSelection(value.cursorPos)
|
||||
addTextChangedListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
open class ListEditTextChange(old: ListState, new: ListState, listManager: ListManager) :
|
||||
ListBatchChange(old, new, listManager)
|
||||
|
|
|
@ -37,8 +37,8 @@ fun decryptDatabase(context: ContextWrapper, passphrase: ByteArray) {
|
|||
fun decryptDatabase(
|
||||
context: Context,
|
||||
passphrase: ByteArray,
|
||||
decryptedFile: File,
|
||||
databaseFile: File,
|
||||
decryptedFile: File,
|
||||
) {
|
||||
val state = SQLCipherUtils.getDatabaseState(databaseFile)
|
||||
if (state == SQLCipherUtils.State.ENCRYPTED) {
|
||||
|
|
|
@ -4,15 +4,13 @@ import android.app.Activity
|
|||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.hardware.biometrics.BiometricManager
|
||||
import android.hardware.biometrics.BiometricPrompt
|
||||
import android.hardware.fingerprint.FingerprintManager
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.philkes.notallyx.R
|
||||
import javax.crypto.Cipher
|
||||
|
||||
|
@ -27,7 +25,7 @@ fun Activity.showBiometricOrPinPrompt(
|
|||
) {
|
||||
showBiometricOrPinPrompt(
|
||||
isForDecrypt,
|
||||
this,
|
||||
this as FragmentActivity,
|
||||
activityResultLauncher,
|
||||
titleResId,
|
||||
descriptionResId,
|
||||
|
@ -48,7 +46,7 @@ fun Fragment.showBiometricOrPinPrompt(
|
|||
) {
|
||||
showBiometricOrPinPrompt(
|
||||
isForDecrypt,
|
||||
requireContext(),
|
||||
activity!!,
|
||||
activityResultLauncher,
|
||||
titleResId,
|
||||
descriptionResId,
|
||||
|
@ -60,7 +58,7 @@ fun Fragment.showBiometricOrPinPrompt(
|
|||
|
||||
private fun showBiometricOrPinPrompt(
|
||||
isForDecrypt: Boolean,
|
||||
context: Context,
|
||||
context: FragmentActivity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||
titleResId: Int,
|
||||
descriptionResId: Int? = null,
|
||||
|
@ -69,142 +67,55 @@ private fun showBiometricOrPinPrompt(
|
|||
onFailure: (errorCode: Int?) -> Unit,
|
||||
) {
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||
// Android 11+ with BiometricPrompt and Authenticators
|
||||
val prompt =
|
||||
BiometricPrompt.Builder(context)
|
||||
.apply {
|
||||
setTitle(context.getString(titleResId))
|
||||
descriptionResId?.let {
|
||||
setDescription(context.getString(descriptionResId))
|
||||
}
|
||||
setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
)
|
||||
}
|
||||
.build()
|
||||
val cipher =
|
||||
if (isForDecrypt) {
|
||||
getInitializedCipherForDecryption(iv = cipherIv!!)
|
||||
} else {
|
||||
getInitializedCipherForEncryption()
|
||||
}
|
||||
prompt.authenticate(
|
||||
BiometricPrompt.CryptoObject(cipher),
|
||||
getCancellationSignal(context),
|
||||
context.mainExecutor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult?
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess.invoke(result!!.cryptoObject!!.cipher)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailure.invoke(null)
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure.invoke(errorCode)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> {
|
||||
// Android 10: Use BiometricPrompt without Authenticators
|
||||
val prompt =
|
||||
BiometricPrompt.Builder(context)
|
||||
.apply {
|
||||
setTitle(context.getString(titleResId))
|
||||
descriptionResId?.let {
|
||||
setDescription(context.getString(descriptionResId))
|
||||
}
|
||||
setNegativeButton(
|
||||
context.getString(R.string.cancel),
|
||||
context.mainExecutor,
|
||||
) { _, _ ->
|
||||
onFailure.invoke(null)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
val cipher =
|
||||
if (isForDecrypt) {
|
||||
getInitializedCipherForDecryption(iv = cipherIv!!)
|
||||
} else {
|
||||
getInitializedCipherForEncryption()
|
||||
}
|
||||
prompt.authenticate(
|
||||
BiometricPrompt.CryptoObject(cipher),
|
||||
getCancellationSignal(context),
|
||||
context.mainExecutor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult?
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess.invoke(result!!.cryptoObject!!.cipher)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailure.invoke(null)
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure.invoke(errorCode)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
|
||||
val fingerprintManager =
|
||||
ContextCompat.getSystemService(context, FingerprintManager::class.java)
|
||||
if (
|
||||
fingerprintManager?.isHardwareDetected == true &&
|
||||
fingerprintManager.hasEnrolledFingerprints()
|
||||
) {
|
||||
val cipher =
|
||||
if (isForDecrypt) {
|
||||
getInitializedCipherForDecryption(iv = cipherIv!!)
|
||||
} else {
|
||||
getInitializedCipherForEncryption()
|
||||
val promptInfo =
|
||||
BiometricPrompt.PromptInfo.Builder()
|
||||
.apply {
|
||||
setTitle(context.getString(titleResId))
|
||||
descriptionResId?.let {
|
||||
setDescription(context.getString(descriptionResId))
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
)
|
||||
} else {
|
||||
setNegativeButtonText(context.getString(R.string.cancel))
|
||||
setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||
)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
val cipher =
|
||||
if (isForDecrypt) {
|
||||
getInitializedCipherForDecryption(iv = cipherIv!!)
|
||||
} else {
|
||||
getInitializedCipherForEncryption()
|
||||
}
|
||||
val authCallback =
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess.invoke(result.cryptoObject!!.cipher!!)
|
||||
}
|
||||
fingerprintManager.authenticate(
|
||||
FingerprintManager.CryptoObject(cipher),
|
||||
getCancellationSignal(context),
|
||||
0,
|
||||
object : FingerprintManager.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: FingerprintManager.AuthenticationResult?
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess.invoke(result!!.cryptoObject!!.cipher)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailure.invoke(null)
|
||||
}
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailure.invoke(null)
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence?,
|
||||
) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure.invoke(errorCode)
|
||||
}
|
||||
},
|
||||
null,
|
||||
)
|
||||
} else {
|
||||
promptPinAuthentication(context, activityResultLauncher, titleResId, onFailure)
|
||||
}
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure.invoke(errorCode)
|
||||
}
|
||||
}
|
||||
val prompt =
|
||||
BiometricPrompt(context, ContextCompat.getMainExecutor(context), authCallback)
|
||||
prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
@ -214,14 +125,6 @@ private fun showBiometricOrPinPrompt(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getCancellationSignal(context: Context): CancellationSignal {
|
||||
return CancellationSignal().apply {
|
||||
setOnCancelListener {
|
||||
Toast.makeText(context, R.string.biometrics_failure, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptPinAuthentication(
|
||||
context: Context,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||
|
|
|
@ -287,9 +287,10 @@ public class SQLCipherUtils {
|
|||
|
||||
final SQLiteStatement st = db.compileStatement("ATTACH DATABASE ? AS plaintext KEY ''");
|
||||
|
||||
if (!decryptedFile.exists()) {
|
||||
decryptedFile.createNewFile();
|
||||
if(decryptedFile.exists()){
|
||||
decryptedFile.delete();
|
||||
}
|
||||
decryptedFile.createNewFile();
|
||||
st.bindString(1, decryptedFile.getAbsolutePath());
|
||||
st.execute();
|
||||
|
||||
|
|
10
app/src/main/res/drawable/convert_to_text.xml
Normal file
10
app/src/main/res/drawable/convert_to_text.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M400,680L560,680L560,600L400,600L400,680ZM400,520L680,520L680,440L400,440L400,520ZM280,360L680,360L680,280L280,280L280,360ZM480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480ZM80,880L80,800L182,800Q134,777 104.5,732Q75,687 75,630Q75,551 130.5,495.5Q186,440 265,440L265,520Q220,520 187.5,552Q155,584 155,630Q155,669 179,699Q203,729 240,737L240,640L320,640L320,880L80,880ZM400,840L400,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,360L120,360L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L400,840Z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/file_json.xml
Normal file
10
app/src/main/res/drawable/file_json.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M190,600L260,600Q277,600 288.5,588.5Q300,577 300,560L300,360L240,360L240,550L200,550L200,500L150,500L150,560Q150,577 161.5,588.5Q173,600 190,600ZM367,600L427,600Q444,600 455.5,588.5Q467,577 467,560L467,500Q467,483 455.5,471.5Q444,460 427,460L377,460L377,410L417,410L417,430L467,430L467,400Q467,383 455.5,371.5Q444,360 427,360L367,360Q350,360 338.5,371.5Q327,383 327,400L327,460Q327,477 338.5,488.5Q350,500 367,500L417,500L417,550L377,550L377,530L327,530L327,560Q327,577 338.5,588.5Q350,600 367,600ZM543,540L543,420L583,420L583,540L543,540ZM533,600L593,600Q610,600 621.5,588.5Q633,577 633,560L633,400Q633,383 621.5,371.5Q610,360 593,360L533,360Q516,360 504.5,371.5Q493,383 493,400L493,560Q493,577 504.5,588.5Q516,600 533,600ZM660,600L710,600L710,495L750,600L800,600L800,360L750,360L750,465L710,360L660,360L660,600ZM120,800Q87,800 63.5,776.5Q40,753 40,720L40,240Q40,207 63.5,183.5Q87,160 120,160L840,160Q873,160 896.5,183.5Q920,207 920,240L920,720Q920,753 896.5,776.5Q873,800 840,800L120,800ZM120,720L840,720Q840,720 840,720Q840,720 840,720L840,240Q840,240 840,240Q840,240 840,240L120,240Q120,240 120,240Q120,240 120,240L120,720Q120,720 120,720Q120,720 120,720ZM120,720Q120,720 120,720Q120,720 120,720L120,240Q120,240 120,240Q120,240 120,240L120,240Q120,240 120,240Q120,240 120,240L120,720Q120,720 120,720Q120,720 120,720Z"/>
|
||||
</vector>
|
|
@ -22,8 +22,8 @@
|
|||
android:layout_gravity="center_vertical"
|
||||
android:padding="8dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="@string/label_visibility"
|
||||
android:tooltipText="@string/label_visibility"
|
||||
android:contentDescription="@string/labels_hidden_in_overview_title"
|
||||
android:tooltipText="@string/labels_hidden_in_overview_title"
|
||||
app:srcCompat="@drawable/visibility"
|
||||
app:tint="?attr/colorControlNormal" />
|
||||
|
||||
|
|
|
@ -187,7 +187,7 @@
|
|||
android:clipToPadding="false"
|
||||
android:overScrollMode="never"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="4dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
|
||||
|
|
|
@ -8,6 +8,5 @@
|
|||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/EditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textCapWords" />
|
||||
android:layout_height="wrap_content" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
|
@ -10,25 +10,25 @@
|
|||
>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/EnumHint"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/date_format_hint"
|
||||
style="@style/MaterialAlertDialog.Material3.Body.Text"
|
||||
android:paddingBottom="8dp"
|
||||
/>
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/DateFormatRadioGroup"
|
||||
android:id="@+id/EnumRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
/>
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/ApplyToNoteView"
|
||||
android:id="@+id/Toggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/date_format_apply_in_note_view"
|
||||
/>
|
||||
|
||||
|
|
@ -1,47 +1,55 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/RepetitionOptions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:padding="16dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/None"
|
||||
android:layout_width="wrap_content"
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/none" />
|
||||
android:scrollbarAlwaysDrawVerticalTrack="true">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/Daily"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/daily" />
|
||||
<RadioGroup
|
||||
android:id="@+id/RepetitionOptions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/Weekly"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/weekly" />
|
||||
<RadioButton
|
||||
android:id="@+id/None"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/none" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/Monthly"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/monthly" />
|
||||
<RadioButton
|
||||
android:id="@+id/Daily"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/daily" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/Yearly"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/yearly" />
|
||||
<RadioButton
|
||||
android:id="@+id/Weekly"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/weekly" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/Monthly"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/monthly" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/Yearly"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/yearly" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/Custom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/custom" />
|
||||
</RadioGroup>
|
||||
</ScrollView>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/Custom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/custom" />
|
||||
</RadioGroup>
|
||||
|
||||
|
|
|
@ -77,7 +77,9 @@
|
|||
app:fastScrollVerticalTrackDrawable="@drawable/scroll_track"
|
||||
app:fastScrollHorizontalThumbDrawable="@drawable/scroll_thumb"
|
||||
app:fastScrollVerticalThumbDrawable="@drawable/scroll_thumb"
|
||||
/>
|
||||
>
|
||||
<requestFocus />
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ImageView"
|
||||
|
|
|
@ -30,6 +30,16 @@
|
|||
android:id="@+id/TextSize"
|
||||
layout="@layout/preference" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/listDivider" />
|
||||
|
||||
<TextView
|
||||
style="@style/PreferenceHeader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/behaviour" />
|
||||
|
||||
<include
|
||||
android:id="@+id/NotesSortOrder"
|
||||
|
@ -39,11 +49,14 @@
|
|||
android:id="@+id/CheckedListItemSorting"
|
||||
layout="@layout/preference" />
|
||||
|
||||
|
||||
<include
|
||||
android:id="@+id/StartView"
|
||||
layout="@layout/preference" />
|
||||
|
||||
<include
|
||||
android:id="@+id/AutoSaveAfterIdle"
|
||||
layout="@layout/preference_seekbar" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
|
@ -75,6 +88,10 @@
|
|||
android:id="@+id/LabelsHiddenInOverview"
|
||||
layout="@layout/preference" />
|
||||
|
||||
<include
|
||||
android:id="@+id/ImagesHiddenInOverview"
|
||||
layout="@layout/preference" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
|
@ -160,6 +177,10 @@
|
|||
android:id="@+id/BackupPassword"
|
||||
layout="@layout/preference" />
|
||||
|
||||
<include
|
||||
android:id="@+id/SecureFlag"
|
||||
layout="@layout/preference" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
|
@ -220,6 +241,20 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/send_feedback" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/Rate"
|
||||
style="@style/Preference"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/rate" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/Donate"
|
||||
style="@style/Preference"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/donate" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/SourceCode"
|
||||
|
@ -229,18 +264,11 @@
|
|||
android:text="@string/source_code" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/Libraries"
|
||||
style="@style/Preference"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/libraries" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/Donate"
|
||||
android:id="@+id/Libraries"
|
||||
style="@style/Preference"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/donate" />
|
||||
android:text="@string/libraries" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/VersionText"
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
android:layout_gravity="center_vertical"
|
||||
android:padding="8dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="@string/label_visibility"
|
||||
android:contentDescription="@string/labels_hidden_in_overview_title"
|
||||
app:srcCompat="@drawable/visibility"
|
||||
app:tint="?attr/colorControlNormal" />
|
||||
|
||||
|
|
|
@ -23,13 +23,13 @@
|
|||
<string name="auto_backup_error_message">Během automatického zálohování došlo k chybě:\n\'%1$s\'\nProsím, zkontrolujte nastavení nebo nahlaste chybu</string>
|
||||
<string name="auto_backup_failed">Automatické zálohování NotallyX selhalo</string>
|
||||
<string name="auto_backup_last">Poslední záloha</string>
|
||||
<string name="auto_backup_on_save">Automaticky zálohovat při uložení poznámky</string>
|
||||
<string name="auto_backup_on_save">Automaticky zálohovat při ukončení poznámky</string>
|
||||
<string name="auto_backup_on_save_hint">Pokud tuto možnost povolíte, záloha („NotallyX_AutoBackup.zip“) bude automaticky vytvořena ve zvoleném „Adresáři záloh“ pokaždé, když bude poznámka uložena. Mějte na paměti, že to může ovlivnit výkon</string>
|
||||
<string name="auto_backup_period">Frekvence automatického zálohování</string>
|
||||
<string name="auto_backups_folder">Adresář pro zálohy</string>
|
||||
<string name="auto_backups_folder_hint">Adresář pro uložení automatických záloh.</string>
|
||||
<string name="auto_backups_folder_rechoose">Vyberte znovu Adresář pro zálohy a udělte tak znovu NotallyX oprávnění do něj zapisovat.\nPro přeskočení můžete stisknout tlačítko Zrušit</string>
|
||||
<string name="auto_backups_folder_set">Nejprve nastavte adresář pro zálohy</string>
|
||||
<string name="auto_save_after_idle_time">Automatické uložení poznámky při nečinnosti </string>
|
||||
<string name="auto_sort_by_checked">Seřadit zaškrtnuté položky na konec</string>
|
||||
<string name="back">Zpět</string>
|
||||
<string name="backup">Zálohování</string>
|
||||
|
@ -38,6 +38,7 @@
|
|||
<string name="backup_period_days">Frekvence automatického zálohování v dnech</string>
|
||||
<string name="backup_periodic">Periodické zálohy</string>
|
||||
<string name="backup_periodic_hint">Pokud tuto možnost povolíte, zálohy budou automaticky vytvářeny ve zvoleném adresáři záloh. Nemusí to fungovat, pokud máte povolený úsporný režim</string>
|
||||
<string name="behaviour">Chování</string>
|
||||
<string name="biometric_lock">Uzamknout aplikaci pomocí biometrických údajů zařízení nebo kódu PIN</string>
|
||||
<string name="biometrics_disable_success">Biometrický zámek/PIN byl vypnut</string>
|
||||
<string name="biometrics_failure">Ověření pomocí biometrických údajů / kódu PIN selhalo</string>
|
||||
|
@ -78,6 +79,8 @@
|
|||
<string name="color_exists">Tato barva již existuje!</string>
|
||||
<string name="content_density">Kompaktnost obsahu</string>
|
||||
<string name="continue_">Pokračovat</string>
|
||||
<string name="convert_to_list_note">Převést na seznam</string>
|
||||
<string name="convert_to_text_note">Převést na textovou poznámku</string>
|
||||
<string name="copied_link">Odkaz zkopírován do schránky</string>
|
||||
<string name="copy">Kopírovat</string>
|
||||
<string name="crash_message">Došlo k neočekávané chybě. \nOmluvte prosím vzniklé nepříjemnosti. </string>
|
||||
|
@ -120,6 +123,7 @@
|
|||
<string name="disable_lock_description">Toto rovněž dešifruje databázi</string>
|
||||
<string name="disable_lock_title">Vypnout zámek pomocí biometrických údajů/PIN</string>
|
||||
<string name="disabled">Zakázat</string>
|
||||
<string name="disallow_screenshots">Zakázat pořizování snímků obrazovky</string>
|
||||
<string name="discard">Zahodit</string>
|
||||
<string name="display_text">Text k zobrazení</string>
|
||||
<string name="donate">Podpořit</string>
|
||||
|
@ -159,6 +163,8 @@
|
|||
<string name="help">Nápověda</string>
|
||||
<string name="hours">Hodiny</string>
|
||||
<string name="image_format_not_supported">Formát obrázku není podporován</string>
|
||||
<string name="images_hidden_in_overview">Pokud tuto funkci povolíte, obrázky poznámek budou v přehledu skryty.</string>
|
||||
<string name="images_hidden_in_overview_title">Skrýt obrázky v přehledu</string>
|
||||
<string name="import_action">Importovat</string>
|
||||
<string name="import_backup">Importovat zálohu</string>
|
||||
<string name="import_backup_password_hint">Pokud vaše záloha není chráněna heslem, jednoduše stiskněte tlačítko Importovat, jinak zadejte správné heslo.</string>
|
||||
|
@ -187,6 +193,8 @@
|
|||
<string name="invalid_link">Zkopírovat platný odkaz do schránky</string>
|
||||
<string name="italic">Kurzíva</string>
|
||||
<string name="item">Položka</string>
|
||||
<string name="json_files">JSON soubory</string>
|
||||
<string name="json_files_help">Pro import poznámek z JSON souborů (jednotlivý soubor nebo složka) klikněte na Importovat. Každý platný JSON soubor je importován jako samostatná poznámka, název souboru se stane názvem poznámky. </string>
|
||||
<string name="label_exists">Štítek již existuje</string>
|
||||
<string name="label_visibility">Skrýt/zobrazit štítek na navigačním panelu</string>
|
||||
<string name="labels">Štítky</string>
|
||||
|
@ -242,6 +250,7 @@
|
|||
<string name="please_grant_notally_notification_auto_backup">Pokud automatické zálohování selže, můžete obdržet oznámení, pokud NotallyX udělíte oprávnění k zasílání oznámení</string>
|
||||
<string name="previous">Předchozí</string>
|
||||
<string name="rate">Ohodnotit aplikaci</string>
|
||||
<string name="read_only">Pouze pro čtení</string>
|
||||
<string name="ready_to_record">Připraveno k záznamu</string>
|
||||
<string name="record_audio">Spustit zvukový záznam</string>
|
||||
<string name="recording">Nahrávání…</string>
|
||||
|
@ -297,6 +306,7 @@
|
|||
<string name="text_default">Výchozí</string>
|
||||
<string name="text_size">Velikost písma</string>
|
||||
<string name="theme">Téma</string>
|
||||
<string name="theme_use_dynamic_colors">Použít barvy z tapety</string>
|
||||
<string name="title">Název</string>
|
||||
<string name="to_record_audio">Chcete-li nahrávat zvuk, povolte NotallyX přístup k mikrofonu. Klepněte na Nastavení > Oprávnění a zapněte Mikrofon.</string>
|
||||
<string name="unarchive">Zrušit archivaci</string>
|
||||
|
|
|
@ -22,13 +22,13 @@
|
|||
<string name="auto_backup_error_message">Beim autom. Backup ist ein Fehler aufgetreten:\n\'%1$s\'\nÜberprüfe deine Einstellungen oder melde einen Bug</string>
|
||||
<string name="auto_backup_failed">NotallyX Auto Backup fehlgeschlagen</string>
|
||||
<string name="auto_backup_last">Letztes Backup</string>
|
||||
<string name="auto_backup_on_save">Autom. Backup beim Notiz-Speichern</string>
|
||||
<string name="auto_backup_on_save_hint">Ist dies aktiviert, wird jedesmal wenn eine Notiz gespeichert wird autom. ein Backup (\"NotallyX_AutoBackup.zip\") im \"Backups Ordner\" erstellt.\nDies kann die App-Performance beeinträchtigen</string>
|
||||
<string name="auto_backup_period">Intervall für autom. Backups</string>
|
||||
<string name="auto_backup_on_save">Autom. Backup beim Notiz Verlassen</string>
|
||||
<string name="auto_backup_on_save_hint">Ist dies aktiviert, wird jedesmal wenn eine Notiz verlassen wird autom. ein Backup (\"NotallyX_AutoBackup.zip\") im \"Backups Ordner\" erstellt.\nDies kann die App-Performance beeinträchtigen</string>
|
||||
<string name="auto_backups_folder">Backups Ordner</string>
|
||||
<string name="auto_backups_folder_hint">Ordner in dem alle autom. Backups erstellt werden</string>
|
||||
<string name="auto_backups_folder_rechoose">Der Backups Ordner muss erneut ausgewählt werden, damit NotallyX Schreibrechte dafür erhält.\nÜber Abbrechen kann der Import dieser Einstellung übersprungen werden.</string>
|
||||
<string name="auto_backups_folder_set">Stelle erst ein Backups Ordner ein</string>
|
||||
<string name="auto_save_after_idle_time">Autom. Speichern nach X Sekunden</string>
|
||||
<string name="auto_sort_by_checked">Abgehakte Elemente ans Ende sortieren</string>
|
||||
<string name="back">Zurück</string>
|
||||
<string name="backup">Backup</string>
|
||||
|
@ -37,6 +37,7 @@
|
|||
<string name="backup_period_days">Auto. Backup Intervall in Tagen</string>
|
||||
<string name="backup_periodic">Periodische Backups</string>
|
||||
<string name="backup_periodic_hint">Ist dies aktiviert, werden autom. Backups im eingestellten Backups Ordner erstellt. \nDies könnte fehlschlagen wenn Energiesparmodus aktiviert ist</string>
|
||||
<string name="behaviour">Funktionsweise</string>
|
||||
<string name="biometric_lock">App per Biometrie/PIN sperren</string>
|
||||
<string name="biometrics_disable_success">Biometrische/PIN Sperre deaktiviert</string>
|
||||
<string name="biometrics_failure">Fehler bei Authentifizierung per Biometrie/PIN</string>
|
||||
|
@ -75,6 +76,8 @@
|
|||
<string name="color_exists">Diese Farbe existiert bereits!</string>
|
||||
<string name="content_density">Notizvorschau</string>
|
||||
<string name="continue_">Weiter</string>
|
||||
<string name="convert_to_list_note">Zu Liste konvertieren</string>
|
||||
<string name="convert_to_text_note">Zu Text-Notiz konvertieren</string>
|
||||
<string name="copied_link">Link kopiert</string>
|
||||
<string name="copy">Kopieren</string>
|
||||
<string name="crash_message">Ein unerwarteter Fehler ist aufgetreten.\nEntschuldigung für die Unannehmlichkeiten.\n</string>
|
||||
|
@ -116,6 +119,7 @@
|
|||
<string name="disable_lock_description">Dies entschlüsselt außerdem die Datenbank</string>
|
||||
<string name="disable_lock_title">Deaktivierte Biometrie/PIN Sperre</string>
|
||||
<string name="disabled">Deaktiviert</string>
|
||||
<string name="disallow_screenshots">Screenshots verbieten</string>
|
||||
<string name="discard">Verwerfen</string>
|
||||
<string name="display_text">Anzeigetext</string>
|
||||
<string name="donate">Spende</string>
|
||||
|
@ -155,6 +159,8 @@
|
|||
<string name="help">Hilfe</string>
|
||||
<string name="hours">Stunden</string>
|
||||
<string name="image_format_not_supported">Bildformat nicht unterstützt</string>
|
||||
<string name="images_hidden_in_overview">Ist dies aktiviert, werden die Bilder der Notizen in der Übersicht ausgeblendet</string>
|
||||
<string name="images_hidden_in_overview_title">Verberge Bilder in Übersicht</string>
|
||||
<string name="import_action">Import</string>
|
||||
<string name="import_backup">Backup importieren</string>
|
||||
<string name="import_backup_password_hint">Falls das Backup nicht passwortgeschützt ist, drücken Sie einfach auf Importieren, andernfalls geben Sie das richtige Passwort ein.</string>
|
||||
|
@ -182,6 +188,8 @@
|
|||
<string name="invalid_link">Kopiere einen validen Link</string>
|
||||
<string name="italic">Kursiv</string>
|
||||
<string name="item">Eintrag</string>
|
||||
<string name="json_files">JSON Dateien</string>
|
||||
<string name="json_files_help">Um deine JSON-Notizen (einzele Datei oder Ordner) zu importieren, klicke Import.\nJede valide Datei wird als einzelne Notiz importiert, der Dateiname wird zum Notiz-Titel.</string>
|
||||
<string name="label_exists">Label existiert bereits</string>
|
||||
<string name="label_visibility">Zeige/Verberge das Label in der Navigation</string>
|
||||
<string name="labels">Labels</string>
|
||||
|
@ -236,6 +244,7 @@
|
|||
<string name="please_grant_notally_notification_auto_backup">Wenn ein autom. Backup fehlschlägt kann eine Benachrichtigung geschickt werden, wenn du NotallyX erlaubst Benachrichtigungen zu schicken</string>
|
||||
<string name="previous">Vorherige</string>
|
||||
<string name="rate">App bewerten</string>
|
||||
<string name="read_only">Lesemodus</string>
|
||||
<string name="ready_to_record">Bereit zum Aufnehmen</string>
|
||||
<string name="record_audio">Sprachnotiz aufnehmen</string>
|
||||
<string name="recording">Nimmt auf...</string>
|
||||
|
@ -290,6 +299,7 @@
|
|||
<string name="text_default">Standard</string>
|
||||
<string name="text_size">Textgröße</string>
|
||||
<string name="theme">Design</string>
|
||||
<string name="theme_use_dynamic_colors">Systemhintergrundfarben verwenden</string>
|
||||
<string name="title">Titel</string>
|
||||
<string name="to_record_audio">Um Sprachnotizen zu erstellen, braucht NotallyX Zugriff auf dein Mikrofon. Klicke Einstellunge > Berechtigungen und aktiviere Mikrofon</string>
|
||||
<string name="unarchive">Archivierung aufheben</string>
|
||||
|
|
|
@ -22,13 +22,13 @@
|
|||
<string name="auto_backup_error_message">Ha ocurrido un error durante una copia automática:\n\'%1$s’\nComprueba tus ajustes o informa de un error</string>
|
||||
<string name="auto_backup_failed">Falló la copia de seguridad automática de Notallyx</string>
|
||||
<string name="auto_backup_last">Última copia de seguridad</string>
|
||||
<string name="auto_backup_on_save">Copia de seguridad automática al guardar una nota</string>
|
||||
<string name="auto_backup_on_save">Copia de seguridad automática al salir una nota</string>
|
||||
<string name="auto_backup_on_save_hint">Al habilitar esta opción, se crea automáticamente una copia de seguridad (\"NotallyX_AutoBackup.zip\") en la \"Directorio de copias de seguridad\" configurada cada vez que se guarda una nota.\nTenga en cuenta que esto puede afectar el rendimiento.</string>
|
||||
<string name="auto_backup_period">Periodo de copia automática</string>
|
||||
<string name="auto_backups_folder">Directorio de copias de seguridad</string>
|
||||
<string name="auto_backups_folder_hint">Carpeta donde se guardan las copias de seguridad automáticas.</string>
|
||||
<string name="auto_backups_folder_rechoose">Debe volver a elegir su directorio de copias de seguridad para que NotallyX tenga permiso para escribir en ella.\nTambién puede presionar Cancelar para omitir la importación del valor del directorio de copias de seguridad</string>
|
||||
<string name="auto_backups_folder_set">Establece primero la carpeta de copia en opción anterior</string>
|
||||
<string name="auto_save_after_idle_time">Autoguardar nota después de un lapso de inactividad</string>
|
||||
<string name="auto_sort_by_checked">Ordenar con los elementos marcados al final</string>
|
||||
<string name="back">Volver</string>
|
||||
<string name="backup">Respaldar</string>
|
||||
|
@ -37,6 +37,7 @@
|
|||
<string name="backup_period_days">Periodo de copia automática en días</string>
|
||||
<string name="backup_periodic">Copias de seguridad periódicas</string>
|
||||
<string name="backup_periodic_hint">Al habilitar esta opción, las copias de seguridad se crean automáticamente en el directorio de copias de seguridad configurada.\nEs posible que esto no funcione si tiene habilitado el modo de ahorro de energía.</string>
|
||||
<string name="behaviour">Comportamiento</string>
|
||||
<string name="biometric_lock">Bloquear aplicación con dispositivo biométrico o con PIN</string>
|
||||
<string name="biometrics_disable_success">El bloqueo biométrico/PIN ha sido deshabilitado</string>
|
||||
<string name="biometrics_failure">Falló la autenticación biómetrica/PIN</string>
|
||||
|
@ -71,9 +72,12 @@
|
|||
<string name="clear_data_message">Todas las notas, imágenes, ficheros, audios serán borrados permanentemente</string>
|
||||
<string name="clear_formatting">Borrar formato</string>
|
||||
<string name="cleared_data">Se han limpiado todos los datos</string>
|
||||
<string name="color">Color</string>
|
||||
<string name="color_exists">¡Este color ya existe!</string>
|
||||
<string name="content_density">Densidad de contenido</string>
|
||||
<string name="continue_">Continuar</string>
|
||||
<string name="convert_to_list_note">Convertir en lista</string>
|
||||
<string name="convert_to_text_note">Convertir en nota de texto</string>
|
||||
<string name="copied_link">Enlace copiado al portapapeles</string>
|
||||
<string name="copy">Copiar</string>
|
||||
<string name="crash_message">Se produjo un error inesperado.\nDisculpe las molestias.</string>
|
||||
|
@ -115,6 +119,7 @@
|
|||
<string name="disable_lock_description">Esto también descifrará la base de datos</string>
|
||||
<string name="disable_lock_title">Deshabilitar bloqueo biométrico/PIN</string>
|
||||
<string name="disabled">Deshabilitado</string>
|
||||
<string name="disallow_screenshots">No permitir capturas de pantalla</string>
|
||||
<string name="discard">Descartar</string>
|
||||
<string name="display_text">Texto a pantalla</string>
|
||||
<string name="donate">Hacer donación</string>
|
||||
|
@ -154,6 +159,8 @@
|
|||
<string name="help">Ayuda</string>
|
||||
<string name="hours">Horas</string>
|
||||
<string name="image_format_not_supported">Formato de imagen no soportado</string>
|
||||
<string name="images_hidden_in_overview">Al habilitar esta opción, se ocultarán las imágenes de notas de la descripción general.</string>
|
||||
<string name="images_hidden_in_overview_title">Ocultar imágenes en vista general</string>
|
||||
<string name="import_action">Importar</string>
|
||||
<string name="import_backup">Importar copia de seguridad</string>
|
||||
<string name="import_backup_password_hint">Si su copia de seguridad no está protegida con contraseña, simplemente presione Importar, de lo contrario ingrese la contraseña correcta.</string>
|
||||
|
@ -181,6 +188,8 @@
|
|||
<string name="invalid_link">Copiar un enlace válido en tu portapapeles</string>
|
||||
<string name="italic">Cursiva</string>
|
||||
<string name="item">Elemento</string>
|
||||
<string name="json_files">Ficheros JSON</string>
|
||||
<string name="json_files_help">Para importar tus notas desde ficheros JSON (fichero único o carpeta), pincha en Importar. Cada fichero fichero JSON válido se importa como nota separada, el nombre del fichero será el título de la nota.</string>
|
||||
<string name="label_exists">Etiqueta existe</string>
|
||||
<string name="label_visibility">Ocultar/mostrar etiqueta en el panel de navegación</string>
|
||||
<string name="labels">Etiquetas</string>
|
||||
|
@ -235,6 +244,7 @@
|
|||
<string name="please_grant_notally_notification_auto_backup">Si falla una copia de seguridad automática, puede recibir una notificación, si otorga permiso a NotallyX para enviar notificaciones</string>
|
||||
<string name="previous">Anterior</string>
|
||||
<string name="rate">Calificar esta app</string>
|
||||
<string name="read_only">Solo lectura</string>
|
||||
<string name="ready_to_record">Preparado para grabar</string>
|
||||
<string name="record_audio">Grabar audio</string>
|
||||
<string name="recording">Grabando…</string>
|
||||
|
@ -289,6 +299,7 @@
|
|||
<string name="text_default">Por defecto</string>
|
||||
<string name="text_size">Tamaño de texto</string>
|
||||
<string name="theme">Tema</string>
|
||||
<string name="theme_use_dynamic_colors">Usar color de fondo del sistema</string>
|
||||
<string name="title">Título</string>
|
||||
<string name="to_record_audio">Para grabar audio, permita que NotallyX acceda a su micrófono. Pulsa Configuración > Permisos y active el micrófono.</string>
|
||||
<string name="unarchive">Desarchivar</string>
|
||||
|
|
|
@ -22,12 +22,13 @@
|
|||
<string name="auto_backup_error_message">Une erreur est survenue lors de la sauvegarde automatique:\n\'%1$s\'\nVeuillez vérifier vos paramètres ou signaler un bug</string>
|
||||
<string name="auto_backup_failed">La sauvegarde automatique de NotallyX a échoué </string>
|
||||
<string name="auto_backup_last">Dernière sauvegarde</string>
|
||||
<string name="auto_backup_on_save">Sauvegarde automatique après chaque édition</string>
|
||||
<string name="auto_backup_on_save">Sauvegarde automatique après avoir quitté la note</string>
|
||||
<string name="auto_backup_on_save_hint">En activant cette option, une sauvegarde (\"NotallyX_AutoBackup.zip\") est automatiquement créée dans le \"Dossier des sauvegardes\" configuré, à chaque fois qu\'une note est enregistrée.\nAttention, cela pourrait affecter les performances.</string>
|
||||
<string name="auto_backups_folder">Dossier des sauvegardes</string>
|
||||
<string name="auto_backups_folder_hint">Dossier dans lequel toutes les sauvegardes automatiques seront stockées.</string>
|
||||
<string name="auto_backups_folder_rechoose">Vous devez re-sélectionner votre dossier de sauvegarde afin que NotallyX ait l\'autorisation d\'y écrire.\nVous pouvez également appuyer sur Annuler pour ignorer l\'importation du chemin du dossier de sauvegarde.</string>
|
||||
<string name="auto_backups_folder_set">Configurer d\'abord le Dossier des sauvegardes</string>
|
||||
<string name="auto_save_after_idle_time">Enregistrer la note après un temps d’inactivité défini</string>
|
||||
<string name="auto_sort_by_checked">Trier les éléments cochés à la fin</string>
|
||||
<string name="back">Retour</string>
|
||||
<string name="backup">Sauvegarde</string>
|
||||
|
@ -36,6 +37,7 @@
|
|||
<string name="backup_period_days">Fréquence de sauvegarde périodique en jours</string>
|
||||
<string name="backup_periodic">Sauvegardes périodiques</string>
|
||||
<string name="backup_periodic_hint">En activant cette option, des sauvegardes sont automatiquement créées dans le Dossier de sauvegardes configuré.\nCela peut ne pas fonctionner si le mode économie d\'énergie est activé. </string>
|
||||
<string name="behaviour">Comportement</string>
|
||||
<string name="biometric_lock">Verrouiller l\'application avec la biométrie ou le code PIN de l\'appareil</string>
|
||||
<string name="biometrics_disable_success">Le verrouillage biométrique/code PIN a été désactivé</string>
|
||||
<string name="biometrics_failure">Échec de l\'authentification biométrique/code PIN</string>
|
||||
|
@ -74,6 +76,8 @@
|
|||
<string name="color_exists">Cette couleur existe déjà !</string>
|
||||
<string name="content_density">Densité d\'affichage</string>
|
||||
<string name="continue_">Continuer</string>
|
||||
<string name="convert_to_list_note">Convertir en liste</string>
|
||||
<string name="convert_to_text_note">Convertir en note simple</string>
|
||||
<string name="copied_link">Lien copié dans le presse-papier</string>
|
||||
<string name="copy">Copier</string>
|
||||
<string name="crash_message">Une erreur inattendue s\'est produite.\nDésolé pour le désagrément.</string>
|
||||
|
@ -115,6 +119,7 @@
|
|||
<string name="disable_lock_description">La base de donnée sera aussi décryptée</string>
|
||||
<string name="disable_lock_title">Désactiver le verrouillage biométrique/code PIN</string>
|
||||
<string name="disabled">Désactivé</string>
|
||||
<string name="disallow_screenshots">Bloquer les captures d’écran</string>
|
||||
<string name="discard">Supprimer</string>
|
||||
<string name="display_text">Texte à afficher</string>
|
||||
<string name="donate">Faire un don</string>
|
||||
|
@ -154,6 +159,8 @@
|
|||
<string name="help">Aide</string>
|
||||
<string name="hours">Heures</string>
|
||||
<string name="image_format_not_supported">Format d\'image non supporté</string>
|
||||
<string name="images_hidden_in_overview">En activant cette option, les images des notes seront masquées dans l\'aperçu</string>
|
||||
<string name="images_hidden_in_overview_title">Masquer les images dans la vue d\'ensemble</string>
|
||||
<string name="import_action">Importer</string>
|
||||
<string name="import_backup">Importer une sauvegarde</string>
|
||||
<string name="import_backup_password_hint">Si votre sauvegarde n\'est pas protégée par mot de passe, cliquez seulement sur \"Importer une sauvegarde\", sinon entrez le mot de passe correspondant.</string>
|
||||
|
@ -181,6 +188,8 @@
|
|||
<string name="invalid_link">Copier un lien valide dans le presse-papier</string>
|
||||
<string name="italic">Italique</string>
|
||||
<string name="item">Élément</string>
|
||||
<string name="json_files">Fichiers JSON</string>
|
||||
<string name="json_files_help">Pour importer vos notes depuis des fichiers JSON (fichier unique ou dossier), cliquez sur Importer. Chaque fichier JSON valide sera importé en tant que note individuelle et son nom de fichier deviendra le titre de la note.</string>
|
||||
<string name="label_exists">L\'étiquette existe déjà</string>
|
||||
<string name="label_visibility">Masquer/Afficher l\'étiquette dans le panneau de navigation</string>
|
||||
<string name="labels">Étiquettes</string>
|
||||
|
@ -235,6 +244,7 @@
|
|||
<string name="please_grant_notally_notification_auto_backup">Si une sauvegarde automatique échoue, vous pouvez recevoir une notification en accordant à NotallyX l\'autorisation d\'envoyer des notifications</string>
|
||||
<string name="previous">Précédent</string>
|
||||
<string name="rate">Noter cette application</string>
|
||||
<string name="read_only">Lecture seule</string>
|
||||
<string name="ready_to_record">Prêt à enregistrer</string>
|
||||
<string name="record_audio">Enregistrer un audio</string>
|
||||
<string name="recording">Enregistrement en cours…</string>
|
||||
|
@ -289,6 +299,7 @@
|
|||
<string name="text_default">Défaut</string>
|
||||
<string name="text_size">Taille du texte</string>
|
||||
<string name="theme">Thème</string>
|
||||
<string name="theme_use_dynamic_colors">Utiliser les couleurs de fond d’écran du système</string>
|
||||
<string name="title">Titre</string>
|
||||
<string name="to_record_audio">Pour enregistrer de l\'audio, autorisez NotallyX à accéder à votre microphone. Cliquez sur Paramètres > Autorisations et activez le microphone.</string>
|
||||
<string name="unarchive">Restaurer</string>
|
||||
|
|
|
@ -22,9 +22,8 @@
|
|||
<string name="auto_backup_error_message">Si é verificato un errore durante il backup automatico:\n\'%1$s\'\nPer favore controlla le tue impostazioni o segnala un bug</string>
|
||||
<string name="auto_backup_failed">Backup automatico di NotallyX non riuscito</string>
|
||||
<string name="auto_backup_last">Ultimo backup</string>
|
||||
<string name="auto_backup_on_save">Backup automatico al salvataggio della nota</string>
|
||||
<string name="auto_backup_on_save">Backup automatico dopo l\'uscita dalla nota</string>
|
||||
<string name="auto_backup_on_save_hint">Abilitando questa opzione, un backup (NotallyX_AutoBackup.zip) verrà creato automaticamente nella \"Cartella di Backup\" configurata ogni volta che una nota viene salvata.\nTieni presente che questa operazione potrebbe influire sulle prestazioni</string>
|
||||
<string name="auto_backup_period">Frequenza dei backup automatici</string>
|
||||
<string name="auto_backups_folder">Cartella backup</string>
|
||||
<string name="auto_backups_folder_hint">Cartella in cui tutti i backup automatici verranno salvati.</string>
|
||||
<string name="auto_backups_folder_rechoose">Devi selezionare nuovamente la tua cartella di backup affinché NotallyX abbia l\'autorizzazione per scriverci.\nPuoi anche premere annulla per saltare l\'importazione del valore della cartella di backup.</string>
|
||||
|
@ -154,6 +153,8 @@
|
|||
<string name="help">Aiuto</string>
|
||||
<string name="hours">Ore</string>
|
||||
<string name="image_format_not_supported">Formato immagine non supportato</string>
|
||||
<string name="images_hidden_in_overview">Abilitando questa opzione le immagini delle note verranno nascoste nella panoramica.</string>
|
||||
<string name="images_hidden_in_overview_title">Nascondi immagini nella panoramica</string>
|
||||
<string name="import_action">Importa</string>
|
||||
<string name="import_backup">Importa backup</string>
|
||||
<string name="import_backup_password_hint">Se il tuo backup non è protetto da password premi semplicemente Importa, altrimenti inserisci la password corretta.</string>
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
<string name="attach_file">Bestand bijvoegen</string>
|
||||
<string name="audio_recordings">Audio-opnames</string>
|
||||
<string name="auto_backup">Automatische back-up</string>
|
||||
<string name="auto_backup_period">Periode van automatische back-up</string>
|
||||
<string name="auto_sort_by_checked">Sorteer de aangevinkte items tot het einde</string>
|
||||
<string name="backup">Back-up</string>
|
||||
<string name="backup_password">Backup Wachtwoord</string>
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
<string name="archive">Zarchwizuj</string>
|
||||
<string name="archived">Zarchiwizowane</string>
|
||||
<plurals name="archived_selected_notes">
|
||||
<item quantity="few">Zarchiwizowano %1$d notatki</item>
|
||||
<item quantity="many">Zarchiwizowano %1$d notatek</item>
|
||||
<item quantity="one">Zarchiwizowano %1$d notatkę</item>
|
||||
<item quantity="other">Zarchiwizowano %1$d notatek</item>
|
||||
</plurals>
|
||||
|
@ -22,13 +24,13 @@
|
|||
<string name="auto_backup_error_message">Wystąpił błąd podczas tworzenia automatycznej kopii zapasowej:\n„%1$s”\nSprawdź ustawienia lub zgłoś błąd</string>
|
||||
<string name="auto_backup_failed">Automatyczna kopia zapasowa NotallyX nie powiodła się</string>
|
||||
<string name="auto_backup_last">Ostatnia kopia zapasowa</string>
|
||||
<string name="auto_backup_on_save">Kopia zapasowa po zapisaniu notatki</string>
|
||||
<string name="auto_backup_on_save">Kopia zapasowa po wyjściu z notatki</string>
|
||||
<string name="auto_backup_on_save_hint">Po włączeniu tej opcji kopia zapasowa („NotallyX_AutoBackup.zip”) jest automatycznie tworzona w skonfigurowanym „Folderze kopii zapasowych” za każdym razem, gdy zapisywana jest notatka.\nPamiętaj, że może mieć to wpływ na wydajność</string>
|
||||
<string name="auto_backup_period">Okres tworzenia automatycznych kopii zapasowych</string>
|
||||
<string name="auto_backups_folder">Folder kopii zapasowych</string>
|
||||
<string name="auto_backups_folder_hint">Folder, w którym będą przechowywane wszystkie automatyczne kopie zapasowe.</string>
|
||||
<string name="auto_backups_folder_rechoose">Musisz ponownie wybrać folder kopii zapasowych, aby NotallyX miał uprawnienia do zapisu w nim.\nMożesz także nacisnąć Anuluj, aby pominąć importowanie zawartości folderu kopii zapasowych</string>
|
||||
<string name="auto_backups_folder_set">Najpierw skonfiguruj folder kopii zapasowych powyżej</string>
|
||||
<string name="auto_save_after_idle_time">Automatycznie zapisz notatkę po określonym czasie bezczynności</string>
|
||||
<string name="auto_sort_by_checked">Przenieś zaznaczone elementy na koniec</string>
|
||||
<string name="back">Cofnij</string>
|
||||
<string name="backup">Kopia zapasowa</string>
|
||||
|
@ -37,6 +39,7 @@
|
|||
<string name="backup_period_days">Częstotliwość tworzenia automatycznych kopii zapasowych w dniach</string>
|
||||
<string name="backup_periodic">Powtarzalne kopie zapasowe</string>
|
||||
<string name="backup_periodic_hint">Po włączeniu tej opcji kopie zapasowe są automatycznie tworzone w skonfigurowanym folderze kopii zapasowych.\nMoże to nie działać, jeśli włączony jest tryb oszczędzania energii</string>
|
||||
<string name="behaviour">Zachowanie</string>
|
||||
<string name="biometric_lock">Zablokuj aplikację za pomocą danych biometrycznych lub PIN-u urządzenia</string>
|
||||
<string name="biometrics_disable_success">Blokada biometryczna/PIN została wyłączona</string>
|
||||
<string name="biometrics_failure">Nie udało się uwierzytelnić za pomocą biometrii/PIN-u</string>
|
||||
|
@ -47,6 +50,8 @@
|
|||
<string name="calculating">Obliczanie…</string>
|
||||
<string name="cancel">Anuluj</string>
|
||||
<plurals name="cant_add_files">
|
||||
<item quantity="few">Nie można dodać %1$d plików</item>
|
||||
<item quantity="many">Nie można dodać %1$d plików</item>
|
||||
<item quantity="one">Nie można dodać %1$d pliku</item>
|
||||
<item quantity="other">Nie można dodać %1$d plików</item>
|
||||
</plurals>
|
||||
|
@ -73,9 +78,12 @@
|
|||
<string name="clear_data_message">Wszystkie notatki, obrazy, pliki i nagrania zostaną bezpowrotnie usunięte</string>
|
||||
<string name="clear_formatting">Usuń formatowanie</string>
|
||||
<string name="cleared_data">Wszystkie dane zostały usunięte</string>
|
||||
<string name="color">Kolor</string>
|
||||
<string name="color_exists">Ten kolor już istnieje!</string>
|
||||
<string name="content_density">Zagęszczenie treści</string>
|
||||
<string name="continue_">Kontynuuj</string>
|
||||
<string name="convert_to_list_note">Zamień na listę</string>
|
||||
<string name="convert_to_text_note">Zamień na notatkę</string>
|
||||
<string name="copied_link">Skopiowano link do schowka</string>
|
||||
<string name="copy">Skopiuj</string>
|
||||
<string name="crash_message">Wystąpił nieoczekiwany błąd.\nPrzepraszam za niedogodności.</string>
|
||||
|
@ -106,6 +114,8 @@
|
|||
<string name="delete_selected_notes">Usunąć zaznaczone notatki\?</string>
|
||||
<string name="deleted">Usunięte</string>
|
||||
<plurals name="deleted_selected_notes">
|
||||
<item quantity="few">Usunięto %1$d notatki</item>
|
||||
<item quantity="many">Usunięto %1$d notatek</item>
|
||||
<item quantity="one">Usunięto %1$d notatkę</item>
|
||||
<item quantity="other">Usunięto %1$d notatek</item>
|
||||
</plurals>
|
||||
|
@ -137,7 +147,7 @@
|
|||
<string name="error_while_renaming_file">Błąd podczas zmiany nazwy pliku</string>
|
||||
<string name="error_while_renaming_image">Błąd podczas zmiany nazwy obrazu</string>
|
||||
<string name="evernote">Evernote</string>
|
||||
<string name="evernote_help">Aby zaimportować notatki z Evernote, musisz wyeksportować notatnik Evernote jako ENEX. Kliknij Pomoc, aby uzyskać więcej informacji.\nJeśli masz już plik ENEX, kliknij Importuj i wybierz go.</string>
|
||||
<string name="evernote_help">Aby zaimportować notatki z Evernote, musisz wyeksportować notatnik Evernote jako ENEX. Kliknij Pomoc, aby uzyskać więcej informacji.\nJeśli masz już plik ENEX, kliknij Przywracanie i wybierz go.</string>
|
||||
<string name="every">Każde</string>
|
||||
<string name="export">Eksportuj</string>
|
||||
<string name="export_backup">Wykonaj kopię zapasową</string>
|
||||
|
@ -151,11 +161,13 @@
|
|||
<string name="folder">Folder</string>
|
||||
<string name="follow_system">Systemowy</string>
|
||||
<string name="google_keep">Google Keep</string>
|
||||
<string name="google_keep_help">Aby zaimportować notatki z Google Keep, musisz pobrać plik ZIP Google Takeout. Wybierz tylko dane „Keep”. Kliknij Pomoc, aby uzyskać więcej informacji.\nJeśli masz już plik ZIP Takeout, kliknij Importuj i wybierz plik ZIP.</string>
|
||||
<string name="google_keep_help">Aby zaimportować notatki z Google Keep, musisz pobrać plik ZIP Google Takeout. Wybierz tylko dane „Keep”. Kliknij Pomoc, aby uzyskać więcej informacji.\nJeśli masz już plik ZIP Takeout, kliknij Przywracanie i wybierz plik ZIP.</string>
|
||||
<string name="grid">Siatka</string>
|
||||
<string name="help">Pomoc</string>
|
||||
<string name="hours">Godzin</string>
|
||||
<string name="image_format_not_supported">Format obrazu nie jest obsługiwany</string>
|
||||
<string name="images_hidden_in_overview">Po włączeniu tej opcji obrazy notatek będą ukryte w przeglądzie</string>
|
||||
<string name="images_hidden_in_overview_title">Ukryj obrazy w przeglądzie</string>
|
||||
<string name="import_action">Przywracanie</string>
|
||||
<string name="import_backup">Przywróć kopię zapasową</string>
|
||||
<string name="import_backup_password_hint">Jeśli kopia zapasowa nie jest chroniona hasłem, po prostu naciśnij Importuj, w przeciwnym razie wprowadź prawidłowe hasło.</string>
|
||||
|
@ -167,6 +179,8 @@
|
|||
<string name="imported_files">Przywrócono pliki</string>
|
||||
<string name="imported_notes">Przywrócono notatki</string>
|
||||
<plurals name="imported_notes">
|
||||
<item quantity="few">Przywrócono %1$s notatki</item>
|
||||
<item quantity="many">Przywrócono %1$s notatek</item>
|
||||
<item quantity="one">Przywrócono %1$s notatkę</item>
|
||||
<item quantity="other">Przywrócono %1$s notatek</item>
|
||||
</plurals>
|
||||
|
@ -183,6 +197,8 @@
|
|||
<string name="invalid_link">Skopiuj właściwy link do schowka</string>
|
||||
<string name="italic">Kursywa</string>
|
||||
<string name="item">Element</string>
|
||||
<string name="json_files">Pliki JSON</string>
|
||||
<string name="json_files_help">Aby zaimportować notatki z plików JSON (pojedynczy plik lub folder), kliknij Przywracanie. Każdy prawidłowy plik JSON jest importowany jako osobna notatka, a nazwa pliku staje się tytułem notatki.</string>
|
||||
<string name="label_exists">Etykieta już istnieje</string>
|
||||
<string name="label_visibility">Ukryj/pokaż etykietę w panelu nawigacyjnym</string>
|
||||
<string name="labels">Etykieta</string>
|
||||
|
@ -206,11 +222,13 @@
|
|||
<string name="medium">Średni</string>
|
||||
<string name="minutes">Minut</string>
|
||||
<string name="modified_date">Zmodyfikowano</string>
|
||||
<string name="monospace">Monospace</string>
|
||||
<string name="monospace">Stała szerokość</string>
|
||||
<string name="monthly">Miesięcznie</string>
|
||||
<string name="months">Miesięcy</string>
|
||||
<string name="more">%1$d więcej</string>
|
||||
<plurals name="more_files">
|
||||
<item quantity="few">…%1$d pliki więcej</item>
|
||||
<item quantity="many">…%1$d plików więcej</item>
|
||||
<item quantity="one">…%1$d plik więcej</item>
|
||||
<item quantity="other">…%1$d plików więcej</item>
|
||||
</plurals>
|
||||
|
@ -229,7 +247,7 @@
|
|||
<string name="pin">Przypnij</string>
|
||||
<string name="pinned">Przypięte</string>
|
||||
<string name="plain_text_files">Zwykłe pliki tekstowe</string>
|
||||
<string name="plain_text_files_help">Aby zaimportować Notatki z plików tekstowych (pojedynczy plik lub folder), kliknij Importuj. Każdy plik jest importowany jako osobna notatka, a nazwa pliku staje się tytułem notatki. Jeśli treść tekstu zaczyna się od składni listy (np. Markdown „- [x]”, składnia NotallyX „[✓]” lub „*”, „-”), zostanie ona przekonwertowana na notatkę listy.</string>
|
||||
<string name="plain_text_files_help">Aby zaimportować Notatki z plików tekstowych (pojedynczy plik lub folder), kliknij Przywracanie. Każdy plik jest importowany jako osobna notatka, a nazwa pliku staje się tytułem notatki. Jeśli treść tekstu zaczyna się od składni listy (np. Markdown „- [x]”, składnia NotallyX „[✓]” lub „*”, „-”), zostanie ona przekonwertowana na notatkę listy.</string>
|
||||
<string name="play">Nagrywaj</string>
|
||||
<string name="please_grant_notally_alarm">Zezwól NotallyX na wysyłanie przypomnień</string>
|
||||
<string name="please_grant_notally_audio">Zezwól NotallyX na nagrywanie dźwięku. Nagrania nigdy nie opuszczą Twojego urządzenia</string>
|
||||
|
@ -237,6 +255,7 @@
|
|||
<string name="please_grant_notally_notification_auto_backup">Jeśli automatyczna kopia zapasowa nie powiedzie się, możesz otrzymać powiadomienie, jeśli zezwolisz NotallyX na wysyłanie powiadomień</string>
|
||||
<string name="previous">Wstecz</string>
|
||||
<string name="rate">Oceń aplikację</string>
|
||||
<string name="read_only">Tylko do odczytu</string>
|
||||
<string name="ready_to_record">Oczekiwanie na nagrywanie</string>
|
||||
<string name="record_audio">Nagrywaj</string>
|
||||
<string name="recording">Nagrywanie…</string>
|
||||
|
@ -255,6 +274,8 @@
|
|||
<string name="restart_app">Uruchom ponownie aplikację</string>
|
||||
<string name="restore">Przywróć</string>
|
||||
<plurals name="restored_selected_notes">
|
||||
<item quantity="few">Przywrócono %1$d notatki</item>
|
||||
<item quantity="many">Przywrócono %1$d notatek</item>
|
||||
<item quantity="one">Przywrócono %1$d notatkę</item>
|
||||
<item quantity="other">Przywrócono %1$d notatek</item>
|
||||
</plurals>
|
||||
|
@ -291,10 +312,13 @@
|
|||
<string name="text_default">Domyślny</string>
|
||||
<string name="text_size">Rozmiar tekstu</string>
|
||||
<string name="theme">Motyw</string>
|
||||
<string name="theme_use_dynamic_colors">Użyj kolorów tapety systemu</string>
|
||||
<string name="title">Tytuł</string>
|
||||
<string name="to_record_audio">Aby nagrywać dźwięk, zezwól NotallyX na dostęp do swojego mikrofonu. Dotknij Ustawienia > Uprawnienia i włącz Mikrofon</string>
|
||||
<string name="unarchive">Cofnij archiwizację</string>
|
||||
<plurals name="unarchived_selected_notes">
|
||||
<item quantity="few">Odarchiwizowano %1$d notatki</item>
|
||||
<item quantity="many">Odarchiwizowano %1$d notatek</item>
|
||||
<item quantity="one">Odarchiwizowano %1$d notatkę</item>
|
||||
<item quantity="other">Odarchiwizowano %1$d notatek</item>
|
||||
</plurals>
|
||||
|
|
|
@ -1,77 +1,334 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="about">О приложении</string>
|
||||
<string name="add_images">Добавить изображение</string>
|
||||
<string name="add_item">Добавить пункт</string>
|
||||
<string name="add_label">Добавить метку</string>
|
||||
<string name="add_reminder">Добавить напоминание</string>
|
||||
<string name="adding_files">Добавление файлов</string>
|
||||
<string name="adding_images">Добавление изображений</string>
|
||||
<string name="all">Все</string>
|
||||
<string name="appearance">Внешний вид</string>
|
||||
<string name="archive">Архивировать</string>
|
||||
<string name="archived">Архив</string>
|
||||
<plurals name="archived_selected_notes">
|
||||
<item quantity="few">Архивировано %1$d заметки</item>
|
||||
<item quantity="one">Архивирована %1$d заметка</item>
|
||||
<item quantity="other">Архивировано %1$d заметок</item>
|
||||
</plurals>
|
||||
<string name="ascending">По возрастанию</string>
|
||||
<string name="attach_file">Прикрепить файл</string>
|
||||
<string name="audio_recordings">Аудиозаписи</string>
|
||||
<string name="auto_backup">Автобэкапы</string>
|
||||
<string name="auto_backup_error_message">Ошибка при авто-резервной копии: \n\'%1$s\'\nПроверьте настройки или сообщите об ошибке</string>
|
||||
<string name="auto_backup_failed">Сбой авто-резервирования NotallyX</string>
|
||||
<string name="auto_backup_last">Последняя резервная копия</string>
|
||||
<string name="auto_backup_on_save">Создавать резервную копию при выходе (авто)</string>
|
||||
<string name="auto_backup_on_save_hint">При включении этой функции в папке резервных копий (Папка резервных копий) автоматически создаётся архив NotallyX_AutoBackup.zip при выходе из заметки.\nЭто может повлиять на производительность</string>
|
||||
<string name="auto_backups_folder">Папка резервных копий</string>
|
||||
<string name="auto_backups_folder_hint">Папка, в которой будут храниться резервные копии</string>
|
||||
<string name="auto_backups_folder_rechoose">Необходимо повторно указать папку для резервных копий для доступа NotallyX.\nМожно отменить и пропустить импорт папки резервных копий</string>
|
||||
<string name="auto_backups_folder_set">Сначала укажите папку для резервных копий</string>
|
||||
<string name="auto_save_after_idle_time">Автосохранение заметки после указанного времени бездействия</string>
|
||||
<string name="auto_sort_by_checked">Сортировать отмеченные элементы в конец</string>
|
||||
<string name="back">Назад</string>
|
||||
<string name="backup">Резервная копия</string>
|
||||
<string name="backup_password">Пароль для резервной копии</string>
|
||||
<string name="backup_password_hint">При включении этой функции все новые ZIP-архивы резервных копий буду зашифрованы и защищены паролем</string>
|
||||
<string name="backup_period_days">Интервал авторезервирования (в днях)</string>
|
||||
<string name="backup_periodic">Переодические резервные копии</string>
|
||||
<string name="backup_periodic_hint">При включении этой функции резервные копии будут автоматически сохраняться в указанную папку.\nФункция может не работать при включенном режиме энергосбережения</string>
|
||||
<string name="behaviour">Биометрия</string>
|
||||
<string name="biometric_lock">Блокировать приложение с помощью биометрии или PIN-кода</string>
|
||||
<string name="biometrics_disable_success">Биометрическая защита/PIN-код отключены</string>
|
||||
<string name="biometrics_failure">Ошибка аутентификации по Биометрии/PIN-коду</string>
|
||||
<string name="biometrics_no_support">На устройстве нет биометрических функций</string>
|
||||
<string name="biometrics_not_setup">Биометрия/PIN-код ещё не настроены</string>
|
||||
<string name="biometrics_setup_success">Защита по Биометрии/PIN-коду включена</string>
|
||||
<string name="bold">Жирный</string>
|
||||
<string name="calculating">Вычисление…</string>
|
||||
<string name="cancel">Отменить</string>
|
||||
<plurals name="cant_add_files">
|
||||
<item quantity="few">Невозможно добавить %1$d файла</item>
|
||||
<item quantity="one">Невозможно добавить %1$d файл</item>
|
||||
<item quantity="other">Невозможно добавить %1$d файлов</item>
|
||||
</plurals>
|
||||
<plurals name="cant_add_images">
|
||||
<item quantity="few">Невозможно добавить %1$d изображения</item>
|
||||
<item quantity="one">Невозможно добавить %1$d изображение</item>
|
||||
<item quantity="other">Невозможно добавить %1$d изображений</item>
|
||||
</plurals>
|
||||
<string name="cant_find_folder">Папка не найдена. Возможно, она была перемещена или удалена</string>
|
||||
<string name="cant_find_note">Заметка не найдена. Возможно она была удалена</string>
|
||||
<string name="cant_load_file">Файл не загружен. Возможно, он был перемещён или удалён</string>
|
||||
<string name="cant_load_image">Изображение не загружено. Проверьте, не было ли оно удалено</string>
|
||||
<string name="cant_open_link">Невозможно открыть ссылку</string>
|
||||
<string name="change_color">Изменить цвет</string>
|
||||
<string name="change_color_message">Выберите цвет или создайте новый.\nЦвет можно изменить долгим нажатием.</string>
|
||||
<string name="change_note">Изменить заметку</string>
|
||||
<string name="check_all_items">Выделить всё</string>
|
||||
<string name="choose_another_folder">Выбрать другую папку</string>
|
||||
<string name="choose_folder">Выбрать папку</string>
|
||||
<string name="choose_other_app">Выберите приложение для импорта</string>
|
||||
<string name="clear">Очистить</string>
|
||||
<string name="clear_data">Очистить данные</string>
|
||||
<string name="clear_data_message">Все Заметки, Изображения, Файлы и Аудиозаписи будут навсегда удалены</string>
|
||||
<string name="clear_formatting">Стандартный стиль</string>
|
||||
<string name="cleared_data">Все данные удалены</string>
|
||||
<string name="color">Цвет</string>
|
||||
<string name="color_exists">Этот цвет уже существует!</string>
|
||||
<string name="content_density">Плотность контента</string>
|
||||
<string name="continue_">Продолжить</string>
|
||||
<string name="convert_to_list_note">Конвертировать в Список</string>
|
||||
<string name="convert_to_text_note">Конвертировать в Заметку</string>
|
||||
<string name="copied_link">Ссылка скопирована в буфер обмена</string>
|
||||
<string name="copy">Копировать</string>
|
||||
<string name="crash_message">Произошла непредвиденная ошибка.\nПриносим извинения за причиненные неудобства.</string>
|
||||
<string name="create_github_issue">Сообщить об ошибке на GitHub</string>
|
||||
<string name="creation_date">Создано</string>
|
||||
<string name="custom">Пользовтаельский</string>
|
||||
<string name="daily">Дни</string>
|
||||
<string name="dark">Тёмная</string>
|
||||
<string name="data_in_public">Сохранять данные в общедоступной папке</string>
|
||||
<string name="data_in_public_message">При включении этой функции внутренняя база данных приложения будет перенесена в его общедоступную папку (Android/media/com.philkes.notallyx).\nЭто позволит синхронизировать данные NotallyX между устройствами с помощью приложений для синхронизации файлов.</string>
|
||||
<string name="date">Дата</string>
|
||||
<string name="date_format">Формат даты</string>
|
||||
<string name="date_format_apply_in_note_view">Применять и в просмотре заметки</string>
|
||||
<string name="date_format_hint">Применяет выбранный формат даты в общем списке заметок</string>
|
||||
<string name="days">Дней</string>
|
||||
<string name="delete">Удалить</string>
|
||||
<string name="delete_all">Удалить всё</string>
|
||||
<string name="delete_all_notes">Удалить все заметки\?</string>
|
||||
<string name="delete_audio_recording_forever">Удалить все аудиозаписи\?</string>
|
||||
<string name="delete_checked_items">Удалить выбранные элементы</string>
|
||||
<string name="delete_color_message">Каким цветом заменить текущий цвет заметок\?</string>
|
||||
<string name="delete_file">Удалить файл \'%1$s\'\?</string>
|
||||
<string name="delete_forever">Удалить навсегда</string>
|
||||
<string name="delete_image_forever">Удалить изображение навсегда\?</string>
|
||||
<string name="delete_label">Удалить метку\?</string>
|
||||
<string name="delete_note_forever">Удалить заметку навсегда\?</string>
|
||||
<string name="delete_reminder">Удалить напоминание\?</string>
|
||||
<string name="delete_selected_notes">Удалить выбранные заметки\?</string>
|
||||
<string name="deleted">Удалённые</string>
|
||||
<plurals name="deleted_selected_notes">
|
||||
<item quantity="few">Удалено %1$d заметки</item>
|
||||
<item quantity="one">Удалена %1$d заметка</item>
|
||||
<item quantity="other">Удалено %1$d заметок</item>
|
||||
</plurals>
|
||||
<string name="deleting_files">Удаление файлов</string>
|
||||
<string name="deleting_images">Удаление изображений</string>
|
||||
<string name="descending">По убыванию</string>
|
||||
<string name="disable">Отключить</string>
|
||||
<string name="disable_data_in_public">Переместить данные во внутреннюю папку</string>
|
||||
<string name="disable_lock_description">Это также расшифрует базу данных</string>
|
||||
<string name="disable_lock_title">Отключить защиту по Биометрии/PIN-коду</string>
|
||||
<string name="disabled">Отключено</string>
|
||||
<string name="disallow_screenshots">Запретить создание скриншотов</string>
|
||||
<string name="discard">Отменить</string>
|
||||
<string name="display_text">Текст для отображения</string>
|
||||
<string name="donate">Сделать пожертвование</string>
|
||||
<string name="drag_handle">Перетащить</string>
|
||||
<string name="edit">Редактировать</string>
|
||||
<string name="edit_color">Изменить цвет</string>
|
||||
<string name="edit_label">Изменить метку</string>
|
||||
<string name="empty_labels">Еще нет меток, создать\?</string>
|
||||
<string name="edit_link">Изменить ссылку</string>
|
||||
<string name="edit_reminders">Изменить напоминания</string>
|
||||
<string name="elapsed">Просроченные</string>
|
||||
<string name="empty_labels">Еще нет меток. Создать\?</string>
|
||||
<string name="empty_list">Пустой список</string>
|
||||
<string name="empty_note">Пустая заметка</string>
|
||||
<string name="export">Экспортировать</string>
|
||||
<string name="export_backup">Экспортировать</string>
|
||||
<string name="empty_reminders">Напоминаний нет. Создать\?</string>
|
||||
<string name="enable_lock_description">Это также зашифрует базу данных</string>
|
||||
<string name="enable_lock_title">Включить защиту по Биометрии/PIN-коду</string>
|
||||
<string name="enabled">Включено</string>
|
||||
<string name="error_while_renaming_file">Ошибка переименования файла</string>
|
||||
<string name="error_while_renaming_image">Ошибка переименования изображения</string>
|
||||
<string name="evernote">Evernote</string>
|
||||
<string name="evernote_help">Чтобы импортировать заметки из Evernote, экспортируйте блокнот в формате ENEX. Нажмите \"Помощь\" для инструкций.\n\nЕсли файл ENEX уже готов, выберите \"Импорт\"</string>
|
||||
<string name="every">Каждый</string>
|
||||
<string name="export">Экспорт</string>
|
||||
<string name="export_backup">Экспорт резервной копии</string>
|
||||
<string name="export_settings">Экспорт настроек</string>
|
||||
<string name="export_settings_failure">Не удалось экспортировать настройки. Возможно, указан недопустимый путь</string>
|
||||
<string name="export_settings_message">Все настройки будут сохранены в JSON-файл для последующего импорта.\n\nОбратите внимание: зашифрованные данные (пароль авторезерва и биометрический ключ) не экспортируются.</string>
|
||||
<string name="export_settings_success">Настройки успешно экспортированы</string>
|
||||
<string name="exporting_backup">Экспорт резервной копии</string>
|
||||
<string name="extracted_files">Файлы извлечены</string>
|
||||
<string name="filter">Фильтры</string>
|
||||
<string name="folder">Папка</string>
|
||||
<string name="follow_system">Как в системе</string>
|
||||
<string name="google_keep">Google Keep</string>
|
||||
<string name="google_keep_help">Для импорта из Google Keep скачайте архив Takeout (только данные \"Keep\"). Нажмите \"Помощь\" для подробностей.\n\nЕсли архив уже есть, выберите его через \"Импорт\"</string>
|
||||
<string name="grid">Сетка</string>
|
||||
<string name="import_backup">Импортировать</string>
|
||||
<string name="help">Помощь</string>
|
||||
<string name="hours">Часов</string>
|
||||
<string name="image_format_not_supported">Формат изображения не поддерживыется</string>
|
||||
<string name="images_hidden_in_overview">Если эта функция включена, изображения заметок будут скрыты из общего списка.</string>
|
||||
<string name="images_hidden_in_overview_title">Скрыть изображения в общем списке</string>
|
||||
<string name="import_action">Импорт</string>
|
||||
<string name="import_backup">Импорт резервной копии</string>
|
||||
<string name="import_backup_password_hint">Если резервная копия не защищена паролем, нажмите \"Импорт\". В противном случае введите пароль.</string>
|
||||
<string name="import_other">Импорт заметок из других приложений</string>
|
||||
<string name="import_settings">Импорт настроек</string>
|
||||
<string name="import_settings_failure">Не удалось импортировать настройки. Вы выбрали правильный файл\?</string>
|
||||
<string name="import_settings_message">Для импорта настроек выберите корректный JSON-файл настроек NotallyX. </string>
|
||||
<string name="import_settings_success">Настройки успешно импортированы</string>
|
||||
<string name="imported_files">Файлы импортированы</string>
|
||||
<string name="imported_notes">Заметки импортированы</string>
|
||||
<plurals name="imported_notes">
|
||||
<item quantity="few">Импортировано %1$s заметки</item>
|
||||
<item quantity="one">Импортирована %1$s заметка</item>
|
||||
<item quantity="other">Импортировано %1$s заметок</item>
|
||||
</plurals>
|
||||
<string name="importing_backup">Импорт резервной копии</string>
|
||||
<string name="insert_an_sd_card_audio">Вставьте SD-карту для записи аудио</string>
|
||||
<string name="insert_an_sd_card_files">Для добавления файлов вставьте SD-карту</string>
|
||||
<string name="insert_an_sd_card_images">Вставьте SD-карту, чтобы загрузить изображение</string>
|
||||
<string name="install_a_browser">Чтобы открыть ссылку, установите браузер</string>
|
||||
<string name="install_an_email">Установите клиент почты для отправки отзыва</string>
|
||||
<string name="invalid_backup">Некорректная резервная копия</string>
|
||||
<string name="invalid_evernote">Недопустимый файл Evernote (ENEX)</string>
|
||||
<string name="invalid_google_keep">Некорректный ZIP-архив Google Takeout</string>
|
||||
<string name="invalid_image">Изображение повреждено</string>
|
||||
<string name="invalid_link">Скопируйте рабочую ссылку в буфер обмена</string>
|
||||
<string name="italic">Курсив</string>
|
||||
<string name="item">Пункт</string>
|
||||
<string name="json_files">JSON файлы</string>
|
||||
<string name="json_files_help">Для импорта заметок из JSON-файлов (отдельный файл или папка) нажмите \"Импорт\". Каждый корректный JSON-файл будет преобразован в отдельную заметку, где имя файла станет её заголовком</string>
|
||||
<string name="label_exists">Метка уже существует</string>
|
||||
<string name="label_visibility">Скрыть/показать метку в панели навигации</string>
|
||||
<string name="labels">Метки</string>
|
||||
<string name="labels_hidden_in_overview">При включении этой функции метки заметок будут скрыты в общем списке</string>
|
||||
<string name="labels_hidden_in_overview_title">Скрывать метки в общем списке</string>
|
||||
<string name="large">Большой</string>
|
||||
<string name="libraries">Библиотеки</string>
|
||||
<string name="light">Светлая</string>
|
||||
<string name="link">Ссылка</string>
|
||||
<string name="link_note">Связать заметку</string>
|
||||
<string name="list">Список</string>
|
||||
<string name="list_item_auto_sort">Сортировать элементы списка</string>
|
||||
<string name="locked">Заблокировано</string>
|
||||
<string name="make_feature_request">Запросить новую функцию</string>
|
||||
<string name="make_list">Создать список</string>
|
||||
<string name="max_backups">Лимит резервных копий</string>
|
||||
<string name="max_items_to_display">Максимум отображаемых пунктов в списке</string>
|
||||
<string name="max_labels_to_display">Максимум отображаемых меток</string>
|
||||
<string name="max_lines_to_display">Максимум отображаемых строк в заметке</string>
|
||||
<string name="max_lines_to_display_title">Максимум отображаемых строк в заголовке</string>
|
||||
<string name="medium">Средний</string>
|
||||
<string name="minutes">Минуты</string>
|
||||
<string name="modified_date">Изменено</string>
|
||||
<string name="monospace">Моноширинный</string>
|
||||
<string name="monthly">Ежемесячно</string>
|
||||
<string name="months">Месяцы</string>
|
||||
<string name="more">Ещё %1$d</string>
|
||||
<plurals name="more_files">
|
||||
<item quantity="few">...ещё %1$d файла</item>
|
||||
<item quantity="one">...ещё %1$d файл</item>
|
||||
<item quantity="other">...ещё %1$d файлов</item>
|
||||
</plurals>
|
||||
<string name="new_color">Новый цвет</string>
|
||||
<string name="next">Следующий</string>
|
||||
<string name="no_auto_sort">Без автосортировки</string>
|
||||
<string name="none">Отсутствует</string>
|
||||
<string name="note">Заметка</string>
|
||||
<string name="notes">Заметки</string>
|
||||
<string name="notes_sorted_by">Сортировать заметки по</string>
|
||||
<string name="open_link">Открыть ссылку</string>
|
||||
<string name="open_note">Открыть заметку</string>
|
||||
<string name="others">Другие</string>
|
||||
<string name="pause">Пауза</string>
|
||||
<string name="paused">Приостановлено</string>
|
||||
<string name="pin">Закрепить</string>
|
||||
<string name="pinned">Закреплённые</string>
|
||||
<string name="plain_text_files">Текстовые файлы</string>
|
||||
<string name="plain_text_files_help">Для импорта заметок из текстовых файлов (отдельный файл или папка) нажмите \"Импорт\". Каждый файл станет отдельной заметкой — имя файла будет её заголовком. Если текст начинается с элементов списка (например, Markdown \"- [x]\", синтаксис NotallyX \"[~/\", или \"*\", ~), он преобразуется в заметку-список.</string>
|
||||
<string name="play">Играть</string>
|
||||
<string name="please_grant_notally_alarm">Разрешите NotallyX отправлять напоминания</string>
|
||||
<string name="please_grant_notally_audio">Разрешите доступ к микрофону.\nЗаписи остаются на вашем устройстве</string>
|
||||
<string name="please_grant_notally_notification">Разрешите отправку уведомлений</string>
|
||||
<string name="please_grant_notally_notification_auto_backup">При сбое авторезервирования вы получите уведомление, если разрешите их отправку. </string>
|
||||
<string name="previous">Предыдущий</string>
|
||||
<string name="rate">Оценить приложение</string>
|
||||
<string name="read_only">Только чтение</string>
|
||||
<string name="ready_to_record">Готово к записи</string>
|
||||
<string name="record_audio">Запись аудио</string>
|
||||
<string name="recording">Запись…</string>
|
||||
<string name="redo">Вернуть</string>
|
||||
<string name="reminder_no_repetition">Без повтора</string>
|
||||
<string name="reminders">Напоминания</string>
|
||||
<string name="remove_link">Удалить ссылку</string>
|
||||
<string name="repetition">Повтор</string>
|
||||
<string name="repetition_custom">Свой вариант повтора</string>
|
||||
<string name="repetition_value_hint">Значение</string>
|
||||
<string name="report_bug">Сообщить об ошибке/баге</string>
|
||||
<string name="report_crash">Отправить отчёт об ошибке</string>
|
||||
<string name="reset_settings">Сбросить настройки</string>
|
||||
<string name="reset_settings_message">Все настройки будут сброшены к значениям по умолчанию</string>
|
||||
<string name="reset_settings_success">Все настройки успешно сброшены</string>
|
||||
<string name="restart_app">Перезапустить приложение</string>
|
||||
<string name="restore">Восстановить</string>
|
||||
<plurals name="restored_selected_notes">
|
||||
<item quantity="few">Восстановлено %1$d заметки</item>
|
||||
<item quantity="one">Восстановлена %1$d заметка</item>
|
||||
<item quantity="other">Восстановлено %1$d заметок</item>
|
||||
</plurals>
|
||||
<string name="resume">Повторить</string>
|
||||
<string name="save">Сохранить</string>
|
||||
<string name="save_recording">Сохранить аудиозапись\?</string>
|
||||
<string name="save_to_device">Сохранить на устройстве</string>
|
||||
<string name="saved_to_device">Сохранено на устройстве</string>
|
||||
<string name="saved_to_notally">Сохранено в NotallyX</string>
|
||||
<string name="search">Поиск</string>
|
||||
<string name="security">Безопасность</string>
|
||||
<string name="select_all">Выбрать всё</string>
|
||||
<string name="select_labels">Выбрать метки</string>
|
||||
<string name="select_note">Выбрать заметки</string>
|
||||
<string name="send_feedback">Сообщить о проблеме</string>
|
||||
<string name="settings">Настройки</string>
|
||||
<string name="share">Поделиться</string>
|
||||
<string name="skip">Пропустить</string>
|
||||
<string name="small">Маленький</string>
|
||||
<string name="something_went_wrong">Что-то пошло не так. Пожалуйста, повторите попытку</string>
|
||||
<string name="sort_direction">Порядок сортировки</string>
|
||||
<string name="source_code">Исходный код</string>
|
||||
<string name="start">Старт</string>
|
||||
<string name="start_view">Стартовый экран</string>
|
||||
<string name="start_view_hint">Выберите, какой экран/ярлык показывать при запуске.\nПо умолчанию — основной список заметок</string>
|
||||
<string name="stop">Стоп</string>
|
||||
<string name="strikethrough">Перечёркнутый</string>
|
||||
<string name="take_note">Создать заметку</string>
|
||||
<string name="tap_for_more_options">Нажмите для подробностей</string>
|
||||
<string name="tap_to_set_up">Нажмите для настройки</string>
|
||||
<string name="text_default">По умолчанию</string>
|
||||
<string name="text_size">Размер текста</string>
|
||||
<string name="theme">Тема</string>
|
||||
<string name="theme_use_dynamic_colors">Использовать цвета обоев</string>
|
||||
<string name="title">Заголовок</string>
|
||||
<string name="to_record_audio">Что-бы записывать аудиозаписи, разрешите NotallyX доступ к микрофону</string>
|
||||
<string name="unarchive">Разархивировать</string>
|
||||
<plurals name="unarchived_selected_notes">
|
||||
<item quantity="few">Извлечено %1$d заметки</item>
|
||||
<item quantity="one">Извлечено %1$d заметка</item>
|
||||
<item quantity="other">Извлечено %1$d заметок</item>
|
||||
</plurals>
|
||||
<string name="uncheck_all_items">Очистить выбор</string>
|
||||
<string name="undo">Вернуть</string>
|
||||
<string name="unknown_error">Неизвестная ошибка</string>
|
||||
<string name="unknown_name">Неизвестное имя</string>
|
||||
<string name="unlabeled">Без названия</string>
|
||||
<string name="unlock">Разблокировка по Биометрии/PIN-коду</string>
|
||||
<string name="unlock_with_biometrics_not_setup">Ранее вы включили биометрическую защиту, но сейчас на устройстве не настроены биометрия или PIN-код.\n\nНажмите «Отключить», чтобы снять блокировку, или настройте биометрию/PIN-код заново</string>
|
||||
<string name="unpin">Открепить</string>
|
||||
<string name="upcoming">Предстоящие</string>
|
||||
<string name="updated_link">Обновить ссылку</string>
|
||||
<string name="view">Вид</string>
|
||||
<string name="view_file">Посмотреть файл</string>
|
||||
<string name="view_note">Посмотреть заметку…</string>
|
||||
<string name="weekly">Еженедельный</string>
|
||||
<string name="weeks">Недели</string>
|
||||
<string name="wrong_password">Неверный пароль</string>
|
||||
<string name="yearly">Ежегодный</string>
|
||||
<string name="years">Года</string>
|
||||
<string name="your_notes_associated">Ваши заметки, связанные с этой меткой, не будут удалены</string>
|
||||
</resources>
|
||||
|
|
|
@ -51,8 +51,6 @@
|
|||
|
||||
<item name="android:statusBarColor">?attr/colorSurface</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:navigationBarColor">?attr/colorSurface</item>
|
||||
|
||||
<item name="android:windowBackground">?attr/colorSurface</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -54,4 +54,9 @@
|
|||
<item name="android:navigationBarColor">?attr/colorSurface</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.App.BottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog">
|
||||
<item name="bottomSheetStyle">@style/ModalBottomSheet</item>
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -24,11 +24,11 @@
|
|||
<string name="auto_backup_last">上次备份</string>
|
||||
<string name="auto_backup_on_save">保存笔记时自动备份</string>
|
||||
<string name="auto_backup_on_save_hint">启用本选项后,每当保存笔记时都会在所配置的”备份文件夹“中自动创建一个备份文件(”NotallyX_AutoBackup.zip)。请注意这可能影响性能</string>
|
||||
<string name="auto_backup_period">自动备份周期</string>
|
||||
<string name="auto_backups_folder">备份文件夹</string>
|
||||
<string name="auto_backups_folder_hint">保存所有自动备份文件的文件夹</string>
|
||||
<string name="auto_backups_folder_rechoose">你需要重新选择备份文件夹,这样 NotallyX 有写入它的权限。你也可以按:取消“跳过导入备份文件夹的值</string>
|
||||
<string name="auto_backups_folder_set">先在上方设置备份文件夹</string>
|
||||
<string name="auto_save_after_idle_time">在指定的空闲时间后自动保存笔记</string>
|
||||
<string name="auto_sort_by_checked">将所选项目排到末尾</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="backup">备份</string>
|
||||
|
@ -37,6 +37,7 @@
|
|||
<string name="backup_period_days">自动备份周期/天</string>
|
||||
<string name="backup_periodic">定期备份</string>
|
||||
<string name="backup_periodic_hint">启用此选项后将自动在配置的备份文件夹中创建备份文件。如果你启用了节能模式,这可能无法正常工作</string>
|
||||
<string name="behaviour">行为</string>
|
||||
<string name="biometric_lock">使用生物识别或PIN解锁应用</string>
|
||||
<string name="biometrics_disable_success">已停用生物特征/Pin 锁</string>
|
||||
<string name="biometrics_failure">未能通过生物特征/PIN验证身份</string>
|
||||
|
@ -75,6 +76,8 @@
|
|||
<string name="color_exists">此颜色已存在!</string>
|
||||
<string name="content_density">内容密度</string>
|
||||
<string name="continue_">继续</string>
|
||||
<string name="convert_to_list_note">转换到清单</string>
|
||||
<string name="convert_to_text_note">转换到文本笔记</string>
|
||||
<string name="copied_link">链接已复制到剪贴板</string>
|
||||
<string name="copy">复制</string>
|
||||
<string name="crash_message">发生了意外错误。抱歉造成不变</string>
|
||||
|
@ -115,6 +118,7 @@
|
|||
<string name="disable_lock_description">这也会解密数据据</string>
|
||||
<string name="disable_lock_title">停用生物特征/PIN锁</string>
|
||||
<string name="disabled">禁用</string>
|
||||
<string name="disallow_screenshots">禁止截屏</string>
|
||||
<string name="discard">取消</string>
|
||||
<string name="display_text">要展示的我呢本</string>
|
||||
<string name="donate">捐赠</string>
|
||||
|
@ -154,10 +158,13 @@
|
|||
<string name="help">帮助</string>
|
||||
<string name="hours">小时</string>
|
||||
<string name="image_format_not_supported">不支持该图片格式</string>
|
||||
<string name="images_hidden_in_overview">如果启用此选项,则注释的图像将不会显示在概览中。</string>
|
||||
<string name="images_hidden_in_overview_title">在概览中隐藏图片</string>
|
||||
<string name="import_action">导入</string>
|
||||
<string name="import_backup">导入备份</string>
|
||||
<string name="import_backup_password_hint">如果你的备份文件没有密码保护,只需按下“导入”即可。如有,起输入正确的密码</string>
|
||||
<string name="import_other">从其它APP导入笔记</string>
|
||||
<string name="import_settings">导入设置</string>
|
||||
<string name="import_settings_failure">导入设置</string>
|
||||
<string name="import_settings_message">为了导入设置,请选择有效的 NotallyX 设置 JSON 文件</string>
|
||||
<string name="import_settings_success">成功地导入了设置</string>
|
||||
|
@ -180,6 +187,8 @@
|
|||
<string name="invalid_link">复制有效链接到你的剪贴板</string>
|
||||
<string name="italic">斜体</string>
|
||||
<string name="item">项目</string>
|
||||
<string name="json_files">JSON 文件</string>
|
||||
<string name="json_files_help">要从 JSON 文件(单一文件或文件夹)导入你的笔记,单击“导入”。每个有效的 JSON 文件会被导入成不同笔记,原始文件名成为笔记的标题。</string>
|
||||
<string name="label_exists">标签已存在</string>
|
||||
<string name="label_visibility">在导航面板中隐藏/显示标签</string>
|
||||
<string name="labels">标签</string>
|
||||
|
@ -234,6 +243,7 @@
|
|||
<string name="please_grant_notally_notification_auto_backup">如果自动备份失败,你会收到一则通知(前提是你授予了 NotallyX 发送通知的权限)</string>
|
||||
<string name="previous">前一个</string>
|
||||
<string name="rate">给应用打分</string>
|
||||
<string name="read_only">只读</string>
|
||||
<string name="ready_to_record">准备好录音了</string>
|
||||
<string name="record_audio">录音</string>
|
||||
<string name="recording">录音中…</string>
|
||||
|
@ -288,6 +298,7 @@
|
|||
<string name="text_default">默认</string>
|
||||
<string name="text_size">文本大小</string>
|
||||
<string name="theme">主题</string>
|
||||
<string name="theme_use_dynamic_colors">使用系统的壁纸颜色</string>
|
||||
<string name="title">标题</string>
|
||||
<string name="to_record_audio">要录音,请允许 NotallyX 访问你的麦克风。轻按“设置”>“权限”,打开麦克风</string>
|
||||
<string name="unarchive">取消存档</string>
|
||||
|
@ -301,13 +312,13 @@
|
|||
<string name="unknown_name">未知名称</string>
|
||||
<string name="unlabeled">未加标签</string>
|
||||
<string name="unlock">通过生物特征/PIN解除锁定</string>
|
||||
<string name="unlock_with_biometrics_not_setup">你先前已启用了生物特征锁,但生物特征/PIN目前在你的设备上没有配置好。如果你虚妄停用生物特征锁,请按下“停用”,不然请为你的设备配置生物特征/PIN</string>
|
||||
<string name="unlock_with_biometrics_not_setup">你先前已启用了生物特征锁,但生物特征/PIN目前在你的设备上没有配置好。如果你希望停用生物特征锁,请按下“停用”,不然请为你的设备配置生物特征/PIN</string>
|
||||
<string name="unpin">取消置顶</string>
|
||||
<string name="upcoming">即将到来</string>
|
||||
<string name="updated_link">更新了链接</string>
|
||||
<string name="view">视图</string>
|
||||
<string name="view_file">查看文件</string>
|
||||
<string name="view_note">查看笔记…</string>
|
||||
<string name="view_note">查看笔记</string>
|
||||
<string name="weekly">一周</string>
|
||||
<string name="weeks">周</string>
|
||||
<string name="wrong_password">提供的密码错误</string>
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
<string name="auto_backup_last">最近的備份</string>
|
||||
<string name="auto_backup_on_save">筆記上的備份會自動儲存</string>
|
||||
<string name="auto_backup_on_save_hint">透過啟用此功能,每當儲存筆記時,都會在選擇的\"備份資料夾\"中自動建立備份(\"NotallyX_AutoBackup.zip\")。\n請注意這可能會影響效能</string>
|
||||
<string name="auto_backup_period">自動備份週期</string>
|
||||
<string name="auto_backups_folder">備份資料夾</string>
|
||||
<string name="auto_backups_folder_hint">所有自動備份都儲存於其中的資料夾。</string>
|
||||
<string name="auto_backups_folder_rechoose">您需要重新選擇備份資料夾,以便 NotallyX 有權限寫入該資料夾。\n您也可以按取消跳過匯入備份資料夾</string>
|
||||
|
@ -135,6 +134,8 @@
|
|||
<string name="help">幫助</string>
|
||||
<string name="hours">小時</string>
|
||||
<string name="image_format_not_supported">不支持的圖片格式</string>
|
||||
<string name="images_hidden_in_overview">啟用此功能後,筆記的圖像將隱藏在概覽中。</string>
|
||||
<string name="images_hidden_in_overview_title">在概覽中隱藏圖片</string>
|
||||
<string name="import_action">匯入</string>
|
||||
<string name="import_backup">匯入備份</string>
|
||||
<string name="import_backup_password_hint">如果您的備份沒有密碼保護,只需按匯入,否則請輸入正確的密碼。</string>
|
||||
|
|
|
@ -23,13 +23,13 @@
|
|||
<string name="auto_backup_error_message">An error occurred during auto backup:\n\'%1$s\'\nPlease check your settings or report a bug</string>
|
||||
<string name="auto_backup_failed">NotallyX Auto Backup failed</string>
|
||||
<string name="auto_backup_last">Last Backup</string>
|
||||
<string name="auto_backup_on_save">Backup on Note save automatically</string>
|
||||
<string name="auto_backup_on_save_hint">By enabling this, a backup (\"NotallyX_AutoBackup.zip\") is automatically created in the configured \"Backups Folder\" whenever a note is saved.\nBe aware this might affect performance</string>
|
||||
<string name="auto_backup_period">Auto backup period</string>
|
||||
<string name="auto_backup_on_save">Backup on exit Note automatically</string>
|
||||
<string name="auto_backup_on_save_hint">By enabling this, a backup (\"NotallyX_AutoBackup.zip\") is automatically created in the configured \"Backups Folder\" whenever a note is exited.\nBe aware this might affect performance</string>
|
||||
<string name="auto_backups_folder">Backups Folder</string>
|
||||
<string name="auto_backups_folder_hint">Folder in which all auto backups will be stored in.</string>
|
||||
<string name="auto_backups_folder_rechoose">You need to re-choose your Backups Folder so that NotallyX has permission to write to it.\nYou can also press cancel to skip importing Backups Folder value</string>
|
||||
<string name="auto_backups_folder_set">Set up a Backups Folder above first</string>
|
||||
<string name="auto_save_after_idle_time">Auto save note after specified idle time</string>
|
||||
<string name="auto_sort_by_checked">Sort checked items to the end</string>
|
||||
<string name="back">Back</string>
|
||||
<string name="backup">Backup</string>
|
||||
|
@ -38,6 +38,7 @@
|
|||
<string name="backup_period_days">Auto backup period in days</string>
|
||||
<string name="backup_periodic">Periodic Backups</string>
|
||||
<string name="backup_periodic_hint">By enabling this, backups are automatically created in the configured Backups Folder.\nThis may not work if you have power saving mode enabled</string>
|
||||
<string name="behaviour">Behaviour</string>
|
||||
<string name="biometric_lock">Lock app with device biometric or PIN</string>
|
||||
<string name="biometrics_disable_success">Biometric/PIN lock has been disabled</string>
|
||||
<string name="biometrics_failure">Failed to authenticate via Biometric/PIN</string>
|
||||
|
@ -76,6 +77,8 @@
|
|||
<string name="color_exists">This color already exists!</string>
|
||||
<string name="content_density">Content density</string>
|
||||
<string name="continue_">Continue</string>
|
||||
<string name="convert_to_list_note">Convert to List</string>
|
||||
<string name="convert_to_text_note">Convert to Text Note</string>
|
||||
<string name="copied_link">Copied Link to Clipboard</string>
|
||||
<string name="copy">Copy</string>
|
||||
<string name="count" translatable="false">%1$d / %2$d</string>
|
||||
|
@ -118,6 +121,7 @@
|
|||
<string name="disable_lock_description">This will also decrypt the database</string>
|
||||
<string name="disable_lock_title">Disable lock via Biometric/PIN</string>
|
||||
<string name="disabled">Disabled</string>
|
||||
<string name="disallow_screenshots">Disallow Screenshots</string>
|
||||
<string name="discard">Discard</string>
|
||||
<string name="display_text">Text to display</string>
|
||||
<string name="donate">Make a Donation</string>
|
||||
|
@ -157,6 +161,8 @@
|
|||
<string name="help">Help</string>
|
||||
<string name="hours">Hours</string>
|
||||
<string name="image_format_not_supported">Image format not supported</string>
|
||||
<string name="images_hidden_in_overview">By enabling this, the notes’ images will be hidden in the overview</string>
|
||||
<string name="images_hidden_in_overview_title">Hide Images in Overview</string>
|
||||
<string name="import_action">Import</string>
|
||||
<string name="import_backup">Import backup</string>
|
||||
<string name="import_backup_password_hint">If your backup is not password-protected simply press Import, otherwise enter the correct password.</string>
|
||||
|
@ -184,6 +190,8 @@
|
|||
<string name="invalid_link">Copy a valid link into your clipboard</string>
|
||||
<string name="italic">Italic</string>
|
||||
<string name="item">Item</string>
|
||||
<string name="json_files">JSON Files</string>
|
||||
<string name="json_files_help">In order to import your Notes from JSON files (single file or folder), click Import. Every valid JSON file is imported as a separate note, the file’s name becomes the note’s title.</string>
|
||||
<string name="label_exists">Label exists</string>
|
||||
<string name="label_visibility">Hide/Show the label in the navigation panel</string>
|
||||
<string name="labels">Labels</string>
|
||||
|
@ -238,6 +246,7 @@
|
|||
<string name="please_grant_notally_notification_auto_backup">If an auto backup fails you can receive a notification, if you grant NotallyX permission to send notifications</string>
|
||||
<string name="previous">Previous</string>
|
||||
<string name="rate">Rate this app</string>
|
||||
<string name="read_only">Read Only</string>
|
||||
<string name="ready_to_record">Ready to record</string>
|
||||
<string name="record_audio">Record audio</string>
|
||||
<string name="recording">Recording…</string>
|
||||
|
@ -292,6 +301,7 @@
|
|||
<string name="text_default">Default</string>
|
||||
<string name="text_size">Text size</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="theme_use_dynamic_colors">Use system\'s wallpaper colors</string>
|
||||
<string name="title">Title</string>
|
||||
<string name="to_record_audio">To record audio, allow NotallyX access to your microphone. Tap Settings > Permissions and turn Microphone on</string>
|
||||
<string name="unarchive">Unarchive</string>
|
||||
|
|
|
@ -124,6 +124,7 @@
|
|||
|
||||
<style name="ThemeOverlay.App.BottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog">
|
||||
<item name="bottomSheetStyle">@style/ModalBottomSheet</item>
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
</style>
|
||||
|
||||
<style name="ModalBottomSheet" parent="Widget.Material3.BottomSheet.Modal">
|
||||
|
|
|
@ -46,6 +46,24 @@ class ChangeHistoryTest {
|
|||
assertTrue(changeHistory.canRedo.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test undoAll when stack has multiple changes`() {
|
||||
val change = mock<Change>()
|
||||
val change1 = mock<Change>()
|
||||
val change2 = mock<Change>()
|
||||
|
||||
changeHistory.push(change)
|
||||
changeHistory.push(change1)
|
||||
changeHistory.push(change2)
|
||||
changeHistory.undoAll()
|
||||
|
||||
verify(change).undo()
|
||||
verify(change1).undo()
|
||||
verify(change2).undo()
|
||||
assertFalse(changeHistory.canUndo.value)
|
||||
assertTrue(changeHistory.canRedo.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test redo when stack has one change`() {
|
||||
val change = mock<Change>()
|
||||
|
@ -59,6 +77,25 @@ class ChangeHistoryTest {
|
|||
assertFalse(changeHistory.canRedo.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test redoAll when stack has multiple changes`() {
|
||||
val change = mock<Change>()
|
||||
val change1 = mock<Change>()
|
||||
val change2 = mock<Change>()
|
||||
|
||||
changeHistory.push(change)
|
||||
changeHistory.push(change1)
|
||||
changeHistory.push(change2)
|
||||
changeHistory.undoAll()
|
||||
changeHistory.redoAll()
|
||||
|
||||
verify(change2).redo()
|
||||
verify(change1).redo()
|
||||
verify(change).redo()
|
||||
assertTrue(changeHistory.canUndo.value)
|
||||
assertFalse(changeHistory.canRedo.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test canUndo and canRedo logic`() {
|
||||
val change = mock<Change>()
|
||||
|
|
|
@ -157,6 +157,7 @@ class NotesImporterTest {
|
|||
ImportSource.GOOGLE_KEEP -> File(tempDir, "Takeout.zip")
|
||||
ImportSource.EVERNOTE -> File(tempDir, "Notebook.enex")
|
||||
ImportSource.PLAIN_TEXT -> File(tempDir, "text.txt")
|
||||
ImportSource.JSON -> File(tempDir, "text.json")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.philkes.notallyx.data.model.BaseNote
|
|||
import com.philkes.notallyx.data.model.FileAttachment
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
import com.philkes.notallyx.data.model.ListItem
|
||||
import com.philkes.notallyx.data.model.NoteViewMode
|
||||
import com.philkes.notallyx.data.model.Reminder
|
||||
import com.philkes.notallyx.data.model.SpanRepresentation
|
||||
import com.philkes.notallyx.data.model.Type
|
||||
|
@ -252,6 +253,7 @@ class GoogleKeepImporterTest {
|
|||
files,
|
||||
audios,
|
||||
reminders,
|
||||
NoteViewMode.EDIT,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
package com.philkes.notallyx.data.model
|
||||
|
||||
import com.philkes.notallyx.test.createListItem
|
||||
import java.util.Date
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito.anyString
|
||||
import org.mockito.Mockito.mockStatic
|
||||
|
||||
class ModelExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `Json toBaseNote()`() {
|
||||
val json =
|
||||
"""
|
||||
{
|
||||
"type": "LIST",
|
||||
"color": "#E2F6D3",
|
||||
"title": "Test",
|
||||
"pinned": false,
|
||||
"timestamp": 1742822848689,
|
||||
"modifiedTimestamp": 1742823434623,
|
||||
"labels": [
|
||||
"TestLabel"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"body": "A",
|
||||
"checked": false,
|
||||
"isChild": false,
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"body": "B",
|
||||
"checked": true,
|
||||
"isChild": true,
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"body": "C",
|
||||
"checked": false,
|
||||
"isChild": true,
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"body": "D",
|
||||
"checked": false,
|
||||
"isChild": false,
|
||||
"order": 3
|
||||
},
|
||||
{
|
||||
"body": "E",
|
||||
"checked": true,
|
||||
"isChild": false,
|
||||
"order": 4
|
||||
}
|
||||
],
|
||||
"reminders": [
|
||||
{
|
||||
"id": 0,
|
||||
"dateTime": 1742822940000,
|
||||
"repetition": "{\"value\":1,\"unit\":\"DAYS\"}"
|
||||
}
|
||||
],
|
||||
"viewMode": "READ_ONLY"
|
||||
}
|
||||
"""
|
||||
val colorMock = mockStatic(android.graphics.Color::class.java)
|
||||
colorMock.`when`<Int> { android.graphics.Color.parseColor(anyString()) }.thenReturn(1)
|
||||
|
||||
val baseNote = json.toBaseNote()
|
||||
|
||||
assertEquals("Test", baseNote.title)
|
||||
assertEquals(false, baseNote.pinned)
|
||||
assertEquals("#E2F6D3", baseNote.color)
|
||||
assertEquals(Folder.NOTES, baseNote.folder)
|
||||
assertEquals(
|
||||
mutableListOf(
|
||||
ListItem("A", false, false, 0, mutableListOf()),
|
||||
ListItem("B", true, true, 1, mutableListOf()),
|
||||
ListItem("C", false, true, 2, mutableListOf()),
|
||||
ListItem("D", false, false, 3, mutableListOf()),
|
||||
ListItem("E", true, false, 4, mutableListOf()),
|
||||
),
|
||||
baseNote.items,
|
||||
)
|
||||
assertEquals(1, baseNote.reminders.size)
|
||||
assertEquals(1742822848689, baseNote.timestamp)
|
||||
assertEquals(1742823434623, baseNote.modifiedTimestamp)
|
||||
assertEquals(NoteViewMode.READ_ONLY, baseNote.viewMode)
|
||||
assertEquals(listOf("TestLabel"), baseNote.labels)
|
||||
assertEquals(Repetition(1, RepetitionTimeUnit.DAYS), baseNote.reminders[0].repetition)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BaseNote toJson()`() {
|
||||
val baseNote =
|
||||
BaseNote(
|
||||
id = 1,
|
||||
Type.LIST,
|
||||
Folder.DELETED,
|
||||
"#E2F6D3",
|
||||
"Title",
|
||||
true,
|
||||
12354632465L,
|
||||
945869546L,
|
||||
listOf("label"),
|
||||
"Body",
|
||||
listOf(SpanRepresentation(0, 10, bold = true)),
|
||||
mutableListOf(
|
||||
createListItem("Item1", true, false),
|
||||
createListItem("Item2", true, true),
|
||||
),
|
||||
listOf(FileAttachment("localImage", "originalImage", "image/jpeg")),
|
||||
listOf(FileAttachment("localFile", "originalFile", "text/plain")),
|
||||
listOf(Audio("audio", 10L, 12312334L)),
|
||||
listOf(Reminder(1, Date(1743253506957), Repetition(10, RepetitionTimeUnit.WEEKS))),
|
||||
NoteViewMode.READ_ONLY,
|
||||
)
|
||||
|
||||
val json = baseNote.toJson()
|
||||
|
||||
assertEquals(
|
||||
"""
|
||||
{
|
||||
"reminders": [{
|
||||
"dateTime": 1743253506957,
|
||||
"id": 1,
|
||||
"repetition": {
|
||||
"unit": "WEEKS",
|
||||
"value": 10
|
||||
}
|
||||
}],
|
||||
"pinned": true,
|
||||
"color": "#E2F6D3",
|
||||
"modifiedTimestamp": 945869546,
|
||||
"type": "LIST",
|
||||
"title": "Title",
|
||||
"viewMode": "READ_ONLY",
|
||||
"items": [
|
||||
{
|
||||
"checked": true,
|
||||
"body": "Item1",
|
||||
"isChild": false
|
||||
},
|
||||
{
|
||||
"checked": true,
|
||||
"body": "Item2",
|
||||
"isChild": true
|
||||
}
|
||||
],
|
||||
"timestamp": 12354632465,
|
||||
"labels": ["label"]
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
.trimStart(),
|
||||
json,
|
||||
)
|
||||
}
|
||||
}
|
Binary file not shown.
20
documentation/.gitignore
vendored
Normal file
20
documentation/.gitignore
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
12
documentation/blog/2019-05-28-first-blog-post.md
Normal file
12
documentation/blog/2019-05-28-first-blog-post.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
slug: first-blog-post
|
||||
title: First Blog Post
|
||||
authors: [slorber, yangshun]
|
||||
tags: [hola, docusaurus]
|
||||
---
|
||||
|
||||
Lorem ipsum dolor sit amet...
|
||||
|
||||
<!-- truncate -->
|
||||
|
||||
...consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
44
documentation/blog/2019-05-29-long-blog-post.md
Normal file
44
documentation/blog/2019-05-29-long-blog-post.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
slug: long-blog-post
|
||||
title: Long Blog Post
|
||||
authors: yangshun
|
||||
tags: [hello, docusaurus]
|
||||
---
|
||||
|
||||
This is the summary of a very long blog post,
|
||||
|
||||
Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
|
||||
|
||||
<!-- truncate -->
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
24
documentation/blog/2021-08-01-mdx-blog-post.mdx
Normal file
24
documentation/blog/2021-08-01-mdx-blog-post.mdx
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
slug: mdx-blog-post
|
||||
title: MDX Blog Post
|
||||
authors: [slorber]
|
||||
tags: [docusaurus]
|
||||
---
|
||||
|
||||
Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
|
||||
|
||||
:::tip
|
||||
|
||||
Use the power of React to create interactive blog posts.
|
||||
|
||||
:::
|
||||
|
||||
{/* truncate */}
|
||||
|
||||
For example, use JSX to create an interactive button:
|
||||
|
||||
```js
|
||||
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||
```
|
||||
|
||||
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
29
documentation/blog/2021-08-26-welcome/index.md
Normal file
29
documentation/blog/2021-08-26-welcome/index.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
slug: welcome
|
||||
title: Welcome
|
||||
authors: [slorber, yangshun]
|
||||
tags: [facebook, hello, docusaurus]
|
||||
---
|
||||
|
||||
[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
|
||||
|
||||
Here are a few tips you might find useful.
|
||||
|
||||
<!-- truncate -->
|
||||
|
||||
Simply add Markdown files (or folders) to the `blog` directory.
|
||||
|
||||
Regular blog authors can be added to `authors.yml`.
|
||||
|
||||
The blog post date can be extracted from filenames, such as:
|
||||
|
||||
- `2019-05-30-welcome.md`
|
||||
- `2019-05-30-welcome/index.md`
|
||||
|
||||
A blog post folder can be convenient to co-locate blog post images:
|
||||
|
||||

|
||||
|
||||
The blog supports tags as well!
|
||||
|
||||
**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.
|
23
documentation/blog/authors.yml
Normal file
23
documentation/blog/authors.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
yangshun:
|
||||
name: Yangshun Tay
|
||||
title: Front End Engineer @ Facebook
|
||||
url: https://github.com/yangshun
|
||||
image_url: https://github.com/yangshun.png
|
||||
page: true
|
||||
socials:
|
||||
x: yangshunz
|
||||
github: yangshun
|
||||
|
||||
slorber:
|
||||
name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
page:
|
||||
# customize the url of the author page at /blog/authors/<permalink>
|
||||
permalink: '/all-sebastien-lorber-articles'
|
||||
socials:
|
||||
x: sebastienlorber
|
||||
linkedin: sebastienlorber
|
||||
github: slorber
|
||||
newsletter: https://thisweekinreact.com
|
19
documentation/blog/tags.yml
Normal file
19
documentation/blog/tags.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
facebook:
|
||||
label: Facebook
|
||||
permalink: /facebook
|
||||
description: Facebook tag description
|
||||
|
||||
hello:
|
||||
label: Hello
|
||||
permalink: /hello
|
||||
description: Hello tag description
|
||||
|
||||
docusaurus:
|
||||
label: Docusaurus
|
||||
permalink: /docusaurus
|
||||
description: Docusaurus tag description
|
||||
|
||||
hola:
|
||||
label: Hola
|
||||
permalink: /hola
|
||||
description: Hola tag description
|
47
documentation/docs/get-started.md
Normal file
47
documentation/docs/get-started.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Tutorial Intro
|
||||
|
||||
Let's discover **Docusaurus in less than 5 minutes**.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Get started by **creating a new site**.
|
||||
|
||||
Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**.
|
||||
|
||||
### What you'll need
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) version 18.0 or above:
|
||||
- When installing Node.js, you are recommended to check all checkboxes related to dependencies.
|
||||
|
||||
## Generate a new site
|
||||
|
||||
Generate a new Docusaurus site using the **classic template**.
|
||||
|
||||
The classic template will automatically be added to your project after you run the command:
|
||||
|
||||
```bash
|
||||
npm init docusaurus@latest my-website classic
|
||||
```
|
||||
|
||||
You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor.
|
||||
|
||||
The command also installs all necessary dependencies you need to run Docusaurus.
|
||||
|
||||
## Start your site
|
||||
|
||||
Run the development server:
|
||||
|
||||
```bash
|
||||
cd my-website
|
||||
npm run start
|
||||
```
|
||||
|
||||
The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there.
|
||||
|
||||
The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/.
|
||||
|
||||
Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes.
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue