store images in notes, basic structure

This commit is contained in:
azivner 2018-11-08 10:11:00 +01:00
parent 5f427e37fe
commit d0d2a7fe47
24 changed files with 11589 additions and 141 deletions

View File

@ -0,0 +1,56 @@
-- allow null for note content (for deleted notes)
CREATE TABLE IF NOT EXISTS "notes_mig" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "note",
`content` TEXT NULL DEFAULT NULL,
`isProtected` INT NOT NULL DEFAULT 0,
`type` TEXT NOT NULL DEFAULT 'text',
`mime` TEXT NOT NULL DEFAULT 'text/html',
`hash` TEXT DEFAULT "" NOT NULL,
`isDeleted` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
PRIMARY KEY(`noteId`)
);
INSERT INTO notes_mig (noteId, title, content, isProtected, isDeleted, dateCreated, dateModified, type, mime, hash)
SELECT noteId, title, content, isProtected, isDeleted, dateCreated, dateModified, type, mime, hash FROM notes;
DROP TABLE notes;
ALTER TABLE notes_mig RENAME TO notes;
CREATE TABLE "links" (
`linkId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`targetNoteId` TEXT NOT NULL,
`type` TEXT NOT NULL,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
PRIMARY KEY(`linkId`)
);
INSERT INTO links (linkId, noteId, targetNoteId, type, isDeleted, dateCreated, dateModified)
SELECT 'L' || SUBSTR(noteImageId, 1), noteId, imageId, 'image', isDeleted, dateCreated, dateModified FROM note_images;
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified, hash, dateCreated)
SELECT 'B' || SUBSTR(noteImageId, 1), imageId, noteId, 100, '', 0, isDeleted, dateModified, hash, dateCreated FROM note_images;
DROP TABLE note_images;
INSERT INTO notes (noteId, title, content, isProtected, isDeleted, dateCreated, dateModified, type, mime, hash)
SELECT imageId, name, data, 0, isDeleted, dateCreated, dateModified, 'image', 'image/' || format, hash FROM images;
DROP TABLE images;
UPDATE sync SET entityName = 'notes' WHERE entityName = 'images';
INSERT INTO sync (entityName, entityId, sourceId, syncDate)
SELECT 'links', 'L' || SUBSTR(entityId, 1), sourceId, syncDate FROM sync WHERE entityName = 'note_images';
INSERT INTO sync (entityName, entityId, sourceId, syncDate)
SELECT 'branches', 'B' || SUBSTR(entityId, 1), sourceId, syncDate FROM sync WHERE entityName = 'note_images';
DELETE FROM sync WHERE entityName = 'note_images';
DELETE FROM sync WHERE entityName = 'images';

View File

@ -2,6 +2,7 @@ const Note = require('../entities/note');
const NoteRevision = require('../entities/note_revision');
const Image = require('../entities/image');
const NoteImage = require('../entities/note_image');
const Link = require('../entities/link');
const Branch = require('../entities/branch');
const Attribute = require('../entities/attribute');
const RecentNote = require('../entities/recent_note');
@ -38,6 +39,9 @@ function createEntityFromRow(row) {
else if (row.noteRevisionId) {
entity = new NoteRevision(row);
}
else if (row.linkId) {
entity = new Link(row);
}
else if (row.noteImageId) {
entity = new NoteImage(row);
}

51
src/entities/link.js Normal file
View File

@ -0,0 +1,51 @@
"use strict";
const Entity = require('./entity');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
/**
* This class represents link from one note to another in the form of hyperlink or image reference. Note that
* this is different concept than attribute/relation.
*
* @param {string} linkId
* @param {string} noteId
* @param {string} targetNoteId
* @param {string} type
* @param {boolean} isDeleted
* @param {string} dateModified
* @param {string} dateCreated
*
* @extends Entity
*/
class Link extends Entity {
static get entityName() { return "links"; }
static get primaryKeyName() { return "linkId"; }
static get hashedProperties() { return ["linkId", "noteId", "targetNoteId", "type", "isDeleted", "dateCreated", "dateModified"]; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
}
async getTargetNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.targetNoteId]);
}
beforeSaving() {
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
super.beforeSaving();
if (this.isChanged) {
this.dateModified = dateUtils.nowDate();
}
}
}
module.exports = Link;

View File

@ -487,6 +487,13 @@ class Note extends Entity {
return await repository.getEntities("SELECT * FROM note_images WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
}
/**
* @returns {Promise<Link[]>}
*/
async getLinks() {
return await repository.getEntities("SELECT * FROM links WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
}
/**
* @returns {Promise<Branch[]>}
*/

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

View File

@ -31,8 +31,8 @@ $("#import-upload").change(async function() {
data: formData,
dataType: 'json',
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
contentType: false, // NEEDED, DON'T REMOVE THIS
processData: false, // NEEDED, DON'T REMOVE THIS
})
.fail((xhr, status, error) => alert('Import error: ' + xhr.responseText))
.done(async note => {

View File

@ -238,7 +238,10 @@ async function loadNoteDetail(noteId) {
async function showChildrenOverview() {
const note = getCurrentNote();
const attributes = await attributePromise;
const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview') || note.type === 'relation-map';
const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview')
|| note.type === 'relation-map'
|| note.type === 'image'
|| note.type === 'file';
if (hideChildrenOverview) {
$childrenOverview.hide();

View File

@ -0,0 +1,52 @@
import utils from "./utils.js";
import server from "./server.js";
import protectedSessionHolder from "./protected_session_holder.js";
import noteDetailService from "./note_detail.js";
const $noteDetailFile = $('#note-detail-file');
const $fileFileName = $("#file-filename");
const $fileFileType = $("#file-filetype");
const $fileFileSize = $("#file-filesize");
const $fileDownload = $("#file-download");
const $fileOpen = $("#file-open");
async function show() {
const currentNote = noteDetailService.getCurrentNote();
const attributes = await server.get('notes/' + currentNote.noteId + '/attributes');
const attributeMap = utils.toObject(attributes, l => [l.name, l.value]);
$noteDetailFile.show();
$fileFileName.text(attributeMap.originalFileName);
$fileFileSize.text(attributeMap.fileSize + " bytes");
$fileFileType.text(currentNote.mime);
}
$fileDownload.click(() => utils.download(getFileUrl()));
$fileOpen.click(() => {
if (utils.isElectron()) {
const open = require("open");
open(getFileUrl());
}
else {
window.location.href = getFileUrl();
}
});
function getFileUrl() {
// electron needs absolute URL so we extract current host, port, protocol
return utils.getHost() + "/api/notes/" + noteDetailService.getCurrentNoteId()
+ "/download?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId());
}
export default {
show,
getContent: () => null,
focus: () => null,
onNoteChange: () => null,
cleanup: () => null
}

View File

@ -81,7 +81,10 @@ function NoteTypeModel() {
return 'Relation Map';
}
else if (type === 'search') {
// ignore and do nothing, "type" will be hidden since it's not possible to switch to and from search
return 'Search note'
}
else if (type === 'image') {
return 'Image'
}
else {
infoService.throwError('Unrecognized type: ' + type);
@ -89,7 +92,7 @@ function NoteTypeModel() {
};
this.isDisabled = function() {
return self.type() === "file";
return ["file", "image", "search"].includes(self.type());
};
async function save() {

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -136,6 +136,10 @@ span.fancytree-node.relation-map > span.fancytree-icon {
background: url("../images/icons/relation-map-16.png") 0 0;
}
span.fancytree-node.image > span.fancytree-icon {
background: url("../images/icons/image-16.png") 0 0;
}
span.fancytree-node.render > span.fancytree-icon {
background: url("../images/icons/play-16.png") 0 0;
}

View File

@ -12,8 +12,8 @@ async function getNote(req) {
return [404, "Note " + noteId + " has not been found."];
}
if (note.type === 'file') {
// no need to transfer (potentially large) file payload for this request
if (note.type === 'file' || note.type === 'image') {
// no need to transfer (potentially large) file/image payload for this request
note.content = null;
}

View File

@ -3,8 +3,8 @@
const build = require('./build');
const packageJson = require('../../package');
const APP_DB_VERSION = 114;
const SYNC_VERSION = 1;
const APP_DB_VERSION = 115;
const SYNC_VERSION = 2;
module.exports = {
appVersion: packageJson.version,

View File

@ -209,7 +209,13 @@ async function runAllChecks() {
FROM
notes
WHERE
type != 'text' AND type != 'code' AND type != 'render' AND type != 'file' AND type != 'search' AND type != 'relation-map'`,
type != 'text'
AND type != 'code'
AND type != 'render'
AND type != 'file'
AND type != 'image'
AND type != 'search'
AND type != 'relation-map'`,
"Note has invalid type", errorList);
await runCheck(`

View File

@ -247,7 +247,8 @@ const primaryKeys = {
"note_images": "noteImageId",
"api_tokens": "apiTokenId",
"options": "name",
"attributes": "attributeId"
"attributes": "attributeId",
"links": "linkId"
};
async function getEntityRow(entityName, entityId) {

View File

@ -28,6 +28,10 @@ async function addRecentNoteSync(branchId, sourceId) {
await addEntitySync("recent_notes", branchId, sourceId);
}
async function addLinkSync(linkId, sourceId) {
await addEntitySync("links", linkId, sourceId);
}
async function addImageSync(imageId, sourceId) {
await addEntitySync("images", imageId, sourceId);
}
@ -91,10 +95,9 @@ async function fillAllSyncRows() {
await fillSyncRows("branches", "branchId");
await fillSyncRows("note_revisions", "noteRevisionId");
await fillSyncRows("recent_notes", "branchId");
await fillSyncRows("images", "imageId");
await fillSyncRows("note_images", "noteImageId");
await fillSyncRows("attributes", "attributeId");
await fillSyncRows("api_tokens", "apiTokenId");
await fillSyncRows("links", "linkId");
await fillSyncRows("options", "name", 'isSynced = 1');
}

View File

@ -24,6 +24,9 @@ async function updateEntity(sync, entity, sourceId) {
else if (entityName === 'recent_notes') {
await updateRecentNotes(entity, sourceId);
}
else if (entityName === 'links') {
await updateLink(entity, sourceId);
}
else if (entityName === 'images') {
await updateImage(entity, sourceId);
}
@ -139,6 +142,20 @@ async function updateRecentNotes(entity, sourceId) {
}
}
async function updateLink(entity, sourceId) {
const origLink = await sql.getRow("SELECT * FROM links WHERE linkId = ?", [entity.linkId]);
if (!origLink || origLink.dateModified <= entity.dateModified) {
await sql.transactional(async () => {
await sql.replace("links", entity);
await syncTableService.addLinkSync(entity.linkId, sourceId);
});
log.info("Update/sync link " + entity.linkId);
}
}
async function updateImage(entity, sourceId) {
if (entity.data !== null) {
entity.data = Buffer.from(entity.data, 'base64');

View File

@ -0,0 +1,28 @@
<div id="note-detail-wrapper">
<div id="note-detail-script-area"></div>
<table id="note-detail-promoted-attributes"></table>
<div id="note-detail-component-wrapper">
<div id="note-detail-text" class="note-detail-component" tabindex="10000"></div>
<div id="note-detail-code" class="note-detail-component"></div>
<input type="file" id="file-upload" style="display: none" />
<% include search.ejs %>
<% include render.ejs %>
<% include file.ejs %>
<% include relation_map.ejs %>
</div>
<div id="children-overview"></div>
<div id="attribute-list">
<button class="btn btn-sm show-attributes-button">Attributes:</button>
<span id="attribute-list-inner"></span>
</div>
</div>

View File

@ -0,0 +1,23 @@
<div id="note-detail-file" class="note-detail-component">
<table id="file-table">
<tr>
<th>File name:</th>
<td id="file-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="file-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="file-filesize"></td>
</tr>
<tr>
<td>
<button id="file-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="file-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div>

View File

@ -0,0 +1,25 @@
<div id="note-detail-relation-map" class="note-detail-component">
<button id="relation-map-add-child-notes" class="btn" type="button"
title="Add all child notes of this relation map note">Add child notes</button>
&nbsp;
<button id="relation-map-create-child-note" class="btn" type="button"
title="Create new child note and add it into this relation map">Create child note</button>
<div class="btn-group" style="float: right; padding-right: 20px;">
<button type="button"
class="btn icon-button24"
title="Zoom In"
id="relation-map-zoom-in"
style="background-image: url('/images/icons/zoom-in-24.png');"/>
<button type="button"
class="btn icon-button24"
title="Zoom Out"
id="relation-map-zoom-out"
style="background-image: url('/images/icons/zoom-out-24.png');"/>
</div>
<div id="relation-map-canvas"></div>
</div>

View File

@ -0,0 +1,9 @@
<div id="note-detail-render" class="note-detail-component">
<div id="note-detail-render-help" class="alert alert-warning">
<p><strong>This help note is shown because this note of type Render HTML doesn't have required relation to function properly.</strong></p>
<p>Render HTML note type is used for <a href="https://github.com/zadam/trilium/wiki/Scripts">scripting</a>. In short, you have a HTML code note (optionally with some JavaScript) and this note will render it. To make it work, you need to define a relation (in <a class="show-attributes-button">Attributes dialog</a>) called "renderNote" pointing to the HTML note to render. Once that's defined you can click on the "play" button to render.</p>
</div>
<div id="note-detail-render-content"></div>
</div>

View File

@ -0,0 +1,39 @@
<div id="note-detail-search" class="note-detail-component">
<div style="display: flex; align-items: center;">
<strong>Search string: &nbsp; &nbsp;</strong>
<textarea rows="4" cols="50" id="search-string"></textarea>
</div>
<br />
<h4>Help</h4>
<p>
<ul>
<li>
<code>@abc</code> - matches notes with label abc</li>
<li>
<code>@!abc</code> - matches notes without abc label (maybe not the best syntax)</li>
<li>
<code>@abc=true</code> - matches notes with label abc having value true</li>
<li><code>@abc!=true</code></li>
<li>
<code>@"weird label"="weird value"</code> - works also with whitespace inside names values</li>
<li>
<code>@abc and @def</code> - matches notes with both abc and def</li>
<li>
<code>@abc @def</code> - AND relation is implicit when specifying multiple labels</li>
<li>
<code>@abc or @def</code> - OR relation</li>
<li>
<code>@abc&lt;=5</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
<li>
<code>some search string @abc @def</code> - combination of fulltext and label search - both of them need to match (OR not supported)</li>
<li>
<code>@abc @def some search string</code> - same combination</li>
</ul>
<button class="btn btn-sm" type="button" data-help-page="Search">
<i class="glyphicon glyphicon-info-sign"></i> Complete help on search
</button>
</p>
</div>

View File

@ -95,6 +95,8 @@
</div>
<div id="tree"></div>
<div class="dropdown-menu dropdown-menu-sm context-menu" id="tree-context-menu"></div>
</div>
<div id="title-container">
@ -140,7 +142,7 @@
&nbsp; &nbsp;
<div class="dropdown" id="note-type" data-bind="visible: type() != 'search'">
<div class="dropdown" id="note-type">
<button data-bind="disable: isDisabled()" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
Type: <span data-bind="text: typeString()"></span>
<span class="caret"></span>
@ -176,133 +178,7 @@
</div>
</div>
<div id="note-detail-wrapper">
<div id="note-detail-script-area"></div>
<table id="note-detail-promoted-attributes"></table>
<div id="note-detail-component-wrapper">
<div id="note-detail-text" class="note-detail-component" tabindex="10000"></div>
<div id="note-detail-search" class="note-detail-component">
<div style="display: flex; align-items: center;">
<strong>Search string: &nbsp; &nbsp;</strong>
<textarea rows="4" cols="50" id="search-string"></textarea>
</div>
<br />
<h4>Help</h4>
<p>
<ul>
<li>
<code>@abc</code> - matches notes with label abc</li>
<li>
<code>@!abc</code> - matches notes without abc label (maybe not the best syntax)</li>
<li>
<code>@abc=true</code> - matches notes with label abc having value true</li>
<li><code>@abc!=true</code></li>
<li>
<code>@"weird label"="weird value"</code> - works also with whitespace inside names values</li>
<li>
<code>@abc and @def</code> - matches notes with both abc and def</li>
<li>
<code>@abc @def</code> - AND relation is implicit when specifying multiple labels</li>
<li>
<code>@abc or @def</code> - OR relation</li>
<li>
<code>@abc&lt;=5</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
<li>
<code>some search string @abc @def</code> - combination of fulltext and label search - both of them need to match (OR not supported)</li>
<li>
<code>@abc @def some search string</code> - same combination</li>
</ul>
<button class="btn btn-sm" type="button" data-help-page="Search">
<i class="glyphicon glyphicon-info-sign"></i> Complete help on search
</button>
</p>
</div>
<div id="note-detail-code" class="note-detail-component"></div>
<div id="note-detail-render" class="note-detail-component">
<div id="note-detail-render-help" class="alert alert-warning">
<p><strong>This help note is shown because this note of type Render HTML doesn't have required relation to function properly.</strong></p>
<p>Render HTML note type is used for <a href="https://github.com/zadam/trilium/wiki/Scripts">scripting</a>. In short, you have a HTML code note (optionally with some JavaScript) and this note will render it. To make it work, you need to define a relation (in <a class="show-attributes-button">Attributes dialog</a>) called "renderNote" pointing to the HTML note to render. Once that's defined you can click on the "play" button to render.</p>
</div>
<div id="note-detail-render-content"></div>
</div>
<div id="note-detail-file" class="note-detail-component">
<table id="file-table">
<tr>
<th>File name:</th>
<td id="file-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="file-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="file-filesize"></td>
</tr>
<tr>
<td>
<button id="file-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="file-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div>
<input type="file" id="file-upload" style="display: none" />
<div id="note-detail-relation-map" class="note-detail-component">
<button id="relation-map-add-child-notes" class="btn" type="button"
title="Add all child notes of this relation map note">Add child notes</button>
&nbsp;
<button id="relation-map-create-child-note" class="btn" type="button"
title="Create new child note and add it into this relation map">Create child note</button>
<div class="btn-group" style="float: right; padding-right: 20px;">
<button type="button"
class="btn icon-button24"
title="Zoom In"
id="relation-map-zoom-in"
style="background-image: url('/images/icons/zoom-in-24.png');"/>
<button type="button"
class="btn icon-button24"
title="Zoom Out"
id="relation-map-zoom-out"
style="background-image: url('/images/icons/zoom-out-24.png');"/>
</div>
<div id="relation-map-canvas"></div>
</div>
</div>
<div id="children-overview"></div>
<div id="attribute-list">
<button class="btn btn-sm show-attributes-button">Attributes:</button>
<span id="attribute-list-inner"></span>
</div>
</div>
<div class="dropdown-menu dropdown-menu-sm context-menu" id="tree-context-menu">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
</div>
<% include details/detail.ejs %>
<% include dialogs/add_link.ejs %>
<% include dialogs/attributes.ejs %>