Compare commits

..

9 commits
main ... v7.2.1

Author SHA1 Message Date
PhilKes
e243ed5155 Bump version v7.2.1 2025-03-18 20:51:26 +01:00
Phil
200694d2e2
Merge pull request #465 from PhilKes/translation/update
Update zh-rCN/strings.xml
2025-03-18 18:00:19 +01:00
PhilKes
9ba90e88c4 Update zh-rCN/strings.xml 2025-03-18 17:59:55 +01:00
Phil
3b72ae619e
Merge pull request #464 from PhilKes/fix/export-backup-database
Fix databaseFile path when creating backup
2025-03-18 17:57:48 +01:00
PhilKes
9fe6fb9e5a Fix databaseFile path when creating backup 2025-03-18 13:41:27 +01:00
PhilKes
ebab977d38 Add rate app in About settings 2025-03-16 18:11:32 +01:00
Phil
250a901fce
Merge pull request #458 from PhilKes/feat/auto-save-idle
Add auto-save after user idle time
2025-03-16 18:05:28 +01:00
PhilKes
9d7b3ab6c5 Add auto-save after user idle time 2025-03-16 18:01:13 +01:00
Phil
eda1d8db3e
Add Google Play badge to README.md 2025-03-12 18:17:29 +01:00
148 changed files with 955 additions and 300880 deletions

View file

@ -1,56 +0,0 @@
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

View file

@ -1,87 +1,5 @@
# 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)

View file

@ -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")

File diff suppressed because it is too large Load diff

View file

@ -11,12 +11,14 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
-dontobfuscate
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable
-printmapping obfuscation/mapping.txt
# 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.
-renamesourcefileattribute SourceFile
-keep class ** extends androidx.navigation.Navigator
-keep class ** implements org.ocpsoft.prettytime.TimeUnit

View file

@ -1,164 +0,0 @@
{
"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')"
]
}
}

View file

@ -6,15 +6,18 @@
<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.USE_BIOMETRIC" />
<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_FINGERPRINT" />
<application
@ -68,30 +71,9 @@
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
<data android:mimeType="text/plain" />
</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" />

View file

@ -1,18 +1,14 @@
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
@ -36,7 +32,7 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class NotallyXApplication : Application(), Application.ActivityLifecycleCallbacks {
class NotallyXApplication : Application() {
private lateinit var biometricLockObserver: Observer<BiometricLock>
private lateinit var preferences: NotallyXPreferences
@ -46,16 +42,10 @@ class NotallyXApplication : Application(), Application.ActivityLifecycleCallback
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 ->
@ -70,7 +60,7 @@ class NotallyXApplication : Application(), Application.ActivityLifecycleCallback
)
}
if (oldTheme != null) {
WidgetProvider.updateWidgets(this, locked = locked.value)
WidgetProvider.updateWidgets(this)
}
}
@ -172,20 +162,4 @@ class NotallyXApplication : Application(), Application.ActivityLifecycleCallback
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) {}
}

View file

@ -19,7 +19,6 @@ 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
@ -33,7 +32,7 @@ import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SupportFactory
@TypeConverters(Converters::class)
@Database(entities = [BaseNote::class, Label::class], version = 9)
@Database(entities = [BaseNote::class, Label::class], version = 8)
abstract class NotallyDatabase : RoomDatabase() {
abstract fun getLabelDao(): LabelDao
@ -132,7 +131,6 @@ abstract class NotallyDatabase : RoomDatabase() {
Migration6,
Migration7,
Migration8,
Migration9,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
SQLiteDatabase.loadLibs(context)
@ -256,21 +254,17 @@ abstract class NotallyDatabase : RoomDatabase() {
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow("id"))
val colorString = cursor.getString(cursor.getColumnIndexOrThrow("color"))
val color = Color.valueOfOrDefault(colorString)
val color =
try {
Color.valueOf(colorString)
} catch (e: Exception) {
Color.DEFAULT
}
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}'"
)
}
}
}
}

View file

@ -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, setOf(Folder.NOTES, Folder.ARCHIVED))
val result = getBaseNotesByLabel(label, Folder.NOTES)
return result.map { list -> list.filter { baseNote -> baseNote.labels.contains(label) } }
}
@Query(
"SELECT * FROM BaseNote WHERE folder IN (:folders) AND labels LIKE '%' || :label || '%' ORDER BY folder DESC, pinned DESC, timestamp DESC"
"SELECT * FROM BaseNote WHERE folder = :folder AND labels LIKE '%' || :label || '%' ORDER BY pinned DESC, timestamp DESC"
)
fun getBaseNotesByLabel(label: String, folders: Collection<Folder>): LiveData<List<BaseNote>>
fun getBaseNotesByLabel(label: String, folder: Folder): LiveData<List<BaseNote>>
@Query(
"SELECT * FROM BaseNote WHERE folder = :folder AND labels == '[]' ORDER BY pinned DESC, timestamp DESC"

View file

@ -9,7 +9,6 @@ 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
@ -40,7 +39,6 @@ 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)
@ -155,13 +153,6 @@ 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"

View file

@ -17,7 +17,6 @@ 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
@ -156,7 +155,6 @@ fun EvernoteNote.mapToBaseNote(): BaseNote {
files = files,
audios = audios,
reminders = mutableListOf(),
NoteViewMode.EDIT,
)
}

View file

@ -14,7 +14,6 @@ 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
@ -163,7 +162,6 @@ class GoogleKeepImporter : ExternalImporter {
files = files,
audios = audios,
reminders = mutableListOf(),
NoteViewMode.EDIT,
)
}

View file

@ -1,52 +0,0 @@
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)
}
}

View file

@ -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 com.philkes.notallyx.utils.readFileContents
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
class PlainTextImporter : ExternalImporter {
@ -35,7 +35,12 @@ class PlainTextImporter : ExternalImporter {
return
}
val fileNameWithoutExtension = file.name?.substringBeforeLast(".") ?: ""
var content = app.contentResolver.readFileContents(file.uri)
var content =
app.contentResolver.openInputStream(file.uri)?.use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader ->
reader.readText()
}
} ?: ""
val listItems = mutableListOf<ListItem>()
content.findListSyntaxRegex()?.let { listSyntaxRegex ->
listItems.addAll(content.extractListItems(listSyntaxRegex))
@ -60,7 +65,6 @@ class PlainTextImporter : ExternalImporter {
files = listOf(),
audios = listOf(),
reminders = listOf(),
NoteViewMode.EDIT,
)
)
}

View file

