harbour-quill 0.2.12: note markdown con sync WebDAV
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: page
|
||||
|
||||
property var dav
|
||||
property string notePath
|
||||
property string noteName
|
||||
property string initialContent: ""
|
||||
property bool skipLoad: false
|
||||
property bool loaded: false
|
||||
property bool dirty: false
|
||||
property string coverText: noteName
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === PageStatus.Active) {
|
||||
if (!loaded) {
|
||||
if (skipLoad) {
|
||||
// New note: content is already known, no GET needed.
|
||||
loaded = true
|
||||
editor.text = initialContent
|
||||
dirty = false
|
||||
} else {
|
||||
loadNote()
|
||||
}
|
||||
}
|
||||
} else if (status === PageStatus.Inactive) {
|
||||
// Auto-save on exit (Obsidian-like: nothing is ever lost).
|
||||
if (loaded && dirty && !dav.busy) saveNow(false)
|
||||
}
|
||||
}
|
||||
|
||||
function loadNote() {
|
||||
loaded = false
|
||||
dav.readFile(notePath)
|
||||
}
|
||||
|
||||
function saveNow(notify) {
|
||||
if (!loaded) return
|
||||
if (dav.busy) {
|
||||
banner.show(qsTr("Save in progress..."), false)
|
||||
return
|
||||
}
|
||||
dav.writeFile(notePath, editor.text)
|
||||
if (notify) banner.show(qsTr("Saving..."), false)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: dav
|
||||
onFileReady: {
|
||||
loaded = true
|
||||
initialContent = content
|
||||
editor.text = content
|
||||
dirty = false
|
||||
}
|
||||
onWriteDone: {
|
||||
initialContent = editor.text
|
||||
dirty = false
|
||||
banner.show(qsTr("Saved"), false)
|
||||
}
|
||||
onFailed: {
|
||||
if (loaded) {
|
||||
banner.show(message, true)
|
||||
} else {
|
||||
loaded = true
|
||||
banner.show(qsTr("Cannot open: %1").arg(message), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: column.height
|
||||
|
||||
Column {
|
||||
id: column
|
||||
width: parent.width
|
||||
|
||||
PageHeader { title: noteName }
|
||||
|
||||
TextArea {
|
||||
id: editor
|
||||
width: parent.width
|
||||
focus: true
|
||||
label: "Markdown"
|
||||
onTextChanged: {
|
||||
dirty = editor.text !== initialContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PullDownMenu {
|
||||
MenuItem {
|
||||
text: qsTr("Save")
|
||||
enabled: page.dirty && page.loaded
|
||||
onClicked: page.saveNow(true)
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Preview")
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("PreviewPage.qml"),
|
||||
{ dav: page.dav, noteName: page.noteName, notePath: page.notePath, markdown: editor.text })
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Delete note")
|
||||
onClicked: {
|
||||
var remorse = remorseComponent.createObject(page)
|
||||
remorse.execute(qsTr("Delete the note?"), function () {
|
||||
dav.deleteFile(notePath)
|
||||
pageStack.pop()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: remorseComponent
|
||||
RemorsePopup { }
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: !page.loaded
|
||||
}
|
||||
}
|
||||
|
||||
Banner { id: banner }
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: page
|
||||
|
||||
property var dav
|
||||
property string currentPath: ""
|
||||
property string coverText: currentPath === "" ? qsTr("Notes") : currentPath
|
||||
property bool loading: false
|
||||
property var pendingEditor: null
|
||||
property var allEntries: []
|
||||
property string filterText: ""
|
||||
|
||||
onFilterTextChanged: page.applyFilter()
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === PageStatus.Active) {
|
||||
if (page.pendingEditor) {
|
||||
// New note accepted: the dialog pops itself, then we open the
|
||||
// editor (push here — after the dialog pop — avoids stack races).
|
||||
var ed = page.pendingEditor
|
||||
page.pendingEditor = null
|
||||
pageStack.push(Qt.resolvedUrl("EditorPage.qml"),
|
||||
{ dav: page.dav,
|
||||
notePath: ed.path,
|
||||
noteName: ed.name,
|
||||
initialContent: "# " + ed.name.replace(/\.md$/, "") + "\n\n",
|
||||
skipLoad: true })
|
||||
} else {
|
||||
reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: reload()
|
||||
|
||||
function reload() {
|
||||
if (!dav || dav.busy) return
|
||||
loading = true
|
||||
dav.listDir(currentPath)
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
notesModel.clear()
|
||||
var f = page.filterText.trim().toLowerCase()
|
||||
for (var i = 0; i < allEntries.length; i++) {
|
||||
var e = allEntries[i]
|
||||
// All files are shown (not only .md) — any file can be opened as text.
|
||||
if (f && e.name.toLowerCase().indexOf(f) < 0) continue
|
||||
notesModel.append({ name: e.name, path: e.path, isDir: e.isDir, mtime: e.mtime, size: e.size })
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: notesModel
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: dav
|
||||
onListReady: {
|
||||
loading = false
|
||||
allEntries = entries
|
||||
console.log("Quill listReady:", entries.length, "entries in", page.currentPath)
|
||||
page.applyFilter()
|
||||
}
|
||||
// Refresh the list whenever a write completes elsewhere (new note,
|
||||
// editor save, delete) — the list under the stack stays fresh.
|
||||
onWriteDone: page.reload()
|
||||
onDeleteDone: page.reload()
|
||||
onFailed: {
|
||||
loading = false
|
||||
banner.show(message, true)
|
||||
}
|
||||
}
|
||||
|
||||
SilicaListView {
|
||||
id: listView
|
||||
anchors.fill: parent
|
||||
model: notesModel
|
||||
|
||||
header: Column {
|
||||
width: parent.width
|
||||
PageHeader { title: "Quill" }
|
||||
SearchField {
|
||||
id: searchField
|
||||
width: parent.width
|
||||
placeholderText: qsTr("Search")
|
||||
// Bound to the page property so the delegates (which cannot
|
||||
// see header ids) can clear it via page.filterText.
|
||||
text: page.filterText
|
||||
onTextChanged: {
|
||||
if (text !== page.filterText) page.filterText = text
|
||||
}
|
||||
}
|
||||
ListItem {
|
||||
visible: page.currentPath !== ""
|
||||
contentHeight: Theme.itemSizeSmall
|
||||
onClicked: {
|
||||
page.filterText = ""
|
||||
page.currentPath = ""
|
||||
page.reload()
|
||||
}
|
||||
Label {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: Theme.paddingLarge
|
||||
width: parent.width - 2 * Theme.paddingLarge
|
||||
color: Theme.highlightColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
text: "▲ " + page.currentPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ViewPlaceholder {
|
||||
enabled: notesModel.count === 0 && !page.loading
|
||||
text: page.currentPath === "" ? qsTr("No notes yet. Create one from the pulley menu.")
|
||||
: qsTr("Empty folder")
|
||||
}
|
||||
|
||||
delegate: ListItem {
|
||||
id: noteItem
|
||||
contentHeight: Theme.itemSizeMedium
|
||||
onClicked: {
|
||||
if (model.isDir) {
|
||||
page.filterText = ""
|
||||
page.currentPath = model.path
|
||||
page.reload()
|
||||
} else {
|
||||
pageStack.push(Qt.resolvedUrl("EditorPage.qml"),
|
||||
{ dav: page.dav, notePath: model.path, noteName: model.name })
|
||||
}
|
||||
}
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: Theme.paddingLarge
|
||||
width: parent.width - 2 * Theme.paddingLarge
|
||||
Label {
|
||||
text: model.name + (model.isDir ? "/" : "")
|
||||
color: model.isDir ? Theme.highlightColor : Theme.primaryColor
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
Label {
|
||||
visible: !model.isDir
|
||||
text: model.mtime
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
truncationMode: TruncationMode.Fade
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
|
||||
PullDownMenu {
|
||||
MenuItem {
|
||||
text: qsTr("Settings")
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"), { dav: page.dav })
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("New note")
|
||||
onClicked: {
|
||||
var dlg = pageStack.push(Qt.resolvedUrl("NewNoteDialog.qml"), { dav: page.dav })
|
||||
dlg.accepted.connect(function () {
|
||||
if (dlg.resultPath) {
|
||||
page.pendingEditor = { path: dlg.resultPath, name: dlg.resultName }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Refresh")
|
||||
onClicked: page.reload()
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: page.loading
|
||||
}
|
||||
}
|
||||
|
||||
Banner { id: banner }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
Dialog {
|
||||
id: dialog
|
||||
|
||||
property var dav
|
||||
property string resultPath: ""
|
||||
property string resultName: ""
|
||||
|
||||
canAccept: nameField.text.length > 0
|
||||
|
||||
onAccepted: {
|
||||
var name = nameField.text.trim()
|
||||
if (!name) return
|
||||
var lower = name.toLowerCase()
|
||||
var hasExt = lower.indexOf(".md") === lower.length - 3
|
||||
resultName = hasExt ? name : name + ".md"
|
||||
resultPath = resultName
|
||||
dav.writeFile(resultPath, "# " + resultName.replace(/\.md$/, "") + "\n\n")
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
|
||||
DialogHeader { title: qsTr("New note") }
|
||||
|
||||
TextField {
|
||||
id: nameField
|
||||
width: parent.width
|
||||
label: qsTr("Name")
|
||||
placeholderText: qsTr("e.g. Idea")
|
||||
EnterKey.enabled: text.length > 0
|
||||
EnterKey.onClicked: dialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../components/Markdown.js" as Markdown
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: page
|
||||
|
||||
property var dav
|
||||
property string noteName
|
||||
property string notePath: ""
|
||||
property string markdown: ""
|
||||
property string htmlText: ""
|
||||
property string baseUrl: ""
|
||||
property var imageMap: ({})
|
||||
property string pendingOpen: ""
|
||||
|
||||
Component.onCompleted: {
|
||||
// Base for relative links: the note's WebDAV folder. Relative links
|
||||
// are resolved here so Qt rich text never resolves them against the
|
||||
// page's .qml file (opening those fails with "text/qml not supported").
|
||||
baseUrl = String(appSettings.serverUrl).replace(/\s+$/, "")
|
||||
if (notePath && notePath.indexOf("/") >= 0) {
|
||||
baseUrl += "/" + notePath.substring(0, notePath.lastIndexOf("/"))
|
||||
}
|
||||
renderPreview()
|
||||
extractImages()
|
||||
}
|
||||
|
||||
function resolveImg(u) {
|
||||
if (/^[a-z]+:/i.test(u) || u.charAt(0) === "/" || u.charAt(0) === "#") return u
|
||||
return String(page.baseUrl).replace(/\/+$/, "") + "/" + u
|
||||
}
|
||||
|
||||
function renderPreview() {
|
||||
try {
|
||||
htmlText = Markdown.render(markdown, baseUrl,
|
||||
{ imageMap: page.imageMap, imageLabel: qsTr("image") })
|
||||
} catch (e) {
|
||||
console.log("Quill markdown render error:", e)
|
||||
htmlText = "<tt>" + String(markdown).replace(/&/g, "&").replace(/</g, "<") + "</tt>"
|
||||
}
|
||||
}
|
||||
|
||||
function extractImages() {
|
||||
var re = /!\[[^\]]*\]\(([^)\s]+)\)/g
|
||||
var m
|
||||
while ((m = re.exec(markdown)) !== null) {
|
||||
var url = resolveImg(m[1])
|
||||
if (page.imageMap[url] === undefined) {
|
||||
page.imageMap[url] = "" // downloading
|
||||
downloader.fetch(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openWikiLink(name) {
|
||||
var dir = ""
|
||||
if (notePath && notePath.indexOf("/") >= 0) {
|
||||
dir = notePath.substring(0, notePath.lastIndexOf("/"))
|
||||
}
|
||||
page.pendingOpen = dir ? dir + "/" + name : name
|
||||
dav.readFile(page.pendingOpen)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: dav
|
||||
onFileReady: {
|
||||
if (page.pendingOpen) {
|
||||
var p = page.pendingOpen
|
||||
page.pendingOpen = ""
|
||||
var parts = p.split("/")
|
||||
pageStack.push(Qt.resolvedUrl("EditorPage.qml"),
|
||||
{ dav: page.dav, notePath: p, noteName: parts[parts.length - 1],
|
||||
initialContent: content, skipLoad: true })
|
||||
}
|
||||
}
|
||||
onFailed: {
|
||||
if (page.pendingOpen) {
|
||||
page.pendingOpen = ""
|
||||
banner.show(qsTr("Cannot open the note: %1").arg(message), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: downloader
|
||||
onFinished: {
|
||||
if (page.imageMap[url] === undefined) return
|
||||
page.imageMap[url] = ok ? path : "error"
|
||||
page.renderPreview()
|
||||
}
|
||||
}
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: previewLabel.height + Theme.paddingLarge * 2
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
|
||||
PageHeader { title: noteName }
|
||||
|
||||
Label {
|
||||
id: previewLabel
|
||||
x: Theme.paddingLarge
|
||||
width: parent.width - 2 * Theme.paddingLarge
|
||||
text: page.htmlText
|
||||
textFormat: Text.RichText
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
linkColor: Theme.highlightColor
|
||||
onLinkActivated: {
|
||||
var l = String(link)
|
||||
if (l.indexOf("quill://") === 0) {
|
||||
page.openWikiLink(decodeURIComponent(l.substring("quill://".length)))
|
||||
} else if (l.indexOf("file:") === 0) {
|
||||
// A link that still resolved to a local file (no base):
|
||||
// opening it would fail with "text/qml not supported".
|
||||
banner.show(qsTr("Local link not supported"), true)
|
||||
} else {
|
||||
Qt.openUrlExternally(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
|
||||
PullDownMenu {
|
||||
MenuItem {
|
||||
text: qsTr("Edit")
|
||||
onClicked: pageStack.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Banner { id: banner }
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import QtQuick 2.2
|
||||
import Sailfish.Silica 1.0
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: page
|
||||
|
||||
property var dav
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: column.height
|
||||
|
||||
Column {
|
||||
id: column
|
||||
width: parent.width
|
||||
|
||||
PageHeader { title: qsTr("Settings") }
|
||||
|
||||
SectionHeader { text: qsTr("Connection") }
|
||||
|
||||
TextField {
|
||||
id: serverField
|
||||
width: parent.width
|
||||
label: qsTr("WebDAV URL (folder)")
|
||||
placeholderText: "https://host/remote.php/dav/files/user/Note"
|
||||
text: appSettings.serverUrl
|
||||
onTextChanged: appSettings.serverUrl = text
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: userField
|
||||
width: parent.width
|
||||
label: qsTr("Username")
|
||||
text: appSettings.username
|
||||
onTextChanged: appSettings.username = text
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: passField
|
||||
width: parent.width
|
||||
label: qsTr("Password")
|
||||
echoMode: TextInput.Password
|
||||
text: appSettings.password
|
||||
onTextChanged: appSettings.password = text
|
||||
}
|
||||
|
||||
Button {
|
||||
width: parent.width
|
||||
text: qsTr("Test connection")
|
||||
onClicked: {
|
||||
if (!appSettings.serverUrl) {
|
||||
banner.show(qsTr("Enter the folder URL"), true)
|
||||
return
|
||||
}
|
||||
banner.show(qsTr("Checking..."), false)
|
||||
dav.testConnection()
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader { text: qsTr("Language") }
|
||||
|
||||
ComboBox {
|
||||
id: langBox
|
||||
width: parent.width
|
||||
label: qsTr("Language")
|
||||
currentIndex: appSettings.language === "it" ? 2 : (appSettings.language === "en" ? 1 : 0)
|
||||
menu: ContextMenu {
|
||||
MenuItem { text: qsTr("System (default)") }
|
||||
MenuItem { text: "English" }
|
||||
MenuItem { text: "Italiano" }
|
||||
}
|
||||
onCurrentIndexChanged: {
|
||||
var v = currentIndex === 2 ? "it" : (currentIndex === 1 ? "en" : "system")
|
||||
if (v !== appSettings.language) {
|
||||
appSettings.language = v
|
||||
banner.show(qsTr("Language will be applied after restarting the app"), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
x: Theme.paddingLarge
|
||||
width: parent.width - 2 * Theme.paddingLarge
|
||||
wrapMode: Text.Wrap
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
text: qsTr("On Nextcloud: Settings → Security → App passwords. ") +
|
||||
qsTr("The URL is https://host/remote.php/dav/files/USER/FOLDER. ") +
|
||||
qsTr("The password is stored locally in the app config file.")
|
||||
}
|
||||
|
||||
SectionHeader { text: qsTr("Notes") }
|
||||
|
||||
Label {
|
||||
x: Theme.paddingLarge
|
||||
width: parent.width - 2 * Theme.paddingLarge
|
||||
wrapMode: Text.Wrap
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
text: qsTr("The app saves automatically when you leave a note. ") +
|
||||
qsTr("Supported: headings, bold, italic, code, lists, quotes, links, tables and rules.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: dav
|
||||
onConnOk: banner.show(qsTr("Connection OK"), false)
|
||||
onFailed: banner.show(message, true)
|
||||
}
|
||||
|
||||
Banner { id: banner }
|
||||
}
|
||||
Reference in New Issue
Block a user