Compare commits

..

42 commits
v7.4.0 ... main

Author SHA1 Message Date
PhilKes
d00300fa0e Bump version 7.4.1 2025-06-04 18:35:01 +02:00
Phil
f13e8227ca
Merge pull request #605 from PhilKes/fix/convert-to-updated-model
Fix convertTo action using updated model
2025-06-04 17:30:55 +02:00
PhilKes
11e472cd30 Fix convertTo action using updated model 2025-06-04 17:26:01 +02:00
Phil
4cc957ccd4
Merge pull request #589 from PhilKes/fix/bold-text-weight
For API >= 33 use font weight 700 for bold
2025-05-10 17:51:23 +02:00
PhilKes
86b74762c5 For API >= 33 use font weight 700 for bold 2025-05-10 17:49:19 +02:00
PhilKes
118285545a Bump versionCode 7404 2025-05-10 16:45:43 +02:00
Phil
402baf8056
Merge pull request #588 from PhilKes/fix/backup-on-save-decrypt
Fix decrypting database on auto-save backup if biometric lock is enabled
2025-05-10 16:43:25 +02:00
PhilKes
de27d40880 Fix decrypting database on auto-save backup if biometric lock is enabled 2025-05-10 15:55:18 +02:00
PhilKes
66ce623e85 Fix NOTALLYX_BACKUP_LOGS_FILE .txt postfix 2025-05-10 15:26:38 +02:00
PhilKes
3c2400c7e6 Bump versionCode 7403 2025-05-10 14:31:18 +02:00
Phil
c64a7b2ed7
Merge pull request #587 from PhilKes/fix/reminder-identifier-requestcode
Use note.id and reminder.id for reminder requestCode
2025-05-10 14:30:49 +02:00
PhilKes
fb687856f1 Use note.id and reminder.id for reminder requestCode 2025-05-10 14:29:23 +02:00
PhilKes
23d678c8a3 Fix double encoded stacktrace on report bug 2025-05-10 13:50:39 +02:00
PhilKes
ade08b52ed Fix widget when displayed note is deleted 2025-05-10 13:18:54 +02:00
PhilKes
62a35132e0 Improve import failed toast message 2025-05-10 12:24:57 +02:00
Phil
9fbe5a6b94
Merge pull request #583 from PhilKes/fix/export-pdf-empty-title
Fix export PDF file name if title is blank
2025-05-08 19:52:31 +02:00
PhilKes
cf7f6f9dda Fix export PDF file name if title is blank 2025-05-08 18:04:50 +02:00
PhilKes
01ac48f930 Bump versionCode 7402 2025-05-07 19:13:36 +02:00
Phil
015f43e94b
Merge pull request #580 from PhilKes/feat/hide-images-overview
Add setting to hide images in overview
2025-05-07 19:13:03 +02:00
PhilKes
830fb6a75c Update translations for new hide images option 2025-05-07 19:05:57 +02:00
PhilKes
c34ee3633e Add setting to hide images in overview 2025-05-07 19:05:57 +02:00
Phil
628bd9d564
Merge pull request #579 from PhilKes/fix/lock-widget
Fix showing widget when actually locked
2025-05-07 19:04:59 +02:00
PhilKes
3e889879fb Fix showing widget when actually locked 2025-05-07 17:52:41 +02:00
PhilKes
b191618a46 Bump versionCode 7401 2025-05-06 18:53:51 +02:00
Phil
5cbc62bdf7
Merge pull request #577 from PhilKes/translation/update
Update cs ru es fr zh-rCN strings.xml
2025-05-06 18:51:23 +02:00
PhilKes
d1e5770180 Update zh-rCN/strings.xml 2025-05-06 18:49:28 +02:00
PhilKes
29cee8faf4 Update fr/strings.xml 2025-05-06 18:48:01 +02:00
PhilKes
4f993af93f Update es/strings.xml 2025-05-06 18:47:34 +02:00
PhilKes
0f0eb80e9b Update ru/strings.xml 2025-05-06 18:46:18 +02:00
PhilKes
1314ab4437 Update cs/strings.xml 2025-05-06 18:45:45 +02:00
Phil
39022edfab
Merge pull request #576 from PhilKes/fix/checked-item-editable
Make checked ListItem non editable
2025-05-06 18:44:14 +02:00
PhilKes
157ecb1b13 Make checked ListItem non editable 2025-05-06 18:44:06 +02:00
Phil
fb35ffdac4
Merge pull request #573 from PhilKes/fix/biometric-lock-open-note
Fix/biometric lock open note
2025-05-06 18:41:46 +02:00
PhilKes
0fee25f022 Use androidx.biometric to fix compatibility issues 2025-05-06 18:41:35 +02:00
PhilKes
2341c30586 Fix hide on create Activity if unlocked 2025-05-06 18:41:35 +02:00
Phil
e553e78efb
Merge pull request #575 from PhilKes/feat/view-intent
Add intent-filter to open any text based file
2025-05-06 18:41:06 +02:00
PhilKes
724d08507a Add intent-filter to open any text based file 2025-05-06 18:40:58 +02:00
Phil
771546a0cb
Merge pull request #572 from PhilKes/fix/backup-logs-duplicates
Fix checking for existing backup logs file
2025-05-06 18:33:44 +02:00
PhilKes
1a6d4083e4 Fix checking for existing backup logs file 2025-05-06 18:33:37 +02:00
Phil
3ac63349d8
Merge pull request #571 from PhilKes/fix/search-actionmode
Update selected notes when search query is changed
2025-05-06 18:32:40 +02:00
PhilKes
06c48ab8d9 Update selected notes when search query is changed 2025-05-05 18:52:03 +02:00
PhilKes
8d20f26eae Add v7.4.0 changelogs 2025-04-18 16:22:17 +02:00
41 changed files with 5852 additions and 5437 deletions

View file

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

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

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

@ -70,7 +70,7 @@ class NotallyXApplication : Application(), Application.ActivityLifecycleCallback
)
}
if (oldTheme != null) {
WidgetProvider.updateWidgets(this)
WidgetProvider.updateWidgets(this, locked = locked.value)
}
}

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 != '[]'"

View file