@ -25,7 +25,6 @@ data class BaseNote(
val files: List<FileAttachment>,
val audios: List<Audio>,
val reminders: List<Reminder>,
val viewMode: NoteViewMode,
) : Item {
companion object {
@ -54,7 +53,6 @@ data class BaseNote(
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
}
@ -75,7 +73,6 @@ data class BaseNote(
result = 31 * result + files.hashCode()
result = 31 * result + audios.hashCode()
result = 31 * result + reminders.hashCode()
result = 31 * result + viewMode.hashCode()
return result
}
}

View file

@ -16,13 +16,6 @@ enum class Color {
companion object {
fun allColorStrings() = entries.map { it.toColorString() }.toList()
fun valueOfOrDefault(value: String) =
try {
Color.valueOf(value)
} catch (e: Exception) {
DEFAULT
}
}
}

View file

@ -10,9 +10,7 @@ object Converters {
@TypeConverter fun labelsToJson(labels: List<String>) = JSONArray(labels).toString()
@TypeConverter fun jsonToLabels(json: String) = jsonToLabels(JSONArray(json))
fun jsonToLabels(jsonArray: JSONArray) = jsonArray.iterable<String>().toList()
@TypeConverter fun jsonToLabels(json: String) = JSONArray(json).iterable<String>().toList()
@TypeConverter
fun filesToJson(files: List<FileAttachment>): String {
@ -26,10 +24,10 @@ object Converters {
return JSONArray(objects).toString()
}
@TypeConverter fun jsonToFiles(json: String) = jsonToFiles(JSONArray(json))
fun jsonToFiles(jsonArray: JSONArray): List<FileAttachment> {
return jsonArray.iterable<JSONObject>().map { jsonObject ->
@TypeConverter
fun jsonToFiles(json: String): List<FileAttachment> {
val iterable = JSONArray(json).iterable<JSONObject>()
return iterable.map { jsonObject ->
val localName = getSafeLocalName(jsonObject)
val originalName = getSafeOriginalName(jsonObject)
val mimeType = jsonObject.getString("mimeType")
@ -49,10 +47,10 @@ object Converters {
return JSONArray(objects).toString()
}
@TypeConverter fun jsonToAudios(json: String) = jsonToAudios(JSONArray(json))
fun jsonToAudios(json: JSONArray): List<Audio> {
return json.iterable<JSONObject>().map { jsonObject ->
@TypeConverter
fun jsonToAudios(json: String): List<Audio> {
val iterable = JSONArray(json).iterable<JSONObject>()
return iterable.map { jsonObject ->
val name = jsonObject.getString("name")
val duration = jsonObject.getSafeLong("duration")
val timestamp = jsonObject.getLong("timestamp")
@ -60,63 +58,31 @@ object Converters {
}
}
@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 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 spansToJson(list: List<SpanRepresentation>) = spansToJSONArray(list).toString()
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")
@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")
val isChild = jsonObject.getSafeBoolean("isChild")
val order = jsonObject.getSafeInt("order")
ListItem(body, checked, isChild, order, mutableListOf())
@ -137,25 +103,39 @@ object Converters {
return JSONArray(objects)
}
@TypeConverter
fun remindersToJson(reminders: List<Reminder>) = remindersToJSONArray(reminders).toString()
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)
}
fun remindersToJSONArray(reminders: List<Reminder>): JSONArray {
@TypeConverter
fun remindersToJson(reminders: List<Reminder>): String {
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 { repetitionToJsonObject(it) })
put("repetition", reminder.repetition?.let { repetitionToJson(it) })
}
}
return JSONArray(objects)
return JSONArray(objects).toString()
}
@TypeConverter fun jsonToReminders(json: String) = jsonToReminders(JSONArray(json))
fun jsonToReminders(jsonArray: JSONArray): List<Reminder> {
return jsonArray.iterable<JSONObject>().map { jsonObject ->
@TypeConverter
fun jsonToReminders(json: String): List<Reminder> {
val iterable = JSONArray(json).iterable<JSONObject>()
return iterable.map { jsonObject ->
val id = jsonObject.getLong("id")
val dateTime = Date(jsonObject.getLong("dateTime"))
val repetition = jsonObject.getSafeString("repetition")?.let { jsonToRepetition(it) }
@ -165,14 +145,10 @@ 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
return jsonObject.toString()
}
@TypeConverter

View file

@ -5,14 +5,5 @@ import java.io.Serializable
enum class Folder : Serializable {
NOTES,
DELETED,
ARCHIVED;
companion object {
fun valueOfOrDefault(value: String) =
try {
valueOf(value)
} catch (e: Exception) {
NOTES
}
}
ARCHIVED,
}

View file

@ -34,7 +34,8 @@ data class ListItem(
if (other !is ListItem) {
return false
}
return (this.body == other.body &&
return (this.id == other.id &&
this.body == other.body &&
this.order == other.order &&
this.checked == other.checked &&
this.isChild == other.isChild)

View file

@ -5,7 +5,6 @@ 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
@ -13,7 +12,6 @@ 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://"
@ -43,15 +41,7 @@ fun String.getNoteTypeFromUrl(): Type {
val FileAttachment.isImage: Boolean
get() {
return mimeType.isImageMimeType
}
val String.isImageMimeType: Boolean
get() {
return startsWith("image/")
}
val String.isAudioMimeType: Boolean
get() {
return startsWith("audio/")
return mimeType.startsWith("image/")
}
fun BaseNote.toTxt(includeTitle: Boolean = true, includeCreationDate: Boolean = true) =
@ -80,8 +70,7 @@ fun BaseNote.toJson(): String {
.put("color", color)
.put("title", title)
.put("pinned", pinned)
.put("timestamp", timestamp)
.put("modifiedTimestamp", modifiedTimestamp)
.put("date-created", timestamp)
.put("labels", JSONArray(labels))
when (type) {
@ -94,85 +83,10 @@ 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)
@ -308,15 +222,3 @@ 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
}
}

View file

@ -1,18 +0,0 @@
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
}
}
}

View file

@ -2,14 +2,5 @@ package com.philkes.notallyx.data.model
enum class Type {
NOTE,
LIST;
companion object {
fun valueOfOrDefault(value: String) =
try {
Type.valueOf(value)
} catch (e: Exception) {
NOTE
}
}
LIST,
}

View file

@ -23,7 +23,6 @@ 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
@ -56,16 +55,13 @@ 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
@ -91,16 +87,12 @@ 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
@ -122,7 +114,7 @@ fun String.applySpans(representations: List<SpanRepresentation>): Editable {
->
try {
if (bold) {
editable.setSpan(createBoldSpan(), start, end)
editable.setSpan(StyleSpan(Typeface.BOLD), start, end)
}
if (italic) {
editable.setSpan(StyleSpan(Typeface.ITALIC), start, end)
@ -144,13 +136,6 @@ 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.
*
@ -241,23 +226,14 @@ fun ViewGroup.addIconButton(
title: Int,
drawable: Int,
marginStart: Int = 10,
onLongClick: View.OnLongClickListener? = null,
onClick: View.OnClickListener? = null,
): ImageButton {
onClick: ((item: View) -> Unit)? = null,
): View {
val view =
ImageButton(ContextThemeWrapper(context, R.style.AppTheme)).apply {
setImageResource(drawable)
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)
contentDescription = context.getString(title)
setBackgroundResource(R.color.Transparent)
setOnClickListener(onClick)
scaleType = ImageView.ScaleType.FIT_CENTER
adjustViewBounds = true
layoutParams =
@ -305,11 +281,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) {
textBefore = text.clone()
stateBefore = EditTextState(getText()!!.clone(), selectionStart)
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
@ -317,14 +293,13 @@ fun EditText.createListTextWatcherWithHistory(
}
override fun afterTextChanged(s: Editable?) {
val textAfter = s!!.clone()
if (textAfter.hasNotChanged(textBefore)) {
return
}
if (!ignoreOriginalChange) {
listManager.changeText(
positionGetter.invoke(),
EditTextState(textAfter, selectionStart),
EditTextState(getText()!!.clone(), selectionStart),
before = stateBefore,
editText = this@createListTextWatcherWithHistory,
listener = this,
)
}
}
@ -348,9 +323,6 @@ 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(
@ -363,10 +335,6 @@ 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 {
@ -389,12 +357,12 @@ fun RadioGroup.checkedTag(): Any {
return this.findViewById<RadioButton?>(this.checkedRadioButtonId).tag
}
fun Context.showKeyboard(view: View) {
fun Activity.showKeyboard(view: View) {
ContextCompat.getSystemService(this, InputMethodManager::class.java)
?.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}
fun Context.hideKeyboard(view: View) {
fun Activity.hideKeyboard(view: View) {
ContextCompat.getSystemService(this, InputMethodManager::class.java)
?.hideSoftInputFromWindow(view.windowToken, 0)
}
@ -538,64 +506,6 @@ 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) {
@ -656,15 +566,10 @@ 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) {
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)
if (resolved) {
return typedValue.data // Returns the color as an Int
} else {
// It's a direct color value
typedValue.data
throw IllegalArgumentException("Attribute not found in current theme")
}
}
@ -835,7 +740,16 @@ fun Context.getContrastFontColor(@ColorInt backgroundColor: Int): Int {
else ContextCompat.getColor(this, R.color.TextLight)
}
fun @receiver:ColorInt Int.isLightColor() = ColorUtils.calculateLuminance(this) > 0.5
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 MaterialAlertDialogBuilder.setCancelButton(listener: DialogInterface.OnClickListener? = null) =
setNegativeButton(R.string.cancel, listener)
@ -872,20 +786,12 @@ fun Context.showToast(message: CharSequence) =
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
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)
fun ViewGroup.addFastScroll(context: Context) {
FastScrollerBuilder(this)
.useMd2Style()
.setTrackDrawable(ContextCompat.getDrawable(context, R.drawable.scroll_track)!!)
.setPadding(0, 0, 2.dp, 0)
.build()
}
@ColorInt
@ -896,14 +802,6 @@ 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
@ -915,8 +813,6 @@ 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
@ -934,13 +830,6 @@ 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
}
}
}
}
}
@ -967,29 +856,6 @@ 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)
}

View file

@ -64,10 +64,7 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
override fun onPause() {
super.onPause()
if (
preferences.biometricLock.value == BiometricLock.ENABLED &&
notallyXApplication.locked.value
) {
if (preferences.biometricLock.value == BiometricLock.ENABLED) {
hide()
}
}

View file

@ -31,6 +31,8 @@ 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
@ -38,6 +40,7 @@ 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
@ -75,7 +78,6 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
var getCurrentFragmentNotes: (() -> Collection<BaseNote>?)? = null
override fun onSupportNavigateUp(): Boolean {
baseModel.keyword = ""
return navController.navigateUp(configuration)
}
@ -93,7 +95,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
val fragmentIdToLoad = intent.getIntExtra(EXTRA_FRAGMENT_TO_OPEN, -1)
if (fragmentIdToLoad != -1) {
navController.navigate(fragmentIdToLoad, intent.extras)
navController.navigate(fragmentIdToLoad, Bundle())
} else if (savedInstanceState == null) {
navigateToStartView()
}
@ -105,10 +107,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
if (baseModel.actionMode.enabled.value) {
return
}
if (
!isStartViewFragment &&
!intent.getBooleanExtra(EXTRA_SKIP_START_VIEW_ON_BACK, false)
) {
if (!isStartViewFragment) {
navigateToStartView()
} else {
finish()
@ -193,7 +192,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
.setCheckable(true)
.setIcon(R.drawable.settings)
}
baseModel.preferences.labelsHidden.observe(this) { hiddenLabels ->
baseModel.preferences.labelsHiddenInNavigation.observe(this) { hiddenLabels ->
hideLabelsInNavigation(hiddenLabels, baseModel.preferences.maxLabels.value)
}
baseModel.preferences.maxLabels.observe(this) { maxLabels ->
@ -242,7 +241,10 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
} else null
configuration = AppBarConfiguration(binding.NavigationView.menu, binding.DrawerLayout)
setupActionBarWithNavController(navController, configuration)
hideLabelsInNavigation(baseModel.preferences.labelsHidden.value, maxLabelsToDisplay)
hideLabelsInNavigation(
baseModel.preferences.labelsHiddenInNavigation.value,
maxLabelsToDisplay,
)
}
private fun navigateToLabel(label: String) {
@ -319,7 +321,12 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
private fun share() {
val baseNote = baseModel.actionMode.getFirstNote()
this.shareNote(baseNote)
val body =
when (baseNote.type) {
Type.NOTE -> baseNote.body.applySpans(baseNote.spans)
Type.LIST -> baseNote.items.toText()
}
this.shareNote(baseNote.title, body)
}
private fun deleteForever() {
@ -682,6 +689,5 @@ 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"
}
}

View file

@ -6,6 +6,7 @@ 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
@ -18,7 +19,6 @@ 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, model)
displayEditLabelDialog(label)
}
}
@ -87,13 +87,13 @@ class LabelsFragment : Fragment(), LabelListener {
override fun onToggleVisibility(position: Int) {
labelAdapter?.currentList?.get(position)?.let { value ->
val hiddenLabels = model.preferences.labelsHidden.value.toMutableSet()
val hiddenLabels = model.preferences.labelsHiddenInNavigation.value.toMutableSet()
if (value.visibleInNavigation) {
hiddenLabels.add(value.label)
} else {
hiddenLabels.remove(value.label)
}
model.savePreference(model.preferences.labelsHidden, hiddenLabels)
model.savePreference(model.preferences.labelsHiddenInNavigation, 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.labelsHidden.value
val hiddenLabels = model.preferences.labelsHiddenInNavigation.value
val labelsData = labels.map { label -> LabelData(label, !hiddenLabels.contains(label)) }
labelAdapter?.submitList(labelsData)
binding?.ImageView?.isVisible = labels.isEmpty()
@ -148,4 +148,37 @@ 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()
}
}
}

View file

@ -195,20 +195,27 @@ abstract class NotallyFragment : Fragment(), ItemListener {
}
}
doAfterTextChanged { text ->
val isSearchFragment = navController.currentDestination?.id == R.id.Search
if (isSearchFragment) {
model.keyword = requireNotNull(text).trim().toString()
}
if (text?.isNotEmpty() == true && !isSearchFragment) {
setText("")
model.keyword = text.trim().toString()
navController.navigate(
R.id.Search,
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)
}
}
setOnFocusChangeListener { v, hasFocus ->
if (hasFocus && 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)
}
}
}
@ -236,8 +243,7 @@ abstract class NotallyFragment : Fragment(), ItemListener {
maxItems.value,
maxLines.value,
maxTitle.value,
labelTagsHiddenInOverview.value,
imagesHiddenInOverview.value,
labelsHiddenInOverview.value,
),
model.imageRoot,
this@NotallyFragment,

View file

@ -6,7 +6,6 @@ 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() {
@ -45,9 +44,6 @@ 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

View file

@ -6,17 +6,15 @@ 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
@ -43,7 +41,6 @@ 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
@ -201,35 +198,28 @@ fun PreferenceBinding.setup(
Value.text = dateFormatValue.getText(context)
root.setOnClickListener {
val layout = DialogPreferenceEnumWithToggleBinding.inflate(layoutInflater, null, false)
layout.EnumHint.apply {
setText(R.string.date_format_hint)
isVisible = true
}
val layout = DialogDateFormatBinding.inflate(layoutInflater, null, false)
DateFormat.entries.forEachIndexed { idx, dateFormat ->
ChoiceItemBinding.inflate(layoutInflater).root.apply {
id = idx
text = dateFormat.getText(context)
tag = dateFormat
layout.EnumRadioGroup.addView(this)
layout.DateFormatRadioGroup.addView(this)
if (dateFormat == dateFormatValue) {
layout.EnumRadioGroup.check(this.id)
layout.DateFormatRadioGroup.check(this.id)
}
}
}
layout.Toggle.apply {
setText(R.string.date_format_apply_in_note_view)
isChecked = applyToNoteViewValue
}
layout.ApplyToNoteView.isChecked = applyToNoteViewValue
MaterialAlertDialogBuilder(context)
.setTitle(dateFormatPreference.titleResId)
.setView(layout.root)
.setPositiveButton(R.string.save) { dialog, _ ->
dialog.cancel()
val dateFormat = layout.EnumRadioGroup.checkedTag() as DateFormat
val applyToNoteView = layout.Toggle.isChecked
val dateFormat = layout.DateFormatRadioGroup.checkedTag() as DateFormat
val applyToNoteView = layout.ApplyToNoteView.isChecked
onSave(dateFormat, applyToNoteView)
}
.setCancelButton()
@ -237,52 +227,6 @@ 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,

View file

@ -31,9 +31,7 @@ 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
@ -53,7 +51,6 @@ 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
@ -98,13 +95,7 @@ class SettingsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupActivityResultLaunchers()
val showImportBackupsFolder =
getExtraBooleanFromBundleOrIntent(
savedInstanceState,
EXTRA_SHOW_IMPORT_BACKUPS_FOLDER,
false,
)
showImportBackupsFolder.let {
savedInstanceState?.getBoolean(EXTRA_SHOW_IMPORT_BACKUPS_FOLDER, false)?.let {
if (it) {
model.refreshBackupsFolder(
requireContext(),
@ -247,27 +238,9 @@ class SettingsFragment : Fragment() {
}
}
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)
theme.observe(viewLifecycleOwner) { value ->
binding.Theme.setup(theme, value, requireContext()) { newValue ->
model.savePreference(theme, newValue)
}
}
@ -340,26 +313,15 @@ class SettingsFragment : Fragment() {
MaxLabels.setup(maxLabels, requireContext()) { newValue ->
model.savePreference(maxLabels, newValue)
}
labelTagsHiddenInOverview.observe(viewLifecycleOwner) { value ->
labelsHiddenInOverview.observe(viewLifecycleOwner) { value ->
binding.LabelsHiddenInOverview.setup(
labelTagsHiddenInOverview,
labelsHiddenInOverview,
value,
requireContext(),
layoutInflater,
R.string.labels_hidden_in_overview,
) { 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)
model.savePreference(labelsHiddenInOverview, enabled)
}
}
}
@ -452,7 +414,7 @@ class SettingsFragment : Fragment() {
when (selectedImportSource.mimeType) {
FOLDER_OR_FILE_MIMETYPE ->
MaterialAlertDialogBuilder(requireContext())
.setTitle(selectedImportSource.displayNameResId)
.setTitle(R.string.plain_text_files)
.setItems(
arrayOf(
getString(R.string.folder),
@ -604,14 +566,6 @@ 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) {
@ -662,11 +616,12 @@ class SettingsFragment : Fragment() {
}
}
}
AutoSaveAfterIdle.setupAutoSaveIdleTime(autoSaveAfterIdleTime, requireContext()) {
newValue ->
binding.AutoSaveAfterIdle.setupAutoSaveIdleTime(
autoSaveAfterIdleTime,
requireContext(),
) { newValue ->
model.savePreference(autoSaveAfterIdleTime, newValue)
}
ClearData.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.clear_data_message)

View file

@ -17,11 +17,9 @@ 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
@ -41,14 +39,10 @@ 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.isImageMimeType
import com.philkes.notallyx.data.model.toText
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
@ -56,11 +50,9 @@ 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
@ -87,7 +79,6 @@ 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
@ -126,10 +117,6 @@ 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) {
@ -188,15 +175,11 @@ abstract class EditActivity(private val type: Type) :
if (persistedId == null || notallyModel.originalNote == null) {
notallyModel.setState(id)
}
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))
}
if (notallyModel.isNewNote && intent.action == Intent.ACTION_SEND) {
handleSharedNote()
} else if (notallyModel.isNewNote) {
intent.getStringExtra(EXTRA_DISPLAYED_LABEL)?.let {
notallyModel.setLabels(listOf(it))
}
}
@ -226,18 +209,6 @@ 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()
@ -505,19 +476,7 @@ abstract class EditActivity(private val type: Type) :
binding.BottomAppBarCenter.apply {
removeAllViews()
undo =
addIconButton(
R.string.undo,
R.drawable.undo,
marginStart = 2,
onLongClick = {
try {
changeHistory.undoAll()
} catch (e: ChangeHistory.ChangeHistoryException) {
application.log(TAG, throwable = e)
}
true
},
) {
addIconButton(R.string.undo, R.drawable.undo, marginStart = 2) {
try {
changeHistory.undo()
} catch (e: ChangeHistory.ChangeHistoryException) {
@ -527,19 +486,7 @@ abstract class EditActivity(private val type: Type) :
.apply { isEnabled = changeHistory.canUndo.value }
redo =
addIconButton(
R.string.redo,
R.drawable.redo,
marginStart = 2,
onLongClick = {
try {
changeHistory.redoAll()
} catch (e: ChangeHistory.ChangeHistoryException) {
application.log(TAG, throwable = e)
}
true
},
) {
addIconButton(R.string.redo, R.drawable.redo, marginStart = 2) {
try {
changeHistory.redo()
} catch (e: ChangeHistory.ChangeHistoryException) {
@ -550,31 +497,14 @@ abstract class EditActivity(private val type: Type) :
}
binding.BottomAppBarRight.apply {
removeAllViews()
addToggleViewMode()
addIconButton(R.string.tap_for_more_options, R.drawable.more_vert, marginStart = 0) {
MoreNoteBottomSheet(
this@EditActivity,
createNoteTypeActions() + createFolderActions(),
colorInt,
)
addIconButton(R.string.more, R.drawable.more_vert, marginStart = 0) {
MoreNoteBottomSheet(this@EditActivity, 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 ->
@ -614,68 +544,12 @@ 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?) {
@ -692,90 +566,26 @@ 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)
@ -892,7 +702,12 @@ abstract class EditActivity(private val type: Type) :
}
override fun share() {
this.shareNote(notallyModel.getBaseNote())
val body =
when (type) {
Type.NOTE -> notallyModel.body
Type.LIST -> notallyModel.items.toMutableList().toText()
}
this.shareNote(notallyModel.title, body)
}
override fun export(mimeType: ExportMimeType) {
@ -1095,9 +910,7 @@ abstract class EditActivity(private val type: Type) :
colorInt = extractColor(notallyModel.color)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
window.statusBarColor = colorInt
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
window.navigationBarColor = colorInt
}
window.navigationBarColor = colorInt
window.setLightStatusAndNavBar(colorInt.isLightColor())
}
binding.apply {

View file

@ -1,17 +1,13 @@
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
@ -73,21 +69,6 @@ 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()
}
@ -104,13 +85,8 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
super.initBottomMenu()
binding.BottomAppBarRight.apply {
removeAllViews()
addToggleViewMode()
addIconButton(R.string.tap_for_more_options, R.drawable.more_vert, marginStart = 0) {
MoreListBottomSheet(
this@EditListActivity,
createNoteTypeActions() + createFolderActions(),
colorInt,
)
addIconButton(R.string.more, R.drawable.more_vert, marginStart = 0) {
MoreListBottomSheet(this@EditListActivity, createFolderActions(), colorInt)
.show(supportFragmentManager, MoreListBottomSheet.TAG)
}
}

View file

@ -21,12 +21,10 @@ 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
@ -40,9 +38,7 @@ 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
@ -69,6 +65,8 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
override fun configureUI() {
binding.EnterTitle.setOnNextAction { binding.EnterBody.requestFocus() }
setupEditor()
if (notallyModel.isNewNote) {
binding.EnterBody.requestFocus()
}
@ -80,17 +78,6 @@ 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 {
@ -185,8 +172,78 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
private fun setupEditor() {
setupMovementMethod()
binding.EnterBody.customSelectionActionModeCallback =
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, 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 =
object : ActionMode.Callback {
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false
@ -198,54 +255,8 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
// 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(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()
add(R.string.link_note, 0, order = Menu.CATEGORY_CONTAINER + 1) {
linkNote(pickNoteNewActivityResultLauncher)
mode?.finish()
}
}
@ -259,69 +270,26 @@ 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
}
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
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
}
} else {
binding.EnterBody.setOnSelectionChange { _, _ -> }
}
binding.ContentLayout.setOnClickListener {
binding.EnterBody.apply {
requestFocus()
if (canEdit) {
setSelection(length())
showKeyboard(this)
}
setSelection(length())
showKeyboard(this)
}
}
}
@ -398,23 +366,19 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
val movementMethod = LinkMovementMethod { span ->
val items =
if (span.url.isNoteUrl()) {
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))
arrayOf(
getString(R.string.remove_link),
getString(R.string.change_note),
getString(R.string.edit),
getString(R.string.open_note),
)
} else {
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))
arrayOf(
getString(R.string.remove_link),
getString(R.string.copy),
getString(R.string.edit),
getString(R.string.open_link),
)
}
MaterialAlertDialogBuilder(this)
.setTitle(
@ -426,16 +390,35 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
)
.setItems(items) { _, which ->
when (which) {
0 -> openLink(span)
0 -> {
binding.EnterBody.removeSpanWithHistory(
span,
span.url.isNoteUrl() ||
span.url == binding.EnterBody.getSpanText(span),
)
}
1 ->
if (span.url.isNoteUrl()) {
removeLink(span)
} else copyLink(span)
2 ->
if (span.url.isNoteUrl()) {
changeNoteLink(span)
} else removeLink(span)
3 -> editLink(span)
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)
}
}
}
}
}
.show()
@ -443,37 +426,6 @@ 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)

View file

@ -51,8 +51,7 @@ open class PickNoteActivity : LockedActivity<ActivityPickNoteBinding>(), ItemLis
maxItems.value,
maxLines.value,
maxTitle.value,
labelTagsHiddenInOverview.value,
imagesHiddenInOverview.value,
labelsHiddenInOverview.value,
),
application.getExternalImagesDirectory(),
this@PickNoteActivity,
@ -72,7 +71,6 @@ 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 {
@ -80,7 +78,7 @@ open class PickNoteActivity : LockedActivity<ActivityPickNoteBinding>(), ItemLis
withContext(Dispatchers.IO) {
val raw =
it.getBaseNoteDao().getAllNotes().filter { it.id != excludedNoteId }
BaseNoteModel.transform(raw, pinned, others, archived)
BaseNoteModel.transform(raw, pinned, others)
}
adapter.submitList(notes)
binding.EmptyView.visibility =

View file

@ -31,6 +31,7 @@ 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
@ -45,7 +46,6 @@ data class BaseNoteVHPreferences(
val maxLines: Int,
val maxTitleLines: Int,
val hideLabels: Boolean,
val hideImages: Boolean,
)
class BaseNoteVH(
@ -203,15 +203,21 @@ class BaseNoteVH(
private fun setColor(color: String) {
binding.root.apply {
val colorInt = context.extractColor(color)
setCardBackgroundColor(colorInt)
setControlsContrastColorForAllViews(colorInt)
if (color == BaseNote.COLOR_DEFAULT) {
setCardBackgroundColor(0)
setControlsContrastColorForAllViews(context.getColorFromAttr(R.attr.colorSurface))
} else {
val colorInt = context.extractColor(color)
setCardBackgroundColor(colorInt)
setControlsContrastColorForAllViews(colorInt)
}
}
}
private fun setImages(images: List<FileAttachment>, mediaRoot: File?) {
binding.apply {
if (images.isNotEmpty() && !preferences.hideImages) {
if (images.isNotEmpty()) {
ImageView.visibility = VISIBLE
Message.visibility = GONE

View file

@ -1,21 +1,17 @@
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)
@ -34,60 +30,33 @@ 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 getTextSafe()
return super.getText()
}
fun getTextClone(): Editable {
return getTextSafe().clone()
return super.getText()!!.clone()
}
fun applyWithoutTextWatcher(
callback: EditTextWithWatcher.() -> Unit
): Pair<Editable, Editable> {
val textBefore = getTextClone()
val textBefore = super.getText()!!.clone()
val editTextWatcher = textWatcher
editTextWatcher?.let { removeTextChangedListener(it) }
callback()
editTextWatcher?.let { addTextChangedListener(it) }
return Pair(textBefore, getTextClone())
return Pair(textBefore, super.getText()!!.clone())
}
fun changeText(callback: (text: Editable) -> Unit): Pair<Editable, Editable> {
return applyWithoutTextWatcher { callback(getTextSafe()!!) }
return applyWithoutTextWatcher { callback(super.getText()!!) }
}
private fun getTextSafe() = super.getText() ?: Editable.Factory.getInstance().newEditable("")
fun focusAndSelect(
start: Int = selectionStart,
end: Int = selectionEnd,

View file

@ -8,7 +8,6 @@ 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(
@ -36,7 +35,7 @@ class TextFormattingAdapter(
private val bold: Toggle =
Toggle(R.string.bold, R.drawable.format_bold, false) {
if (!it.checked) {
editText.applySpan(createBoldSpan())
editText.applySpan(StyleSpan(Typeface.BOLD))
} else {
editText.clearFormatting(type = StylableEditTextWithHistory.TextStyleType.BOLD)
}

View file

@ -31,7 +31,7 @@ class MoreNoteBottomSheet(
setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
setOnClickListener {
callbacks.export(mimeType)
fragment.dismiss()
fragment.hide()
}
}
}

View file

@ -5,8 +5,6 @@ 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
@ -151,20 +149,9 @@ 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 }
@ -263,7 +250,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) }
}

View file

@ -1,5 +1,6 @@
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
@ -83,28 +84,28 @@ class ListManager(
internal fun setState(state: ListState) {
adapter.submitList(state.items) {
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,
)
// 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,
)
}
}
}
}
}
}
this.itemsChecked?.setItems(state.checkedItems!!)
}
fun add(
@ -281,15 +282,23 @@ class ListManager(
}
}
fun changeText(position: Int, value: EditTextState, pushChange: Boolean = true) {
val stateBefore = getState()
fun changeText(
position: Int,
value: EditTextState,
before: EditTextState? = null,
pushChange: Boolean = true,
editText: EditText?,
listener: TextWatcher?,
) {
// if(!pushChange) {
endSearch?.invoke()
// }
val item = items[position]
item.body = value.text.toString()
if (pushChange) {
changeHistory.push(ListEditTextChange(stateBefore, getState(), this))
changeHistory.push(
ListEditTextChange(editText!!, position, before!!, value, listener!!, this)
)
// TODO: fix focus change
// refreshSearch?.invoke(editText)
}

View file

@ -6,7 +6,6 @@ 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
@ -39,12 +38,6 @@ 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
}
@ -58,7 +51,7 @@ class CheckedListItemAdapter(
}
override fun onBindViewHolder(holder: ListItemVH, position: Int) {
itemAdapterBase.onBindViewHolder(holder, position, viewMode)
itemAdapterBase.onBindViewHolder(holder, position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =

View file

@ -7,7 +7,6 @@ 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
@ -38,12 +37,6 @@ 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
@ -62,7 +55,7 @@ class ListItemAdapter(
}
override fun onBindViewHolder(holder: ListItemVH, position: Int) {
itemAdapterBase.onBindViewHolder(holder, position, viewMode)
itemAdapterBase.onBindViewHolder(holder, position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =

View file

@ -7,7 +7,6 @@ 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
@ -41,7 +40,7 @@ abstract class ListItemAdapterBase(
touchHelper.attachToRecyclerView(recyclerView)
}
fun onBindViewHolder(holder: ListItemVH, position: Int, viewMode: NoteViewMode) {
fun onBindViewHolder(holder: ListItemVH, position: Int) {
val item = getItem(position)
holder.bind(
backgroundColor,
@ -49,7 +48,6 @@ abstract class ListItemAdapterBase(
position,
highlights[position],
preferences.listItemSorting.value,
viewMode,
)
}

View file

@ -1,11 +1,8 @@
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
@ -21,7 +18,6 @@ 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
@ -33,7 +29,6 @@ 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,
@ -50,6 +45,11 @@ class ListItemVH(
binding.EditText.apply {
setTextSize(TypedValue.COMPLEX_UNIT_SP, body)
setOnNextAction {
val position = bindingAdapterPosition + 1
listManager.add(position)
}
textWatcher =
createListTextWatcherWithHistory(
listManager,
@ -61,6 +61,10 @@ class ListItemVH(
false
}
}
setOnFocusChangeListener { _, hasFocus ->
binding.Delete.visibility = if (hasFocus) VISIBLE else INVISIBLE
}
}
binding.DragHandle.setOnTouchListener { _, event ->
@ -91,21 +95,20 @@ class ListItemVH(
position: Int,
highlights: List<ListItemHighlight>?,
autoSort: ListItemSort,
viewMode: NoteViewMode,
) {
updateEditText(item, position, viewMode)
updateEditText(item, position)
updateCheckBox(item, position)
updateDeleteButton(item, position, viewMode)
updateDeleteButton(item, position)
updateSwipe(item.isChild, viewMode == NoteViewMode.EDIT && position != 0 && !item.checked)
updateSwipe(item.isChild, position != 0 && !item.checked)
binding.DragHandle.apply {
visibility =
when {
viewMode != NoteViewMode.EDIT -> GONE
item.checked && autoSort == ListItemSort.AUTO_SORT_BY_CHECKED -> INVISIBLE
else -> VISIBLE
if (item.checked && autoSort == ListItemSort.AUTO_SORT_BY_CHECKED) {
INVISIBLE
} else {
VISIBLE
}
contentDescription = "Drag$position"
}
@ -127,14 +130,9 @@ class ListItemVH(
binding.EditText.focusAndSelect(selectionStart, selectionEnd, inputMethodManager)
}
private fun updateDeleteButton(item: ListItem, position: Int, viewMode: NoteViewMode) {
private fun updateDeleteButton(item: ListItem, position: Int) {
binding.Delete.apply {
visibility =
when {
viewMode != NoteViewMode.EDIT -> GONE
item.checked -> VISIBLE
else -> INVISIBLE
}
visibility = if (item.checked) VISIBLE else INVISIBLE
setOnClickListener {
listManager.delete(absoluteAdapterPosition, inCheckedList = inCheckedList)
}
@ -142,50 +140,10 @@ class ListItemVH(
}
}
private fun updateEditText(item: ListItem, position: Int, viewMode: NoteViewMode) {
private fun updateEditText(item: ListItem, position: Int) {
binding.EditText.apply {
setText(item.body)
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) }
isEnabled = !item.checked
setOnKeyListener { _, keyCode, event ->
if (
event.action == KeyEvent.ACTION_DOWN &&
@ -204,6 +162,7 @@ class ListItemVH(
false
}
}
contentDescription = "EditText$position"
}
}
@ -211,12 +170,12 @@ class ListItemVH(
private fun updateCheckBox(item: ListItem, position: Int) {
if (checkBoxListener == null) {
checkBoxListener = OnCheckedChangeListener { _, isChecked ->
binding.CheckBox.setOnCheckedChangeListener(null)
checkBoxListener = OnCheckedChangeListener { buttonView, isChecked ->
buttonView!!.setOnCheckedChangeListener(null)
if (absoluteAdapterPosition != NO_POSITION) {
listManager.changeChecked(absoluteAdapterPosition, isChecked, inCheckedList)
}
binding.CheckBox.setOnCheckedChangeListener(checkBoxListener)
buttonView.setOnCheckedChangeListener(checkBoxListener)
}
}
binding.CheckBox.apply {
@ -276,7 +235,13 @@ class ListItemVH(
private fun EditText.changeText(position: Int, after: CharSequence) {
setText(after)
val stateAfter = EditTextState(editableText.clone(), selectionStart)
listManager.changeText(position, stateAfter, pushChange = false)
listManager.changeText(
position,
stateAfter,
pushChange = false,
editText = null,
listener = null,
)
}
fun getSelection() = with(binding.EditText) { Pair(selectionStart, selectionEnd) }

View file

@ -5,6 +5,7 @@ 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
@ -12,7 +13,6 @@ 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,9 +38,7 @@ 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
@ -116,7 +114,6 @@ 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)
@ -129,7 +126,6 @@ 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)
@ -153,12 +149,11 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
allNotes = baseNoteDao.getAllAsync()
allNotes!!.observeForever(allNotesObserver!!)
labelsHiddenObserver?.let { preferences.labelsHidden.removeObserver(it) }
labelsHiddenObserver = Observer { labelsHidden ->
baseNotes = null
initBaseNotes(labelsHidden)
if (baseNotes == null) {
baseNotes = Content(baseNoteDao.getFrom(Folder.NOTES), ::transform)
} else {
baseNotes!!.setObserver(baseNoteDao.getFrom(Folder.NOTES))
}
preferences.labelsHidden.observeForever(labelsHiddenObserver!!)
if (deletedNotes == null) {
deletedNotes = Content(baseNoteDao.getFrom(Folder.DELETED), ::transform)
@ -192,18 +187,6 @@ 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)
@ -215,7 +198,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, archived)
private fun transform(list: List<BaseNote>) = transform(list, pinned, others)
fun disableBackups() {
val value = preferences.backupsFolder.value
@ -335,7 +318,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("${app.getString(R.string.invalid_backup)}: ${throwable.message}")
app.showToast(R.string.invalid_backup)
}
val backupDir = app.getBackupDir()
@ -347,7 +330,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
fun importXmlBackup(uri: Uri) {
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
app.log(TAG, throwable = throwable)
app.showToast("${app.getString(R.string.invalid_backup)}: ${throwable.message}")
app.showToast(R.string.invalid_backup)
}
viewModelScope.launch(exceptionHandler) {
@ -365,15 +348,18 @@ 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 ->
app.log(TAG, throwable = throwable)
if (throwable is ImportException) {
app.showToast(throwable.textResId)
} else {
app.showToast("${app.getString(R.string.invalid_backup)}: ${throwable.message}")
}
}
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)
}
viewModelScope.launch(exceptionHandler) {
val importedNotes =
withContext(Dispatchers.IO) {
@ -550,7 +536,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
fun deleteLabel(value: String) {
viewModelScope.launch(Dispatchers.IO) { commonDao.deleteLabel(value) }
val labelsHiddenPreference = preferences.labelsHidden
val labelsHiddenPreference = preferences.labelsHiddenInNavigation
val labelsHidden = labelsHiddenPreference.value.toMutableSet()
if (labelsHidden.contains(value)) {
labelsHidden.remove(value)
@ -566,7 +552,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.labelsHidden
val labelsHiddenPreference = preferences.labelsHiddenInNavigation
val labelsHidden = labelsHiddenPreference.value.toMutableSet()
if (labelsHidden.contains(oldValue)) {
labelsHidden.remove(oldValue)
@ -609,7 +595,6 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
clearPersistedUriPermissions(backupsFolder)
}
callback()
app.restartApplication(R.id.Settings)
}
fun importPreferences(
@ -622,7 +607,6 @@ 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)
@ -634,7 +618,6 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
finishImportPreferences(
oldBackupsFolder,
themeBefore,
useDynamicColorsBefore,
oldStartView,
context,
askForUriPermissions,
@ -648,7 +631,6 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
finishImportPreferences(
oldBackupsFolder,
themeBefore,
useDynamicColorsBefore,
oldStartView,
context,
askForUriPermissions,
@ -662,18 +644,15 @@ 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() && !hasUseDynamicColorsChange) {
if (themeBefore == preferences.theme.getFreshValue()) {
refreshBackupsFolder(context, backupFolder, askForUriPermissions)
}
} else {
@ -685,9 +664,6 @@ 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(
@ -740,35 +716,24 @@ 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,
archived: Header,
): List<Item> {
fun transform(list: List<BaseNote>, pinned: Header, others: Header): List<Item> {
if (list.isEmpty()) {
return list
} else {
val firstPinnedNote = list.indexOfFirst { baseNote -> baseNote.pinned }
val firstUnpinnedNote =
list.indexOfFirst { baseNote ->
!baseNote.pinned && baseNote.folder != Folder.ARCHIVED
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 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
newList
} else list
}
}
}

View file

@ -19,14 +19,11 @@ 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
@ -87,13 +84,10 @@ 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>>>()
@ -252,7 +246,6 @@ 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)
@ -350,7 +343,6 @@ class NotallyModel(private val app: Application) : AndroidViewModel(app) {
files.value,
audios.value,
reminders.value,
viewMode.value,
)
}
@ -449,34 +441,6 @@ 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,

View file

@ -29,7 +29,6 @@ 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 =
@ -76,21 +75,15 @@ class NotallyXPreferences private constructor(private val context: Context) {
10,
R.string.max_lines_to_display_title,
)
val labelsHidden = StringSetPreference("labelsHiddenInNavigation", preferences, setOf())
val labelTagsHiddenInOverview =
val labelsHiddenInNavigation =
StringSetPreference("labelsHiddenInNavigation", preferences, setOf())
val labelsHiddenInOverview =
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",
@ -142,8 +135,6 @@ 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)
@ -212,7 +203,7 @@ class NotallyXPreferences private constructor(private val context: Context) {
context.importPreferences(uri, preferences.edit()).also { reload() }
fun reset() {
preferences.edit().clear().commit()
preferences.edit().clear().apply()
encryptedPreferences.edit().clear().apply()
backupsFolder.refresh()
dataInPublicFolder.refresh()
@ -232,15 +223,13 @@ class NotallyXPreferences private constructor(private val context: Context) {
maxItems,
maxLines,
maxTitle,
secureFlag,
labelsHidden,
labelTagsHiddenInOverview,
labelsHiddenInNavigation,
labelsHiddenInOverview,
maxLabels,
periodicBackups,
backupPassword,
backupOnSave,
autoSaveAfterIdleTime,
imagesHiddenInOverview,
)
.forEach { it.refresh() }
}

View file

@ -11,7 +11,6 @@ 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
@ -43,10 +42,9 @@ 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, locked = app.locked.value)
updateWidgets(context, noteIds)
}
}
ACTION_OPEN_NOTE -> openActivity(context, intent, EditNoteActivity::class.java)
@ -87,8 +85,7 @@ class WidgetProvider : AppWidgetProvider() {
baseNoteDao.updateChecked(noteId, childrenPositions + position, checked!!)
}
} finally {
val app = context.applicationContext as NotallyXApplication
updateWidgets(context, longArrayOf(noteId), locked = app.locked.value)
updateWidgets(context, longArrayOf(noteId))
pendingResult.finish()
}
}
@ -138,19 +135,19 @@ class WidgetProvider : AppWidgetProvider() {
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
val app = context.applicationContext as NotallyXApplication
val app = context.applicationContext as Application
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, locked = app.locked.value)
updateWidget(app, appWidgetManager, id, noteId, noteType)
}
}
companion object {
fun updateWidgets(context: Context, noteIds: LongArray? = null, locked: Boolean) {
fun updateWidgets(context: Context, noteIds: LongArray? = null, locked: Boolean = false) {
val app = context.applicationContext as Application
val preferences = NotallyXPreferences.getInstance(app)
@ -184,90 +181,65 @@ class WidgetProvider : AppWidgetProvider() {
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
intent.embedIntentExtras()
MainScope().launch {
if (!locked) {
val database = NotallyDatabase.getDatabase(context).value
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),
)
}
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 {
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.Layout,
R.id.Empty,
Intent(context, WidgetProvider::class.java)
.setOpenNoteIntent(noteType, noteId)
.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)
}
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)
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)
}
}

View file

@ -44,13 +44,6 @@ 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

View file

@ -80,7 +80,7 @@ private fun Context.createReminderAlarmIntent(noteId: Long, reminderId: Long): P
intent.putExtra(ReminderReceiver.EXTRA_NOTE_ID, noteId)
return PendingIntent.getBroadcast(
this,
(noteId.toString() + reminderId.toString()).toInt(),
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
)

View file

@ -1,6 +1,7 @@
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
@ -11,11 +12,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
@ -32,19 +33,16 @@ 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
@ -98,7 +96,7 @@ fun ClipboardManager.getLatestText(): CharSequence? {
return primaryClip?.let { if (it.itemCount > 0) it.getItemAt(0)!!.text else null }
}
fun Context.copyToClipBoard(text: CharSequence) {
fun Activity.copyToClipBoard(text: CharSequence) {
ContextCompat.getSystemService(this, ClipboardManager::class.java)?.let {
val clip = ClipData.newPlainText("label", text)
it.setPrimaryClip(clip)
@ -123,15 +121,30 @@ fun Context.getFileName(uri: Uri): String? =
}
fun Context.canAuthenticateWithBiometrics(): Int {
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
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()
} else {
androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
val biometricManager: BiometricManager =
this.getSystemService(BiometricManager::class.java)
return biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
}
return biometricManager.canAuthenticate(authenticators)
}
return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
fun Context.getUriForFile(file: File): Uri =
@ -139,8 +152,6 @@ 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,
@ -155,7 +166,8 @@ fun ContextWrapper.log(
fun ContextWrapper.getLastExceptionLog(): String? {
val logFile = getLogFile()
if (logFile.exists()) {
return logFile.readText().substringAfterLast("[Start]")
val logContents = logFile.readText().substringAfterLast("[Start]")
return URLEncoder.encode(logContents, StandardCharsets.UTF_8.toString())
}
return null
}
@ -185,24 +197,16 @@ fun Context.logToFile(
val logFile =
folder.findFile(fileName).let {
if (it == null || !it.exists()) {
folder.createFile("text/plain", fileName.removeSuffix(".txt"))
folder.createFile("text/plain", fileName)
} else if (it.isLargerThanKb(MAX_LOGS_FILE_SIZE_KB)) {
it.delete()
folder.createFile("text/plain", fileName.removeSuffix(".txt"))
folder.createFile("text/plain", fileName)
} else it
}
logFile?.let { file ->
val contentResolver = contentResolver
val (outputStream, logFileContents) =
try {
Pair(contentResolver.openOutputStream(file.uri, "wa"), null)
} catch (e: UnsupportedOperationException) {
Pair(
contentResolver.openOutputStream(file.uri, "w"),
contentResolver.readFileContents(file.uri),
)
}
val outputStream = contentResolver.openOutputStream(file.uri, "wa")
outputStream?.use { output ->
val writer = PrintWriter(OutputStreamWriter(output, Charsets.UTF_8))
@ -210,7 +214,6 @@ 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]")
}
@ -240,17 +243,6 @@ 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)) }
}
@ -287,34 +279,16 @@ fun Context.createReportBugIntent(
.wrapWithChooser(this)
}
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>) {
fun Context.shareNote(title: String, body: CharSequence) {
val text = body.truncate(150_000)
val intent =
Intent(if (imageUris.size > 1) Intent.ACTION_SEND_MULTIPLE else Intent.ACTION_SEND)
Intent(Intent.ACTION_SEND)
.apply {
type = if (imageUris.isEmpty()) "text/*" else "image/*"
type = "text/plain"
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)
@ -426,12 +400,3 @@ 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() }
} ?: ""

View file

@ -149,7 +149,7 @@ fun Context.getExportedPath() = getEmptyFolder("exported")
fun ContextWrapper.getLogsDir() = File(filesDir, "logs").also { it.mkdir() }
const val APP_LOG_FILE_NAME = "notallyx-logs.txt"
const val APP_LOG_FILE_NAME = "Log.v1.txt"
fun ContextWrapper.getLogFile(): File {
return File(getLogsDir(), APP_LOG_FILE_NAME)

View file

@ -124,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())}.zip"
log(msg = "Creating '$uri/$name'...")
val name = "$backupFilePrefix${formatter.format(System.currentTimeMillis())}"
log(msg = "Creating '$uri/$name.zip'...")
val zipUri = requireNotNull(folder.createFile(MIME_TYPE_ZIP, name)).uri
val exportedNotes = app.exportAsZip(zipUri, password = preferences.backupPassword.value)
log(msg = "Exported $exportedNotes notes")
@ -174,7 +174,7 @@ fun ContextWrapper.autoBackupOnSave(backupPath: String, password: String, savedN
backupFile = folder.createFile(MIME_TYPE_ZIP, ON_SAVE_BACKUP_FILE)
exportAsZip(backupFile!!.uri, password = password)
} else {
val (_, file) = copyDatabase()
NotallyDatabase.getDatabase(this, observePreferences = false).value.checkpoint()
val files =
with(savedNote) {
images.map {
@ -192,7 +192,10 @@ fun ContextWrapper.autoBackupOnSave(backupPath: String, password: String, savedN
audios.map {
BackupFile(SUBFOLDER_AUDIOS, File(getExternalAudioDirectory(), it.name))
} +
BackupFile(null, file)
BackupFile(
null,
NotallyDatabase.getCurrentDatabaseFile(this@autoBackupOnSave),
)
}
try {
exportToZip(backupFile.uri, files, password)
@ -414,7 +417,7 @@ 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, databaseFile, decryptedFile)
decryptDatabase(this, passphrase, decryptedFile, databaseFile)
Pair(database, decryptedFile)
} else {
val dbFile = File(cacheDir, DATABASE_NAME)
@ -514,14 +517,13 @@ fun exportPdfFile(
total: Int? = null,
duplicateFileCount: Int = 1,
) {
val validFileName = fileName.ifBlank { app.getString(R.string.note) }
val filePath = "$validFileName.${ExportMimeType.PDF.fileExtension}"
val filePath = "$fileName.${ExportMimeType.PDF.fileExtension}"
if (folder.findFile(filePath)?.exists() == true) {
return exportPdfFile(
app,
note,
folder,
"${validFileName.removeTrailingParentheses()} ($duplicateFileCount)",
"${fileName.removeTrailingParentheses()} ($duplicateFileCount)",
pdfPrintListener,
progress,
counter,
@ -579,9 +581,8 @@ suspend fun exportPlainTextFile(
)
}
return withContext(Dispatchers.IO) {
val validFileName = fileName.takeIf { it.isNotBlank() } ?: app.getString(R.string.note)
val file =
folder.createFile(exportType.mimeType, validFileName)?.let {
folder.createFile(exportType.mimeType, fileName)?.let {
app.contentResolver.openOutputStream(it.uri)?.use { stream ->
OutputStreamWriter(stream).use { writer ->
writer.write(

View file

@ -22,13 +22,11 @@ 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
@ -45,8 +43,6 @@ 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
@ -89,31 +85,12 @@ 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(dbFile.path, null, SQLiteDatabase.OPEN_READONLY)
SQLiteDatabase.openDatabase(
File(databaseFolder, NotallyDatabase.DATABASE_NAME).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)
@ -199,11 +176,14 @@ 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 {
throw e
log(TAG, throwable = e)
showToast(R.string.invalid_backup)
}
} catch (e: Exception) {
showToast(R.string.invalid_backup)
log(TAG, throwable = e)
} finally {
importingBackup?.value = ImportProgress(inProgress = false)
}
@ -269,8 +249,8 @@ private fun Cursor.toBaseNote(): BaseNote {
else -> throw IllegalArgumentException("pinned must be 0 or 1")
}
val type = Type.valueOfOrDefault(typeTmp)
val folder = Folder.valueOfOrDefault(folderTmp)
val type = Type.valueOf(typeTmp)
val folder = Folder.valueOf(folderTmp)
val labels = Converters.jsonToLabels(labelsTmp)
val spans = Converters.jsonToSpans(spansTmp)
@ -300,11 +280,6 @@ 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,
@ -322,7 +297,6 @@ private fun Cursor.toBaseNote(): BaseNote {
files,
audios,
reminders,
viewMode,
)
}
@ -356,7 +330,8 @@ fun Context.importPreferences(jsonFile: Uri, to: SharedPreferences.Editor): Bool
else -> to.putString(key, value.toString())
}
}
return to.commit()
to.apply()
return true
} catch (e: Exception) {
if (this is ContextWrapper) {
log(TAG, "Import preferences from '$jsonFile' failed", throwable = e)

View file

@ -4,7 +4,6 @@ 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
@ -114,7 +113,6 @@ private fun XmlPullParser.parseBaseNote(rootTag: String, folder: Folder): BaseNo
emptyList(),
emptyList(),
emptyList(),
NoteViewMode.EDIT,
)
}

View file

@ -35,12 +35,6 @@ 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!}")
@ -51,12 +45,6 @@ class ChangeHistory {
stackPointer.value -= 1
}
fun undoAll() {
while (stackPointer.value >= 0) {
undo()
}
}
fun reset() {
stackPointer.value = -1
changeStack.clear()

View file

@ -1,7 +1,33 @@
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(old: ListState, new: ListState, listManager: ListManager) :
ListBatchChange(old, new, listManager)
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)
}
}
}

View file

@ -37,8 +37,8 @@ fun decryptDatabase(context: ContextWrapper, passphrase: ByteArray) {
fun decryptDatabase(
context: Context,
passphrase: ByteArray,
databaseFile: File,
decryptedFile: File,
databaseFile: File,
) {
val state = SQLCipherUtils.getDatabaseState(databaseFile)
if (state == SQLCipherUtils.State.ENCRYPTED) {

View file

@ -4,13 +4,15 @@ 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
@ -25,7 +27,7 @@ fun Activity.showBiometricOrPinPrompt(
) {
showBiometricOrPinPrompt(
isForDecrypt,
this as FragmentActivity,
this,
activityResultLauncher,
titleResId,
descriptionResId,
@ -46,7 +48,7 @@ fun Fragment.showBiometricOrPinPrompt(
) {
showBiometricOrPinPrompt(
isForDecrypt,
activity!!,
requireContext(),
activityResultLauncher,
titleResId,
descriptionResId,
@ -58,7 +60,7 @@ fun Fragment.showBiometricOrPinPrompt(
private fun showBiometricOrPinPrompt(
isForDecrypt: Boolean,
context: FragmentActivity,
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,
titleResId: Int,
descriptionResId: Int? = null,
@ -67,24 +69,65 @@ private fun showBiometricOrPinPrompt(
onFailure: (errorCode: Int?) -> Unit,
) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
val promptInfo =
BiometricPrompt.PromptInfo.Builder()
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))
}
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
)
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()
@ -94,13 +137,16 @@ private fun showBiometricOrPinPrompt(
} else {
getInitializedCipherForEncryption()
}
val authCallback =
prompt.authenticate(
BiometricPrompt.CryptoObject(cipher),
getCancellationSignal(context),
context.mainExecutor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
result: BiometricPrompt.AuthenticationResult?
) {
super.onAuthenticationSucceeded(result)
onSuccess.invoke(result.cryptoObject!!.cipher!!)
onSuccess.invoke(result!!.cryptoObject!!.cipher)
}
override fun onAuthenticationFailed() {
@ -108,14 +154,57 @@ private fun showBiometricOrPinPrompt(
onFailure.invoke(null)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
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))
},
)
}
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()
}
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 onAuthenticationError(
errorCode: Int,
errString: CharSequence?,
) {
super.onAuthenticationError(errorCode, errString)
onFailure.invoke(errorCode)
}
},
null,
)
} else {
promptPinAuthentication(context, activityResultLauncher, titleResId, onFailure)
}
}
else -> {
@ -125,6 +214,14 @@ 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>,

View file

@ -287,10 +287,9 @@ public class SQLCipherUtils {
final SQLiteStatement st = db.compileStatement("ATTACH DATABASE ? AS plaintext KEY ''");
if(decryptedFile.exists()){
decryptedFile.delete();
if (!decryptedFile.exists()) {
decryptedFile.createNewFile();
}
decryptedFile.createNewFile();
st.bindString(1, decryptedFile.getAbsolutePath());
st.execute();

View file

@ -1,10 +0,0 @@
<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>

View file

@ -1,10 +0,0 @@
<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>

View file

@ -22,8 +22,8 @@
android:layout_gravity="center_vertical"
android:padding="8dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/labels_hidden_in_overview_title"
android:tooltipText="@string/labels_hidden_in_overview_title"
android:contentDescription="@string/label_visibility"
android:tooltipText="@string/label_visibility"
app:srcCompat="@drawable/visibility"
app:tint="?attr/colorControlNormal" />

View file

@ -187,7 +187,7 @@
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingTop="4dp"
android:paddingStart="8dp"
android:paddingStart="14dp"
android:paddingEnd="4dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

View file

@ -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/EnumRadioGroup"
android:id="@+id/DateFormatRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
/>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/Toggle"
android:id="@+id/ApplyToNoteView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/date_format_apply_in_note_view"
/>

View file

@ -8,5 +8,6 @@
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/EditText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:inputType="textCapWords" />
</com.google.android.material.textfield.TextInputLayout>

View file

@ -1,55 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
<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"
android:layout_height="wrap_content"
android:scrollbarAlwaysDrawVerticalTrack="true">
android:checked="true"
android:text="@string/none" />
<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/Daily"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/daily" />
<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/Weekly"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/weekly" />
<RadioButton
android:id="@+id/Daily"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/daily" />
<RadioButton
android:id="@+id/Monthly"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/monthly" />
<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/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>

View file

@ -77,9 +77,7 @@
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"

View file

@ -88,10 +88,6 @@
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"
@ -177,10 +173,6 @@
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"

View file

@ -23,7 +23,7 @@
android:layout_gravity="center_vertical"
android:padding="8dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/labels_hidden_in_overview_title"
android:contentDescription="@string/label_visibility"
app:srcCompat="@drawable/visibility"
app:tint="?attr/colorControlNormal" />

View file

@ -25,11 +25,11 @@
<string name="auto_backup_last">Poslední záloha</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,7 +38,6 @@
<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>
@ -79,8 +78,6 @@
<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>
@ -123,7 +120,6 @@
<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>
@ -163,8 +159,6 @@
<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>
@ -193,8 +187,6 @@
<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>
@ -250,7 +242,6 @@
<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>
@ -306,7 +297,6 @@
<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í &gt; Oprávnění a zapněte Mikrofon.</string>
<string name="unarchive">Zrušit archivaci</string>

View file

@ -24,11 +24,11 @@
<string name="auto_backup_last">Letztes Backup</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_backup_period">Intervall für autom. Backups</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,7 +37,6 @@
<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>
@ -76,8 +75,6 @@
<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>
@ -119,7 +116,6 @@
<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>
@ -159,8 +155,6 @@
<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>
@ -188,8 +182,6 @@
<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>
@ -244,7 +236,6 @@
<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>
@ -299,7 +290,6 @@
<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 &gt; Berechtigungen und aktiviere Mikrofon</string>
<string name="unarchive">Archivierung aufheben</string>

View file

@ -24,11 +24,11 @@
<string name="auto_backup_last">Última copia de seguridad</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,7 +37,6 @@
<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>
@ -72,12 +71,9 @@
<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>
@ -119,7 +115,6 @@
<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>
@ -159,8 +154,6 @@
<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>
@ -188,8 +181,6 @@
<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>
@ -244,7 +235,6 @@
<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>
@ -299,7 +289,6 @@
<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 &gt; Permisos y active el micrófono.</string>
<string name="unarchive">Desarchivar</string>

View file

@ -28,7 +28,6 @@
<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 dinactivité 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>
@ -37,7 +36,6 @@
<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>
@ -76,8 +74,6 @@
<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>
@ -119,7 +115,6 @@
<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>
@ -159,8 +154,6 @@
<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>
@ -188,8 +181,6 @@
<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>
@ -244,7 +235,6 @@
<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>
@ -299,7 +289,6 @@
<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 &gt; Autorisations et activez le microphone.</string>
<string name="unarchive">Restaurer</string>

View file

@ -24,6 +24,7 @@
<string name="auto_backup_last">Ultimo backup</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>
@ -153,8 +154,6 @@
<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>

View file

@ -17,6 +17,7 @@
<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>

View file

@ -12,8 +12,6 @@
<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>
@ -26,11 +24,11 @@
<string name="auto_backup_last">Ostatnia kopia zapasowa</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>
@ -39,7 +37,6 @@
<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>
@ -50,8 +47,6 @@
<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>
@ -78,12 +73,9 @@
<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>
@ -114,8 +106,6 @@
<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>
@ -147,7 +137,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 Przywracanie 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 Importuj i wybierz go.</string>
<string name="every">Każde</string>
<string name="export">Eksportuj</string>
<string name="export_backup">Wykonaj kopię zapasową</string>
@ -161,13 +151,11 @@
<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 Przywracanie 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 Importuj 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>
@ -179,8 +167,6 @@
<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>
@ -197,8 +183,6 @@
<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>
@ -222,13 +206,11 @@
<string name="medium">Średni</string>
<string name="minutes">Minut</string>
<string name="modified_date">Zmodyfikowano</string>
<string name="monospace">Stała szerokość</string>
<string name="monospace">Monospace</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>
@ -247,7 +229,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 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="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="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>
@ -255,7 +237,6 @@
<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>
@ -274,8 +255,6 @@
<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>
@ -312,13 +291,10 @@
<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 &gt; 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>

View file

@ -1,334 +1,77 @@
<?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="edit_link">Изменить ссылку</string>
<string name="edit_reminders">Изменить напоминания</string>
<string name="elapsed">Просроченные</string>
<string name="empty_labels">Еще нет меток. Создать\?</string>
<string name="empty_labels">Еще нет меток, создать\?</string>
<string name="empty_list">Пустой список</string>
<string name="empty_note">Пустая заметка</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="export">Экспортировать</string>
<string name="export_backup">Экспортировать</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="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="import_backup">Импортировать</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>

View file

@ -51,6 +51,8 @@
<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>

View file

@ -54,9 +54,4 @@
<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>

View file

@ -24,6 +24,7 @@
<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>
@ -76,8 +77,6 @@
<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>
@ -118,7 +117,6 @@
<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>
@ -158,8 +156,6 @@
<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>
@ -187,8 +183,6 @@
<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>
@ -243,7 +237,6 @@
<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>
@ -298,7 +291,6 @@
<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 访问你的麦克风。轻按“设置”&gt;“权限”,打开麦克风</string>
<string name="unarchive">取消存档</string>
@ -312,13 +304,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>

View file

@ -24,6 +24,7 @@
<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>
@ -134,8 +135,6 @@
<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>

View file

@ -25,6 +25,7 @@
<string name="auto_backup_last">Last Backup</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_backup_period">Auto backup period</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>
@ -77,8 +78,6 @@
<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>
@ -121,7 +120,6 @@
<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>
@ -161,8 +159,6 @@
<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>
@ -190,8 +186,6 @@
<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 files name becomes the notes 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>
@ -246,7 +240,6 @@
<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>
@ -301,7 +294,6 @@
<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 &gt; Permissions and turn Microphone on</string>
<string name="unarchive">Unarchive</string>

View file

@ -124,7 +124,6 @@
<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">

View file

@ -46,24 +46,6 @@ 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>()
@ -77,25 +59,6 @@ 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>()

View file

@ -157,7 +157,6 @@ 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")
}
}

View file

@ -5,7 +5,6 @@ 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
@ -253,7 +252,6 @@ class GoogleKeepImporterTest {
files,
audios,
reminders,
NoteViewMode.EDIT,
)
}
}

View file

@ -1,161 +0,0 @@
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.

View file

@ -1,20 +0,0 @@
# 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*

View file

@ -1,12 +0,0 @@
---
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

View file

@ -1,44 +0,0 @@
---
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

View file

@ -1,24 +0,0 @@
---
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.

Before

Width:  |  Height:  |  Size: 94 KiB

View file

@ -1,29 +0,0 @@
---
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:
![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg)
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.

View file

@ -1,23 +0,0 @@
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

View file

@ -1,19 +0,0 @@
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

View file

@ -1,47 +0,0 @@
---
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.

View file

@ -1,8 +0,0 @@
{
"label": "Tutorial - Basics",
"position": 2,
"link": {
"type": "generated-index",
"description": "5 minutes to learn the most important Docusaurus concepts."
}
}

Some files were not shown because too many files have changed in this diff Show more