diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 3aa9fac9..6fdfc73c 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -10,6 +10,11 @@
|--------------|---------------|----------|
| English | [@msoultanidis](https://github.com/msoultanidis) | Complete |
| Greek (`el`) | [@msoultanidis](https://github.com/msoultanidis) | Complete |
+| Polish (`pl`) | [@TheDidek](https://github.com/TheDidek) | Complete |
+| Brazilian Portuguese (`pt-rBR`) | [@RodolfoCandido](https://github.com/RodolfoCandido) | Complete |
+| Italian (`it`) | [@danigarau](https://github.com/danigarau) | Complete |
+| French (`fr`) | [@locness3](https://github.com/locness3) | Complete |
+| Spanish (`es`) | [@urizev](https://github.com/urizev) | Complete |
You can help Quillnote grow by translating it in languages it does not support yet or by improving existing translations.
diff --git a/app/build.gradle b/app/build.gradle
index 9867c1c3..270814b4 100755
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -16,8 +16,8 @@ android {
applicationId "org.qosp.notes"
minSdkVersion 21
targetSdkVersion 30
- versionCode 3
- versionName "1.2.0"
+ versionCode 4
+ versionName "1.3.0"
testInstrumentationRunner "org.qosp.notes.TestRunner"
}
@@ -30,6 +30,18 @@ android {
}
}
+ flavorDimensions "versions"
+ productFlavors {
+ googleFlavor {
+ dimension "versions"
+ buildConfigField "boolean", "IS_GOOGLE", "true"
+ }
+ defaultFlavor {
+ dimension "versions"
+ buildConfigField "boolean", "IS_GOOGLE", "false"
+ }
+ }
+
kotlinOptions {
jvmTarget = "1.8"
}
@@ -104,7 +116,7 @@ dependencies {
implementation "io.noties.markwon:linkify:$markwon_version"
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
implementation "io.noties.markwon:ext-tables:$markwon_version"
- implementation "io.noties.markwon:image-coil:$markwon_version"
+ implementation "io.noties.markwon:ext-tasklist:$markwon_version"
implementation "me.saket:better-link-movement-method:2.2.0"
// Work Manager
@@ -135,6 +147,6 @@ dependencies {
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
- // LeakCanary
- debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanary_version"
+ // LeakCanary
+ debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanary_version"
}
diff --git a/app/src/googleFlavor/res/values/no_translate_strings.xml b/app/src/googleFlavor/res/values/no_translate_strings.xml
new file mode 100644
index 00000000..5ddb93cf
--- /dev/null
+++ b/app/src/googleFlavor/res/values/no_translate_strings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/googleFlavor/res/values/strings.xml b/app/src/googleFlavor/res/values/strings.xml
new file mode 100755
index 00000000..39e2523e
--- /dev/null
+++ b/app/src/googleFlavor/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/qosp/notes/data/dao/NoteDao.kt b/app/src/main/java/org/qosp/notes/data/dao/NoteDao.kt
index 20687cce..a69c05b7 100755
--- a/app/src/main/java/org/qosp/notes/data/dao/NoteDao.kt
+++ b/app/src/main/java/org/qosp/notes/data/dao/NoteDao.kt
@@ -148,4 +148,16 @@ interface NoteDao {
}
return Pair(column, order)
}
+
+ fun getNotesWithoutNotebook(sortMethod: SortMethod): Flow> {
+ val (column, order) = getOrderByMethod(sortMethod)
+ return rawGetQuery(
+ SimpleSQLiteQuery(
+ """
+ SELECT * FROM notes WHERE isArchived = 0 AND isDeleted = 0 AND notebookId IS NULL
+ ORDER BY isPinned DESC, $column $order
+ """
+ )
+ )
+ }
}
diff --git a/app/src/main/java/org/qosp/notes/data/repo/NoteRepository.kt b/app/src/main/java/org/qosp/notes/data/repo/NoteRepository.kt
index 34dbc562..ed0b1a92 100644
--- a/app/src/main/java/org/qosp/notes/data/repo/NoteRepository.kt
+++ b/app/src/main/java/org/qosp/notes/data/repo/NoteRepository.kt
@@ -182,6 +182,10 @@ class NoteRepository(
return noteDao.getNonRemoteNotes(sortMethod, provider)
}
+ fun getNotesWithoutNotebook(sortMethod: SortMethod = defaultOf()): Flow> {
+ return noteDao.getNotesWithoutNotebook(sortMethod)
+ }
+
suspend fun moveRemotelyDeletedNotesToBin(idsInUse: List, provider: CloudService) {
noteDao.moveRemotelyDeletedNotesToBin(idsInUse, provider)
}
diff --git a/app/src/main/java/org/qosp/notes/data/sync/core/ProviderConfig.kt b/app/src/main/java/org/qosp/notes/data/sync/core/ProviderConfig.kt
index 83cd50c8..6c3aa9fc 100644
--- a/app/src/main/java/org/qosp/notes/data/sync/core/ProviderConfig.kt
+++ b/app/src/main/java/org/qosp/notes/data/sync/core/ProviderConfig.kt
@@ -6,4 +6,5 @@ interface ProviderConfig {
val remoteAddress: String
val username: String
val provider: CloudService
+ val authenticationHeaders: Map
}
diff --git a/app/src/main/java/org/qosp/notes/data/sync/core/SyncManager.kt b/app/src/main/java/org/qosp/notes/data/sync/core/SyncManager.kt
index 4cb1a9e5..1298bc6f 100644
--- a/app/src/main/java/org/qosp/notes/data/sync/core/SyncManager.kt
+++ b/app/src/main/java/org/qosp/notes/data/sync/core/SyncManager.kt
@@ -33,6 +33,7 @@ class SyncManager(
}
val config = prefs.map { prefs -> prefs.config }
+ .stateIn(syncingScope, SharingStarted.WhileSubscribed(5000), null)
@OptIn(ObsoleteCoroutinesApi::class)
private val actor = syncingScope.actor {
diff --git a/app/src/main/java/org/qosp/notes/data/sync/nextcloud/NextcloudConfig.kt b/app/src/main/java/org/qosp/notes/data/sync/nextcloud/NextcloudConfig.kt
index 481ef7ad..a957d2a2 100644
--- a/app/src/main/java/org/qosp/notes/data/sync/nextcloud/NextcloudConfig.kt
+++ b/app/src/main/java/org/qosp/notes/data/sync/nextcloud/NextcloudConfig.kt
@@ -16,7 +16,10 @@ data class NextcloudConfig(
) : ProviderConfig {
val credentials = ("Basic " + Base64.encodeToString("$username:$password".toByteArray(), Base64.NO_WRAP)).trim()
+
override val provider: CloudService = CloudService.NEXTCLOUD
+ override val authenticationHeaders: Map
+ get() = mapOf("Authorization" to credentials)
companion object {
@OptIn(ExperimentalCoroutinesApi::class)
diff --git a/app/src/main/java/org/qosp/notes/di/MarkwonModule.kt b/app/src/main/java/org/qosp/notes/di/MarkwonModule.kt
index ad41df70..816714b4 100644
--- a/app/src/main/java/org/qosp/notes/di/MarkwonModule.kt
+++ b/app/src/main/java/org/qosp/notes/di/MarkwonModule.kt
@@ -6,7 +6,7 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.FragmentComponent
-import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.FragmentScoped
import io.noties.markwon.*
import io.noties.markwon.editor.MarkwonEditor
@@ -14,21 +14,23 @@ import io.noties.markwon.editor.handler.EmphasisEditHandler
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TablePlugin
-import io.noties.markwon.image.coil.CoilImagesPlugin
+import io.noties.markwon.ext.tasklist.TaskListPlugin
import io.noties.markwon.linkify.LinkifyPlugin
import io.noties.markwon.movement.MovementMethodPlugin
import me.saket.bettermovementmethod.BetterLinkMovementMethod
+import org.qosp.notes.R
+import org.qosp.notes.data.sync.core.SyncManager
import org.qosp.notes.ui.editor.markdown.*
-import javax.inject.Named
+import org.qosp.notes.ui.utils.coil.CoilImagesPlugin
+import org.qosp.notes.ui.utils.resolveAttribute
@Module
@InstallIn(FragmentComponent::class)
object MarkwonModule {
- const val SUPPORTS_IMAGES = "SUPPORTS_IMAGES"
@Provides
@FragmentScoped
- fun provideBaseMarkwonBuilder(@ApplicationContext context: Context): Markwon.Builder {
+ fun provideMarkwon(@ActivityContext context: Context, syncManager: SyncManager): Markwon {
return Markwon.builder(context)
.usePlugin(LinkifyPlugin.create(Linkify.EMAIL_ADDRESSES or Linkify.WEB_URLS))
.usePlugin(SoftBreakAddsNewLinePlugin.create())
@@ -40,26 +42,15 @@ object MarkwonModule {
builder.linkResolver(LinkResolverDef())
}
})
- }
-
- @Provides
- @Named(SUPPORTS_IMAGES)
- @FragmentScoped
- fun provideMarkwonInstanceWithImageSupport(
- @ApplicationContext context: Context,
- builder: Markwon.Builder
- ): Markwon {
- return builder
- .usePlugin(CoilImagesPlugin.create(context))
+ .usePlugin(CoilImagesPlugin.create(context, syncManager))
+ .apply {
+ val mainColor = context.resolveAttribute(R.attr.colorMarkdownTask) ?: return@apply
+ val backgroundColor = context.resolveAttribute(R.attr.colorBackground) ?: return@apply
+ usePlugin(TaskListPlugin.create(mainColor, mainColor, backgroundColor))
+ }
.build()
}
- @Provides
- @FragmentScoped
- fun provideBaseMarkwonInstance(builder: Markwon.Builder): Markwon {
- return builder.build()
- }
-
@Provides
@FragmentScoped
fun provideMarkwonEditor(markwon: Markwon): MarkwonEditor {
diff --git a/app/src/main/java/org/qosp/notes/preferences/AppPreferences.kt b/app/src/main/java/org/qosp/notes/preferences/AppPreferences.kt
index 798d6bf7..efb26971 100644
--- a/app/src/main/java/org/qosp/notes/preferences/AppPreferences.kt
+++ b/app/src/main/java/org/qosp/notes/preferences/AppPreferences.kt
@@ -12,6 +12,8 @@ data class AppPreferences(
val dateFormat: DateFormat = defaultOf(),
val timeFormat: TimeFormat = defaultOf(),
val openMediaIn: OpenMediaIn = defaultOf(),
+ val showDate: ShowDate = defaultOf(),
+ val groupNotesWithoutNotebook: GroupNotesWithoutNotebook = defaultOf(),
val cloudService: CloudService = defaultOf(),
val syncMode: SyncMode = defaultOf(),
val backgroundSync: BackgroundSync = defaultOf(),
diff --git a/app/src/main/java/org/qosp/notes/preferences/PreferenceEnums.kt b/app/src/main/java/org/qosp/notes/preferences/PreferenceEnums.kt
index 7ecb901b..8a24d552 100755
--- a/app/src/main/java/org/qosp/notes/preferences/PreferenceEnums.kt
+++ b/app/src/main/java/org/qosp/notes/preferences/PreferenceEnums.kt
@@ -72,6 +72,18 @@ enum class OpenMediaIn(override val nameResource: Int) : HasNameResource, EnumPr
EXTERNAL(R.string.preferences_open_media_in_external),
}
+enum class ShowDate(override val nameResource: Int) : HasNameResource, EnumPreference by key("show_date") {
+ YES(R.string.yes) { override val isDefault = true },
+ NO(R.string.no),
+}
+
+enum class GroupNotesWithoutNotebook(
+ override val nameResource: Int,
+) : HasNameResource, EnumPreference by key("group_notes_without_notebook") {
+ YES(R.string.yes),
+ NO(R.string.no) { override val isDefault = true },
+}
+
enum class CloudService(override val nameResource: Int) : HasNameResource, EnumPreference by key("cloud_service") {
DISABLED(R.string.preferences_cloud_service_disabled) { override val isDefault = true },
NEXTCLOUD(R.string.preferences_cloud_service_nextcloud),
diff --git a/app/src/main/java/org/qosp/notes/preferences/PreferenceRepository.kt b/app/src/main/java/org/qosp/notes/preferences/PreferenceRepository.kt
index f577e90a..8cb47332 100755
--- a/app/src/main/java/org/qosp/notes/preferences/PreferenceRepository.kt
+++ b/app/src/main/java/org/qosp/notes/preferences/PreferenceRepository.kt
@@ -40,6 +40,8 @@ class PreferenceRepository(
dateFormat = prefs.getEnum(),
timeFormat = prefs.getEnum(),
openMediaIn = prefs.getEnum(),
+ showDate = prefs.getEnum(),
+ groupNotesWithoutNotebook = prefs.getEnum(),
cloudService = prefs.getEnum(),
syncMode = prefs.getEnum(),
backgroundSync = prefs.getEnum(),
diff --git a/app/src/main/java/org/qosp/notes/ui/ActivityViewModel.kt b/app/src/main/java/org/qosp/notes/ui/ActivityViewModel.kt
index 8a078e2a..7f8ad218 100755
--- a/app/src/main/java/org/qosp/notes/ui/ActivityViewModel.kt
+++ b/app/src/main/java/org/qosp/notes/ui/ActivityViewModel.kt
@@ -8,7 +8,10 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import me.msoul.datastore.defaultOf
import org.qosp.notes.components.MediaStorageManager
import org.qosp.notes.data.model.Note
import org.qosp.notes.data.model.Notebook
@@ -35,8 +38,18 @@ class ActivityViewModel @Inject constructor(
private val syncManager: SyncManager,
) : ViewModel() {
- val notebooks: StateFlow> = notebookRepository.getAll()
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), listOf())
+ @OptIn(ExperimentalCoroutinesApi::class)
+ val notebooks: StateFlow>> =
+ preferenceRepository.get().flatMapLatest { groupNotesWithoutNotebook ->
+ notebookRepository.getAll().map { notebooks ->
+ (groupNotesWithoutNotebook == GroupNotesWithoutNotebook.YES) to notebooks
+ }
+ }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5000),
+ initialValue = (defaultOf() == GroupNotesWithoutNotebook.YES) to listOf(),
+ )
var showHiddenNotes: Boolean = false
var notesToBackup: Set? = null
@@ -134,6 +147,20 @@ class ActivityViewModel @Inject constructor(
)
}
+ fun disableMarkdown(vararg notes: Note) = update(*notes) { note ->
+ note.copy(
+ isMarkdownEnabled = false,
+ modifiedDate = Instant.now().epochSecond,
+ )
+ }
+
+ fun enableMarkdown(vararg notes: Note) = update(*notes) { note ->
+ note.copy(
+ isMarkdownEnabled = true,
+ modifiedDate = Instant.now().epochSecond,
+ )
+ }
+
fun duplicateNotes(vararg notes: Note) = notes.forEachAsync { note ->
val oldId = note.id
val cloned = note.copy(
diff --git a/app/src/main/java/org/qosp/notes/ui/MainActivity.kt b/app/src/main/java/org/qosp/notes/ui/MainActivity.kt
index 3f462826..b6e1262d 100755
--- a/app/src/main/java/org/qosp/notes/ui/MainActivity.kt
+++ b/app/src/main/java/org/qosp/notes/ui/MainActivity.kt
@@ -125,7 +125,7 @@ class MainActivity : BaseActivity() {
val textViewUsername = header.findViewById(R.id.text_view_username)
val textViewProvider = header.findViewById(R.id.text_view_provider)
- // Fixes bug that causes the header to have large padding when the keybord is open
+ // Fixes bug that causes the header to have large padding when the keyboard is open
ViewCompat.setOnApplyWindowInsetsListener(header) { view, insets ->
header.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.systemBars()).top, 0, 0)
WindowInsetsCompat.CONSUMED
@@ -166,20 +166,35 @@ class MainActivity : BaseActivity() {
}
}
- private fun setupDrawerMenuItemClickListeners() {
+ private fun setupDrawerMenuItems() {
// Alternative of setupWithNavController(), NavigationUI.java
// Sets up click listeners for all drawer menu items except from notebooks.
// Those are handled in createNotebookMenuItems()
- (topLevelMenu.children + notebooksMenu.children).forEach { item ->
- if (item.itemId !in primaryDestinations + secondaryDestinations) return@forEach
+ (topLevelMenu.children + listOfNotNull(notebooksMenu.findItem(R.id.fragment_manage_notebooks)))
+ .forEach { item ->
+ if (item.itemId !in primaryDestinations + secondaryDestinations) return@forEach
- item.setOnMenuItemClickListener {
- binding.drawer.closeAndThen {
- navController.navigateSafely(item.itemId)
+ item.setOnMenuItemClickListener {
+ binding.drawer.closeAndThen {
+ navController.navigateSafely(item.itemId)
+ }
+ false // Returning true would cause the menu item to become checked.
+ // We check the menu items only when the destination changes.
}
- false // Returning true would cause the menu item to become checked.
- // We check the menu items only when the destination changes.
}
+
+ notebooksMenu.findItem(R.id.nav_default_notebook)?.setOnMenuItemClickListener {
+ binding.drawer.closeAndThen {
+ navController.navigateSafely(
+ R.id.fragment_notebook,
+ bundleOf(
+ "notebookId" to R.id.nav_default_notebook.toLong(),
+ "notebookName" to getString(R.string.default_notebook),
+ )
+ )
+ }
+ false // Returning true would cause the menu item to become checked.
+ // We check the menu items only when the destination changes.
}
}
@@ -222,7 +237,7 @@ class MainActivity : BaseActivity() {
navController =
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
- setupDrawerMenuItemClickListeners()
+ setupDrawerMenuItems()
navController.addOnDestinationChangedListener { controller, destination, arguments ->
currentFocus?.hideKeyboard()
@@ -231,8 +246,8 @@ class MainActivity : BaseActivity() {
setDrawerEnabled(destination.id != R.id.fragment_editor)
}
- activityModel.notebooks.collect(this) { notebooks ->
- val notebookIds = notebooks.map { it.id.toInt() }.toSet()
+ activityModel.notebooks.collect(this) { (showDefaultNotebook, notebooks) ->
+ val notebookIds = (notebooks.map { it.id.toInt() } + R.id.nav_default_notebook).toSet()
// Remove deleted notebooks from the menu
(primaryDestinations + secondaryDestinations + notebookIds).let { dests ->
@@ -244,6 +259,12 @@ class MainActivity : BaseActivity() {
}
createNotebookMenuItems(notebooks)
+
+ val defaultTitle = getString(R.string.default_notebook)
+ notebooksMenu.findItem(R.id.nav_default_notebook)?.apply {
+ isVisible = showDefaultNotebook
+ title = defaultTitle + " (${getString(R.string.default_string)})".takeIf { notebooks.any { it.name == defaultTitle } }.orEmpty()
+ }
}
}
diff --git a/app/src/main/java/org/qosp/notes/ui/about/AboutFragment.kt b/app/src/main/java/org/qosp/notes/ui/about/AboutFragment.kt
index 92581614..f0681c6d 100644
--- a/app/src/main/java/org/qosp/notes/ui/about/AboutFragment.kt
+++ b/app/src/main/java/org/qosp/notes/ui/about/AboutFragment.kt
@@ -5,8 +5,10 @@ import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
+import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.Markwon
+import org.qosp.notes.BuildConfig
import org.qosp.notes.R
import org.qosp.notes.databinding.FragmentAboutBinding
import org.qosp.notes.ui.common.BaseDialog
@@ -41,14 +43,20 @@ class AboutFragment : BaseFragment(resId = R.layout.fragment_about) {
binding.layoutAppBar.appBar,
requireContext().resources.getDimension(R.dimen.app_bar_elevation)
)
+
+ if (!BuildConfig.IS_GOOGLE) {
+ binding.actionSupport.isVisible = true
+ }
}
private fun setupListeners() = with(binding) {
actionWebsite.setOnClickListener { launchUrl(requireContext().getString(R.string.app_website)) }
actionContribute.setOnClickListener { launchUrl(requireContext().getString(R.string.app_repo)) }
actionVisitDeveloper.setOnClickListener { launchUrl(requireContext().getString(R.string.app_developer_repo)) }
- actionSupport.setOnClickListener { launchUrl(requireContext().getString(R.string.app_support_page)) }
actionViewLibraries.setOnClickListener { showLibrariesDialog() }
+ if (!BuildConfig.IS_GOOGLE) {
+ actionSupport.setOnClickListener { launchUrl(requireContext().getString(R.string.app_support_page)) }
+ }
}
private fun showLibrariesDialog() {
diff --git a/app/src/main/java/org/qosp/notes/ui/attachments/recycler/AttachmentViewHolder.kt b/app/src/main/java/org/qosp/notes/ui/attachments/recycler/AttachmentViewHolder.kt
index 5f95fc0b..a2e1a589 100755
--- a/app/src/main/java/org/qosp/notes/ui/attachments/recycler/AttachmentViewHolder.kt
+++ b/app/src/main/java/org/qosp/notes/ui/attachments/recycler/AttachmentViewHolder.kt
@@ -16,7 +16,7 @@ import org.qosp.notes.R
import org.qosp.notes.data.model.Attachment
import org.qosp.notes.databinding.LayoutAttachmentBinding
import org.qosp.notes.ui.attachments.uri
-import org.qosp.notes.ui.utils.AlbumArtFetcher
+import org.qosp.notes.ui.utils.coil.AlbumArtFetcher
class AttachmentViewHolder(
private val context: Context,
diff --git a/app/src/main/java/org/qosp/notes/ui/common/AbstractNotesFragment.kt b/app/src/main/java/org/qosp/notes/ui/common/AbstractNotesFragment.kt
index 98b91a89..58c604d5 100755
--- a/app/src/main/java/org/qosp/notes/ui/common/AbstractNotesFragment.kt
+++ b/app/src/main/java/org/qosp/notes/ui/common/AbstractNotesFragment.kt
@@ -479,6 +479,12 @@ abstract class AbstractNotesFragment(@LayoutRes resId: Int) : BaseFragment(resId
action(R.string.action_hide, R.drawable.ic_hidden, condition = !note.isHidden) {
activityModel.hideNotes(note)
}
+ action(R.string.action_disable_markdown, R.drawable.ic_markdown, condition = !note.isDeleted && note.isMarkdownEnabled) {
+ activityModel.disableMarkdown(note)
+ }
+ action(R.string.action_enable_markdown, R.drawable.ic_markdown, condition = !note.isDeleted && !note.isMarkdownEnabled) {
+ activityModel.enableMarkdown(note)
+ }
action(R.string.action_duplicate, R.drawable.ic_duplicate, condition = isNormal) {
activityModel.duplicateNotes(note)
}
diff --git a/app/src/main/java/org/qosp/notes/ui/common/Dialogs.kt b/app/src/main/java/org/qosp/notes/ui/common/Dialogs.kt
index c3df3b69..c673229b 100644
--- a/app/src/main/java/org/qosp/notes/ui/common/Dialogs.kt
+++ b/app/src/main/java/org/qosp/notes/ui/common/Dialogs.kt
@@ -9,13 +9,13 @@ import org.qosp.notes.ui.utils.navigateSafely
fun BaseFragment.showMoveToNotebookDialog(vararg notes: Note) {
lifecycleScope.launch {
- val notebooks = activityModel.notebooks.value
+ val (_, notebooks) = activityModel.notebooks.value
var selected = 0
val notebooksMap: MutableMap =
mutableMapOf(null to requireContext().getString(R.string.notebooks_unassigned))
// If notes are in the same notebook (or if it's just a single note)
- // we will display the selected note
+ // we will display the selected notebook
val notesInSameNotebook = notes.all { it.notebookId == notes[0].notebookId }
notebooks.forEachIndexed { index, notebook ->
notebooksMap[notebook.id] = notebook.name
diff --git a/app/src/main/java/org/qosp/notes/ui/common/recycler/NoteRecyclerAdapter.kt b/app/src/main/java/org/qosp/notes/ui/common/recycler/NoteRecyclerAdapter.kt
index 6233bcd9..02cedb1c 100755
--- a/app/src/main/java/org/qosp/notes/ui/common/recycler/NoteRecyclerAdapter.kt
+++ b/app/src/main/java/org/qosp/notes/ui/common/recycler/NoteRecyclerAdapter.kt
@@ -84,6 +84,7 @@ class NoteRecyclerAdapter(
Payload.RemindersChanged to (oldItem.reminders != newItem.reminders),
Payload.TagsChanged to (oldItem.tags != newItem.tags),
Payload.AttachmentsChanged to (oldItem.attachments != newItem.attachments),
+ Payload.TasksChanged to (oldItem.taskList != newItem.taskList),
)
.filter { (_, condition) -> condition }
.map { (payload, _) -> payload }
@@ -103,5 +104,6 @@ class NoteRecyclerAdapter(
TagsChanged,
RemindersChanged,
AttachmentsChanged,
+ TasksChanged,
}
}
diff --git a/app/src/main/java/org/qosp/notes/ui/common/recycler/NoteViewHolder.kt b/app/src/main/java/org/qosp/notes/ui/common/recycler/NoteViewHolder.kt
index 6400cca1..2e53c1a4 100755
--- a/app/src/main/java/org/qosp/notes/ui/common/recycler/NoteViewHolder.kt
+++ b/app/src/main/java/org/qosp/notes/ui/common/recycler/NoteViewHolder.kt
@@ -9,12 +9,14 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.noties.markwon.Markwon
+import org.commonmark.node.Code
import org.qosp.notes.R
import org.qosp.notes.data.model.*
import org.qosp.notes.databinding.LayoutNoteBinding
import org.qosp.notes.ui.attachments.recycler.AttachmentViewHolder
import org.qosp.notes.ui.attachments.recycler.AttachmentsAdapter
import org.qosp.notes.ui.attachments.recycler.AttachmentsPreviewGridManager
+import org.qosp.notes.ui.editor.markdown.applyTo
import org.qosp.notes.ui.tasks.TasksAdapter
import org.qosp.notes.ui.utils.ellipsize
import org.qosp.notes.ui.utils.resId
@@ -99,8 +101,11 @@ class NoteViewHolder(
tasksAdapter.submitList(taskList)
textViewContent.ellipsize()
- if (note.isMarkdownEnabled) {
- markwon.setMarkdown(textViewContent, note.content)
+ if (note.isMarkdownEnabled && note.content.isNotBlank()) {
+ markwon.applyTo(textViewContent, note.content) {
+ maximumTableColumns = 4
+ tableReplacement = { Code(context.getString(R.string.message_cannot_preview_table)) }
+ }
} else {
textViewContent.text = note.content
}
@@ -115,7 +120,6 @@ class NoteViewHolder(
val list = attachments.take(attachments.size.coerceAtMost(4))
val remaining = attachments.size - list.size
layoutManager.allocateSpans(list.size)
- list
attachmentsAdapter.submitList(list)
if (remaining > 0) {
@@ -155,6 +159,7 @@ class NoteViewHolder(
note,
note.reminders.isNotEmpty()
)
+ NoteRecyclerAdapter.Payload.TasksChanged -> setContent(note)
}
}
}
diff --git a/app/src/main/java/org/qosp/notes/ui/editor/EditorFragment.kt b/app/src/main/java/org/qosp/notes/ui/editor/EditorFragment.kt
index eb8963e7..9985a956 100755
--- a/app/src/main/java/org/qosp/notes/ui/editor/EditorFragment.kt
+++ b/app/src/main/java/org/qosp/notes/ui/editor/EditorFragment.kt
@@ -38,6 +38,7 @@ import io.noties.markwon.editor.MarkwonEditor
import io.noties.markwon.editor.MarkwonEditorTextWatcher
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
+import org.commonmark.node.Code
import org.qosp.notes.R
import org.qosp.notes.data.model.Attachment
import org.qosp.notes.data.model.Note
@@ -45,7 +46,6 @@ import org.qosp.notes.data.model.NoteColor
import org.qosp.notes.data.model.NoteTask
import org.qosp.notes.databinding.FragmentEditorBinding
import org.qosp.notes.databinding.LayoutAttachmentBinding
-import org.qosp.notes.di.MarkwonModule.SUPPORTS_IMAGES
import org.qosp.notes.ui.attachments.dialog.EditAttachmentDialog
import org.qosp.notes.ui.attachments.fromUri
import org.qosp.notes.ui.attachments.recycler.AttachmentRecyclerListener
@@ -55,8 +55,15 @@ import org.qosp.notes.ui.attachments.uri
import org.qosp.notes.ui.common.BaseDialog
import org.qosp.notes.ui.common.BaseFragment
import org.qosp.notes.ui.common.showMoveToNotebookDialog
+import org.qosp.notes.ui.editor.dialog.InsertHyperlinkDialog
+import org.qosp.notes.ui.editor.dialog.InsertImageDialog
+import org.qosp.notes.ui.editor.dialog.InsertTableDialog
import org.qosp.notes.ui.editor.markdown.MarkdownSpan
+import org.qosp.notes.ui.editor.markdown.addListItemListener
+import org.qosp.notes.ui.editor.markdown.applyTo
import org.qosp.notes.ui.editor.markdown.insertMarkdown
+import org.qosp.notes.ui.editor.markdown.setMarkdownTextSilently
+import org.qosp.notes.ui.editor.markdown.toggleCheckmarkCurrentLine
import org.qosp.notes.ui.media.MediaActivity
import org.qosp.notes.ui.recorder.RECORDED_ATTACHMENT
import org.qosp.notes.ui.recorder.RECORD_CODE
@@ -67,14 +74,12 @@ import org.qosp.notes.ui.tasks.TaskViewHolder
import org.qosp.notes.ui.tasks.TasksAdapter
import org.qosp.notes.ui.utils.*
import org.qosp.notes.ui.utils.views.BottomSheet
-import org.qosp.notes.ui.utils.views.setMarkdownTextSilently
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.Executors
import javax.inject.Inject
-import javax.inject.Named
private typealias Data = EditorViewModel.Data
@@ -105,7 +110,6 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
private lateinit var tasksAdapter: TasksAdapter
@Inject
- @Named(SUPPORTS_IMAGES)
lateinit var markwon: Markwon
@Inject
@@ -155,7 +159,7 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
- target: RecyclerView.ViewHolder
+ target: RecyclerView.ViewHolder,
): Boolean {
tasksAdapter.moveItem(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
return true
@@ -168,7 +172,7 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
dX: Float,
dY: Float,
actionState: Int,
- isCurrentlyActive: Boolean
+ isCurrentlyActive: Boolean,
) {
when (actionState) {
ACTION_STATE_DRAG -> {
@@ -275,7 +279,7 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
setupAttachmentsRecycler()
setupTasksRecycler()
- setupObservers()
+ observeData()
setupEditTexts()
setupMarkdown()
setupListeners()
@@ -292,6 +296,16 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
model.insertAttachments(attachment)
}
+ setFragmentResultListener(MARKDOWN_DIALOG_RESULT) { s, bundle ->
+ val markdown = bundle.getString(MARKDOWN_DIALOG_RESULT) ?: return@setFragmentResultListener
+ binding.editTextContent.apply {
+ if (selectedText?.isNotEmpty() == true) {
+ text?.replace(selectionStart, selectionEnd, "")
+ }
+ text?.insert(selectionStart, markdown)
+ }
+ }
+
binding.fabChangeMode.setOnClickListener {
updateEditMode(!model.inEditMode)
if (model.inEditMode) requestFocusForFields(true) else view.hideKeyboard()
@@ -372,7 +386,11 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
RecordAudioDialog().show(parentFragmentManager, null)
}
R.id.action_enable_disable_markdown -> {
- if (note.isMarkdownEnabled) model.disableMarkdown() else model.enableMarkdown()
+ if (note.isMarkdownEnabled) {
+ activityModel.disableMarkdown(note)
+ } else {
+ activityModel.enableMarkdown(note)
+ }
}
else -> false
}
@@ -380,6 +398,11 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
return super.onOptionsItemSelected(item)
}
+ override fun onPause() {
+ model.selectedRange = with(binding.editTextContent) { selectionStart to selectionEnd }
+ super.onPause()
+ }
+
override fun onDestroyView() {
// Dismiss the snackbar which is shown for deleted notes
snackbar?.dismiss()
@@ -533,6 +556,8 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
contentHasFocus = hasFocus
setMarkdownToolbarVisibility()
}
+
+ setOnEditorActionListener(addListItemListener)
}
// Used to clear focus and hide the keyboard when touching outside of the edit texts
@@ -588,7 +613,7 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
}
}
- private fun setupObservers() = with(binding) {
+ private fun observeData() = with(binding) {
model.data.collect(viewLifecycleOwner) { data ->
if (data.note == null && data.isInitialized) {
return@collect run { findNavController().navigateUp() }
@@ -619,6 +644,11 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
else -> {
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
editTextContent.setMarkdownTextSilently(data.note.content)
+
+ val (selStart, selEnd) = model.selectedRange
+ if (selStart >= 0 && selEnd <= editTextContent.length()) {
+ editTextContent.setSelection(selStart, selEnd)
+ }
}
}
}
@@ -645,7 +675,12 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
if (isMarkdownEnabled) {
// Seems to be crashing often without wrapping it in a post { } call
- textViewContentPreview.post { markwon.setMarkdown(textViewContentPreview, data.note.content) }
+ textViewContentPreview.post {
+ markwon.applyTo(textViewContentPreview, data.note.content) {
+ tableReplacement = { Code(getString(R.string.message_cannot_preview_table)) }
+ maximumTableColumns = 15
+ }
+ }
} else {
textViewContentPreview.text = data.note.content
}
@@ -676,7 +711,9 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
formatter =
DateTimeFormatter.ofPattern("${getString(dateFormat.patternResource)}, ${getString(timeFormat.patternResource)}")
- if (formatter != null) {
+
+ textViewDate.isVisible = data.showDates
+ if (formatter != null && data.showDates) {
textViewDate.text =
getString(R.string.indicator_note_date, creationDate.format(formatter), modifiedDate.format(formatter))
}
@@ -723,6 +760,7 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
private fun setupListeners() = with(binding) {
bottomToolbar.setOnMenuItemClickListener {
+
val span = when (it.itemId) {
R.id.action_insert_bold -> MarkdownSpan.BOLD
R.id.action_insert_italics -> MarkdownSpan.ITALICS
@@ -730,9 +768,42 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
R.id.action_insert_code -> MarkdownSpan.CODE
R.id.action_insert_quote -> MarkdownSpan.QUOTE
R.id.action_insert_heading -> MarkdownSpan.HEADING
+ R.id.action_insert_link -> {
+ clearFragmentResult(MARKDOWN_DIALOG_RESULT)
+ InsertHyperlinkDialog
+ .build(editTextContent.selectedText ?: "")
+ .show(parentFragmentManager, null)
+ null
+ }
+ R.id.action_insert_image -> {
+ clearFragmentResult(MARKDOWN_DIALOG_RESULT)
+ InsertImageDialog
+ .build(editTextContent.selectedText ?: "")
+ .show(parentFragmentManager, null)
+ null
+ }
+ R.id.action_insert_table -> {
+ clearFragmentResult(MARKDOWN_DIALOG_RESULT)
+ InsertTableDialog().show(parentFragmentManager, null)
+ null
+ }
+ R.id.action_toggle_check_line -> {
+ editTextContent.toggleCheckmarkCurrentLine()
+ null
+ }
+ R.id.action_scroll_to_top -> {
+ scrollView.smoothScrollTo(0, 0)
+ editTextContent.setSelection(0)
+ null
+ }
+ R.id.action_scroll_to_bottom -> {
+ scrollView.smoothScrollTo(0, editTextContent.bottom + editTextContent.paddingBottom + editTextContent.marginBottom)
+ editTextContent.setSelection(editTextContent.length())
+ null
+ }
else -> return@setOnMenuItemClickListener false
}
- editTextContent.insertMarkdown(span)
+ editTextContent.insertMarkdown(span ?: return@setOnMenuItemClickListener false)
true
}
@@ -920,4 +991,8 @@ class EditorFragment : BaseFragment(R.layout.fragment_editor) {
NoteColor.Yellow -> R.string.preferences_color_scheme_yellow
}
)
+
+ companion object {
+ const val MARKDOWN_DIALOG_RESULT = "MARKDOWN_DIALOG_RESULT"
+ }
}
diff --git a/app/src/main/java/org/qosp/notes/ui/editor/EditorViewModel.kt b/app/src/main/java/org/qosp/notes/ui/editor/EditorViewModel.kt
index 8fa83f42..8e6483f4 100755
--- a/app/src/main/java/org/qosp/notes/ui/editor/EditorViewModel.kt
+++ b/app/src/main/java/org/qosp/notes/ui/editor/EditorViewModel.kt
@@ -28,6 +28,8 @@ class EditorViewModel @Inject constructor(
private var syncJob: Job? = null
private val noteIdFlow: MutableStateFlow = MutableStateFlow(null)
+ var selectedRange = 0 to 0
+
@OptIn(ExperimentalCoroutinesApi::class)
val data = noteIdFlow
.filterNotNull()
@@ -41,6 +43,7 @@ class EditorViewModel @Inject constructor(
notebook = notebook,
dateTimeFormats = prefs.dateFormat to prefs.timeFormat,
openMediaInternally = prefs.openMediaIn == OpenMediaIn.INTERNAL,
+ showDates = prefs.showDate == ShowDate.YES,
isInitialized = true,
)
}
@@ -148,20 +151,6 @@ class EditorViewModel @Inject constructor(
)
}
- fun disableMarkdown() = update { note ->
- note.copy(
- isMarkdownEnabled = false,
- modifiedDate = Instant.now().epochSecond,
- )
- }
-
- fun enableMarkdown() = update { note ->
- note.copy(
- isMarkdownEnabled = true,
- modifiedDate = Instant.now().epochSecond,
- )
- }
-
private inline fun update(crossinline transform: suspend (Note) -> Note) {
viewModelScope.launch(Dispatchers.IO) {
val note = data.value.note ?: return@launch
@@ -183,6 +172,7 @@ class EditorViewModel @Inject constructor(
val notebook: Notebook? = null,
val dateTimeFormats: Pair = defaultOf() to defaultOf(),
val openMediaInternally: Boolean = true,
+ val showDates: Boolean = true,
val isInitialized: Boolean = false,
)
}
diff --git a/app/src/main/java/org/qosp/notes/ui/editor/dialog/InsertHyperlinkDialog.kt b/app/src/main/java/org/qosp/notes/ui/editor/dialog/InsertHyperlinkDialog.kt
new file mode 100644
index 00000000..f3fe05b7
--- /dev/null
+++ b/app/src/main/java/org/qosp/notes/ui/editor/dialog/InsertHyperlinkDialog.kt
@@ -0,0 +1,67 @@
+package org.qosp.notes.ui.editor.dialog
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import androidx.appcompat.app.AlertDialog
+import androidx.core.os.bundleOf
+import androidx.fragment.app.setFragmentResult
+import dagger.hilt.android.AndroidEntryPoint
+import org.qosp.notes.R
+import org.qosp.notes.databinding.DialogInsertLinkBinding
+import org.qosp.notes.ui.common.BaseDialog
+import org.qosp.notes.ui.editor.EditorFragment
+import org.qosp.notes.ui.editor.markdown.hyperlinkMarkdown
+import org.qosp.notes.ui.utils.requestFocusAndKeyboard
+
+@AndroidEntryPoint
+class InsertHyperlinkDialog : BaseDialog() {
+
+ private var text: String? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ text = arguments?.getString(TEXT)
+ }
+
+ override fun createBinding(inflater: LayoutInflater) = DialogInsertLinkBinding.inflate(layoutInflater)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ binding.editTextHyperlinkText.setText(text.toString())
+
+ dialog.apply {
+ setTitle(getString(R.string.action_insert_link))
+ setButton(AlertDialog.BUTTON_NEUTRAL, getString(R.string.action_cancel)) { _, _ -> }
+ setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.action_insert)) { _, _ ->
+ val markdown = hyperlinkMarkdown(
+ url = binding.editTextHyperlink.text.toString(),
+ content = binding.editTextHyperlinkText.text.toString(),
+ )
+ setFragmentResult(
+ EditorFragment.MARKDOWN_DIALOG_RESULT,
+ bundleOf(
+ EditorFragment.MARKDOWN_DIALOG_RESULT to markdown
+ )
+ )
+ }
+ }
+
+ if (binding.editTextHyperlinkText.text?.isEmpty() == true) {
+ binding.editTextHyperlinkText.requestFocusAndKeyboard()
+ } else {
+ binding.editTextHyperlink.requestFocusAndKeyboard()
+ }
+ }
+
+ companion object {
+ private const val TEXT = "TEXT"
+
+ fun build(text: String): InsertHyperlinkDialog {
+ return InsertHyperlinkDialog().apply {
+ arguments = bundleOf(
+ TEXT to text,
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/qosp/notes/ui/editor/dialog/InsertImageDialog.kt b/app/src/main/java/org/qosp/notes/ui/editor/dialog/InsertImageDialog.kt
new file mode 100644
index 00000000..def82750
--- /dev/null
+++ b/app/src/main/java/org/qosp/notes/ui/editor/dialog/InsertImageDialog.kt
@@ -0,0 +1,67 @@
+package org.qosp.notes.ui.editor.dialog
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import androidx.appcompat.app.AlertDialog
+import androidx.core.os.bundleOf
+import androidx.fragment.app.setFragmentResult
+import dagger.hilt.android.AndroidEntryPoint
+import org.qosp.notes.R
+import org.qosp.notes.databinding.DialogInsertImageBinding
+import org.qosp.notes.ui.common.BaseDialog
+import org.qosp.notes.ui.editor.EditorFragment
+import org.qosp.notes.ui.editor.markdown.imageMarkdown
+import org.qosp.notes.ui.utils.requestFocusAndKeyboard
+
+@AndroidEntryPoint
+class InsertImageDialog : BaseDialog() {
+
+ private var text: String? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ text = arguments?.getString(TEXT)
+ }
+
+ override fun createBinding(inflater: LayoutInflater) = DialogInsertImageBinding.inflate(layoutInflater)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ binding.editTextImageDescription.setText(text.toString())
+
+ dialog.apply {
+ setTitle(getString(R.string.action_insert_image))
+ setButton(AlertDialog.BUTTON_NEUTRAL, getString(R.string.action_cancel)) { _, _ -> }
+ setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.action_insert)) { _, _ ->
+ val markdown = imageMarkdown(
+ url = binding.editTextImagePath.text.toString(),
+ description = binding.editTextImageDescription.text.toString(),
+ )
+ setFragmentResult(
+ EditorFragment.MARKDOWN_DIALOG_RESULT,
+ bundleOf(
+ EditorFragment.MARKDOWN_DIALOG_RESULT to markdown
+ )
+ )
+ }
+ }
+
+ if (binding.editTextImageDescription.text?.isEmpty() == true) {
+ binding.editTextImageDescription.requestFocusAndKeyboard()
+ } else {
+ binding.editTextImagePath.requestFocusAndKeyboard()
+ }
+ }
+
+ companion object {
+ private const val TEXT = "TEXT"
+
+ fun build(text: String): InsertImageDialog {
+ return InsertImageDialog().apply {
+ arguments = bundleOf(
+ TEXT to text,
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/qosp/notes/ui/editor/dialog/InsertTableDialog.kt b/app/src/main/java/org/qosp/notes/ui/editor/dialog/InsertTableDialog.kt
new file mode 100644
index 00000000..23cb18dc
--- /dev/null
+++ b/app/src/main/java/org/qosp/notes/ui/editor/dialog/InsertTableDialog.kt
@@ -0,0 +1,57 @@
+package org.qosp.notes.ui.editor.dialog
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.core.os.bundleOf
+import androidx.core.text.isDigitsOnly
+import androidx.fragment.app.setFragmentResult
+import dagger.hilt.android.AndroidEntryPoint
+import org.qosp.notes.R
+import org.qosp.notes.databinding.DialogInsertTableBinding
+import org.qosp.notes.ui.common.BaseDialog
+import org.qosp.notes.ui.common.setButton
+import org.qosp.notes.ui.editor.EditorFragment
+import org.qosp.notes.ui.editor.markdown.tableMarkdown
+import org.qosp.notes.ui.utils.requestFocusAndKeyboard
+
+@AndroidEntryPoint
+class InsertTableDialog : BaseDialog() {
+ override fun createBinding(inflater: LayoutInflater) = DialogInsertTableBinding.inflate(layoutInflater)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ dialog.apply {
+ setTitle(getString(R.string.action_insert_table))
+ setButton(AlertDialog.BUTTON_NEUTRAL, getString(R.string.action_cancel)) { _, _ -> }
+ setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.action_insert), this@InsertTableDialog) {
+ val rows = binding.editTextRows.text.toString()
+ val columns = binding.editTextColumns.text.toString()
+
+ if (rows.isBlank() || columns.isBlank() || !rows.isDigitsOnly() || !columns.isDigitsOnly()) {
+ Toast.makeText(requireContext(), getString(R.string.message_invalid_number_rows_columns), Toast.LENGTH_SHORT).show()
+ return@setButton
+ }
+
+ val markdown = tableMarkdown(
+ rows = rows.toInt(),
+ columns = columns.toInt(),
+ )
+ setFragmentResult(
+ EditorFragment.MARKDOWN_DIALOG_RESULT,
+ bundleOf(
+ EditorFragment.MARKDOWN_DIALOG_RESULT to markdown
+ )
+ )
+ dismiss()
+ }
+ }
+
+ if (binding.editTextColumns.text?.isEmpty() == true) {
+ binding.editTextColumns.requestFocusAndKeyboard()
+ } else {
+ binding.editTextRows.requestFocusAndKeyboard()
+ }
+ }
+}
diff --git a/app/src/main/java/org/qosp/notes/ui/editor/markdown/MarkdownOptions.kt b/app/src/main/java/org/qosp/notes/ui/editor/markdown/MarkdownOptions.kt
new file mode 100644
index 00000000..c9483655
--- /dev/null
+++ b/app/src/main/java/org/qosp/notes/ui/editor/markdown/MarkdownOptions.kt
@@ -0,0 +1,56 @@
+package org.qosp.notes.ui.editor.markdown
+
+import android.widget.TextView
+import io.noties.markwon.Markwon
+import org.commonmark.ext.gfm.tables.TableBlock
+import org.commonmark.ext.gfm.tables.TableCell
+import org.commonmark.ext.gfm.tables.TableRow
+import org.commonmark.node.AbstractVisitor
+import org.commonmark.node.Code
+import org.commonmark.node.CustomBlock
+import org.commonmark.node.CustomNode
+import org.commonmark.node.Node
+
+class MarkdownOptions {
+ var maximumTableColumns: Int = 100
+ var tableReplacement: () -> Node = { Code("...") }
+}
+
+inline fun Markwon.applyTo(textView: TextView, content: String, withOptions: MarkdownOptions.() -> Unit = {}) {
+ val options = MarkdownOptions()
+ withOptions(options)
+
+ val node = parse(content)
+ val visitor = OptionsVisitor(options)
+ node.accept(visitor)
+
+ setParsedMarkdown(textView, render(node))
+}
+
+class OptionsVisitor(private val options: MarkdownOptions) : AbstractVisitor() {
+ override fun visit(customBlock: CustomBlock?) {
+ if (customBlock is TableBlock) {
+ val visitor = TableRowVisitor()
+ customBlock.firstChild?.firstChild?.accept(visitor)
+
+ if (visitor.cellCount > options.maximumTableColumns) {
+ val replacement = options.tableReplacement()
+ customBlock.insertAfter(replacement)
+ customBlock.unlink()
+ }
+ } else {
+ visitChildren(customBlock)
+ }
+ }
+
+ private class TableRowVisitor : AbstractVisitor() {
+ var cellCount = 0
+
+ override fun visit(customNode: CustomNode?) {
+ when (customNode) {
+ is TableRow -> visitChildren(customNode)
+ is TableCell -> cellCount++
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/qosp/notes/ui/editor/markdown/MarkdownUtils.kt b/app/src/main/java/org/qosp/notes/ui/editor/markdown/MarkdownUtils.kt
index 43fde1d7..3972ad78 100644
--- a/app/src/main/java/org/qosp/notes/ui/editor/markdown/MarkdownUtils.kt
+++ b/app/src/main/java/org/qosp/notes/ui/editor/markdown/MarkdownUtils.kt
@@ -1,6 +1,10 @@
package org.qosp.notes.ui.editor.markdown
-import android.widget.EditText
+import android.view.KeyEvent
+import android.view.inputmethod.EditorInfo
+import android.widget.TextView
+import io.noties.markwon.editor.MarkwonEditorTextWatcher
+import org.qosp.notes.ui.utils.views.ExtendedEditText
enum class MarkdownSpan(val value: String) {
BOLD("**"),
@@ -11,15 +15,14 @@ enum class MarkdownSpan(val value: String) {
HEADING("#"),
}
-fun EditText.insertMarkdown(markdownSpan: MarkdownSpan) {
+fun ExtendedEditText.insertMarkdown(markdownSpan: MarkdownSpan) {
val start = selectionStart
val end = selectionEnd
+
if (start < 0) return
- val textBefore = text.substring(0 until start)
- val lineStart = textBefore.lastIndexOf("\n") + 1
- val currentLine = textBefore.filter { it == '\n' }.length
- var line = text.lines()[currentLine]
+ var line = text?.lines()?.get(currentLineIndex) ?: return
+ val lineStart = currentLineStartPos
val oldLength = line.length
val s = markdownSpan.value
@@ -33,7 +36,7 @@ fun EditText.insertMarkdown(markdownSpan: MarkdownSpan) {
else -> "$s $line"
}
- text.replace(lineStart, lineStart + oldLength, line)
+ text?.replace(lineStart, lineStart + oldLength, line)
setSelection(lineStart + line.length)
}
MarkdownSpan.QUOTE -> {
@@ -42,13 +45,93 @@ fun EditText.insertMarkdown(markdownSpan: MarkdownSpan) {
else -> "$s $line"
}
- text.replace(lineStart, lineStart + oldLength, line)
+ text?.replace(lineStart, lineStart + oldLength, line)
setSelection(lineStart + line.length)
}
else -> {
- text.insert(start, s)
- text.insert(end + s.length, s)
+ text?.insert(start, s)
+ text?.insert(end + s.length, s)
setSelection(start + s.length)
}
}
}
+
+fun ExtendedEditText.toggleCheckmarkCurrentLine() {
+ var line = text?.lines()?.get(currentLineIndex) ?: return
+ val lineStart = currentLineStartPos
+ val oldLength = line.length
+
+ line = when {
+ line.matches(Regex("-[ ]*\\[ \\][ ]+.*")) -> {
+ line.replaceFirst("[ ]", "[x]").trimEnd() + " " // There's a strange bug which causes
+ // text to be duplicated after pressing Enter
+ // .trimEnd() + " " seems to be fixing it
+ }
+ line.matches(Regex("-[ ]*\\[x\\][ ]+.*")) -> {
+ line.replaceFirst("[x]", "[ ]").trimEnd() + " "
+ }
+ else -> "- [ ] $line"
+ }
+
+ text?.replace(lineStart, lineStart + oldLength, line)
+ setSelection(lineStart + line.length)
+}
+
+/**
+ * Sets the EditText's text without notifying any TextWatchers which are not [MarkwonEditorTextWatcher].
+ *
+ * @param text Text to set
+ */
+fun ExtendedEditText.setMarkdownTextSilently(text: CharSequence?) {
+ val watchers = textWatchers
+ .filterNot { it is MarkwonEditorTextWatcher }
+ .toList()
+
+ watchers.forEach { removeTextChangedListener(it) }
+
+ setText(text)
+
+ watchers.forEach { addTextChangedListener(it) }
+}
+
+fun hyperlinkMarkdown(url: String, content: String): String {
+ return "[$content]($url)"
+}
+
+fun imageMarkdown(url: String, description: String): String {
+ return "![alt text]($url \"$description\")"
+}
+
+fun tableMarkdown(rows: Int, columns: Int): String {
+ var markdown = ""
+
+ for (r in 0..rows) {
+ val space = if (r != 1) " " else "----"
+ for (c in 0 until columns) {
+ markdown += "|$space"
+ }
+ markdown += "|\n"
+ }
+ return markdown
+}
+
+val ExtendedEditText.addListItemListener: TextView.OnEditorActionListener
+ get() = TextView.OnEditorActionListener { v: TextView, actionId: Int, event: KeyEvent ->
+ if (actionId == EditorInfo.TYPE_NULL && event.action == KeyEvent.ACTION_DOWN) {
+ val text = text ?: return@OnEditorActionListener true
+ text.insert(selectionStart, "\n")
+
+ val previousLine = text.lines().getOrNull(currentLineIndex - 1) ?: return@OnEditorActionListener true
+
+ when {
+ previousLine.matches(Regex("-[ ]*\\[( |x)\\][ ]+.*")) -> text.insert(currentLineStartPos, "- [ ] ")
+ previousLine.matches(Regex("-[ ]+.*")) -> text.insert(currentLineStartPos, "- ")
+ previousLine.matches(Regex("[1-9]+[0-9]*[.][ ]+.*")) -> {
+ val inc = Regex("[1-9]+[0-9]*").findAll(previousLine).first().value.toInt().inc()
+ text.insert(currentLineStartPos, "$inc. ")
+ }
+ }
+ }
+
+ true
+ }
diff --git a/app/src/main/java/org/qosp/notes/ui/main/MainViewModel.kt b/app/src/main/java/org/qosp/notes/ui/main/MainViewModel.kt
index 52c6e3ad..72e580a4 100755
--- a/app/src/main/java/org/qosp/notes/ui/main/MainViewModel.kt
+++ b/app/src/main/java/org/qosp/notes/ui/main/MainViewModel.kt
@@ -5,6 +5,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
+import org.qosp.notes.R
import org.qosp.notes.data.repo.NoteRepository
import org.qosp.notes.data.repo.NotebookRepository
import org.qosp.notes.data.sync.core.SyncManager
@@ -26,7 +27,11 @@ class MainViewModel @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
override val provideNotes = { sortMethod: SortMethod ->
notebookIdFlow.flatMapLatest { id ->
- if (id == null) noteRepository.getNonDeletedOrArchived(sortMethod) else noteRepository.getByNotebook(id, sortMethod)
+ when (id) {
+ null -> noteRepository.getNonDeletedOrArchived(sortMethod)
+ R.id.nav_default_notebook.toLong() -> noteRepository.getNotesWithoutNotebook(sortMethod)
+ else -> noteRepository.getByNotebook(id, sortMethod)
+ }
}
}
diff --git a/app/src/main/java/org/qosp/notes/ui/media/MusicService.kt b/app/src/main/java/org/qosp/notes/ui/media/MusicService.kt
index 4387fb0c..006320b5 100644
--- a/app/src/main/java/org/qosp/notes/ui/media/MusicService.kt
+++ b/app/src/main/java/org/qosp/notes/ui/media/MusicService.kt
@@ -100,10 +100,12 @@ class MusicServiceBinder(
val builder: NotificationCompat.Builder = NotificationCompat.Builder(applicationContext, App.PLAYBACK_CHANNEL_ID)
var notificationId: Int? = null
+ private var pausedByUser: Boolean = true
+
private val audioFocusListener = AudioManager.OnAudioFocusChangeListener {
when (it) {
- AudioManager.AUDIOFOCUS_GAIN -> startPlaying()
- else -> pausePlaying()
+ AudioManager.AUDIOFOCUS_GAIN -> if (state != State.COMPLETED && !pausedByUser) startPlaying()
+ else -> pausePlaying(byUser = false)
}
}
@@ -157,6 +159,7 @@ class MusicServiceBinder(
.build()
state = newState
+ pausedByUser = if (newState != State.PAUSED) true else pausedByUser
val playbackState = when (newState) {
State.INITIALIZED -> buildPlaybackState(PlaybackState.STATE_NONE)
@@ -294,10 +297,11 @@ class MusicServiceBinder(
}
}
- fun pausePlaying() {
+ fun pausePlaying(byUser: Boolean = true) {
if (state == State.STARTED) {
mediaPlayer.pause()
setState(State.PAUSED)
+ pausedByUser = byUser
}
}
diff --git a/app/src/main/java/org/qosp/notes/ui/notebooks/ManageNotebooksFragment.kt b/app/src/main/java/org/qosp/notes/ui/notebooks/ManageNotebooksFragment.kt
index 1d6b3d0d..376dfdfb 100755
--- a/app/src/main/java/org/qosp/notes/ui/notebooks/ManageNotebooksFragment.kt
+++ b/app/src/main/java/org/qosp/notes/ui/notebooks/ManageNotebooksFragment.kt
@@ -40,8 +40,8 @@ class ManageNotebooksFragment : BaseFragment(R.layout.fragment_manage_notebooks)
setupRecyclerView()
- activityModel.notebooks.collect(viewLifecycleOwner) {
- adapter.submitList(it)
+ activityModel.notebooks.collect(viewLifecycleOwner) { (_, notebooks) ->
+ adapter.submitList(notebooks)
}
binding.layoutAppBar.toolbarSelection.apply {
diff --git a/app/src/main/java/org/qosp/notes/ui/notebooks/NotebookFragment.kt b/app/src/main/java/org/qosp/notes/ui/notebooks/NotebookFragment.kt
index c6be387f..ce1103c0 100755
--- a/app/src/main/java/org/qosp/notes/ui/notebooks/NotebookFragment.kt
+++ b/app/src/main/java/org/qosp/notes/ui/notebooks/NotebookFragment.kt
@@ -14,7 +14,7 @@ class NotebookFragment : MainFragment() {
private val args: NotebookFragmentArgs by navArgs()
override val notebookId: Long?
- get() = args.notebookId.takeIf { it >= 0L }
+ get() = args.notebookId.takeIf { it >= 0L || it == R.id.nav_default_notebook.toLong() }
override val toolbarTitle: String
get() = args.notebookName
@@ -28,7 +28,7 @@ class NotebookFragment : MainFragment() {
.setNoteId(noteId)
.setNewNoteAttachments(attachments.toTypedArray())
.setNewNoteIsList(isList)
- .setNewNoteNotebookId(notebookId ?: 0L)
+ .setNewNoteNotebookId(notebookId.takeUnless { it == R.id.nav_default_notebook.toLong() } ?: 0L)
override fun actionToSearch(searchQuery: String) =
NotebookFragmentDirections.actionNotebookToSearch().setSearchQuery(searchQuery)
@@ -38,7 +38,7 @@ class NotebookFragment : MainFragment() {
// Check if notebook exists in database. If it doesn't then go back
lifecycleScope.launch {
- if (!model.notebookExists(args.notebookId)) {
+ if (!model.notebookExists(args.notebookId) && args.notebookId != R.id.nav_default_notebook.toLong()) {
findNavController().navigateUp()
}
}
diff --git a/app/src/main/java/org/qosp/notes/ui/search/SearchFragment.kt b/app/src/main/java/org/qosp/notes/ui/search/SearchFragment.kt
index cf960ff8..eaedadf2 100644
--- a/app/src/main/java/org/qosp/notes/ui/search/SearchFragment.kt
+++ b/app/src/main/java/org/qosp/notes/ui/search/SearchFragment.kt
@@ -3,7 +3,7 @@ package org.qosp.notes.ui.search
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
-import androidx.core.widget.doOnTextChanged
+import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
@@ -15,7 +15,6 @@ import org.qosp.notes.data.model.Note
import org.qosp.notes.databinding.FragmentSearchBinding
import org.qosp.notes.databinding.LayoutNoteBinding
import org.qosp.notes.ui.common.AbstractNotesFragment
-import org.qosp.notes.ui.utils.hideKeyboard
import org.qosp.notes.ui.utils.navigateSafely
import org.qosp.notes.ui.utils.requestFocusAndKeyboard
import org.qosp.notes.ui.utils.viewBinding
@@ -43,16 +42,22 @@ class SearchFragment : AbstractNotesFragment(resId = R.layout.fragment_search) {
super.onViewCreated(view, savedInstanceState)
recyclerAdapter.searchMode = true
- binding.editTextSearch.doOnTextChanged { text, start, before, count ->
+ binding.editTextSearch.doAfterTextChanged { text ->
model.setSearchQuery(text.toString())
+ }
- if (text.isNullOrEmpty()) {
+ when {
+ !model.isFirstLoad -> return
+ args.searchQuery.isNotEmpty() -> {
+ binding.editTextSearch.setText(args.searchQuery)
+ binding.editTextSearch.requestFocusAndMoveCaret()
+ }
+ binding.editTextSearch.text?.isEmpty() == true -> {
binding.editTextSearch.requestFocusAndKeyboard()
}
}
- binding.editTextSearch.setText(args.searchQuery)
- binding.editTextSearch.hideKeyboard()
+ model.isFirstLoad = false
}
override fun onNoteClick(noteId: Long, position: Int, viewBinding: LayoutNoteBinding) {
diff --git a/app/src/main/java/org/qosp/notes/ui/search/SearchViewModel.kt b/app/src/main/java/org/qosp/notes/ui/search/SearchViewModel.kt
index c80749a1..91ea945c 100755
--- a/app/src/main/java/org/qosp/notes/ui/search/SearchViewModel.kt
+++ b/app/src/main/java/org/qosp/notes/ui/search/SearchViewModel.kt
@@ -24,6 +24,8 @@ class SearchViewModel @Inject constructor(
) : AbstractNotesViewModel(preferenceRepository, syncManager) {
private val searchKeyData: MutableStateFlow = MutableStateFlow("")
+ var isFirstLoad = true
+
@OptIn(ExperimentalCoroutinesApi::class, kotlinx.coroutines.FlowPreview::class)
override val provideNotes = { sortMethod: SortMethod ->
notebookRepository.getAll().distinctUntilChanged().flatMapLatest { notebooks ->
diff --git a/app/src/main/java/org/qosp/notes/ui/settings/SettingsFragment.kt b/app/src/main/java/org/qosp/notes/ui/settings/SettingsFragment.kt
index 1625b002..9a9604a3 100644
--- a/app/src/main/java/org/qosp/notes/ui/settings/SettingsFragment.kt
+++ b/app/src/main/java/org/qosp/notes/ui/settings/SettingsFragment.kt
@@ -44,9 +44,11 @@ class SettingsFragment : BaseFragment(resId = R.layout.fragment_settings) {
setupThemeModeListener()
setupLayoutModeListener()
setupSortMethodListener()
+ setupGroupNotesWithoutNotebookListener()
setupOpenMediaInListener()
setupNoteDeletionTimeListener()
setupBackupStrategyListener()
+ setupShowDateListener()
setupDateFormatListener()
setupTimeFormatListener()
setupSyncSettingsListener()
@@ -87,6 +89,10 @@ class SettingsFragment : BaseFragment(resId = R.layout.fragment_settings) {
binding.settingBackupStrategy.subText = getString(backupStrategy.nameResource)
binding.settingOpenMedia.subText = getString(openMediaIn.nameResource)
binding.settingNoteDeletion.subText = getString(noteDeletionTime.nameResource)
+
+ binding.settingGroupNotesWithoutNotebook.subText = getString(groupNotesWithoutNotebook.nameResource)
+ binding.settingShowDate.subText = getString(showDate.nameResource)
+
with(DateTimeFormatter.ofPattern(getString(dateFormat.patternResource))) {
binding.settingDateFormat.subText = format(LocalDate.now())
}
@@ -139,6 +145,12 @@ class SettingsFragment : BaseFragment(resId = R.layout.fragment_settings) {
}
}
+ private fun setupGroupNotesWithoutNotebookListener() = binding.settingGroupNotesWithoutNotebook.setOnClickListener {
+ showPreferenceDialog(R.string.preferences_group_notes_without_notebook, appPreferences.groupNotesWithoutNotebook) { selected ->
+ model.setPreference(selected)
+ }
+ }
+
private fun setupOpenMediaInListener() = binding.settingOpenMedia.setOnClickListener {
showPreferenceDialog(R.string.preferences_open_media_in, appPreferences.openMediaIn) { selected ->
model.setPreference(selected)
@@ -151,6 +163,12 @@ class SettingsFragment : BaseFragment(resId = R.layout.fragment_settings) {
}
}
+ private fun setupShowDateListener() = binding.settingShowDate.setOnClickListener {
+ showPreferenceDialog(R.string.preferences_show_date, appPreferences.showDate) { selected ->
+ model.setPreference(selected)
+ }
+ }
+
private fun setupTimeFormatListener() = binding.settingTimeFormat.setOnClickListener {
val localTime = LocalTime.now()
val items = TimeFormat.values()
diff --git a/app/src/main/java/org/qosp/notes/ui/utils/ViewUtils.kt b/app/src/main/java/org/qosp/notes/ui/utils/ViewUtils.kt
index ed56031f..2358d650 100644
--- a/app/src/main/java/org/qosp/notes/ui/utils/ViewUtils.kt
+++ b/app/src/main/java/org/qosp/notes/ui/utils/ViewUtils.kt
@@ -13,15 +13,21 @@ import androidx.core.widget.NestedScrollView
import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
+import org.qosp.notes.ui.utils.views.ExtendedEditText
fun Int.dp(context: Context): Int {
return (context.resources.displayMetrics.density * this).toInt()
}
fun View.requestFocusAndKeyboard() {
- post {
- requestFocus()
- if (hasWindowFocus()) return@post showKeyboard()
+ postDelayed(100) {
+ if (this is ExtendedEditText) {
+ requestFocusAndMoveCaret()
+ } else {
+ requestFocus()
+ }
+
+ if (hasWindowFocus()) return@postDelayed showKeyboard()
viewTreeObserver.addOnWindowFocusChangeListener(
object : ViewTreeObserver.OnWindowFocusChangeListener {
@@ -37,17 +43,13 @@ fun View.requestFocusAndKeyboard() {
}
fun View.showKeyboard() {
- post {
- val inputMethodManager = context.getSystemService()
- inputMethodManager?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
- }
+ val inputMethodManager = context.getSystemService()
+ inputMethodManager?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}
fun View.hideKeyboard() {
- post {
- val inputMethodManager = context.getSystemService()
- inputMethodManager?.hideSoftInputFromWindow(windowToken, 0)
- }
+ val inputMethodManager = context.getSystemService()
+ inputMethodManager?.hideSoftInputFromWindow(windowToken, 0)
}
fun View.liftAppBarOnScroll(
diff --git a/app/src/main/java/org/qosp/notes/ui/utils/AlbumArtFetcher.kt b/app/src/main/java/org/qosp/notes/ui/utils/coil/AlbumArtFetcher.kt
similarity index 98%
rename from app/src/main/java/org/qosp/notes/ui/utils/AlbumArtFetcher.kt
rename to app/src/main/java/org/qosp/notes/ui/utils/coil/AlbumArtFetcher.kt
index 822a7db1..5905a900 100644
--- a/app/src/main/java/org/qosp/notes/ui/utils/AlbumArtFetcher.kt
+++ b/app/src/main/java/org/qosp/notes/ui/utils/coil/AlbumArtFetcher.kt
@@ -1,4 +1,4 @@
-package org.qosp.notes.ui.utils
+package org.qosp.notes.ui.utils.coil
import android.content.Context
import android.graphics.Bitmap
@@ -22,6 +22,7 @@ import coil.size.PixelSize
import coil.size.Size
import org.qosp.notes.R
import org.qosp.notes.ui.attachments.getAlbumArtBitmap
+import org.qosp.notes.ui.utils.getDrawableCompat
import kotlin.math.roundToInt
// Modified VideoFrameFetcher to load album art images
diff --git a/app/src/main/java/org/qosp/notes/ui/utils/coil/CoilImagesPlugin.kt b/app/src/main/java/org/qosp/notes/ui/utils/coil/CoilImagesPlugin.kt
new file mode 100644
index 00000000..1a051520
--- /dev/null
+++ b/app/src/main/java/org/qosp/notes/ui/utils/coil/CoilImagesPlugin.kt
@@ -0,0 +1,157 @@
+package org.qosp.notes.ui.utils.coil
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.text.Spanned
+import android.widget.TextView
+import coil.Coil.imageLoader
+import coil.ImageLoader
+import coil.request.Disposable
+import coil.request.ImageRequest
+import coil.target.Target
+import io.noties.markwon.AbstractMarkwonPlugin
+import io.noties.markwon.MarkwonConfiguration
+import io.noties.markwon.MarkwonSpansFactory
+import io.noties.markwon.image.AsyncDrawable
+import io.noties.markwon.image.AsyncDrawableLoader
+import io.noties.markwon.image.AsyncDrawableScheduler
+import io.noties.markwon.image.DrawableUtils
+import io.noties.markwon.image.ImageSpanFactory
+import org.commonmark.node.Image
+import org.qosp.notes.data.sync.core.SyncManager
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * Now adds an authentication header to the image requests so we can load images from sync providers like Nextcloud
+ *
+ * Original version:
+ * @author Tyler Wong
+ * @since 4.2.0
+ */
+class CoilImagesPlugin internal constructor(coilStore: CoilStore, imageLoader: ImageLoader) :
+ AbstractMarkwonPlugin() {
+ interface CoilStore {
+ fun load(drawable: AsyncDrawable): ImageRequest
+ fun cancel(disposable: Disposable)
+ }
+
+ private val coilAsyncDrawableLoader: CoilAsyncDrawableLoader = CoilAsyncDrawableLoader(coilStore, imageLoader)
+
+ override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
+ builder.setFactory(Image::class.java, ImageSpanFactory())
+ }
+
+ override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
+ builder.asyncDrawableLoader(coilAsyncDrawableLoader)
+ }
+
+ override fun beforeSetText(textView: TextView, markdown: Spanned) {
+ AsyncDrawableScheduler.unschedule(textView)
+ }
+
+ override fun afterSetText(textView: TextView) {
+ AsyncDrawableScheduler.schedule(textView)
+ }
+
+ private class CoilAsyncDrawableLoader internal constructor(
+ private val coilStore: CoilStore,
+ private val imageLoader: ImageLoader,
+ ) :
+ AsyncDrawableLoader() {
+ private val cache: MutableMap = HashMap(2)
+ override fun load(drawable: AsyncDrawable) {
+ val loaded = AtomicBoolean(false)
+ val target: Target = AsyncDrawableTarget(drawable, loaded)
+ val request = coilStore.load(drawable).newBuilder()
+ .target(target)
+ .build()
+ // @since 4.5.1 execute can return result _before_ disposable is created,
+ // thus `execute` would finish before we put disposable in cache (and thus result is
+ // not delivered)
+ val disposable = imageLoader.enqueue(request)
+ // if flag was not set, then job is running (else - finished before we got here)
+ if (!loaded.get()) {
+ // mark flag
+ loaded.set(true)
+ cache[drawable] = disposable
+ }
+ }
+
+ override fun cancel(drawable: AsyncDrawable) {
+ val disposable = cache.remove(drawable)
+ if (disposable != null) {
+ coilStore.cancel(disposable)
+ }
+ }
+
+ override fun placeholder(drawable: AsyncDrawable): Drawable? {
+ return null
+ }
+
+ private inner class AsyncDrawableTarget(
+ private val drawable: AsyncDrawable,
+ private val loaded: AtomicBoolean,
+ ) : Target {
+ override fun onSuccess(result: Drawable) {
+ // @since 4.5.1 check finished flag (result can be delivered _before_ disposable is created)
+ if (cache.remove(drawable) != null ||
+ !loaded.get()
+ ) {
+ // mark
+ loaded.set(true)
+ if (drawable.isAttached) {
+ DrawableUtils.applyIntrinsicBoundsIfEmpty(result)
+ drawable.result = result
+ }
+ }
+ }
+
+ override fun onStart(placeholder: Drawable?) {
+ if (placeholder != null && drawable.isAttached) {
+ DrawableUtils.applyIntrinsicBoundsIfEmpty(placeholder)
+ drawable.result = placeholder
+ }
+ }
+
+ override fun onError(error: Drawable?) {
+ if (cache.remove(drawable) != null) {
+ if (error != null && drawable.isAttached) {
+ DrawableUtils.applyIntrinsicBoundsIfEmpty(error)
+ drawable.result = error
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ fun create(context: Context, syncManager: SyncManager): CoilImagesPlugin {
+ return create(
+ object : CoilStore {
+ override fun load(drawable: AsyncDrawable): ImageRequest {
+ return ImageRequest.Builder(context)
+ .data(drawable.destination)
+ .apply {
+ syncManager.config.value?.authenticationHeaders?.forEach { (key, value) ->
+ addHeader(key, value)
+ }
+ }
+ .build()
+ }
+
+ override fun cancel(disposable: Disposable) {
+ disposable.dispose()
+ }
+ },
+ imageLoader(context)
+ )
+ }
+
+ fun create(
+ coilStore: CoilStore,
+ imageLoader: ImageLoader,
+ ): CoilImagesPlugin {
+ return CoilImagesPlugin(coilStore, imageLoader)
+ }
+ }
+}
diff --git a/app/src/main/java/org/qosp/notes/ui/utils/views/ExtendedEditText.kt b/app/src/main/java/org/qosp/notes/ui/utils/views/ExtendedEditText.kt
index 2b0a31d0..ac0721c5 100644
--- a/app/src/main/java/org/qosp/notes/ui/utils/views/ExtendedEditText.kt
+++ b/app/src/main/java/org/qosp/notes/ui/utils/views/ExtendedEditText.kt
@@ -8,7 +8,6 @@ import android.text.TextWatcher
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.content.getSystemService
-import io.noties.markwon.editor.MarkwonEditorTextWatcher
class ExtendedEditText : AppCompatEditText {
constructor(context: Context) : super(context)
@@ -17,6 +16,18 @@ class ExtendedEditText : AppCompatEditText {
val textWatchers: MutableList = mutableListOf()
+ private val textBeforeSelection get() = text?.substring(0 until selectionStart).orEmpty()
+ val currentLineStartPos get() = textBeforeSelection.lastIndexOf("\n") + 1
+ val currentLineIndex get() = textBeforeSelection.filter { it == '\n' }.length
+
+ val selectedText get() = text?.substring(selectionStart, selectionEnd)
+
+ fun requestFocusAndMoveCaret(): Boolean {
+ return requestFocus().also { tookFocus ->
+ if (tookFocus && text != null) setSelection(length())
+ }
+ }
+
// With a regular EditText, users can paste rich text inside which may look out of place.
// This function prevents that from happening by changing the clip board
override fun onTextContextMenuItem(id: Int): Boolean {
@@ -49,7 +60,7 @@ class ExtendedEditText : AppCompatEditText {
}
/**
- * Set's the EditText's text without notifying any TextWatchers.
+ * Sets the EditText's text without notifying any TextWatchers.
*
* @param text Text to set
*/
@@ -62,20 +73,3 @@ class ExtendedEditText : AppCompatEditText {
watchers.forEach { addTextChangedListener(it) }
}
}
-
-/**
- * Set's the EditText's text without notifying any TextWatchers which are not [MarkwonEditorTextWatcher].
- *
- * @param text Text to set
- */
-fun ExtendedEditText.setMarkdownTextSilently(text: CharSequence?) {
- val watchers = textWatchers
- .filterNot { it is MarkwonEditorTextWatcher }
- .toList()
-
- watchers.forEach { removeTextChangedListener(it) }
-
- setText(text)
-
- watchers.forEach { addTextChangedListener(it) }
-}
diff --git a/app/src/main/res/drawable/ic_date_time.xml b/app/src/main/res/drawable/ic_date_time.xml
new file mode 100644
index 00000000..b3379457
--- /dev/null
+++ b/app/src/main/res/drawable/ic_date_time.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_down.xml b/app/src/main/res/drawable/ic_down.xml
new file mode 100644
index 00000000..2c9da8a1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_down.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml
new file mode 100644
index 00000000..d5cadaa5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_link.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_markdown.xml b/app/src/main/res/drawable/ic_markdown.xml
new file mode 100644
index 00000000..837183d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_markdown.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_table.xml b/app/src/main/res/drawable/ic_table.xml
new file mode 100644
index 00000000..9fdbc4d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_table.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_up.xml b/app/src/main/res/drawable/ic_up.xml
index 165526cd..1141033e 100755
--- a/app/src/main/res/drawable/ic_up.xml
+++ b/app/src/main/res/drawable/ic_up.xml
@@ -4,7 +4,7 @@
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
-
+
diff --git a/app/src/main/res/layout/dialog_insert_image.xml b/app/src/main/res/layout/dialog_insert_image.xml
new file mode 100644
index 00000000..a3a07b56
--- /dev/null
+++ b/app/src/main/res/layout/dialog_insert_image.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_insert_link.xml b/app/src/main/res/layout/dialog_insert_link.xml
new file mode 100644
index 00000000..3c95ca28
--- /dev/null
+++ b/app/src/main/res/layout/dialog_insert_link.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_insert_table.xml b/app/src/main/res/layout/dialog_insert_table.xml
new file mode 100644
index 00000000..0ee3cffb
--- /dev/null
+++ b/app/src/main/res/layout/dialog_insert_table.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml
index ff1b301e..287ae53f 100644
--- a/app/src/main/res/layout/fragment_about.xml
+++ b/app/src/main/res/layout/fragment_about.xml
@@ -67,6 +67,7 @@
android:id="@+id/action_support"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:visibility="gone"
app:text="@string/about_support"
app:subText="@string/about_support_subtext"
app:iconSrc="@drawable/ic_heart"/>
diff --git a/app/src/main/res/layout/fragment_editor.xml b/app/src/main/res/layout/fragment_editor.xml
index 4fa8b320..428293f7 100755
--- a/app/src/main/res/layout/fragment_editor.xml
+++ b/app/src/main/res/layout/fragment_editor.xml
@@ -173,13 +173,17 @@
android:layout_height="1dp"
android:background="?attr/colorOutline"/>
-
+
+
+
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
index 047b4328..c5fbf45b 100644
--- a/app/src/main/res/layout/fragment_settings.xml
+++ b/app/src/main/res/layout/fragment_settings.xml
@@ -80,6 +80,13 @@
app:text="@string/preferences_sort_method"
app:iconSrc="@drawable/ic_sort"/>
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/nav_drawer.xml b/app/src/main/res/menu/nav_drawer.xml
index 4a51f50f..18930e62 100755
--- a/app/src/main/res/menu/nav_drawer.xml
+++ b/app/src/main/res/menu/nav_drawer.xml
@@ -32,6 +32,11 @@
android:id="@+id/fragment_manage_notebooks"
android:icon="@drawable/ic_manage_notebooks"
android:title="@string/nav_your_notebooks"/>
+
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 6414f59c..3cd70cde 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -1,6 +1,5 @@
- Quillnote
Σημειώσεις
Αρχειοθετημένες
Διαγραμμένες
@@ -34,9 +33,11 @@
Ημερομηνία δημιουργίας (φθίνουσα)
Ημερομηνία τροποποίησης (αύξουσα)
Ημερομηνία τροποποίησης (φθίνουσα)
+ Προβολή ημερομηνίας δημιουργίας/τροποποίησης
Μορφή ημερομηνίας
Μορφή ώρας
Άλλα
+ Ομαδοποίηση των σημειώσεων που δεν ανήκουν σε κάποιο τετράδιο
Άνοιγμα πολυμέσων
Μέσα στην εφαρμογή
Με άλλη εφαρμογή
@@ -118,6 +119,7 @@
Επισύναψη αρχείων
Τραβήξτε φωτογραφία
Ηχογραφημένο κλιπ
+ Άλλες σημειώσεις
Νέο τετράδιο
Όνομα τετραδίου
Χωρίς Τετράδιο
@@ -188,7 +190,7 @@
Είστε συνδεδεμένοι ως %s.
Δεν είστε συνδεδεμένοι.
Σύνδεση
- Λογαριασμός εκτός σύνδεσης.
+ Λογαριασμός εκτός σύνδεσης
Γίνεται σύνδεση…
Κάτι πήγε λάθος.
Δεν μπορεί να γίνει σύνδεση λόγω λανθασμένων στοιχείων.
@@ -202,6 +204,18 @@
Κρατήστε μια σημείωση
Φτιάξτε μια λίστα
Προεπιλεγμένο
+ Εισαγωγή συνδέσμου
+ Εισαγωγή
+ Εισαγωγή πίνακα
+ Μη έγκυρος αριθμός στηλών και σειρών
+ Εισαγωγή εικόνας
+ Περιγραφή
+ Διεύθυνση εικόνας
+ Κείμενο
+ Διεύθυνση URL
+ Αριθμός στηλών
+ Αριθμός σειρών
+ Δεν είναι δυνατή η προεπισκόπηση του πίνακα.
- +%d στοιχείο
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..eb8e80c4
--- /dev/null
+++ b/app/src/main/res/values-es/strings.xml
@@ -0,0 +1,267 @@
+
+
+
+
+ Notas
+ Archivo
+ Eliminadas
+ Ajustes
+ Acerca de
+ Etiquetas
+ Todas las notas
+ Libretas
+ Tus libretas
+
+
+ General
+ Tema
+ Oscuro
+ Claro
+ Del sistema
+ Vista
+ Disposición
+ Cuadrícula
+ Lista
+ Esquema de colores
+ Azul
+ Rosa
+ Verde
+ Naranja
+ Morado
+ Amarillo
+ Rojo
+ Ordenar por
+ Título (ascendente)
+ Título (descendente)
+ Fecha de creación (ascendente)
+ Fecha de creación (descendente)
+ Fecha de modificación (ascendente)
+ Fecha de modificación (descendente)
+ Mostrar fecha de creación/modificación
+ Formato de fecha
+ Formato de hora
+ Otros
+ Agrupar notas que pertenecen a ninguna libreta
+ Abrir multimedia con
+ Reproductor interno
+ Reproductor externo
+ Eliminar notas de la papelera
+ Instantáneamente
+ Tras 7 días
+ Tras 14 días
+ Tras 30 días
+ Copia de seguridad
+ Crear copia de seguridad
+ Exportar todas las notas a una copia de seguridad en un archivo.
+ Recuperar
+ Recuperar notas de una copia de seguridad en un fichero.
+ Estrategia de copia de seguridad para los archivos adjuntos
+ Guardar todo
+ Guardar sólo la descripción y la ruta local
+ No guardar los adjuntos
+ Sincronización
+ Ir a ajustes de sincronización
+ Sincronizando actualmente con %s
+ No sincronizando actualmente
+ Servicio de sincronización
+ Deshabilitado
+ Nextcloud
+ Por el momento la sincronización es una característica experimental.\nPodrían aparecer errores.
+ Recuerda que las Nextcloud Notes no soporta etiquetas, adjuntos, recordatorios, listas de tareas y más.
+ Cuenta Nextcloud
+ URL de la instancia Nextcloud
+ Introduce tus credenciales
+ Introduce las URL del servidor
+ Wi-Fi
+ Wi-Fi o datos
+ Sincronizar con
+ Sincronización en segundo plano
+ Habilitada
+ Deshabilitada
+ Sincronizar notas nuevas
+
+
+ Versión
+ Sitio web
+ Desarrollador
+ Contribuye
+ Soporte
+ Crear y mantener proyectos de código abierto consume tiempo y no está retribuido.\n¡Invita a los desarrolladores a una cerveza!
+ Librerías
+ Ver librerías y licencias de terceros.
+
+
+ Por defecto
+ Sí
+ No
+ Ok
+ Tus notas se mostrarán aquí.
+ Sin título
+ Nombre de usuario
+ Contraseña
+ Guardar
+ Cancelar
+ Eliminar
+ ¡Hecho!
+ Eliminar permanentemente
+ Seleccionar todo
+ Crear una lista
+ Mostrar notas ocultas
+ Desarchivar
+ Exportar
+ Ocultar
+ Mostrar
+ Archivar
+ Fijar
+ Soltar
+ Restaurar
+ Compartir
+ Fijar / Soltar
+ Mover a…
+ Duplicar
+ Seleccionar más…
+
+
+ Editar descripción
+ Descripción del adjunto
+ Grabar audio
+ Grabando (%1$s)
+ Adjuntar archivos
+ Hacer una foto
+ Grabación de audio
+
+
+ Otro
+ Nueva libreta
+ Nombre de libreta
+ No hay libretas
+ No hay libretas.
+ Crear una nota
+ Renombrar libreta
+ Nueva libreta
+ Ya existe una libreta con el nombre %1$s.
+
+
+ Las notas archivadas se mostrarán aquí.
+
+
+ Vaciar papelera
+ ¿Estás seguro?
+ Todas las notas se perderán.
+ Las notas de la papelera no se pueden editar.
+ Las notas eliminadas se mostrarán aquí durante %1$d días.
+ Las notas se eliminarán al instante.\nPuedes cambiar esto en los ajustes.
+ Notas eliminadas permanentemente.
+ Nota eliminada permanentemente.
+
+
+ Recordatorios
+ Recordatorio
+ Nombre del recordatorio
+ Establecer fecha
+ Establecer hora
+ Nuevo recordatorio
+ Tienes un recordatorio.
+ No se puede crear un recordatorio para una fecha pasada
+
+
+ Nueva etiqueta
+ Nombre de etiqueta
+ No hay etiquetas.
+ Renombrar etiqueta
+ Ya existe una etiqueta con el nombre %1$s.
+
+
+ Buscar…
+ Buscar
+ Los resultados de la búsqueda se mostrarán aquí.
+ No se encontraron resultados.
+
+
+ Título
+ ¡Tomar nota!
+ Tarea
+ Convertir a nota
+ Convertir a lista
+ No sincronizar
+ Cambiar color
+ Habilitar formato
+ Deshabilitar formato
+ Insertar formato negrita
+ Insertar formato cursiva
+ Insertar formato tachado
+ Insertar formato cabecera
+ Insertar formato de cita
+ Insertar formato de código
+ Creada el %1$s\nModificadas el %2$s
+ Notas restauradas
+ Nota restaurada
+ Notas archivadas
+ Nota archivada
+ Las notas se han trasladado a la papelera
+ La nota se ha trasladado a la papelera
+ Se ha descartado la nota vacía
+ Insertar enlace
+ Insertar
+ Insertar tabla
+ Número inválido de filas y columnas
+ Insertar imagen
+ Descripción
+ Ruta de la imagen
+ Texto
+ URL
+ Número de columnas
+ Número de filas
+ Cannot preview table.
+
+
+ Creando copia de seguridad de tus notas…
+ ¡Copia de seguridad completada!
+ Copia de seguridad fallida
+ Restaurando notas…
+ Restauración completa
+ Restauración fallida
+
+
+ Recordatorios
+ Copias de seguridad
+ Reproducción multimedia
+
+
+ Está no es una URL HTTPS válida.
+ Sesión iniciada como %s.
+ No hay iniciado sesión.
+ Autenticar
+ Cuenta sin conexión
+ Conectando…
+ Algo fue mal.
+ Autenticación con el servidor fallida por credenciales incorrectas.
+ ¡Inicio de sesión correcto!
+ La versión del servidor no es compatible con esta aplicación.
+ No hay conexión a Internet.
+ No dejes en blanco las credenciales.
+ Limpiar credenciales y URL del servidor
+ Reproducir / Pausar
+ Detener
+
+
+ Tomar nota
+ Crear lista
+
+ - +%d elemento
+ - +%d elementos
+
+
+ - %d nota seleccionada
+ - %d notas seleccionadas
+
+
+ - %d libreta seleccionada
+ - %d libretas seleccionadas
+
+
+ - %d etiqueta seleccionada
+ - %d etiquetas seleccionadas
+
+
+
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..ce6f167e
--- /dev/null
+++ b/app/src/main/res/values-fr/strings.xml
@@ -0,0 +1,286 @@
+
+
+ Notes
+ Archivées
+ Supprimées
+ Paramètres
+ À propos
+ Étiquettes
+ Toutes les notes
+ Carnets
+ Vos carnets
+
+
+ Général
+ Thème
+ Sombre
+ Clair
+ Thème du système
+
+ Affichage
+ Mise en page
+ Grille
+ Liste
+
+ Palette de couleurs
+ Bleu
+ Rose
+ Vert
+ Orange
+ Violet
+ Jaune
+ Rouge
+
+ Trier par
+ Titre (croissant)
+ Titre (décroissant)
+ Date de création (croissant)
+ Date de création (décroissant)
+ Date de modification (croissant)
+ Date de modification (décroissant)
+
+ Afficher la date de création et modification
+ Format de la date
+ Format de l\'heure
+
+ Autre
+ Grouper les notes qui ne sont pas dans un carnet
+ Ouvrir les médias dans
+ Lecteur interne
+ Lecteur externe
+ Supprimer les notes dans la corbeille
+ Immédiatement
+ Après 7 jours
+ Après 14 jours
+ Après 30 jours
+
+ Sauvegarde
+ Créer une sauvegarde
+ Exporter toutes les notes dans un fichier de sauvegarde.
+ Restaurer
+ Charger les notes depuis un fichier de sauvegarde.
+ Sauvegarde des pièces jointes
+ Tout sauvegarder
+ Sauvegarder la description et le chemin d\'accès local
+ Ne pas sauvegarder les pièces jointes
+
+ Synchronisation
+ Paramètres de synchronisation
+ Synchronisation avec %s
+ Pas de synchronisation
+ Service de synchronisation
+ Désactivé
+ Nextcloud
+ La synchronisation est actuellement expérimentale et pourrait contenir des bugs.
+ Sachez que Nextcloud Notes ne supporte pas les étiquettes, pièces jointes, rappels, listes de tâches et d\'autres fonctionnalités encore.
+ Compte Nextcloud
+ Adresse du serveur Nextcloud
+ Définissez vos identifiants
+ Définissez l\'URL du serveur
+ Wi-Fi
+ Wi-Fi ou données mobiles
+ Synchroniser avec la connexion
+ Synchroniser en arrière-plan
+ Activé
+ Désactivé
+ Synchroniser les nouvelles notes
+
+
+
+ Version
+ Site web
+ Développeur
+ Contribuer
+ Soutenir
+ "Créer et maintenir des projets open-source prend du temps et ne fait pas de bénéfice. Achetez une bière aux développeurs! "
+ Bibliothèques
+ Voir les bibliothèques utilisées et leurs licences.
+
+
+
+ Par défaut
+ Oui
+ Non
+ OK
+ Vos notes apparaîtront ici.
+ Sans titre
+ Nom d\'utilisateur
+ Mot de passe
+ Sauvegarder
+ Annuler
+ Supprimer
+ Terminé
+ Supprimer définitivement
+ Tout sélectionner
+ Créer une liste
+ Afficher les notes cachées
+ Désarchiver
+ Exporter
+ Cacher
+ Montrer
+ Archiver
+ Épingler
+ Désépingler
+ Restaurer
+ Partager
+ Épingler / Désépingler
+ Déplacer vers…
+ Dupliquer
+ Sélectionner plus
+
+
+
+ Modifier la description
+ Description de la pièce jointe
+ Enregistrer de l\'audio
+ Enregistrement en cours (%1$s)
+ Joindre des fichiers
+ Prendre une photo
+ Son enregistré
+
+
+
+ Autre
+ Nouveau carnet
+ Nom du carnet
+ Pas de carnet
+ Vous n\'avez pas de carnets.
+ Créer un carnet
+ Renommer le carnet
+ Nouveau carnet
+ Un carnet nommé %1$s existe déja.
+
+
+
+ Vos notes archivées apparaîtront ici.
+
+
+
+ Vider la corbeille
+ Êtes-vous sûr?
+ Toutes les notes supprimées vont être perdues.
+ Les notes dans la corbeille ne peuvent être modifiées.
+ Vos notes supprimées apparaîtront ici pendant %1$d jours.
+ Les notes sont configurées pour être immédiatement supprimées.\nVous pouvez changer cela dans les Paramètres..
+ Les notes ont été définitivement supprimées.
+ La note a été définitvement supprimée.
+
+
+
+ Rappels
+ Rappel
+ Nom du rappel
+ Définir la date
+ Définir l\'heure
+ Nouveau reappel
+ Vous avez un rappel.
+ Impossible d\'utiliser une date antérieure pour un rappel
+
+
+
+ Nouvelle étiquette
+ Nom de l\'étiquette
+ Vous n\'avez pas créé d\'étiquettes.
+ Renommer l\'étiquette
+ Une étiquette nommée %1$s existe déja.
+
+
+
+ Recherche…
+ Rechercher
+ Les résultats de la recherche apparaîtront ici.
+ Pas de résultats.
+
+
+
+ Titre
+ Prenez une note!
+ Tâche
+ Convertir en note
+ Convertir en liste
+ Ne pas synchroniser
+ Changer la couleur
+ Activer le Markdown
+ Désactiver le markown
+ Insérer le markdown de mise en gras
+ Insérer le markdown de mise en italique
+ Insérer le markdown pour barrer le texte
+ Insérer le markdown d\'en-tête
+ Insérer le markdown de mise en citation
+ Insérer le markdown de formatage de code
+ Création: %1$s\nDernière modification: %2$s
+ Notes restaurées
+ Note restaurée
+ Notes archivées
+ Note archivée
+ Notes déplacées dans la corbeille
+ Note déplacée dans la corbeille
+ Note vide annulée
+ Insérer un lien
+ Insérer
+ Insérer un tableau
+ Nombres de lignes et colonnes invalide.
+ Insérer une image
+ Description
+ Chemin de l\'image
+ Texte
+ URL
+ Nombre de colonnes
+ Nombre de lignes
+ Impossible de prévisualiser le tableau.
+
+
+ Sauvegarde des notes…
+ Sauvegarde terminée!
+ Sauvegarde échouée
+ Restauration des notes…
+ Restauration terminée!
+ Restauration échouée
+
+
+
+ Rappels
+ Sauvegardes
+ Lecture de médias
+
+
+ Ceci n\'est pas une URL HTTPS valide.
+ Connecté en tant que %s.
+ Vous n\'êtes pas connecté.
+ S\'authentifier
+ Compte hors ligne
+ Connexion…
+ Il y a eu un problème.
+ Impossible de s\'authentifier : les identifiants sont invalides.
+ Connexion réussie!
+ La version du serveur n\'est pas compatible avec cette application.
+ Aucune connexion internet disponible.
+ Les identifiants ne peuvent pas être vides.
+ Effacer les identifiants et l\'adresse
+ Lecture / Pause
+ Arrêter
+
+
+ Prendre une note
+ Créer une liste
+
+
+ - +%d élément
+ - +%d éléments
+
+
+
+ - %d note sélectionnée
+ - %d notes sélectionnées
+
+
+
+ - %d carnet sélectionné
+ - %d carnets sélectionnés
+
+
+
+ - %d étiquette sélectionnée
+ - %d étiquettes sélectionnées
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
new file mode 100644
index 00000000..9d04ddaa
--- /dev/null
+++ b/app/src/main/res/values-it/strings.xml
@@ -0,0 +1,287 @@
+
+
+ Note
+ Archivio
+ Cestino
+ Impostazioni
+ Info
+ Etichette
+ Tutte le Note
+ Quaderni
+ I tuoi Quaderni
+
+
+ Generali
+ Tema
+ Scuro
+ Chiaro
+ Segui sistema
+
+ Vista
+ Disposizione
+ Griglia
+ Lista
+
+ Schema colore
+ Blu
+ Rosa
+ Verde
+ Arancione
+ Viola
+ Giallo
+ Rosso
+
+ Ordina per
+ Titolo (ascendente)
+ Titolo (discendente)
+ Data di creazione (ascendente)
+ Data di creazione (discendente)
+ Ultima modifica (ascendente)
+ Ultima modifica (discendente)
+
+ Mostra data di creazione/modifica
+ Formato data
+ Formato ora
+
+ Altro
+ Raggruppa note di nessun quaderno
+ Apri media in
+ Lettore interno
+ Lettore esterno
+ Cancella note nel cestino
+ Subito
+ Dopo 7 giorni
+ Dopo 14 giorni
+ Dopo 30 giorni
+
+ Backup
+ Crea un backup
+ Esporta le note in un file di backup.
+ Ripristina
+ Carica note da un file di backup.
+ Metodo di backup per gli allegati
+ Tutto
+ Solo descrizione e percorso locale
+ Nessuno
+
+ Sincro
+ Imposta sincro
+ In sincro con %s
+ Nessuna sincro
+ Servizio sincro
+ Nessuno
+ Nextcloud
+ Funzione sincro in fase sperimentale.\nPossono esserci dei bachi.
+ Tieni presente che Nextcloud Notes non supporta funzioni come etichette, allegati, promemoria, compiti ed altro.
+ Profilo Nextcloud
+ Istanza URL Nextcloud
+ Imposta credenziali
+ Imposta URL del server
+ Wi-Fi
+ Wi-Fi o rete cellulare
+ Sincro tramite
+ Sincro in background
+ Attivato
+ Disattivato
+ Sincro per nuove note
+
+
+
+ Versione
+ Sito
+ Sviluppatore
+ Contribuisci
+ Supporta
+ Creare e mantenere progetti open-source richiede tempo e non offre profitto.\nOffri una birra agli sviluppatori!
+
+ Librerie
+ Vedi librerie e licenze di terze parti.
+
+
+
+ Predefinito
+ Si
+ No
+ Ok
+ Le tue note appariranno qui.
+ Senza nome
+ Nome utente
+ Chiave
+ Salva
+ Annulla
+ Cestina
+ Fatto!
+ Cancella
+ Seleziona Tutto
+ Crea una lista
+ Mostra note nascoste
+ Disarchivia
+ Esporta
+ Nascondi
+ Mostra
+ Archivia
+ Fissa
+ Rilascia
+ Recupera
+ Condividi
+ Fissa / Rilascia
+ Muovi…
+ Duplica
+ Seleziona…
+
+
+
+ Modifica descrizione
+ Descrizione allegato
+ Registra audio
+ Registrando (%1$s)
+ Allega file
+ Fai una foto
+ Registrazione
+
+
+
+ Altro
+ Nuovo quaderno
+ Nome quaderno
+ Nessun Quaderno
+ Non hai quaderni.
+ Crea un quaderno
+ Rinomina quaderno
+ Nuovo quaderno
+ Quaderno di nome %1$s esistente.
+
+
+
+ Le tue note archiviate appariranno qui.
+
+
+
+ Svuota cestino
+ Confermare?
+ Le note cestinate andranno perse.
+ Le note nel cestino non si possono modificare.
+ Le tue note cestinate appariranno qui per %1$d giorni.
+ Note impostate per essere cancellate subito.\nPuoi cambiarlo in Impostazioni.
+ Note cancellate.
+ Nota cancellata.
+
+
+
+ Promemoria
+ Promemoria
+ Nome promemoria
+ Imposta data
+ Imposta ora
+ Nuovo promemoria
+ Hai un promemoria.
+ Promemoria per il passato non applicabile
+
+
+
+ Nuova etichetta
+ Nome etichetta
+ Non hai etichette.
+ Rinomina etichetta
+ Etichetta di nome %1$s esistente.
+
+
+
+ Cerca…
+ Cerca
+ I risultati della ricerca appariranno qui.
+ Nessun risultato trovato.
+
+
+
+ Titolo
+ Prendi nota!
+ Compito
+ Converti in nota
+ Converti in lista
+ Non sincronizzare
+ Cambia colore
+ Abilita markdown
+ Disabilita markdown
+ Inserisci markdown grassetto
+ Inserisci markdown corsivo
+ Inserisci markdown sottolineato
+ Inserisci markdown titolo
+ Inserisci markdown quotazione
+ Inserisci markdown codice
+ Creata il %1$s\nUltima modifica il %2$s
+ Note recuperate
+ Nota recuperata
+ Note archiviate
+ Nota archiviata
+ Note cestinate
+ Nota cestinata
+ Nota vuota scartata
+ Inserisci collegamento
+ Inserisci
+ Inserisci tabella
+ Numero di righe o colonne non valido
+ Inserisci immagine
+ Descrizione
+ Percorso immagine
+ Testo
+ URL
+ Numero di colonne
+ Numero di righe
+ Impossibile visualizzare l\'anteprima della tabella.
+
+
+ Ripristinando le tue note…
+ Backup completato!
+ Backup fallito
+ Recuperando le tue note…
+ Recupero completato
+ Recupero fallito
+
+
+
+ Promemoria
+ Backup
+ Riproduzione media
+
+
+ Url HTTPS non valido.
+ Sei collegato come %s.
+ Attualmente non collegato.
+ Autenticazione
+ Profilo non in linea
+ In connessione…
+ Qualcosa non ha funzionato.
+ Credenziali non valide.
+ Collegato con successo!
+ Versione del server non compatibile con questa app.
+ Connessione internet con disponibile.
+ Credenziali vuote non valide.
+ Pulisci credenziali e server URL
+ Avvia / Pausa
+ Ferma
+
+
+ Prendi una nota
+ Fai una lista
+
+
+ - +%d oggetto
+ - +%d oggetti
+
+
+
+ - Selezionata %d nota
+ - Selezionate %d note
+
+
+
+ - Selezionato %d quaderno
+ - Selezionati %d quaderni
+
+
+
+ - Selezionata %d etichetta
+ - Selezionate %d etichette
+
+
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index c6c615f3..efae718b 100755
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -24,7 +24,7 @@
- #B3FFFFFF
- #89000000
- #80FFFFFF
-
+ - #80FFFFFF
- #CC000000
- #CCF44336
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
new file mode 100644
index 00000000..c1dfeafd
--- /dev/null
+++ b/app/src/main/res/values-pl/strings.xml
@@ -0,0 +1,287 @@
+
+
+ Notatki
+ Archiwum
+ Kosz
+ Ustawienia
+ O aplikacji
+ Znaczniki
+ Wszystkie Notatki
+ Notatniki
+ Twoje Notatniki
+
+
+ Ogólne
+ Motyw
+ Ciemny
+ Jasny
+ Zgodny z systemem
+
+ Widok
+ Układ
+ Siatka
+ Lista
+
+ Schemat kolorystyczny
+ Niebieski
+ Różowy
+ Zielony
+ Pomarańczowy
+ Fioletowy
+ Żółty
+ Czerwony
+
+ Sortuj według
+ Tytuł (rosnąco)
+ Tytuł (malejąco)
+ Data utworzenia (rosnąco)
+ Data utworzenia (malejąco)
+ Data modyfikacji (rosnąco)
+ Data modyfikacji (malejąco)
+
+ Pokaż datę utworzenia/modyfikacji
+ Format daty
+ Format czasu
+
+ Inne
+ Grupuj notatki które nie są w żadnym notatniku
+ Otwieraj multimedia w
+ Wewnętrzny odtwarzacz
+ Zewnętrzny odtwarzacz
+ Usuwaj notatni w koszu
+ Natychmiastowo
+ Po 7 dniach
+ Po 14 dniach
+ Po 30 dniach
+
+ Kopia zapasowa
+ Utwórz kopię zapasową
+ Eksportuj wszystkie notatni do pliku z kopią.
+ Przywróć z kopii
+ Importuj notatni z pliku z kopią.
+ Reguła kopii zapasowej dla załączników
+ Kopiuj wszystko
+ Kopiuj jedynie opis i ścieżkę lokalną
+ Nie kopiuj załączników
+
+ Synchronizacja
+ Przejdź do ustawień synchronizacji
+ Aktualnie synchronizuje z %s
+ Aktualnie nie synchronizuje
+ Usługa synchronizacji
+ Wyłączona
+ Nextcloud
+ Synchronizacja jest w fazie eksperynetalnej.\nMożesz napotkać błędy.
+ Pamiętaj że notatki Nextcloud nie wspierają funkcji takich jak znaczniki, załączniki, przypomnienia, listy zadań itd.
+ Konto Nextcloud
+ Adres URL instancji Nextcloud
+ Ustaw dane logowania
+ Ustaw adres URL serwera
+ Wi-Fi
+ Wi-Fi lub Dane mobilne
+ Synchronizuj korzystając z
+ Synchronizacja w tle
+ Włączona
+ Wyłączona
+ Włącz synchronizację dla nowych notatek
+
+
+
+ Wersja
+ Strona internetowa
+ Programista
+ Pomóż w rozwoju
+ Wsparcie
+ Tworzenie i rozwijanie projektów open-source jest czasochłonne i nie przynosi dochodów.\nPostaw więc twórcom piwo!
+ Biblioteki
+ Zobacz biblioteki i licencje stron trzecich.
+
+
+
+ Domyślne
+ Tak
+ Nie
+ Okej
+ Twoje notatki pojawią się tutaj.
+ Bez tytułu
+ Nazwa użytkownika
+ Hasło
+ Zapisz
+ Anuluj
+ Usuń
+ Gotowe!
+ Usuń na stałe
+ Zaznacz wszystko
+ Stwórz listę
+ Pokaż ukryte notatki
+ Przywróć z archiwum
+ Eksportuj
+ Ukryj
+ Pokaż
+ Zarchiwizuj
+ Przypnij
+ Odepnij
+ Przywróć
+ Udostępnij
+ Przypnij / Odepnij
+ Przenieś do…
+ Duplikuj
+ Zaznacz więcej…
+
+
+
+ Edytuj opis
+ Opis załącznika
+ Nagraj audio
+ Nagrywanie (%1$s)
+ Dołącz pliki
+ Zrób zdjęcie
+ Nagranie
+
+
+
+ Inne
+ Nowy notatnik
+ Nazwa notatnika
+ Brak Notatnika
+ Nie masz notatników.
+ Stwórz notatnik
+ Zmień nazwę notatnika
+ Nowy notatnik
+ Notatnik z nazwą %1$s już istnieje.
+
+
+
+ Twoje zarchiwizowane notatki pojawią się tutaj.
+
+
+
+ Pusty kosz
+ Jesteś pewnien?
+ Wszystkie usunięte notatki nie będą możliwe do odzyskania.
+ Notatki w koszu nie mogą być zmieniane.
+ Twoje usunięte notatki pojawią się tutaj na %1$d dni.
+ Notatki są usuwane natychmiastowo.\nMożesz zmienić to w ustawieniach.
+ Usunięto notatki na stałe.
+ Usunięto notatkę na stałe.
+
+
+
+ Przypomnienia
+ Przypomnienie
+ Nazwa przypomnienia
+ Ustaw datę
+ Ustaw czas
+ Nowe przypomnienie
+ Masz przypomnienie.
+ Nie można ustawić przypomnienia na przeszłą datę
+
+
+
+ Nowy znacznik
+ Nazwa znacznika
+ Nie masz znaczników.
+ Zmień nazwę znacznika
+ Znacznik o nazwie %1$s już istnieje.
+
+
+
+ Szukaj…
+ Szukaj
+ Wyniki wyszukiwania będą widoczne tutaj.
+ Brak wyników.
+
+
+
+ Tytuł
+ Zanotuj coś!
+ Zadanie
+ Konwertuj na notatkę
+ Konwertuj na listę
+ Nie synchronizuj
+ Zmień kolor
+ Włącz markdown
+ Wyłącz markdown
+ Wstaw pogrubienie
+ Wstaw kursywę
+ Wstaw przekreślenie
+ Wstaw nagłówek
+ Wstaw cytat
+ Wstaw kod
+ Utworzono %1$s\nZmodyfikowano %2$s
+ Przywrócono notatki
+ Przywrócono notatkę
+ Zarchiwizowano notatki
+ Zarchiwizowano notatkę
+ Przeniesiono notatki do kosza
+ Przeniesiono notatkę do kosza
+ Odrzucono pustą notatkę
+ Wstaw link
+ Wstaw
+ Wstaw tabelę
+ Nieprawidłowa liczba kolumn lub wierszy
+ Wstaw obrazek
+ Opis
+ Ścieżka do obrazka
+ Tekst
+ URL
+ Liczba kolum
+ Liczba wierszy
+ Nie można wyświetlić podglądu tabeli.
+
+
+ Kopiowanie twoich notatek…
+ Stworzono kopię zapasową!
+ Tworznie kopii zapasowej nie powiodło się
+ Przywracanie twoich notatek…
+ Przywracanie zakończone
+ Wystąpił błąd przy przywracaniu
+
+
+
+ Przypomnienia
+ Kopie zapasowe
+ Multimedia
+
+
+ To nie jest poprawny adres HTTPS.
+ Jesteś zalogowany jako %s.
+ Nie jesteś zalogowany.
+ Autoryzuj
+ Konto lokalne
+ Łączenie…
+ Coś poszło nie tak.
+ Niepoprawne dane logowania.
+ Zalogowano pomyślnie!
+ Wersja serwera nie jest kompatybilna z tą aplikacją.
+ Połączenie internetowe nie jest dostępne.
+ Dane nie mogą być puste.
+ Wyczyść dane logowania i adres serwera
+ Play / Pauza
+ Zatrzymaj
+
+
+ Nowa notatka
+ Nowa lista
+
+
+
+ - +%d element
+ - +%d elementy
+
+
+
+ - Zaznaczono %d notatkę
+ - Zaznaczono %d notatki
+
+
+
+ - Zaznaczono %d notatnik
+ - Zaznaczono %d notatniki
+
+
+
+ - Zaznaczono %d znacznik
+ - Zaznaczono %d znaczniki
+
+
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 00000000..967399aa
--- /dev/null
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,288 @@
+
+
+ Anotações
+ Arquivo
+ Excluído
+ Configurações
+ Sobre
+ Etiquetas
+ Todas Anotações
+ Caderno de Notas
+ Seu Caderno de Notas
+
+
+ Geral
+ Modo de tema
+ Escuro
+ Claro
+ Seguir tema do sistema
+
+ Visualização
+ Modo de exibição
+ Grelha
+ Lista
+
+ Esquema de cores
+ Azul
+ Rosa
+ Verde
+ Laranja
+ Roxo
+ Amarelo
+ Vermelho
+
+ Ordenar por
+ Título (crescente)
+ Título (decrescente)
+ Data criado (crescente)
+ Data criado (decrescente)
+ Data modificado (crescente)
+ Data modificado (decrescente)
+
+ Mostrar data que foi criado/modificado
+ Formato de data
+ Formato de hora
+
+ Outro
+ Agrupar anotações que não estão em nenhum caderno de notas
+ Abrir mídia em
+ Tocador interno
+ Tocador externo
+ Excluir anotações na lixeira
+ Instantaneamente
+ Após 7 dias
+ Após 14 dias
+ Após 30 dias
+
+ Cópia de segurança
+ Criar uma cópia de segurança
+ Exportar todas as anotações em um arquivo de cópia de segurança.
+ Restaurar
+ Carregar anotações de um arquivo de cópia de segurança.
+ Estratégia de cópia de segurança para anexos
+ Criar cópia de tudo
+ Criar cópia apenas da descrição e do caminho
+ Não criar cópia dos anexos
+
+ Sincronização
+ Ir para configurações de sincronização
+ Sincronizando atualmente com %s
+ Atualmente não está sincronizando
+ Serviço de sincronia
+ Desabilitado
+ Nextcloud
+ Função de sincronização atualmente é experimental. Você pode encontrar bugs.
+ Tenha em mente que o Nextcloud Notes não suporta recursos como etiquetas, anexos, lembretes, listas de tarefas e mais.
+ Conta Nextcloud
+ URL de instancia do Nextcloud
+ Coloque suas credenciais
+ Coloque a URL do server
+ Wi-Fi
+ Wi-Fi ou Dados Móveis
+ Sincronizar quando estiver no
+ Sincronização em segundo plano
+ Habilitado
+ Desabilitado
+ Novas anotações sincronizáveis
+
+
+
+ Versão
+ Site
+ Desenvolvedor
+ Contribua
+ Suporte
+ Criar e manter projetos de código aberto consome tempo e
+ não gera renda.\nPague uma cerveja aos desenvolvedores!
+
+ Bibliotecas
+ Veja as bibliotecas de terceiro e suas licenças.
+
+
+
+ Padrão
+ Sim
+ Não
+ OK
+ Suas anotações aparecerão aqui.
+ Sem titulo
+ Nome de usuário
+ Senha
+ Salvar
+ Cancelar
+ Excluir
+ Feito!
+ Excluir permanentemente
+ Selecionar todas
+ Criar uma lista
+ Exibir anotações escondidas
+ Desarquivar
+ Exportar
+ Esconder
+ Exibir
+ Arquivar
+ Fixar
+ Desafixar
+ Restaurar
+ Compartilhar
+ Fixar / Desafixar
+ Mover para…
+ Duplicar
+ Selecionar mais…
+
+
+
+ Editar descrição
+ Descrição do anexo
+ Gravar áudio
+ Gravando (%1$s)
+ Anexar arquivos
+ Tirar uma foto
+ Clipe Gravado
+
+
+
+ Outro
+ Novo caderno de notas
+ Nome do caderno de notas
+ Nenhum Caderno
+ Você não tem nenhum caderno de notas.
+ Criar um caderno de notas
+ Renomear caderno de notas
+ Novo caderno de notas
+ Caderno de notas com o nome %1$s já existe.
+
+
+
+ Suas anotações arquivadas aparecerão aqui.
+
+
+
+ Esvaziar lixeira
+ Tem certeza?
+ Todas as anotações excluídas serão perdidas.
+ Anotações na lixeira não podem ser editadas.
+ Suas anotações excluídas aparecerão aqui por %1$d dias.
+ As anotações estão definidas para serem excluídas instantaneamente.\nVocê pode mudar isso em Configurações.
+ Anotações excluídas permanentemente.
+ Anotação excluída permanentemente.
+
+
+
+ Lembretes
+ Lembrete
+ Nome do lembrete
+ Definir data
+ Definir hora
+ Novo lembrete
+ Você tem um lembrete.
+ Não é possível definir um lembrete para uma data passada
+
+
+
+ Nova etiqueta
+ Nome da etiqueta
+ Você não tem etiquetas.
+ Renomear etiqueta
+ Etiqueta com o nome %1$s já existe.
+
+
+
+ Pesquisar…
+ Pesquisar
+ Resultados de busca aparecerão aqui.
+ Nenhum resultado encontrado.
+
+
+
+ Título
+ Faça uma anotação!
+ Tarefa
+ Converter para anotação
+ Converter para lista
+ Não sincronizar
+ Mudar cor
+ Habilitar marcação
+ Desabilitar marcação
+ Inserir marcador de negrito
+ Inserir marcador de itálico
+ Inserir marcador de tachado
+ Inserir marcador de cabeçalho
+ Inserir marcador de citação
+ Inserir marcador de código
+ Criado em %1$s\nUltima modificação em %2$s
+ Anotações restauradas
+ Anotação restaurada
+ Anotações arquivadas
+ Anotação arquivada
+ Anotações movidas para a lixeira
+ Anotação movida para a lixeira
+ Anotação vazia descartada
+ Inserir link
+ Inserir
+ Inserir tabela
+ Número de linhas e colunas inválido
+ Inserir imagem
+ Descrição
+ Caminho da imagem
+ Texto
+ URL
+ Número de colunas
+ Número de linhas
+ Não é possível visualizar a tabela.
+
+
+ Criando cópia de segurança das suas anotações…
+ Cópia de segurança concluída!
+ A cópia de segurança falhou
+ Restaurando suas anotações…
+ Restauração concluída
+ A restauração falhou
+
+
+
+ Lembrete
+ Cópias de segurança
+ Reprodução de mídia
+
+
+ Isto não é uma URL HTTPS válida.
+ Você atualmente está conectado como %s.
+ Você atualmente não está conectado.
+ Autenticar
+ Contas offline
+ Conectando…
+ Algo deu errado.
+ Não foi possível autenticar com o servidor devido a credenciais inválidas.
+ Conectado com sucesso!
+ Versão do servidor não é compatível com esse app.
+ Conexão com a Internet não está disponível.
+ As credenciais não podem ficar em branco.
+ Limpar credenciais e URL do servidor
+ Reproduzir / Pausar
+ Parar
+
+
+ Fazer uma anotação
+ Fazer uma lista
+
+
+ - +%d item
+ - +%d itens
+
+
+
+ - %d anotação selecionada
+ - %d anotações selecionadas
+
+
+
+ - %d caderno de notas selecionado
+ - %d cadernos de notas selecionados
+
+
+
+ - %d etiqueta selecionada
+ - %d etiquetas selecionadas
+
+
diff --git a/app/src/main/res/values/attr.xml b/app/src/main/res/values/attr.xml
index 03a89ab5..471a1f93 100755
--- a/app/src/main/res/values/attr.xml
+++ b/app/src/main/res/values/attr.xml
@@ -32,6 +32,7 @@
+
diff --git a/app/src/main/res/values/no_translate_strings.xml b/app/src/main/res/values/no_translate_strings.xml
index 162099d8..83520c4f 100644
--- a/app/src/main/res/values/no_translate_strings.xml
+++ b/app/src/main/res/values/no_translate_strings.xml
@@ -1,7 +1,7 @@
- Quillnote
- 1.2.0
+ Quillnote
+ 1.3.0
https://qosp.org
Michael Soultanidis
https://github.com/msoultanidis
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8b6e7086..60af76f4 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -39,10 +39,12 @@
Date modified (ascending)
Date modified (descending)
+ Show date created/modified
Date format
Time format
Other
+ Group notes which are not in any notebook
Open media in
Internal player
External player
@@ -140,6 +142,7 @@
+ Other
New notebook
Notebook name
No Notebook
@@ -215,7 +218,18 @@
Moved notes to bin
Moved note to bin
Empty note discarded
-
+ Insert link
+ Insert
+ Insert table
+ Invalid number of rows and columns
+ Insert image
+ Description
+ Image path
+ Text
+ URL
+ Number of columns
+ Number of rows
+ Cannot preview table.
Backing up your notes…
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 4f87da09..7b241afd 100755
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -10,6 +10,7 @@