From 147172fe720f8af8a219ba384c0407ca751e0321 Mon Sep 17 00:00:00 2001 From: Florrie Date: Wed, 23 Oct 2019 12:20:27 -0300 Subject: Add text/notes editor, using tui-text-editor :D! --- ui.js | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 160 insertions(+), 9 deletions(-) (limited to 'ui.js') diff --git a/ui.js b/ui.js index 03337f5..cfb3e87 100644 --- a/ui.js +++ b/ui.js @@ -15,9 +15,12 @@ const { cloneGrouplike, countTotalTracks, flattenGrouplike, + getCorrespondingFileForItem, getItemPath, getNameWithoutTrackNumber, isGroup, + isOpenable, + isPlayable, isTrack, parentSymbol, reverseOrderOfGroups, @@ -47,10 +50,15 @@ const { } } = require('tui-lib') -const open = require('open') +const TuiTextEditor = require('tui-text-editor') -const isPlayable = item => isGroup(item) || isTrack(item) -const isOpenable = item => !!(item && item.url) +const { promisify } = require('util') +const fs = require('fs') +const open = require('open') +const path = require('path') +const url = require('url') +const readFile = promisify(fs.readFile) +const writeFile = promisify(fs.writeFile) const input = {} @@ -101,6 +109,10 @@ const keyBindings = [ ['isActOnPlayer', [0x1b, 'a']], ['isActOnPlayer', [0x1b, '!']], + ['isSaveTextEditor', [0x13]], // ^S + ['isDeselectTextEditor', [0x18]], // ^X + ['isDeselectTextEditor', telc.isEscape], + // Number pad ['isUp', '8'], ['isDown', '2'], @@ -200,6 +212,15 @@ class AppElement extends FocusElement { this.queuePane = new Pane() this.addChild(this.queuePane) + this.textInfoPane = new Pane() + this.addChild(this.textInfoPane) + + this.textEditor = new NotesTextEditor() + this.textInfoPane.addChild(this.textEditor) + this.textInfoPane.visible = false + + this.textEditor.on('deselect', () => this.root.select(this.tabber)) + if (!this.config.showTabberPane) { this.tabberPane.visible = false } @@ -677,6 +698,20 @@ class AppElement extends FocusElement { grouplikeListing.pathElement.on('select', item => this.reveal(item)) grouplikeListing.on('menu', (item, el) => this.showMenuForItemElement(el, grouplikeListing)) + + grouplikeListing.on('selected', async item => { + const status = await this.textEditor.openItem(item, { + doubleCheckItem: () => item === grouplikeListing.currentItem + }) + + if (status === true) { + this.textInfoPane.visible = true + this.fixLayout() + } else if (status === false) { + this.textInfoPane.visible = false + this.fixLayout() + } + }) } showContextMenu(opts) { @@ -800,6 +835,10 @@ class AppElement extends FocusElement { openSpecialOrThroughSystem(item) { if (item.url.endsWith('.json')) { return this.handlePlaylistSource(item.url, true) + } else if (item.url.endsWith('.txt')) { + if (this.textInfoPane.visible) { + this.root.select(this.textEditor) + } } else { return this.openThroughSystem(item) } @@ -921,11 +960,9 @@ class AppElement extends FocusElement { canProcessMetadata && isGroup(item) && {label: 'Process metadata (new entries)', action: () => this.processMetadata(item, false)}, canProcessMetadata && isGroup(item) && {label: 'Process metadata (reprocess)', action: () => this.processMetadata(item, true)}, canProcessMetadata && isTrack(item) && {label: 'Process metadata', action: () => this.processMetadata(item, true)}, - canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)}, - {divider: true}, - isOpenable(item) && item.url.endsWith('.json') && {label: 'Open (JSON Playlist)', action: () => this.openSpecialOrThroughSystem(item)}, isOpenable(item) && {label: 'Open (System)', action: () => this.openThroughSystem(item)}, + canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)}, {divider: true}, item === this.markGrouplike && {label: 'Deselect', action: () => this.deselectAll()} @@ -999,6 +1036,7 @@ class AppElement extends FocusElement { await this.backend.stopPlayingAll() } + await this.textEditor.save() this.emit('quitRequested') } @@ -1057,15 +1095,37 @@ class AppElement extends FocusElement { this.alignPartyLabel() } + const leftWidth = Math.max(Math.floor(0.8 * this.contentW), this.contentW - 80) + + if (this.textInfoPane.visible) { + this.textInfoPane.w = leftWidth + if (this.textEditor.isSelected) { + this.textInfoPane.h = 8 + } else { + this.textEditor.w = this.textInfoPane.contentW + this.textInfoPane.h = Math.min(8, this.textEditor.getOptimalHeight() + 2) + } + + this.textEditor.fillParent() + this.textEditor.fixLayout() + } + if (this.tabberPane.visible) { - this.tabberPane.w = Math.max(Math.floor(0.8 * this.contentW), this.contentW - 80) + this.tabberPane.w = leftWidth this.tabberPane.y = bottomY this.tabberPane.h = topY - this.tabberPane.y + if (this.textInfoPane.visible) { + this.tabberPane.h -= this.textInfoPane.h + this.textInfoPane.y = this.tabberPane.bottom + } this.queuePane.x = this.tabberPane.right this.queuePane.w = this.contentW - this.tabberPane.right } else { this.queuePane.x = 0 this.queuePane.w = this.contentW + if (this.textInfoPane.visible) { + this.textInfoPane.y = bottomY + } } this.queuePane.y = bottomY @@ -1440,9 +1500,10 @@ class GrouplikeListingElement extends Form { this.form = this.getNewForm() this.addInput(this.form) - this.form.on('selected input', input => { + this.form.on('selected', input => { if (input && this.pathElement) { this.pathElement.showItem(input.item) + this.emit('selected', input.item) } }) @@ -1490,6 +1551,7 @@ class GrouplikeListingElement extends Form { selected() { this.curIndex = 0 this.root.select(this.form) + this.emit('selected', this.currentItem) } clicked(button) { @@ -1782,7 +1844,7 @@ class GrouplikeListingForm extends ListScrollForm { set curIndex(newIndex) { this.setDep('curIndex', newIndex) - this.emit('selected input', this.inputs[this.curIndex]) + this.emit('selected', this.inputs[this.curIndex]) return newIndex } @@ -3884,4 +3946,93 @@ class PartyBanner extends DisplayElement { } } +class NotesTextEditor extends TuiTextEditor { + constructor() { + super() + + this.openedItem = null + } + + keyPressed(keyBuf) { + if (input.isDeselectTextEditor(keyBuf)) { + this.emit('deselect') + return false + } else if (input.isSaveTextEditor(keyBuf)) { + this.saveManually() + return false + } else { + return super.keyPressed(keyBuf) + } + } + + async openItem(item, {doubleCheckItem}) { + if (this.hasBeenEdited) { + // Save in the background. + this.save() + } + + const textFile = getCorrespondingFileForItem(item, '.txt') + if (!textFile) { + this.openedItem = null + return false + } + + if (textFile === this.openedItem) { + // This file is already open - don't do anything. + return null + } + + let filePath + try { + filePath = url.fileURLToPath(new URL(textFile.url)) + } catch (error) { + this.openedItem = null + return false + } + + let buffer + try { + buffer = await readFile(filePath) + } catch (error) { + this.openedItem = null + return false + } + + if (!doubleCheckItem(item)) { + return null + } + + this.openedItem = textFile + this.openedPath = filePath + this.clearSourceAndLoadText(buffer.toString()) + return true + } + + async saveManually() { + if (!this.openedItem || !this.openedPath) { + return + } + + const item = this.openedItem + + if (await this.save()) { + if (item === this.openedItem) { + this.showStatusMessage('Saved manually.') + } + } + } + + async save() { + const text = this.getSourceText() + try { + await writeFile(this.openedPath, text) + this.clearEditStatus() + return true + } catch (error) { + this.showStatusMessage(`Failed to save (${path.basename(this.openedPath)}: ${error.code}).`) + return false + } + } +} + module.exports = AppElement -- cgit 1.3.0-6-gf8a5