mirror of
https://github.com/PhilKes/NotallyX.git
synced 2025-06-29 20:59:54 +00:00
Compare commits
42 commits
Author | SHA1 | Date | |
---|---|---|---|
|
d00300fa0e | ||
|
f13e8227ca | ||
|
11e472cd30 | ||
|
4cc957ccd4 | ||
|
86b74762c5 | ||
|
118285545a | ||
|
402baf8056 | ||
|
de27d40880 | ||
|
66ce623e85 | ||
|
3c2400c7e6 | ||
|
c64a7b2ed7 | ||
|
fb687856f1 | ||
|
23d678c8a3 | ||
|
ade08b52ed | ||
|
62a35132e0 | ||
|
9fbe5a6b94 | ||
|
cf7f6f9dda | ||
|
01ac48f930 | ||
|
015f43e94b | ||
|
830fb6a75c | ||
|
c34ee3633e | ||
|
628bd9d564 | ||
|
3e889879fb | ||
|
b191618a46 | ||
|
5cbc62bdf7 | ||
|
d1e5770180 | ||
|
29cee8faf4 | ||
|
4f993af93f | ||
|
0f0eb80e9b | ||
|
1314ab4437 | ||
|
39022edfab | ||
|
157ecb1b13 | ||
|
fb35ffdac4 | ||
|
0fee25f022 | ||
|
2341c30586 | ||
|
e553e78efb | ||
|
724d08507a | ||
|
771546a0cb | ||
|
1a6d4083e4 | ||
|
3ac63349d8 | ||
|
06c48ab8d9 | ||
|
8d20f26eae |
41 changed files with 5852 additions and 5437 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -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)
|
||||
|
|
|
@ -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
|
@ -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" />
|
||||
|
|
|
@ -70,7 +70,7 @@ class NotallyXApplication : Application(), Application.ActivityLifecycleCallback
|
|||
)
|
||||
}
|
||||
if (oldTheme != null) {
|
||||
WidgetProvider.updateWidgets(this)
|
||||
WidgetProvider.updateWidgets(this, locked = locked.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 != '[]'"
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -237,6 +237,7 @@ abstract class NotallyFragment : Fragment(), ItemListener {
|
|||
maxLines.value,
|
||||
maxTitle.value,
|
||||
labelTagsHiddenInOverview.value,
|
||||
imagesHiddenInOverview.value,
|
||||
),
|
||||
model.imageRoot,
|
||||
this@NotallyFragment,
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.view.View
|
|||
import androidx.core.os.BundleCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import com.philkes.notallyx.data.model.Folder
|
||||
|
||||
class SearchFragment : NotallyFragment() {
|
||||
|
@ -44,6 +45,9 @@ class SearchFragment : NotallyFragment() {
|
|||
isVisible = true
|
||||
}
|
||||
} else binding?.ChipGroup?.isVisible = false
|
||||
getObservable().observe(viewLifecycleOwner) { items ->
|
||||
model.actionMode.updateSelected(items?.filterIsInstance<BaseNote>()?.map { it.id })
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBackground() = R.drawable.search
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -52,6 +52,7 @@ open class PickNoteActivity : LockedActivity<ActivityPickNoteBinding>(), ItemLis
|
|||
maxLines.value,
|
||||
maxTitle.value,
|
||||
labelTagsHiddenInOverview.value,
|
||||
imagesHiddenInOverview.value,
|
||||
),
|
||||
application.getExternalImagesDirectory(),
|
||||
this@PickNoteActivity,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.philkes.notallyx.NotallyXApplication
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.NotallyDatabase
|
||||
import com.philkes.notallyx.data.dao.BaseNoteDao
|
||||
|
@ -42,9 +43,10 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
super.onReceive(context, intent)
|
||||
when (intent.action) {
|
||||
ACTION_NOTES_MODIFIED -> {
|
||||
val app = context.applicationContext as NotallyXApplication
|
||||
val noteIds = intent.getLongArrayExtra(EXTRA_MODIFIED_NOTES)
|
||||
if (noteIds != null) {
|
||||
updateWidgets(context, noteIds)
|
||||
updateWidgets(context, noteIds, locked = app.locked.value)
|
||||
}
|
||||
}
|
||||
ACTION_OPEN_NOTE -> openActivity(context, intent, EditNoteActivity::class.java)
|
||||
|
@ -85,7 +87,8 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
baseNoteDao.updateChecked(noteId, childrenPositions + position, checked!!)
|
||||
}
|
||||
} finally {
|
||||
updateWidgets(context, longArrayOf(noteId))
|
||||
val app = context.applicationContext as NotallyXApplication
|
||||
updateWidgets(context, longArrayOf(noteId), locked = app.locked.value)
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
|
@ -135,19 +138,19 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
val app = context.applicationContext as Application
|
||||
val app = context.applicationContext as NotallyXApplication
|
||||
val preferences = NotallyXPreferences.getInstance(app)
|
||||
|
||||
appWidgetIds.forEach { id ->
|
||||
val noteId = preferences.getWidgetData(id)
|
||||
val noteType = preferences.getWidgetNoteType(id) ?: return
|
||||
updateWidget(app, appWidgetManager, id, noteId, noteType)
|
||||
updateWidget(app, appWidgetManager, id, noteId, noteType, locked = app.locked.value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun updateWidgets(context: Context, noteIds: LongArray? = null, locked: Boolean = false) {
|
||||
fun updateWidgets(context: Context, noteIds: LongArray? = null, locked: Boolean) {
|
||||
val app = context.applicationContext as Application
|
||||
val preferences = NotallyXPreferences.getInstance(app)
|
||||
|
||||
|
@ -181,65 +184,90 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
|
||||
intent.embedIntentExtras()
|
||||
|
||||
if (!locked) {
|
||||
MainScope().launch {
|
||||
val database = NotallyDatabase.getDatabase(context).value
|
||||
MainScope().launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val color = database.getBaseNoteDao().getColorOfNote(noteId)
|
||||
val preferences = NotallyXPreferences.getInstance(context)
|
||||
val (backgroundColor, _) = context.extractWidgetColors(color, preferences)
|
||||
val view =
|
||||
RemoteViews(context.packageName, R.layout.widget).apply {
|
||||
setRemoteAdapter(R.id.ListView, intent)
|
||||
setEmptyView(R.id.ListView, R.id.Empty)
|
||||
setOnClickPendingIntent(
|
||||
R.id.Empty,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.apply {
|
||||
action = ACTION_SELECT_NOTE
|
||||
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
|
||||
}
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
setPendingIntentTemplate(
|
||||
R.id.ListView,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
|
||||
noteType?.let {
|
||||
setOnClickPendingIntent(
|
||||
R.id.Layout,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.setOpenNoteIntent(noteType, noteId)
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
}
|
||||
|
||||
setInt(R.id.Layout, "setBackgroundColor", backgroundColor)
|
||||
}
|
||||
manager.updateAppWidget(id, view)
|
||||
manager.notifyAppWidgetViewDataChanged(id, R.id.ListView)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val view =
|
||||
RemoteViews(context.packageName, R.layout.widget_locked).apply {
|
||||
noteType?.let {
|
||||
val lockedPendingIntent =
|
||||
context.getOpenNotePendingIntent(noteId, noteType)
|
||||
setOnClickPendingIntent(R.id.Layout, lockedPendingIntent)
|
||||
setOnClickPendingIntent(R.id.Text, lockedPendingIntent)
|
||||
val color =
|
||||
withContext(Dispatchers.IO) { database.getBaseNoteDao().getColorOfNote(noteId) }
|
||||
if (color == null) {
|
||||
val app = context.applicationContext as Application
|
||||
val preferences = NotallyXPreferences.getInstance(app)
|
||||
preferences.deleteWidget(id)
|
||||
val view =
|
||||
RemoteViews(context.packageName, R.layout.widget).apply {
|
||||
setRemoteAdapter(R.id.ListView, intent)
|
||||
setEmptyView(R.id.ListView, R.id.Empty)
|
||||
setOnClickPendingIntent(
|
||||
R.id.Empty,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.apply {
|
||||
action = ACTION_SELECT_NOTE
|
||||
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
|
||||
}
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
setPendingIntentTemplate(
|
||||
R.id.ListView,
|
||||
Intent(context, WidgetProvider::class.java).asPendingIntent(context),
|
||||
)
|
||||
}
|
||||
setTextViewCompoundDrawablesRelative(
|
||||
R.id.Text,
|
||||
0,
|
||||
R.drawable.lock_big,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
manager.updateAppWidget(id, view)
|
||||
|
||||
manager.updateAppWidget(id, view)
|
||||
manager.notifyAppWidgetViewDataChanged(id, R.id.ListView)
|
||||
return@launch
|
||||
}
|
||||
if (!locked) {
|
||||
val view =
|
||||
RemoteViews(context.packageName, R.layout.widget).apply {
|
||||
setRemoteAdapter(R.id.ListView, intent)
|
||||
setEmptyView(R.id.ListView, R.id.Empty)
|
||||
setOnClickPendingIntent(
|
||||
R.id.Empty,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.apply {
|
||||
action = ACTION_SELECT_NOTE
|
||||
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
|
||||
}
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
setPendingIntentTemplate(
|
||||
R.id.ListView,
|
||||
Intent(context, WidgetProvider::class.java).asPendingIntent(context),
|
||||
)
|
||||
|
||||
val preferences = NotallyXPreferences.getInstance(context)
|
||||
val (backgroundColor, _) =
|
||||
context.extractWidgetColors(color, preferences)
|
||||
noteType?.let {
|
||||
setOnClickPendingIntent(
|
||||
R.id.Layout,
|
||||
Intent(context, WidgetProvider::class.java)
|
||||
.setOpenNoteIntent(noteType, noteId)
|
||||
.asPendingIntent(context),
|
||||
)
|
||||
}
|
||||
setInt(R.id.Layout, "setBackgroundColor", backgroundColor)
|
||||
}
|
||||
manager.updateAppWidget(id, view)
|
||||
manager.notifyAppWidgetViewDataChanged(id, R.id.ListView)
|
||||
} else {
|
||||
val view =
|
||||
RemoteViews(context.packageName, R.layout.widget_locked).apply {
|
||||
noteType?.let {
|
||||
val lockedPendingIntent =
|
||||
context.getOpenNotePendingIntent(noteId, noteType)
|
||||
setOnClickPendingIntent(R.id.Layout, lockedPendingIntent)
|
||||
setOnClickPendingIntent(R.id.Text, lockedPendingIntent)
|
||||
}
|
||||
setTextViewCompoundDrawablesRelative(
|
||||
R.id.Text,
|
||||
0,
|
||||
R.drawable.lock_big,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
manager.updateAppWidget(id, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,13 @@ class ActionMode {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateSelected(availableItemIds: List<Long>?) {
|
||||
selectedNotes.keys
|
||||
.filter { availableItemIds?.contains(it) == false }
|
||||
.forEach { selectedNotes.remove(it) }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun isEnabled() = enabled.value
|
||||
|
||||
// We assume selectedNotes.size is 1
|
||||
|
|
|
@ -80,7 +80,7 @@ private fun Context.createReminderAlarmIntent(noteId: Long, reminderId: Long): P
|
|||
intent.putExtra(ReminderReceiver.EXTRA_NOTE_ID, noteId)
|
||||
return PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
(noteId.toString() + reminderId.toString()).toInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
|
||||
)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package com.philkes.notallyx.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
|
@ -12,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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -37,8 +37,8 @@ fun decryptDatabase(context: ContextWrapper, passphrase: ByteArray) {
|
|||
fun decryptDatabase(
|
||||
context: Context,
|
||||
passphrase: ByteArray,
|
||||
decryptedFile: File,
|
||||
databaseFile: File,
|
||||
decryptedFile: File,
|
||||
) {
|
||||
val state = SQLCipherUtils.getDatabaseState(databaseFile)
|
||||
if (state == SQLCipherUtils.State.ENCRYPTED) {
|
||||
|
|
|
@ -4,15 +4,13 @@ import android.app.Activity
|
|||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.hardware.biometrics.BiometricManager
|
||||
import android.hardware.biometrics.BiometricPrompt
|
||||
import android.hardware.fingerprint.FingerprintManager
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.philkes.notallyx.R
|
||||
import javax.crypto.Cipher
|
||||
|
||||
|
@ -27,7 +25,7 @@ fun Activity.showBiometricOrPinPrompt(
|
|||
) {
|
||||
showBiometricOrPinPrompt(
|
||||
isForDecrypt,
|
||||
this,
|
||||
this as FragmentActivity,
|
||||
activityResultLauncher,
|
||||
titleResId,
|
||||
descriptionResId,
|
||||
|
@ -48,7 +46,7 @@ fun Fragment.showBiometricOrPinPrompt(
|
|||
) {
|
||||
showBiometricOrPinPrompt(
|
||||
isForDecrypt,
|
||||
requireContext(),
|
||||
activity!!,
|
||||
activityResultLauncher,
|
||||
titleResId,
|
||||
descriptionResId,
|
||||
|
@ -60,7 +58,7 @@ fun Fragment.showBiometricOrPinPrompt(
|
|||
|
||||
private fun showBiometricOrPinPrompt(
|
||||
isForDecrypt: Boolean,
|
||||
context: Context,
|
||||
context: FragmentActivity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||
titleResId: Int,
|
||||
descriptionResId: Int? = null,
|
||||
|
@ -69,142 +67,55 @@ private fun showBiometricOrPinPrompt(
|
|||
onFailure: (errorCode: Int?) -> Unit,
|
||||
) {
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||
// Android 11+ with BiometricPrompt and Authenticators
|
||||
val prompt =
|
||||
BiometricPrompt.Builder(context)
|
||||
.apply {
|
||||
setTitle(context.getString(titleResId))
|
||||
descriptionResId?.let {
|
||||
setDescription(context.getString(descriptionResId))
|
||||
}
|
||||
setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
)
|
||||
}
|
||||
.build()
|
||||
val cipher =
|
||||
if (isForDecrypt) {
|
||||
getInitializedCipherForDecryption(iv = cipherIv!!)
|
||||
} else {
|
||||
getInitializedCipherForEncryption()
|
||||
}
|
||||
prompt.authenticate(
|
||||
BiometricPrompt.CryptoObject(cipher),
|
||||
getCancellationSignal(context),
|
||||
context.mainExecutor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult?
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess.invoke(result!!.cryptoObject!!.cipher)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailure.invoke(null)
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure.invoke(errorCode)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> {
|
||||
// Android 10: Use BiometricPrompt without Authenticators
|
||||
val prompt =
|
||||
BiometricPrompt.Builder(context)
|
||||
.apply {
|
||||
setTitle(context.getString(titleResId))
|
||||
descriptionResId?.let {
|
||||
setDescription(context.getString(descriptionResId))
|
||||
}
|
||||
setNegativeButton(
|
||||
context.getString(R.string.cancel),
|
||||
context.mainExecutor,
|
||||
) { _, _ ->
|
||||
onFailure.invoke(null)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
val cipher =
|
||||
if (isForDecrypt) {
|
||||
getInitializedCipherForDecryption(iv = cipherIv!!)
|
||||
} else {
|
||||
getInitializedCipherForEncryption()
|
||||
}
|
||||
prompt.authenticate(
|
||||
BiometricPrompt.CryptoObject(cipher),
|
||||
getCancellationSignal(context),
|
||||
context.mainExecutor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult?
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess.invoke(result!!.cryptoObject!!.cipher)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailure.invoke(null)
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure.invoke(errorCode)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
|
||||
val fingerprintManager =
|
||||
ContextCompat.getSystemService(context, FingerprintManager::class.java)
|
||||
if (
|
||||
fingerprintManager?.isHardwareDetected == true &&
|
||||
fingerprintManager.hasEnrolledFingerprints()
|
||||
) {
|
||||
val cipher =
|
||||
if (isForDecrypt) {
|
||||
getInitializedCipherForDecryption(iv = cipherIv!!)
|
||||
} else {
|
||||
getInitializedCipherForEncryption()
|
||||
val promptInfo =
|
||||
BiometricPrompt.PromptInfo.Builder()
|
||||
.apply {
|
||||
setTitle(context.getString(titleResId))
|
||||
descriptionResId?.let {
|
||||
setDescription(context.getString(descriptionResId))
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
)
|
||||
} else {
|
||||
setNegativeButtonText(context.getString(R.string.cancel))
|
||||
setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||
)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
val cipher =
|
||||
if (isForDecrypt) {
|
||||
getInitializedCipherForDecryption(iv = cipherIv!!)
|
||||
} else {
|
||||
getInitializedCipherForEncryption()
|
||||
}
|
||||
val authCallback =
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess.invoke(result.cryptoObject!!.cipher!!)
|
||||
}
|
||||
fingerprintManager.authenticate(
|
||||
FingerprintManager.CryptoObject(cipher),
|
||||
getCancellationSignal(context),
|
||||
0,
|
||||
object : FingerprintManager.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: FingerprintManager.AuthenticationResult?
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onSuccess.invoke(result!!.cryptoObject!!.cipher)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailure.invoke(null)
|
||||
}
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onFailure.invoke(null)
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence?,
|
||||
) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure.invoke(errorCode)
|
||||
}
|
||||
},
|
||||
null,
|
||||
)
|
||||
} else {
|
||||
promptPinAuthentication(context, activityResultLauncher, titleResId, onFailure)
|
||||
}
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
onFailure.invoke(errorCode)
|
||||
}
|
||||
}
|
||||
val prompt =
|
||||
BiometricPrompt(context, ContextCompat.getMainExecutor(context), authCallback)
|
||||
prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
@ -214,14 +125,6 @@ private fun showBiometricOrPinPrompt(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getCancellationSignal(context: Context): CancellationSignal {
|
||||
return CancellationSignal().apply {
|
||||
setOnCancelListener {
|
||||
Toast.makeText(context, R.string.biometrics_failure, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptPinAuthentication(
|
||||
context: Context,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue