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 + + 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 @@