@ -122,7 +122,7 @@ fun String.applySpans(representations: List<SpanRepresentation>): Editable {
->
try {
if (bold) {
editable.setSpan(StyleSpan(Typeface.BOLD), start, end)
editable.setSpan(createBoldSpan(), start, end)
}
if (italic) {
editable.setSpan(StyleSpan(Typeface.ITALIC), start, end)
@ -144,6 +144,13 @@ fun String.applySpans(representations: List<SpanRepresentation>): Editable {
return editable
}
fun createBoldSpan() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
StyleSpan(Typeface.BOLD, 700)
} else {
StyleSpan(Typeface.BOLD)
}
/**
* Adjusts or removes spans based on the selection range.
*

View file

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

View file

@ -237,6 +237,7 @@ abstract class NotallyFragment : Fragment(), ItemListener {
maxLines.value,
maxTitle.value,
labelTagsHiddenInOverview.value,
imagesHiddenInOverview.value,
),
model.imageRoot,
this@NotallyFragment,

View file

@ -6,6 +6,7 @@ import android.view.View
import androidx.core.os.BundleCompat
import androidx.core.view.isVisible
import com.philkes.notallyx.R
import com.philkes.notallyx.data.model.BaseNote
import com.philkes.notallyx.data.model.Folder
class SearchFragment : NotallyFragment() {
@ -44,6 +45,9 @@ class SearchFragment : NotallyFragment() {
isVisible = true
}
} else binding?.ChipGroup?.isVisible = false
getObservable().observe(viewLifecycleOwner) { items ->
model.actionMode.updateSelected(items?.filterIsInstance<BaseNote>()?.map { it.id })
}
}
override fun getBackground() = R.drawable.search

View file

@ -351,6 +351,17 @@ class SettingsFragment : Fragment() {
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)
}
}
}
}

View file

@ -188,14 +188,15 @@ abstract class EditActivity(private val type: Type) :
if (persistedId == null || notallyModel.originalNote == null) {
notallyModel.setState(id)
}
if (
notallyModel.isNewNote &&
intent.action in setOf(Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE)
) {
handleSharedNote()
} else if (notallyModel.isNewNote) {
intent.getStringExtra(EXTRA_DISPLAYED_LABEL)?.let {
notallyModel.setLabels(listOf(it))
if (notallyModel.isNewNote) {
when (intent.action) {
Intent.ACTION_SEND,
Intent.ACTION_SEND_MULTIPLE -> handleSharedNote()
Intent.ACTION_VIEW -> handleViewNote()
else ->
intent.getStringExtra(EXTRA_DISPLAYED_LABEL)?.let {
notallyModel.setLabels(listOf(it))
}
}
}
@ -632,6 +633,7 @@ abstract class EditActivity(private val type: Type) :
}
private fun convertTo(type: Type) {
updateModel()
lifecycleScope.launch {
notallyModel.convertTo(type)
val intent =
@ -756,6 +758,26 @@ abstract class EditActivity(private val type: Type) :
}
}
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)
override fun recordAudio() {
val permission = Manifest.permission.RECORD_AUDIO

View file

@ -40,6 +40,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
@ -212,7 +213,7 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
0,
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER,
) {
binding.EnterBody.applySpan(StyleSpan(Typeface.BOLD))
binding.EnterBody.applySpan(createBoldSpan())
mode?.finish()
}
add(

View file

@ -52,6 +52,7 @@ open class PickNoteActivity : LockedActivity<ActivityPickNoteBinding>(), ItemLis
maxLines.value,
maxTitle.value,
labelTagsHiddenInOverview.value,
imagesHiddenInOverview.value,
),
application.getExternalImagesDirectory(),
this@PickNoteActivity,

View file

@ -45,6 +45,7 @@ data class BaseNoteVHPreferences(
val maxLines: Int,
val maxTitleLines: Int,
val hideLabels: Boolean,
val hideImages: Boolean,
)
class BaseNoteVH(
@ -209,9 +210,8 @@ class BaseNoteVH(
}
private fun setImages(images: List<FileAttachment>, mediaRoot: File?) {
binding.apply {
if (images.isNotEmpty()) {
if (images.isNotEmpty() && !preferences.hideImages) {
ImageView.visibility = VISIBLE
Message.visibility = GONE

View file

@ -8,6 +8,7 @@ import android.text.style.TypefaceSpan
import android.text.style.URLSpan
import androidx.annotation.ColorInt
import com.philkes.notallyx.R
import com.philkes.notallyx.presentation.createBoldSpan
import com.philkes.notallyx.presentation.view.misc.StylableEditTextWithHistory
class TextFormattingAdapter(
@ -35,7 +36,7 @@ class TextFormattingAdapter(
private val bold: Toggle =
Toggle(R.string.bold, R.drawable.format_bold, false) {
if (!it.checked) {
editText.applySpan(StyleSpan(Typeface.BOLD))
editText.applySpan(createBoldSpan())
} else {
editText.clearFormatting(type = StylableEditTextWithHistory.TextStyleType.BOLD)
}

View file

@ -163,6 +163,7 @@ class ListItemVH(
binding.Content.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
}
setCanEdit(viewMode == NoteViewMode.EDIT)
isFocusable = !item.checked
when (viewMode) {
NoteViewMode.EDIT -> {
setOnClickListener(null)

View file

@ -5,7 +5,6 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
@ -336,7 +335,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
fun importZipBackup(uri: Uri, password: String) {
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
app.log(TAG, throwable = throwable)
app.showToast(R.string.invalid_backup)
app.showToast("${app.getString(R.string.invalid_backup)}: ${throwable.message}")
}
val backupDir = app.getBackupDir()
@ -348,7 +347,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
fun importXmlBackup(uri: Uri) {
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
app.log(TAG, throwable = throwable)
app.showToast(R.string.invalid_backup)
app.showToast("${app.getString(R.string.invalid_backup)}: ${throwable.message}")
}
viewModelScope.launch(exceptionHandler) {
@ -366,18 +365,15 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
fun importFromOtherApp(uri: Uri, importSource: ImportSource) {
val database = NotallyDatabase.getDatabase(app, observePreferences = false).value
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Toast.makeText(
app,
if (throwable is ImportException) {
throwable.textResId
} else R.string.invalid_backup,
Toast.LENGTH_LONG,
)
.show()
app.log(TAG, throwable = throwable)
if (throwable is ImportException) {
app.showToast(throwable.textResId)
} else {
app.showToast("${app.getString(R.string.invalid_backup)}: ${throwable.message}")
}
}
viewModelScope.launch(exceptionHandler) {
val importedNotes =
withContext(Dispatchers.IO) {

View file

@ -84,6 +84,13 @@ class NotallyXPreferences private constructor(private val context: Context) {
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",
@ -233,6 +240,7 @@ class NotallyXPreferences private constructor(private val context: Context) {
backupPassword,
backupOnSave,
autoSaveAfterIdleTime,
imagesHiddenInOverview,
)
.forEach { it.refresh() }
}

View file

@ -11,6 +11,7 @@ import android.net.Uri
import android.os.Build
import android.widget.RemoteViews
import androidx.core.content.ContextCompat
import com.philkes.notallyx.NotallyXApplication
import com.philkes.notallyx.R
import com.philkes.notallyx.data.NotallyDatabase
import com.philkes.notallyx.data.dao.BaseNoteDao
@ -42,9 +43,10 @@ class WidgetProvider : AppWidgetProvider() {
super.onReceive(context, intent)
when (intent.action) {
ACTION_NOTES_MODIFIED -> {
val app = context.applicationContext as NotallyXApplication
val noteIds = intent.getLongArrayExtra(EXTRA_MODIFIED_NOTES)
if (noteIds != null) {
updateWidgets(context, noteIds)
updateWidgets(context, noteIds, locked = app.locked.value)
}
}
ACTION_OPEN_NOTE -> openActivity(context, intent, EditNoteActivity::class.java)
@ -85,7 +87,8 @@ class WidgetProvider : AppWidgetProvider() {
baseNoteDao.updateChecked(noteId, childrenPositions + position, checked!!)
}
} finally {
updateWidgets(context, longArrayOf(noteId))
val app = context.applicationContext as NotallyXApplication
updateWidgets(context, longArrayOf(noteId), locked = app.locked.value)
pendingResult.finish()
}
}
@ -135,19 +138,19 @@ class WidgetProvider : AppWidgetProvider() {
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
val app = context.applicationContext as Application
val app = context.applicationContext as NotallyXApplication
val preferences = NotallyXPreferences.getInstance(app)
appWidgetIds.forEach { id ->
val noteId = preferences.getWidgetData(id)
val noteType = preferences.getWidgetNoteType(id) ?: return
updateWidget(app, appWidgetManager, id, noteId, noteType)
updateWidget(app, appWidgetManager, id, noteId, noteType, locked = app.locked.value)
}
}
companion object {
fun updateWidgets(context: Context, noteIds: LongArray? = null, locked: Boolean = false) {
fun updateWidgets(context: Context, noteIds: LongArray? = null, locked: Boolean) {
val app = context.applicationContext as Application
val preferences = NotallyXPreferences.getInstance(app)
@ -181,65 +184,90 @@ class WidgetProvider : AppWidgetProvider() {
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
intent.embedIntentExtras()
if (!locked) {
MainScope().launch {
val database = NotallyDatabase.getDatabase(context).value
MainScope().launch {
withContext(Dispatchers.IO) {
val color = database.getBaseNoteDao().getColorOfNote(noteId)
val preferences = NotallyXPreferences.getInstance(context)
val (backgroundColor, _) = context.extractWidgetColors(color, preferences)
val view =
RemoteViews(context.packageName, R.layout.widget).apply {
setRemoteAdapter(R.id.ListView, intent)
setEmptyView(R.id.ListView, R.id.Empty)
setOnClickPendingIntent(
R.id.Empty,
Intent(context, WidgetProvider::class.java)
.apply {
action = ACTION_SELECT_NOTE
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
}
.asPendingIntent(context),
)
setPendingIntentTemplate(
R.id.ListView,
Intent(context, WidgetProvider::class.java)
.asPendingIntent(context),
)
noteType?.let {
setOnClickPendingIntent(
R.id.Layout,
Intent(context, WidgetProvider::class.java)
.setOpenNoteIntent(noteType, noteId)
.asPendingIntent(context),
)
}
setInt(R.id.Layout, "setBackgroundColor", backgroundColor)
}
manager.updateAppWidget(id, view)
manager.notifyAppWidgetViewDataChanged(id, R.id.ListView)
}
}
} else {
val view =
RemoteViews(context.packageName, R.layout.widget_locked).apply {
noteType?.let {
val lockedPendingIntent =
context.getOpenNotePendingIntent(noteId, noteType)
setOnClickPendingIntent(R.id.Layout, lockedPendingIntent)
setOnClickPendingIntent(R.id.Text, lockedPendingIntent)
val color =
withContext(Dispatchers.IO) { database.getBaseNoteDao().getColorOfNote(noteId) }
if (color == null) {
val app = context.applicationContext as Application
val preferences = NotallyXPreferences.getInstance(app)
preferences.deleteWidget(id)
val view =
RemoteViews(context.packageName, R.layout.widget).apply {
setRemoteAdapter(R.id.ListView, intent)
setEmptyView(R.id.ListView, R.id.Empty)
setOnClickPendingIntent(
R.id.Empty,
Intent(context, WidgetProvider::class.java)
.apply {
action = ACTION_SELECT_NOTE
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
}
.asPendingIntent(context),
)
setPendingIntentTemplate(
R.id.ListView,
Intent(context, WidgetProvider::class.java).asPendingIntent(context),
)
}
setTextViewCompoundDrawablesRelative(
R.id.Text,
0,
R.drawable.lock_big,
0,
0,
)
}
manager.updateAppWidget(id, view)
manager.updateAppWidget(id, view)
manager.notifyAppWidgetViewDataChanged(id, R.id.ListView)
return@launch
}
if (!locked) {
val view =
RemoteViews(context.packageName, R.layout.widget).apply {
setRemoteAdapter(R.id.ListView, intent)
setEmptyView(R.id.ListView, R.id.Empty)
setOnClickPendingIntent(
R.id.Empty,
Intent(context, WidgetProvider::class.java)
.apply {
action = ACTION_SELECT_NOTE
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
}
.asPendingIntent(context),
)
setPendingIntentTemplate(
R.id.ListView,
Intent(context, WidgetProvider::class.java).asPendingIntent(context),
)
val preferences = NotallyXPreferences.getInstance(context)
val (backgroundColor, _) =
context.extractWidgetColors(color, preferences)
noteType?.let {
setOnClickPendingIntent(
R.id.Layout,
Intent(context, WidgetProvider::class.java)
.setOpenNoteIntent(noteType, noteId)
.asPendingIntent(context),
)
}
setInt(R.id.Layout, "setBackgroundColor", backgroundColor)
}
manager.updateAppWidget(id, view)
manager.notifyAppWidgetViewDataChanged(id, R.id.ListView)
} else {
val view =
RemoteViews(context.packageName, R.layout.widget_locked).apply {
noteType?.let {
val lockedPendingIntent =
context.getOpenNotePendingIntent(noteId, noteType)
setOnClickPendingIntent(R.id.Layout, lockedPendingIntent)
setOnClickPendingIntent(R.id.Text, lockedPendingIntent)
}
setTextViewCompoundDrawablesRelative(
R.id.Text,
0,
R.drawable.lock_big,
0,
0,
)
}
manager.updateAppWidget(id, view)
}
}
}

View file

@ -44,6 +44,13 @@ class ActionMode {
}
}
fun updateSelected(availableItemIds: List<Long>?) {
selectedNotes.keys
.filter { availableItemIds?.contains(it) == false }
.forEach { selectedNotes.remove(it) }
refresh()
}
fun isEnabled() = enabled.value
// We assume selectedNotes.size is 1

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,
0,
(noteId.toString() + reminderId.toString()).toInt(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
)

View file

@ -1,7 +1,6 @@
package com.philkes.notallyx.utils
import android.app.Activity
import android.app.KeyguardManager
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
@ -12,7 +11,6 @@ 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
@ -47,7 +45,6 @@ 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
@ -126,30 +123,15 @@ fun Context.getFileName(uri: Uri): String? =
}
fun Context.canAuthenticateWithBiometrics(): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val keyguardManager = ContextCompat.getSystemService(this, KeyguardManager::class.java)
val packageManager: PackageManager = this.packageManager
if (!packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
}
if (keyguardManager?.isKeyguardSecure == false) {
return BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
}
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
val biometricManager: BiometricManager =
this.getSystemService(BiometricManager::class.java)
return biometricManager.canAuthenticate()
val biometricManager = androidx.biometric.BiometricManager.from(this)
val authenticators =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG or
androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
} else {
val biometricManager: BiometricManager =
this.getSystemService(BiometricManager::class.java)
return biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
}
}
return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
return biometricManager.canAuthenticate(authenticators)
}
fun Context.getUriForFile(file: File): Uri =
@ -173,8 +155,7 @@ fun ContextWrapper.log(
fun ContextWrapper.getLastExceptionLog(): String? {
val logFile = getLogFile()
if (logFile.exists()) {
val logContents = logFile.readText().substringAfterLast("[Start]")
return URLEncoder.encode(logContents, StandardCharsets.UTF_8.toString())
return logFile.readText().substringAfterLast("[Start]")
}
return null
}
@ -204,10 +185,10 @@ fun Context.logToFile(
val logFile =
folder.findFile(fileName).let {
if (it == null || !it.exists()) {
folder.createFile("text/plain", fileName)
folder.createFile("text/plain", fileName.removeSuffix(".txt"))
} else if (it.isLargerThanKb(MAX_LOGS_FILE_SIZE_KB)) {
it.delete()
folder.createFile("text/plain", fileName)
folder.createFile("text/plain", fileName.removeSuffix(".txt"))
} else it
}

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 = "Log.v1.txt"
const val APP_LOG_FILE_NAME = "notallyx-logs.txt"
fun ContextWrapper.getLogFile(): File {
return File(getLogsDir(), APP_LOG_FILE_NAME)

View file

@ -89,7 +89,7 @@ import net.lingala.zip4j.model.enums.EncryptionMethod
private const val TAG = "ExportExtensions"
private const val NOTIFICATION_CHANNEL_ID = "AutoBackups"
private const val NOTIFICATION_ID = 123412
private const val NOTALLYX_BACKUP_LOGS_FILE = "notallyx-backup-logs"
private const val NOTALLYX_BACKUP_LOGS_FILE = "notallyx-backup-logs.txt"
private const val OUTPUT_DATA_BACKUP_URI = "backupUri"
const val AUTO_BACKUP_WORK_NAME = "com.philkes.notallyx.AutoBackupWork"
@ -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 {
NotallyDatabase.getDatabase(this, observePreferences = false).value.checkpoint()
val (_, file) = copyDatabase()
val files =
with(savedNote) {
images.map {
@ -192,10 +192,7 @@ fun ContextWrapper.autoBackupOnSave(backupPath: String, password: String, savedN
audios.map {
BackupFile(SUBFOLDER_AUDIOS, File(getExternalAudioDirectory(), it.name))
} +
BackupFile(
null,
NotallyDatabase.getCurrentDatabaseFile(this@autoBackupOnSave),
)
BackupFile(null, file)
}
try {
exportToZip(backupFile.uri, files, password)
@ -417,7 +414,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, decryptedFile, databaseFile)
decryptDatabase(this, passphrase, databaseFile, decryptedFile)
Pair(database, decryptedFile)
} else {
val dbFile = File(cacheDir, DATABASE_NAME)
@ -517,13 +514,14 @@ fun exportPdfFile(
total: Int? = null,
duplicateFileCount: Int = 1,
) {
val filePath = "$fileName.${ExportMimeType.PDF.fileExtension}"
val validFileName = fileName.ifBlank { app.getString(R.string.note) }
val filePath = "$validFileName.${ExportMimeType.PDF.fileExtension}"
if (folder.findFile(filePath)?.exists() == true) {
return exportPdfFile(
app,
note,
folder,
"${fileName.removeTrailingParentheses()} ($duplicateFileCount)",
"${validFileName.removeTrailingParentheses()} ($duplicateFileCount)",
pdfPrintListener,
progress,
counter,

View file

@ -28,6 +28,7 @@ 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
@ -44,6 +45,8 @@ import com.philkes.notallyx.utils.log
import com.philkes.notallyx.utils.mimeTypeToFileExtension
import com.philkes.notallyx.utils.rename
import com.philkes.notallyx.utils.scheduleNoteReminders
import com.philkes.notallyx.utils.security.SQLCipherUtils
import com.philkes.notallyx.utils.security.decryptDatabase
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
@ -86,12 +89,31 @@ suspend fun ContextWrapper.importZip(
NotallyDatabase.DATABASE_NAME,
)
var dbFile = File(databaseFolder, NotallyDatabase.DATABASE_NAME)
val state = SQLCipherUtils.getDatabaseState(dbFile)
if (state == SQLCipherUtils.State.ENCRYPTED) {
val fallbackEncryptionKey =
NotallyXPreferences.getInstance(this@importZip)
.fallbackDatabaseEncryptionKey
.value
if (fallbackEncryptionKey != null) {
val dbFileDecrypted =
File(databaseFolder, "${NotallyDatabase.DATABASE_NAME}-decrypted")
decryptDatabase(
this@importZip,
fallbackEncryptionKey,
dbFile,
dbFileDecrypted,
)
dbFile = dbFileDecrypted
} else {
throw IllegalArgumentException(
"Backup contains encrypted database and 'fallbackDatabaseEncryptionKey' has no value!"
)
}
}
val database =
SQLiteDatabase.openDatabase(
File(databaseFolder, NotallyDatabase.DATABASE_NAME).path,
null,
SQLiteDatabase.OPEN_READONLY,
)
SQLiteDatabase.openDatabase(dbFile.path, null, SQLiteDatabase.OPEN_READONLY)
val labelCursor = database.query("Label", null, null, null, null, null, null)
val baseNoteCursor = database.query("BaseNote", null, null, null, null, null, null)
@ -177,14 +199,11 @@ suspend fun ContextWrapper.importZip(
showToast(message)
} catch (e: ZipException) {
if (e.type == ZipException.Type.WRONG_PASSWORD) {
log(TAG, throwable = e)
showToast(R.string.wrong_password)
} else {
log(TAG, throwable = e)
showToast(R.string.invalid_backup)
throw e
}
} catch (e: Exception) {
showToast(R.string.invalid_backup)
log(TAG, throwable = e)
} finally {
importingBackup?.value = ImportProgress(inProgress = false)
}

View file

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

View file

@ -4,15 +4,13 @@ import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.hardware.biometrics.BiometricManager
import android.hardware.biometrics.BiometricPrompt
import android.hardware.fingerprint.FingerprintManager
import android.os.Build
import android.os.CancellationSignal
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.philkes.notallyx.R
import javax.crypto.Cipher
@ -27,7 +25,7 @@ fun Activity.showBiometricOrPinPrompt(
) {
showBiometricOrPinPrompt(
isForDecrypt,
this,
this as FragmentActivity,
activityResultLauncher,
titleResId,
descriptionResId,
@ -48,7 +46,7 @@ fun Fragment.showBiometricOrPinPrompt(
) {
showBiometricOrPinPrompt(
isForDecrypt,
requireContext(),
activity!!,
activityResultLauncher,
titleResId,
descriptionResId,
@ -60,7 +58,7 @@ fun Fragment.showBiometricOrPinPrompt(
private fun showBiometricOrPinPrompt(
isForDecrypt: Boolean,
context: Context,
context: FragmentActivity,
activityResultLauncher: ActivityResultLauncher<Intent>,
titleResId: Int,
descriptionResId: Int? = null,
@ -69,142 +67,55 @@ private fun showBiometricOrPinPrompt(
onFailure: (errorCode: Int?) -> Unit,
) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// Android 11+ with BiometricPrompt and Authenticators
val prompt =
BiometricPrompt.Builder(context)
.apply {
setTitle(context.getString(titleResId))
descriptionResId?.let {
setDescription(context.getString(descriptionResId))
}
setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
}
.build()
val cipher =
if (isForDecrypt) {
getInitializedCipherForDecryption(iv = cipherIv!!)
} else {
getInitializedCipherForEncryption()
}
prompt.authenticate(
BiometricPrompt.CryptoObject(cipher),
getCancellationSignal(context),
context.mainExecutor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult?
) {
super.onAuthenticationSucceeded(result)
onSuccess.invoke(result!!.cryptoObject!!.cipher)
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure.invoke(null)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
super.onAuthenticationError(errorCode, errString)
onFailure.invoke(errorCode)
}
},
)
}
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> {
// Android 10: Use BiometricPrompt without Authenticators
val prompt =
BiometricPrompt.Builder(context)
.apply {
setTitle(context.getString(titleResId))
descriptionResId?.let {
setDescription(context.getString(descriptionResId))
}
setNegativeButton(
context.getString(R.string.cancel),
context.mainExecutor,
) { _, _ ->
onFailure.invoke(null)
}
}
.build()
val cipher =
if (isForDecrypt) {
getInitializedCipherForDecryption(iv = cipherIv!!)
} else {
getInitializedCipherForEncryption()
}
prompt.authenticate(
BiometricPrompt.CryptoObject(cipher),
getCancellationSignal(context),
context.mainExecutor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult?
) {
super.onAuthenticationSucceeded(result)
onSuccess.invoke(result!!.cryptoObject!!.cipher)
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure.invoke(null)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
super.onAuthenticationError(errorCode, errString)
onFailure.invoke(errorCode)
}
},
)
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
val fingerprintManager =
ContextCompat.getSystemService(context, FingerprintManager::class.java)
if (
fingerprintManager?.isHardwareDetected == true &&
fingerprintManager.hasEnrolledFingerprints()
) {
val cipher =
if (isForDecrypt) {
getInitializedCipherForDecryption(iv = cipherIv!!)
} else {
getInitializedCipherForEncryption()
val promptInfo =
BiometricPrompt.PromptInfo.Builder()
.apply {
setTitle(context.getString(titleResId))
descriptionResId?.let {
setDescription(context.getString(descriptionResId))
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
} else {
setNegativeButtonText(context.getString(R.string.cancel))
setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG
)
}
}
.build()
val cipher =
if (isForDecrypt) {
getInitializedCipherForDecryption(iv = cipherIv!!)
} else {
getInitializedCipherForEncryption()
}
val authCallback =
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
onSuccess.invoke(result.cryptoObject!!.cipher!!)
}
fingerprintManager.authenticate(
FingerprintManager.CryptoObject(cipher),
getCancellationSignal(context),
0,
object : FingerprintManager.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: FingerprintManager.AuthenticationResult?
) {
super.onAuthenticationSucceeded(result)
onSuccess.invoke(result!!.cryptoObject!!.cipher)
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure.invoke(null)
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure.invoke(null)
}
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence?,
) {
super.onAuthenticationError(errorCode, errString)
onFailure.invoke(errorCode)
}
},
null,
)
} else {
promptPinAuthentication(context, activityResultLauncher, titleResId, onFailure)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onFailure.invoke(errorCode)
}
}
val prompt =
BiometricPrompt(context, ContextCompat.getMainExecutor(context), authCallback)
prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
else -> {
@ -214,14 +125,6 @@ private fun showBiometricOrPinPrompt(
}
}
private fun getCancellationSignal(context: Context): CancellationSignal {
return CancellationSignal().apply {
setOnCancelListener {
Toast.makeText(context, R.string.biometrics_failure, Toast.LENGTH_SHORT).show()
}
}
}
private fun promptPinAuthentication(
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,

View file

@ -88,6 +88,10 @@
android:id="@+id/LabelsHiddenInOverview"
layout="@layout/preference" />
<include
android:id="@+id/ImagesHiddenInOverview"
layout="@layout/preference" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"

View file

@ -123,6 +123,7 @@
<string name="disable_lock_description">Toto rovněž dešifruje databázi</string>
<string name="disable_lock_title">Vypnout zámek pomocí biometrických údajů/PIN</string>
<string name="disabled">Zakázat</string>
<string name="disallow_screenshots">Zakázat pořizování snímků obrazovky</string>
<string name="discard">Zahodit</string>
<string name="display_text">Text k zobrazení</string>
<string name="donate">Podpořit</string>
@ -162,6 +163,8 @@
<string name="help">Nápověda</string>
<string name="hours">Hodiny</string>
<string name="image_format_not_supported">Formát obrázku není podporován</string>
<string name="images_hidden_in_overview">Pokud tuto funkci povolíte, obrázky poznámek budou v přehledu skryty.</string>
<string name="images_hidden_in_overview_title">Skrýt obrázky v přehledu</string>
<string name="import_action">Importovat</string>
<string name="import_backup">Importovat zálohu</string>
<string name="import_backup_password_hint">Pokud vaše záloha není chráněna heslem, jednoduše stiskněte tlačítko Importovat, jinak zadejte správné heslo.</string>

View file

@ -159,6 +159,8 @@
<string name="help">Hilfe</string>
<string name="hours">Stunden</string>
<string name="image_format_not_supported">Bildformat nicht unterstützt</string>
<string name="images_hidden_in_overview">Ist dies aktiviert, werden die Bilder der Notizen in der Übersicht ausgeblendet</string>
<string name="images_hidden_in_overview_title">Verberge Bilder in Übersicht</string>
<string name="import_action">Import</string>
<string name="import_backup">Backup importieren</string>
<string name="import_backup_password_hint">Falls das Backup nicht passwortgeschützt ist, drücken Sie einfach auf Importieren, andernfalls geben Sie das richtige Passwort ein.</string>

View file

@ -119,6 +119,7 @@
<string name="disable_lock_description">Esto también descifrará la base de datos</string>
<string name="disable_lock_title">Deshabilitar bloqueo biométrico/PIN</string>
<string name="disabled">Deshabilitado</string>
<string name="disallow_screenshots">No permitir capturas de pantalla</string>
<string name="discard">Descartar</string>
<string name="display_text">Texto a pantalla</string>
<string name="donate">Hacer donación</string>
@ -158,6 +159,8 @@
<string name="help">Ayuda</string>
<string name="hours">Horas</string>
<string name="image_format_not_supported">Formato de imagen no soportado</string>
<string name="images_hidden_in_overview">Al habilitar esta opción, se ocultarán las imágenes de notas de la descripción general.</string>
<string name="images_hidden_in_overview_title">Ocultar imágenes en vista general</string>
<string name="import_action">Importar</string>
<string name="import_backup">Importar copia de seguridad</string>
<string name="import_backup_password_hint">Si su copia de seguridad no está protegida con contraseña, simplemente presione Importar, de lo contrario ingrese la contraseña correcta.</string>

View file

@ -119,6 +119,7 @@
<string name="disable_lock_description">La base de donnée sera aussi décryptée</string>
<string name="disable_lock_title">Désactiver le verrouillage biométrique/code PIN</string>
<string name="disabled">Désactivé</string>
<string name="disallow_screenshots">Bloquer les captures décran</string>
<string name="discard">Supprimer</string>
<string name="display_text">Texte à afficher</string>
<string name="donate">Faire un don</string>
@ -158,6 +159,8 @@
<string name="help">Aide</string>
<string name="hours">Heures</string>
<string name="image_format_not_supported">Format d\'image non supporté</string>
<string name="images_hidden_in_overview">En activant cette option, les images des notes seront masquées dans l\'aperçu</string>
<string name="images_hidden_in_overview_title">Masquer les images dans la vue d\'ensemble</string>
<string name="import_action">Importer</string>
<string name="import_backup">Importer une sauvegarde</string>
<string name="import_backup_password_hint">Si votre sauvegarde n\'est pas protégée par mot de passe, cliquez seulement sur \"Importer une sauvegarde\", sinon entrez le mot de passe correspondant.</string>

View file

@ -153,6 +153,8 @@
<string name="help">Aiuto</string>
<string name="hours">Ore</string>
<string name="image_format_not_supported">Formato immagine non supportato</string>
<string name="images_hidden_in_overview">Abilitando questa opzione le immagini delle note verranno nascoste nella panoramica.</string>
<string name="images_hidden_in_overview_title">Nascondi immagini nella panoramica</string>
<string name="import_action">Importa</string>
<string name="import_backup">Importa backup</string>
<string name="import_backup_password_hint">Se il tuo backup non è protetto da password premi semplicemente Importa, altrimenti inserisci la password corretta.</string>

View file

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

View file

@ -1,77 +1,334 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about">О приложении</string>
<string name="add_images">Добавить изображение</string>
<string name="add_item">Добавить пункт</string>
<string name="add_label">Добавить метку</string>
<string name="add_reminder">Добавить напоминание</string>
<string name="adding_files">Добавление файлов</string>
<string name="adding_images">Добавление изображений</string>
<string name="all">Все</string>
<string name="appearance">Внешний вид</string>
<string name="archive">Архивировать</string>
<string name="archived">Архив</string>
<plurals name="archived_selected_notes">
<item quantity="few">Архивировано %1$d заметки</item>
<item quantity="one">Архивирована %1$d заметка</item>
<item quantity="other">Архивировано %1$d заметок</item>
</plurals>
<string name="ascending">По возрастанию</string>
<string name="attach_file">Прикрепить файл</string>
<string name="audio_recordings">Аудиозаписи</string>
<string name="auto_backup">Автобэкапы</string>
<string name="auto_backup_error_message">Ошибка при авто-резервной копии: \n\'%1$s\'\nПроверьте настройки или сообщите об ошибке</string>
<string name="auto_backup_failed">Сбой авто-резервирования NotallyX</string>
<string name="auto_backup_last">Последняя резервная копия</string>
<string name="auto_backup_on_save">Создавать резервную копию при выходе (авто)</string>
<string name="auto_backup_on_save_hint">При включении этой функции в папке резервных копий (Папка резервных копий) автоматически создаётся архив NotallyX_AutoBackup.zip при выходе из заметки.\nЭто может повлиять на производительность</string>
<string name="auto_backups_folder">Папка резервных копий</string>
<string name="auto_backups_folder_hint">Папка, в которой будут храниться резервные копии</string>
<string name="auto_backups_folder_rechoose">Необходимо повторно указать папку для резервных копий для доступа NotallyX.\nМожно отменить и пропустить импорт папки резервных копий</string>
<string name="auto_backups_folder_set">Сначала укажите папку для резервных копий</string>
<string name="auto_save_after_idle_time">Автосохранение заметки после указанного времени бездействия</string>
<string name="auto_sort_by_checked">Сортировать отмеченные элементы в конец</string>
<string name="back">Назад</string>
<string name="backup">Резервная копия</string>
<string name="backup_password">Пароль для резервной копии</string>
<string name="backup_password_hint">При включении этой функции все новые ZIP-архивы резервных копий буду зашифрованы и защищены паролем</string>
<string name="backup_period_days">Интервал авторезервирования (в днях)</string>
<string name="backup_periodic">Переодические резервные копии</string>
<string name="backup_periodic_hint">При включении этой функции резервные копии будут автоматически сохраняться в указанную папку.\nФункция может не работать при включенном режиме энергосбережения</string>
<string name="behaviour">Биометрия</string>
<string name="biometric_lock">Блокировать приложение с помощью биометрии или PIN-кода</string>
<string name="biometrics_disable_success">Биометрическая защита/PIN-код отключены</string>
<string name="biometrics_failure">Ошибка аутентификации по Биометрии/PIN-коду</string>
<string name="biometrics_no_support">На устройстве нет биометрических функций</string>
<string name="biometrics_not_setup">Биометрия/PIN-код ещё не настроены</string>
<string name="biometrics_setup_success">Защита по Биометрии/PIN-коду включена</string>
<string name="bold">Жирный</string>
<string name="calculating">Вычисление…</string>
<string name="cancel">Отменить</string>
<plurals name="cant_add_files">
<item quantity="few">Невозможно добавить %1$d файла</item>
<item quantity="one">Невозможно добавить %1$d файл</item>
<item quantity="other">Невозможно добавить %1$d файлов</item>
</plurals>
<plurals name="cant_add_images">
<item quantity="few">Невозможно добавить %1$d изображения</item>
<item quantity="one">Невозможно добавить %1$d изображение</item>
<item quantity="other">Невозможно добавить %1$d изображений</item>
</plurals>
<string name="cant_find_folder">Папка не найдена. Возможно, она была перемещена или удалена</string>
<string name="cant_find_note">Заметка не найдена. Возможно она была удалена</string>
<string name="cant_load_file">Файл не загружен. Возможно, он был перемещён или удалён</string>
<string name="cant_load_image">Изображение не загружено. Проверьте, не было ли оно удалено</string>
<string name="cant_open_link">Невозможно открыть ссылку</string>
<string name="change_color">Изменить цвет</string>
<string name="change_color_message">Выберите цвет или создайте новый.\nЦвет можно изменить долгим нажатием.</string>
<string name="change_note">Изменить заметку</string>
<string name="check_all_items">Выделить всё</string>
<string name="choose_another_folder">Выбрать другую папку</string>
<string name="choose_folder">Выбрать папку</string>
<string name="choose_other_app">Выберите приложение для импорта</string>
<string name="clear">Очистить</string>
<string name="clear_data">Очистить данные</string>
<string name="clear_data_message">Все Заметки, Изображения, Файлы и Аудиозаписи будут навсегда удалены</string>
<string name="clear_formatting">Стандартный стиль</string>
<string name="cleared_data">Все данные удалены</string>
<string name="color">Цвет</string>
<string name="color_exists">Этот цвет уже существует!</string>
<string name="content_density">Плотность контента</string>
<string name="continue_">Продолжить</string>
<string name="convert_to_list_note">Конвертировать в Список</string>
<string name="convert_to_text_note">Конвертировать в Заметку</string>
<string name="copied_link">Ссылка скопирована в буфер обмена</string>
<string name="copy">Копировать</string>
<string name="crash_message">Произошла непредвиденная ошибка.\nПриносим извинения за причиненные неудобства.</string>
<string name="create_github_issue">Сообщить об ошибке на GitHub</string>
<string name="creation_date">Создано</string>
<string name="custom">Пользовтаельский</string>
<string name="daily">Дни</string>
<string name="dark">Тёмная</string>
<string name="data_in_public">Сохранять данные в общедоступной папке</string>
<string name="data_in_public_message">При включении этой функции внутренняя база данных приложения будет перенесена в его общедоступную папку (Android/media/com.philkes.notallyx).\nЭто позволит синхронизировать данные NotallyX между устройствами с помощью приложений для синхронизации файлов.</string>
<string name="date">Дата</string>
<string name="date_format">Формат даты</string>
<string name="date_format_apply_in_note_view">Применять и в просмотре заметки</string>
<string name="date_format_hint">Применяет выбранный формат даты в общем списке заметок</string>
<string name="days">Дней</string>
<string name="delete">Удалить</string>
<string name="delete_all">Удалить всё</string>
<string name="delete_all_notes">Удалить все заметки\?</string>
<string name="delete_audio_recording_forever">Удалить все аудиозаписи\?</string>
<string name="delete_checked_items">Удалить выбранные элементы</string>
<string name="delete_color_message">Каким цветом заменить текущий цвет заметок\?</string>
<string name="delete_file">Удалить файл \'%1$s\'\?</string>
<string name="delete_forever">Удалить навсегда</string>
<string name="delete_image_forever">Удалить изображение навсегда\?</string>
<string name="delete_label">Удалить метку\?</string>
<string name="delete_note_forever">Удалить заметку навсегда\?</string>
<string name="delete_reminder">Удалить напоминание\?</string>
<string name="delete_selected_notes">Удалить выбранные заметки\?</string>
<string name="deleted">Удалённые</string>
<plurals name="deleted_selected_notes">
<item quantity="few">Удалено %1$d заметки</item>
<item quantity="one">Удалена %1$d заметка</item>
<item quantity="other">Удалено %1$d заметок</item>
</plurals>
<string name="deleting_files">Удаление файлов</string>
<string name="deleting_images">Удаление изображений</string>
<string name="descending">По убыванию</string>
<string name="disable">Отключить</string>
<string name="disable_data_in_public">Переместить данные во внутреннюю папку</string>
<string name="disable_lock_description">Это также расшифрует базу данных</string>
<string name="disable_lock_title">Отключить защиту по Биометрии/PIN-коду</string>
<string name="disabled">Отключено</string>
<string name="disallow_screenshots">Запретить создание скриншотов</string>
<string name="discard">Отменить</string>
<string name="display_text">Текст для отображения</string>
<string name="donate">Сделать пожертвование</string>
<string name="drag_handle">Перетащить</string>
<string name="edit">Редактировать</string>
<string name="edit_color">Изменить цвет</string>
<string name="edit_label">Изменить метку</string>
<string name="empty_labels">Еще нет меток, создать\?</string>
<string name="edit_link">Изменить ссылку</string>
<string name="edit_reminders">Изменить напоминания</string>
<string name="elapsed">Просроченные</string>
<string name="empty_labels">Еще нет меток. Создать\?</string>
<string name="empty_list">Пустой список</string>
<string name="empty_note">Пустая заметка</string>
<string name="export">Экспортировать</string>
<string name="export_backup">Экспортировать</string>
<string name="empty_reminders">Напоминаний нет. Создать\?</string>
<string name="enable_lock_description">Это также зашифрует базу данных</string>
<string name="enable_lock_title">Включить защиту по Биометрии/PIN-коду</string>
<string name="enabled">Включено</string>
<string name="error_while_renaming_file">Ошибка переименования файла</string>
<string name="error_while_renaming_image">Ошибка переименования изображения</string>
<string name="evernote">Evernote</string>
<string name="evernote_help">Чтобы импортировать заметки из Evernote, экспортируйте блокнот в формате ENEX. Нажмите \"Помощь\" для инструкций.\n\nЕсли файл ENEX уже готов, выберите \"Импорт\"</string>
<string name="every">Каждый</string>
<string name="export">Экспорт</string>
<string name="export_backup">Экспорт резервной копии</string>
<string name="export_settings">Экспорт настроек</string>
<string name="export_settings_failure">Не удалось экспортировать настройки. Возможно, указан недопустимый путь</string>
<string name="export_settings_message">Все настройки будут сохранены в JSON-файл для последующего импорта.\n\nОбратите внимание: зашифрованные данные (пароль авторезерва и биометрический ключ) не экспортируются.</string>
<string name="export_settings_success">Настройки успешно экспортированы</string>
<string name="exporting_backup">Экспорт резервной копии</string>
<string name="extracted_files">Файлы извлечены</string>
<string name="filter">Фильтры</string>
<string name="folder">Папка</string>
<string name="follow_system">Как в системе</string>
<string name="google_keep">Google Keep</string>
<string name="google_keep_help">Для импорта из Google Keep скачайте архив Takeout (только данные \"Keep\"). Нажмите \"Помощь\" для подробностей.\n\nЕсли архив уже есть, выберите его через \"Импорт\"</string>
<string name="grid">Сетка</string>
<string name="import_backup">Импортировать</string>
<string name="help">Помощь</string>
<string name="hours">Часов</string>
<string name="image_format_not_supported">Формат изображения не поддерживыется</string>
<string name="images_hidden_in_overview">Если эта функция включена, изображения заметок будут скрыты из общего списка.</string>
<string name="images_hidden_in_overview_title">Скрыть изображения в общем списке</string>
<string name="import_action">Импорт</string>
<string name="import_backup">Импорт резервной копии</string>
<string name="import_backup_password_hint">Если резервная копия не защищена паролем, нажмите \"Импорт\". В противном случае введите пароль.</string>
<string name="import_other">Импорт заметок из других приложений</string>
<string name="import_settings">Импорт настроек</string>
<string name="import_settings_failure">Не удалось импортировать настройки. Вы выбрали правильный файл\?</string>
<string name="import_settings_message">Для импорта настроек выберите корректный JSON-файл настроек NotallyX. </string>
<string name="import_settings_success">Настройки успешно импортированы</string>
<string name="imported_files">Файлы импортированы</string>
<string name="imported_notes">Заметки импортированы</string>
<plurals name="imported_notes">
<item quantity="few">Импортировано %1$s заметки</item>
<item quantity="one">Импортирована %1$s заметка</item>
<item quantity="other">Импортировано %1$s заметок</item>
</plurals>
<string name="importing_backup">Импорт резервной копии</string>
<string name="insert_an_sd_card_audio">Вставьте SD-карту для записи аудио</string>
<string name="insert_an_sd_card_files">Для добавления файлов вставьте SD-карту</string>
<string name="insert_an_sd_card_images">Вставьте SD-карту, чтобы загрузить изображение</string>
<string name="install_a_browser">Чтобы открыть ссылку, установите браузер</string>
<string name="install_an_email">Установите клиент почты для отправки отзыва</string>
<string name="invalid_backup">Некорректная резервная копия</string>
<string name="invalid_evernote">Недопустимый файл Evernote (ENEX)</string>
<string name="invalid_google_keep">Некорректный ZIP-архив Google Takeout</string>
<string name="invalid_image">Изображение повреждено</string>
<string name="invalid_link">Скопируйте рабочую ссылку в буфер обмена</string>
<string name="italic">Курсив</string>
<string name="item">Пункт</string>
<string name="json_files">JSON файлы</string>
<string name="json_files_help">Для импорта заметок из JSON-файлов (отдельный файл или папка) нажмите \"Импорт\". Каждый корректный JSON-файл будет преобразован в отдельную заметку, где имя файла станет её заголовком</string>
<string name="label_exists">Метка уже существует</string>
<string name="label_visibility">Скрыть/показать метку в панели навигации</string>
<string name="labels">Метки</string>
<string name="labels_hidden_in_overview">При включении этой функции метки заметок будут скрыты в общем списке</string>
<string name="labels_hidden_in_overview_title">Скрывать метки в общем списке</string>
<string name="large">Большой</string>
<string name="libraries">Библиотеки</string>
<string name="light">Светлая</string>
<string name="link">Ссылка</string>
<string name="link_note">Связать заметку</string>
<string name="list">Список</string>
<string name="list_item_auto_sort">Сортировать элементы списка</string>
<string name="locked">Заблокировано</string>
<string name="make_feature_request">Запросить новую функцию</string>
<string name="make_list">Создать список</string>
<string name="max_backups">Лимит резервных копий</string>
<string name="max_items_to_display">Максимум отображаемых пунктов в списке</string>
<string name="max_labels_to_display">Максимум отображаемых меток</string>
<string name="max_lines_to_display">Максимум отображаемых строк в заметке</string>
<string name="max_lines_to_display_title">Максимум отображаемых строк в заголовке</string>
<string name="medium">Средний</string>
<string name="minutes">Минуты</string>
<string name="modified_date">Изменено</string>
<string name="monospace">Моноширинный</string>
<string name="monthly">Ежемесячно</string>
<string name="months">Месяцы</string>
<string name="more">Ещё %1$d</string>
<plurals name="more_files">
<item quantity="few">...ещё %1$d файла</item>
<item quantity="one">...ещё %1$d файл</item>
<item quantity="other">...ещё %1$d файлов</item>
</plurals>
<string name="new_color">Новый цвет</string>
<string name="next">Следующий</string>
<string name="no_auto_sort">Без автосортировки</string>
<string name="none">Отсутствует</string>
<string name="note">Заметка</string>
<string name="notes">Заметки</string>
<string name="notes_sorted_by">Сортировать заметки по</string>
<string name="open_link">Открыть ссылку</string>
<string name="open_note">Открыть заметку</string>
<string name="others">Другие</string>
<string name="pause">Пауза</string>
<string name="paused">Приостановлено</string>
<string name="pin">Закрепить</string>
<string name="pinned">Закреплённые</string>
<string name="plain_text_files">Текстовые файлы</string>
<string name="plain_text_files_help">Для импорта заметок из текстовых файлов (отдельный файл или папка) нажмите \"Импорт\". Каждый файл станет отдельной заметкой — имя файла будет её заголовком. Если текст начинается с элементов списка (например, Markdown \"- [x]\", синтаксис NotallyX \"[~/\", или \"*\", ~), он преобразуется в заметку-список.</string>
<string name="play">Играть</string>
<string name="please_grant_notally_alarm">Разрешите NotallyX отправлять напоминания</string>
<string name="please_grant_notally_audio">Разрешите доступ к микрофону.\nЗаписи остаются на вашем устройстве</string>
<string name="please_grant_notally_notification">Разрешите отправку уведомлений</string>
<string name="please_grant_notally_notification_auto_backup">При сбое авторезервирования вы получите уведомление, если разрешите их отправку. </string>
<string name="previous">Предыдущий</string>
<string name="rate">Оценить приложение</string>
<string name="read_only">Только чтение</string>
<string name="ready_to_record">Готово к записи</string>
<string name="record_audio">Запись аудио</string>
<string name="recording">Запись…</string>
<string name="redo">Вернуть</string>
<string name="reminder_no_repetition">Без повтора</string>
<string name="reminders">Напоминания</string>
<string name="remove_link">Удалить ссылку</string>
<string name="repetition">Повтор</string>
<string name="repetition_custom">Свой вариант повтора</string>
<string name="repetition_value_hint">Значение</string>
<string name="report_bug">Сообщить об ошибке/баге</string>
<string name="report_crash">Отправить отчёт об ошибке</string>
<string name="reset_settings">Сбросить настройки</string>
<string name="reset_settings_message">Все настройки будут сброшены к значениям по умолчанию</string>
<string name="reset_settings_success">Все настройки успешно сброшены</string>
<string name="restart_app">Перезапустить приложение</string>
<string name="restore">Восстановить</string>
<plurals name="restored_selected_notes">
<item quantity="few">Восстановлено %1$d заметки</item>
<item quantity="one">Восстановлена %1$d заметка</item>
<item quantity="other">Восстановлено %1$d заметок</item>
</plurals>
<string name="resume">Повторить</string>
<string name="save">Сохранить</string>
<string name="save_recording">Сохранить аудиозапись\?</string>
<string name="save_to_device">Сохранить на устройстве</string>
<string name="saved_to_device">Сохранено на устройстве</string>
<string name="saved_to_notally">Сохранено в NotallyX</string>
<string name="search">Поиск</string>
<string name="security">Безопасность</string>
<string name="select_all">Выбрать всё</string>
<string name="select_labels">Выбрать метки</string>
<string name="select_note">Выбрать заметки</string>
<string name="send_feedback">Сообщить о проблеме</string>
<string name="settings">Настройки</string>
<string name="share">Поделиться</string>
<string name="skip">Пропустить</string>
<string name="small">Маленький</string>
<string name="something_went_wrong">Что-то пошло не так. Пожалуйста, повторите попытку</string>
<string name="sort_direction">Порядок сортировки</string>
<string name="source_code">Исходный код</string>
<string name="start">Старт</string>
<string name="start_view">Стартовый экран</string>
<string name="start_view_hint">Выберите, какой экран/ярлык показывать при запуске.\nПо умолчанию — основной список заметок</string>
<string name="stop">Стоп</string>
<string name="strikethrough">Перечёркнутый</string>
<string name="take_note">Создать заметку</string>
<string name="tap_for_more_options">Нажмите для подробностей</string>
<string name="tap_to_set_up">Нажмите для настройки</string>
<string name="text_default">По умолчанию</string>
<string name="text_size">Размер текста</string>
<string name="theme">Тема</string>
<string name="theme_use_dynamic_colors">Использовать цвета обоев</string>
<string name="title">Заголовок</string>
<string name="to_record_audio">Что-бы записывать аудиозаписи, разрешите NotallyX доступ к микрофону</string>
<string name="unarchive">Разархивировать</string>
<plurals name="unarchived_selected_notes">
<item quantity="few">Извлечено %1$d заметки</item>
<item quantity="one">Извлечено %1$d заметка</item>
<item quantity="other">Извлечено %1$d заметок</item>
</plurals>
<string name="uncheck_all_items">Очистить выбор</string>
<string name="undo">Вернуть</string>
<string name="unknown_error">Неизвестная ошибка</string>
<string name="unknown_name">Неизвестное имя</string>
<string name="unlabeled">Без названия</string>
<string name="unlock">Разблокировка по Биометрии/PIN-коду</string>
<string name="unlock_with_biometrics_not_setup">Ранее вы включили биометрическую защиту, но сейчас на устройстве не настроены биометрия или PIN-код.\n\nНажмите «Отключить», чтобы снять блокировку, или настройте биометрию/PIN-код заново</string>
<string name="unpin">Открепить</string>
<string name="upcoming">Предстоящие</string>
<string name="updated_link">Обновить ссылку</string>
<string name="view">Вид</string>
<string name="view_file">Посмотреть файл</string>
<string name="view_note">Посмотреть заметку…</string>
<string name="weekly">Еженедельный</string>
<string name="weeks">Недели</string>
<string name="wrong_password">Неверный пароль</string>
<string name="yearly">Ежегодный</string>
<string name="years">Года</string>
<string name="your_notes_associated">Ваши заметки, связанные с этой меткой, не будут удалены</string>
</resources>

View file

@ -118,6 +118,7 @@
<string name="disable_lock_description">这也会解密数据据</string>
<string name="disable_lock_title">停用生物特征/PIN锁</string>
<string name="disabled">禁用</string>
<string name="disallow_screenshots">禁止截屏</string>
<string name="discard">取消</string>
<string name="display_text">要展示的我呢本</string>
<string name="donate">捐赠</string>
@ -157,6 +158,8 @@
<string name="help">帮助</string>
<string name="hours">小时</string>
<string name="image_format_not_supported">不支持该图片格式</string>
<string name="images_hidden_in_overview">如果启用此选项,则注释的图像将不会显示在概览中。</string>
<string name="images_hidden_in_overview_title">在概览中隐藏图片</string>
<string name="import_action">导入</string>
<string name="import_backup">导入备份</string>
<string name="import_backup_password_hint">如果你的备份文件没有密码保护,只需按下“导入”即可。如有,起输入正确的密码</string>

View file

@ -134,6 +134,8 @@
<string name="help">幫助</string>
<string name="hours">小時</string>
<string name="image_format_not_supported">不支持的圖片格式</string>
<string name="images_hidden_in_overview">啟用此功能後,筆記的圖像將隱藏在概覽中。</string>
<string name="images_hidden_in_overview_title">在概覽中隱藏圖片</string>
<string name="import_action">匯入</string>
<string name="import_backup">匯入備份</string>
<string name="import_backup_password_hint">如果您的備份沒有密碼保護,只需按匯入,否則請輸入正確的密碼。</string>

View file

@ -161,6 +161,8 @@
<string name="help">Help</string>
<string name="hours">Hours</string>
<string name="image_format_not_supported">Image format not supported</string>
<string name="images_hidden_in_overview">By enabling this, the notes images will be hidden in the overview</string>
<string name="images_hidden_in_overview_title">Hide Images in Overview</string>
<string name="import_action">Import</string>
<string name="import_backup">Import backup</string>
<string name="import_backup_password_hint">If your backup is not password-protected simply press Import, otherwise enter the correct password.</string>

Binary file not shown.

View file

@ -20,6 +20,6 @@ org.gradle.jvmargs=-Xmx1536m -XX:+UseParallelGC
org.gradle.parallel=true
android.experimental.enableNewResourceShrinker.preciseShrinking=true
android.defaults.buildfeatures.buildconfig=true
app.lastVersionName=7.3.1
app.versionCode=7400
app.versionName=7.4.0
app.lastVersionName=7.4.0
app.versionCode=7410
app.versionName=7.4.1