diff --git a/CHANGELOG.md b/CHANGELOG.md index ddeacd7..aa8a911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - None +## [1.2.0] - 2021-02-17 + +### Added + +- Ability to drag & drop notes from [note-tabs plugin](https://github.com/benji300/joplin-note-tabs) to add as favorite +- Ability to rename and delete favorites directly in panel (vertical layout only) + - Via new hover buttons on the right side + +### Changed + +- Drag & drop behavior to add notebooks, notes or to-dos + - Move them onto the panel to add new favorite at the dropped position +- Scroll horizontally without holding `Shift` key +- plugin command labels (Removed `Favs:` prefix) + +### Fixed + +- Search favorites with phrases in query cannot be opened, edited or deleted ([#4](https://github.com/benji300/joplin-favorites/issues/4)) + ## [1.1.0] - 2021-01-21 ### Changed diff --git a/README.md b/README.md index b8f7075..28b69f9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Joplin Favorites -Joplin Favorites is a plugin to extend the UX and UI of [Joplin's](https://joplinapp.org/) desktop application. +Favorites is a plugin to extend the UX and UI of [Joplin's](https://joplinapp.org/) desktop application. It allows to save any notebook, note, to-do, tag, or search as favorite in an extra panel view for quick access. @@ -35,7 +35,7 @@ It allows to save any notebook, note, to-do, tag, or search as favorite in an ex - Search - Not fully supported right now - see [here](#open-saved-search) for details - Set and edit user defined names for the favorites -- Right-click on favorites to edit or remove +- Right-click on favorites to open edit dialog - Change position of favorites within the panel via drag & drop - Drag notebooks and notes from sidebar or note list directly to favorites - Configurable style attributes @@ -48,7 +48,7 @@ It allows to save any notebook, note, to-do, tag, or search as favorite in an ex ![favorites-top-horizontal](./assets/favorites-top-horizontal.png) -### Favorites in sidebar (vertical layout) +#### Favorites in sidebar (vertical layout) ![favorites-sidebar-vertical](./assets/favorites-sidebar-vertical.png) @@ -65,7 +65,7 @@ It allows to save any notebook, note, to-do, tag, or search as favorite in an ex ### Manual -- Download the latest released JPL package (`joplin.plugin.benji.favorites.jpl`) from [here](https://github.com/benji300/joplin-favorites/releases) +- Download the latest released JPL package (`*.jpl`) from [here](https://github.com/benji300/joplin-favorites/releases) - Open Joplin and navigate to `Tools > Options > Plugins` - Press `Install plugin` and select the previously downloaded `jpl` file - Confirm selection @@ -74,8 +74,7 @@ It allows to save any notebook, note, to-do, tag, or search as favorite in an ex ### Uninstall -- Open Joplin -- Navigate to `Tools > Options > Plugins` +- Open Joplin and navigate to `Tools > Options > Plugins` - Search for the `Favorites` plugin - Press `Delete` to remove the plugin completely - Alternatively you can also disable the plugin by clicking on the toggle button @@ -90,47 +89,52 @@ By default the panel will be on the right side of the screen, this can be adjust - `View > Change application layout` - Use the arrow keys (the displayed ones, not keyboard keys) to move the panel at the desired position - Move the splitter to reach the desired height/width of the panel + - As soon as the width of the panel goes below `400px`, it automatically switches from horizontal to vertical layout - Press `ESC` to save the layout and return to normal mode ### Add favorite - To add a new favorite to the panel, you have to trigger the corresponding [command](#commands) - - In the table you can see also from which menu context the commands can be triggered -- Notebooks, notes and to-dos can also be added via drag & drop the selected entries onto the `FAVORITES` title of the panel - - To enable this feature, the option `Show favorites panel title` must be enabled -The `Edit favorite before add` option lets you choose whether or not to edit the name before adding a new favorite. + - In the table you can see from which menu context the commands can be triggered + +- Selected notebooks, notes or to-dos can also be added via drag & drop into the panel + + - This will add new favorites at the dropped position -- This is not supported when adding multiple selected notes -- For searches the dialog is always opened to enter the search query +- The `Edit favorite before add` option lets you choose whether or not to edit the name before adding a new favorite -![add-dialog](./assets/add-dialog.png) + - This is not supported when adding multiple selected notes + - For searches the dialog is always opened to enter the search query + + ![add-dialog](./assets/add-dialog.png) ### Edit favorite - Right click on one of the favorites to open the edit dialog -In the edit dialog you can change the name of any favorite. + ![edit-dialog](./assets/edit-dialog.png) -![edit-dialog](./assets/edit-dialog.png) +- For searches, you can also edit the search query in the dialog -For searches, you can also edit the search query. + ![edit-search-dialog](./assets/edit-search-dialog.png) -![edit-search-dialog](./assets/edit-search-dialog.png) +- Rename favorite by clicking the rename icon on the right side in the vertical layout ### Remove favorite -- Right click on one of the favorites to open the edit dialog (see screenshots above) -- Press `Delete` to remove the favorite +- Right click on a favorite to open the edit dialog (see screenshots above) and press `Delete` to remove it + +- Remove favorite by clicking the delete icon on the right side in the vertical layout -Alternatively you can remove all favorites at once via the `Favorites: Remove all favorites` command. +- Remove all favorites at once via the `Remove all Favorites` command ### Open saved search Currently favorites for searches are not fully supported. Due to restrictions of the App it is not possible to open the global search with a handled search query. To open a saved search follow this workaround: -- Save your search via the `Favorites: Add search` command +- Save your search via the `Add search to Favorites` command - You can enter a name and the search query in the dialog - Click on the search favorite to copy its query to the clipboard - This will also set the focus to the global search bar @@ -140,14 +144,14 @@ To open a saved search follow this workaround: This plugin provides additional commands as described in the following table. -| Command Label | Command ID | Description | Menu contexts | -| ------------------------------- | ---------------------- | -------------------------------------- | ------------------------------------------------------------------------ | -| Favorites: Add notebook | `favsAddFolder` | Add favorite for selected notebook | `Tools>Favorites`, `FolderContext`, `Command palette` | -| Favorites: Add note | `favsAddNote` | Add favorite for selected note(s) | `Tools>Favorites`, `NoteListContext`, `EditorContext`, `Command palette` | -| Favorites: Add tag | `favsAddTag` | Add favorite for selected tag | `TagContext` | -| Favorites: Add search | `favsAddSearch` | Add favorite with entered search query | `Tools>Favorites`, `Command palette` | -| Favorites: Remove all favorites | `favsClear` | Remove all favorites | `Tools>Favorites`, `Command palette` | -| Favorites: Toggle visibility | `favsToggleVisibility` | Toggle panel visibility | `Tools>Favorites`, `Command palette` | +| Command Label | Command ID | Description | Menu contexts | +| --------------------------------- | ---------------------- | -------------------------------------- | ------------------------------------------------------------------------ | +| Add notebook to Favorites | `favsAddFolder` | Add favorite for selected notebook | `Tools>Favorites`, `FolderContext`, `Command palette` | +| Add note to Favorites | `favsAddNote` | Add favorite for selected note(s) | `Tools>Favorites`, `NoteListContext`, `EditorContext`, `Command palette` | +| Add tag to Favorites | `favsAddTag` | Add favorite for selected tag | `TagContext` | +| Add search to Favorites | `favsAddSearch` | Add favorite with entered search query | `Tools>Favorites`, `Command palette` | +| Remove all Favorites | `favsClear` | Remove all favorites | `Tools>Favorites`, `Command palette` | +| Toggle Favorites panel visibility | `favsToggleVisibility` | Toggle panel visibility | `Tools>Favorites`, `Command palette` | ### Keyboard shortcuts @@ -173,9 +177,9 @@ This plugin adds provides user options which can be changed via `Tools > Options ## Support -You like this plugin as much as I do and it helps you in your daily work with Joplin? +You like this plugin as much as I do and it improves your daily work with Joplin? -Then I would be very happy if you would buy me a beer via [PayPal](https://www.paypal.com/donate?hosted_button_id=6FHDGK3PTNU22) :wink::beer: +Then I would be very happy if you buy me a beer via [PayPal](https://www.paypal.com/donate?hosted_button_id=6FHDGK3PTNU22) :wink::beer: ## Development diff --git a/package-lock.json b/package-lock.json index d0ad14b..4af2a2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "joplin-plugin-benji-favorites", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 613d895..606fd51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "joplin-plugin-benji-favorites", - "version": "1.1.0", + "version": "1.2.0", "description": "Save any notebook, note, to-do, tag, or search as favorite in an extra panel view for quick access.", "author": "Benji300", "homepage": "https://github.com/benji300/joplin-favorites", diff --git a/src/assets/fontawesome/webfonts/fa-regular-400.svg b/src/assets/fontawesome/webfonts/fa-regular-400.svg new file mode 100644 index 0000000..60414e1 --- /dev/null +++ b/src/assets/fontawesome/webfonts/fa-regular-400.svg @@ -0,0 +1,801 @@ + + + + +Created by FontForge 20200314 at Wed Jan 13 11:57:54 2021 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/fontawesome/webfonts/fa-regular-400.ttf b/src/assets/fontawesome/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000..2775fa1 Binary files /dev/null and b/src/assets/fontawesome/webfonts/fa-regular-400.ttf differ diff --git a/src/assets/fontawesome/webfonts/fa-regular-400.woff b/src/assets/fontawesome/webfonts/fa-regular-400.woff new file mode 100644 index 0000000..e4acf91 Binary files /dev/null and b/src/assets/fontawesome/webfonts/fa-regular-400.woff differ diff --git a/src/assets/fontawesome/webfonts/fa-regular-400.woff2 b/src/assets/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000..708621f Binary files /dev/null and b/src/assets/fontawesome/webfonts/fa-regular-400.woff2 differ diff --git a/src/dialog.ts b/src/dialog.ts new file mode 100644 index 0000000..a00276c --- /dev/null +++ b/src/dialog.ts @@ -0,0 +1,97 @@ +import joplin from 'api'; +import { ButtonSpec, DialogResult } from 'api/types'; +import { FavoriteType, FavoriteDesc } from './favorites'; + +export class Dialog { + private _dialog: any; + private _title: string; + + constructor(title: string) { + this._title = title; + } + + static async showMessage(message: string): Promise { + const result: number = await joplin.views.dialogs.showMessageBox(message); + return result; + } + + /** + * Gets the full path, tag name or search query for the favorite. + */ + private async getFullPath(value: string, type: FavoriteType): Promise { + switch (type) { + case FavoriteType.Folder: + case FavoriteType.Note: + case FavoriteType.Todo: + const item = await joplin.data.get([FavoriteDesc[type].dataType, value], { fields: ['title', 'parent_id'] }); + if (item) { + let parents: any[] = new Array(); + let parent_id: string = item.parent_id; + + while (parent_id) { + const parent: any = await joplin.data.get(['folders', parent_id], { fields: ['title', 'parent_id'] }); + if (!parent) break; + parent_id = parent.parent_id; + parents.push(parent.title); + } + parents.reverse().push(item.title); + return parents.join('/'); + } + + case FavoriteType.Tag: + const tag = await joplin.data.get([FavoriteDesc[type].dataType, value], { fields: ['title'] }); + if (tag) { + return tag.title; + } + + case FavoriteType.Search: + return value; + + default: + break; + } + return ''; + } + + /** + * Prepare dialog html content. + */ + private async prepareDialogHtml(value: string, title: string, type: FavoriteType): Promise { + const path: string = await this.getFullPath(value, type); + const disabled: string = (type === FavoriteType.Search) ? '' : 'disabled'; + + return ` +
+

${this._title} ${FavoriteDesc[type].name} Favorite

+
+ + + + +
+
+ `; + } + + /** + * Register the dialog. + */ + async register(buttons?: ButtonSpec[]) { + this._dialog = await joplin.views.dialogs.create('dialog' + this._title); + await joplin.views.dialogs.addScript(this._dialog, './assets/fontawesome/css/all.min.css'); + await joplin.views.dialogs.addScript(this._dialog, './webview_dialog.css'); + if (buttons) { + await joplin.views.dialogs.setButtons(this._dialog, buttons); + } + } + + /** + * Open the dialog width the handled values and return result. + */ + async open(value: string, title: string, type: FavoriteType): Promise { + const dialogHtml: string = await this.prepareDialogHtml(value, title, type); + await joplin.views.dialogs.setHtml(this._dialog, dialogHtml); + const result: DialogResult = await joplin.views.dialogs.open(this._dialog); + return result; + } +} \ No newline at end of file diff --git a/src/favorites.ts b/src/favorites.ts new file mode 100644 index 0000000..a08aade --- /dev/null +++ b/src/favorites.ts @@ -0,0 +1,231 @@ +/** + * Favorite type definitions. + */ +export enum FavoriteType { + Folder = 0, + Note = 1, + Todo = 2, + Tag = 3, + Search = 4 +} + +/** + * Definition of favorite entries. + */ +export interface IFavorite { + // Favorite value = folderId|noteId|tagId|searchQuery + value: string, + // User configured title + title: string, + // Type of the favorite + type: FavoriteType +} + +/** + * Definition of the favorite descriptions. + */ +interface IFavoriteDesc { + name: string, + icon: string, + dataType: string, + label: string +} + +/** + * Array of favorite descriptions. Order must match with FavoriteType enum. + */ +export const FavoriteDesc: IFavoriteDesc[] = [ + { name: 'Notebook', icon: 'fa-book', dataType: 'folders', label: 'Full path' }, // Folder + { name: 'Note', icon: 'fa-file-alt', dataType: 'notes', label: 'Full path' }, // Note + { name: 'To-do', icon: 'fa-check-square', dataType: 'notes', label: 'Full path' }, // Todo + { name: 'Tag', icon: 'fa-tag', dataType: 'tags', label: 'Tag' }, // Tag + { name: 'Search', icon: 'fa-search', dataType: 'searches', label: 'Search query' } // Search +]; + +/** + * Helper class to work with favorites array. + * - Read settings array once at startup. + * - Then work on this._tabs array. + */ +export class Favorites { + /** + * Temporary array to work with favorites. + */ + private _store: IFavorite[]; + + /** + * Init with stored values from settings array. + */ + constructor(settingsArray: IFavorite[]) { + this._store = settingsArray; + } + + //#region GETTER + + /** + * All entries. + */ + get all(): IFavorite[] { + return this._store; + } + + /** + * Number of entries. + */ + get length(): number { + return this._store.length; + } + + //#endregion + + /** + * Inserts handled favorite at specified index. + */ + private async insertAtIndex(index: number, favorite: IFavorite) { + if (index < 0 || favorite === undefined) return; + + this._store.splice(index, 0, favorite); + } + + /** + * Gets a value whether the handled index would lead to out of bound access. + */ + private indexOutOfBounds(index: number): boolean { + return (index < 0 || index >= this.length); + } + + /** + * Escapes HTML special characters. + * From https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc/src/index.ts + */ + private encodeHtml(unsafe: string): string { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + .trim(); + } + + /** + * Decodes escaped HTML characters back. + */ + private decodeHtml(unsafe: string): string { + return unsafe + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .trim(); + } + + /** + * Gets the favorites with the handled value. Null if not exist. + */ + get(index: number): IFavorite { + if (this.indexOutOfBounds(index)) return; + return this._store[index]; + } + + /** + * Gets the HTML decoded value of the handled favorite. + * Workaround to copy search strings to clipboard. + */ + getDecodedValue(favorite: IFavorite): string { + if (favorite === undefined) return; + return this.decodeHtml(favorite.value); + } + + /** + * Gets index of favorite with handled value. -1 if not exist. + */ + indexOf(value: string): number { + if (value) { + for (let i: number = 0; i < this.length; i++) { + if (this._store[i]['value'] === value) return i; + } + } + return -1; + } + + /** + * Gets a value whether a favorite with the handled value exists or not. + */ + hasFavorite(value: string): boolean { + return this.indexOf(value) < 0 ? false : true; + } + + /** + * Adds note as new favorite at the handled index or at the end. + */ + async add(newValue: string, newTitle: string, newType: FavoriteType, targetIdx?: number) { + if (newValue === undefined || newTitle === undefined || newType === undefined) return; + + const newFavorite = { value: this.encodeHtml(newValue), title: this.encodeHtml(newTitle), type: newType }; + if (targetIdx) { + await this.insertAtIndex(targetIdx, newFavorite); + } else { + this._store.push(newFavorite); + } + } + + /** + * Changes the title of the handled favorite. + */ + async changeValue(index: number, newValue: string) { + if (index < 0 || newValue === undefined || newValue === '') return; + this._store[index].value = this.encodeHtml(newValue); + } + + /** + * Changes the title of the handled favorite. + */ + async changeTitle(index: number, newTitle: string) { + if (index < 0 || newTitle === undefined || newTitle === '') return; + this._store[index].title = this.encodeHtml(newTitle); + } + + /** + * Changes the type of the handled favorite. + */ + async changeType(index: number, newType: FavoriteType) { + if (index < 0 || newType === undefined) return; + this._store[index].type = newType; + } + + /** + * Moves the favorite from source index to the target index. + */ + async moveWithIndex(sourceIdx: number, targetIdx?: number) { + if (this.indexOutOfBounds(sourceIdx)) return; + if (targetIdx && this.indexOutOfBounds(targetIdx)) return; + + // undefined targetIdx => move to the end + let target: number = this.length - 1; + if (targetIdx) { + // else move at desired index + target = targetIdx; + } + const favorite: IFavorite = this._store[sourceIdx]; + this._store.splice(sourceIdx, 1); + this._store.splice(target, 0, favorite); + } + + /** + * Removes favorite with handled index. + */ + async delete(index: number) { + if (index >= 0) { + this._store.splice(index, 1); + } + } + + /** + * Clears the stored array. + */ + async clearAll() { + this._store = []; + } +} diff --git a/src/helpers.ts b/src/helpers.ts deleted file mode 100644 index 426f87c..0000000 --- a/src/helpers.ts +++ /dev/null @@ -1,215 +0,0 @@ -import joplin from 'api'; - -/** - * Advanced style setting default values. - * Used when setting is set to 'default'. - */ -export enum SettingDefaults { - Default = 'default', - FontFamily = 'Roboto', - FontSize = 'var(--joplin-font-size)', - Background = 'var(--joplin-background-color3)', - HoverBackground = 'var(--joplin-background-color-hover3)', // var(--joplin-background-hover) - Foreground = 'var(--joplin-color-faded)', - DividerColor = 'var(--joplin-divider-color)' -} - -/** - * Favorite type definition. - */ -export enum FavoriteType { - Folder = 0, - Note = 1, - Todo = 2, - Tag = 3, - Search = 4 -} - -/** - * Definition of the favorite descriptions. - */ -interface IFavoriteDesc { - name: string, - icon: string, - dataType: string, - label -} - -/** - * Array of favorite descriptions. Order must match with FavoriteType enum. - */ -export const FavoriteDesc: IFavoriteDesc[] = [ - { name: 'Notebook', icon: 'fa-book', dataType: 'folders', label: 'Full path' }, // Folder - { name: 'Note', icon: 'fa-file-alt', dataType: 'notes', label: 'Full path' }, // Note - { name: 'To-do', icon: 'fa-check-square', dataType: 'notes', label: 'Full path' }, // Todo - { name: 'Tag', icon: 'fa-tag', dataType: 'tags', label: 'Tag' }, // Tag - { name: 'Search', icon: 'fa-search', dataType: 'searches', label: 'Search query' } // Search -]; - -/** - * Helper class to work with favorites array. - */ -export class Favorites { - // [ - // { - // "value": "folderId|noteId|tagId|searchQuery", - // "title": "userConfiguredTitle", - // "type": FavoriteType - // } - // ] - private _favs: any[]; - - constructor() { - this._favs = new Array(); - } - - /** - * Reads the favorites settings array. - */ - async read() { - this._favs = await joplin.settings.value('favorites'); - } - - /** - * Writes the temporay tabs store back to the settings array. - */ - private async store() { - await joplin.settings.setValue('favorites', this._favs); - } - - /** - * Gets a value whether the handled index would lead to out of bound access. - */ - private indexOutOfBounds(index: number): boolean { - return (index < 0 || index >= this.length()); - } - - /** - * Gets the number of favorites. - */ - length(): number { - return this._favs.length; - } - - /** - * Gets all favorites. - */ - getAll(): any[] { - return this._favs; - } - - /** - * Gets the favorites with the handled value. Null if not exist. - */ - get(value: string): any { - if (value == null) return; - - for (let i: number = 0; i < this.length(); i++) { - if (this._favs[i]['value'] === value) return this._favs[i]; - } - return null; - } - - /** - * Gets index of favorite with handled value. -1 if not exist. - */ - indexOf(value: string): number { - if (value) { - for (let i: number = 0; i < this.length(); i++) { - if (this._favs[i]['value'] === value) return i; - } - } - return -1; - } - - /** - * Gets a value whether a favorite with the handled value exists or not. - */ - hasFavorite(value: string): boolean { - return this.indexOf(value) < 0 ? false : true; - } - - /** - * Adds new favorite at the end. - */ - async add(newValue: string, newTitle: string, newType: FavoriteType) { - if (newValue == null || newTitle == null || newType == null) return; - - this._favs.push({ value: newValue, title: newTitle, type: newType }); - await this.store(); - } - - /** - * Changes the title of the handled favorite. - */ - async changeValue(value: string, newValue: string) { - if (!newValue) return; - const index: number = this.indexOf(value); - if (index < 0) return; - this._favs[index].value = newValue; - await this.store(); - } - - /** - * Changes the title of the handled favorite. - */ - async changeTitle(value: string, newTitle: string) { - if (!newTitle) return; - const index: number = this.indexOf(value); - if (index < 0) return; - this._favs[index].title = newTitle; - await this.store(); - } - - /** - * Changes the type of the handled favorite. - */ - async changeType(value: string, newType: FavoriteType) { - const index: number = this.indexOf(value); - if (index < 0) return; - - this._favs[index].type = newType; - await this.store(); - } - - /** - * Moves the favorite from source index to the target index. - */ - async moveWithIndex(sourceIdx: number, targetIdx: number) { - if (this.indexOutOfBounds(sourceIdx)) return; - if (this.indexOutOfBounds(targetIdx)) return; - - const favorite: any = this._favs[sourceIdx]; - this._favs.splice(sourceIdx, 1); - this._favs.splice((targetIdx == 0 ? 0 : targetIdx), 0, favorite); - await this.store(); - } - - /** - * Moves the source favorite to the index of the target favorite. - */ - async moveWithValue(sourceValue: string, targetValue: string) { - if (sourceValue == null || targetValue == null) return; - - await this.moveWithIndex(this.indexOf(sourceValue), this.indexOf(targetValue)); - } - - /** - * Removes favorite with handled value. - */ - async delete(value: string) { - const index = this.indexOf(value); - if (index >= 0) { - this._favs.splice(index, 1); - } - await this.store(); - } - - /** - * Clears the stored favorites array. - */ - async clearAll() { - this._favs = []; - await this.store(); - } -} diff --git a/src/index.ts b/src/index.ts index 0f691ff..459494b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,224 +1,48 @@ import joplin from 'api'; -import { MenuItem, MenuItemLocation, SettingItemType } from 'api/types'; +import { MenuItem, MenuItemLocation } from 'api/types'; import { ChangeEvent } from 'api/JoplinSettings'; -import { FavoriteType, FavoriteDesc, Favorites } from './helpers'; -import { SettingDefaults, } from './helpers'; +import { FavoriteType, IFavorite, FavoriteDesc, Favorites } from './favorites'; +import { Settings } from './settings'; +import { Panel } from './panel'; +import { Dialog } from './dialog'; joplin.plugins.register({ onStart: async function () { const COMMANDS = joplin.commands; const DATA = joplin.data; - const DIALOGS = joplin.views.dialogs; - const PANELS = joplin.views.panels; const SETTINGS = joplin.settings; const WORKSPACE = joplin.workspace; - - //#region SETTINGS - - await SETTINGS.registerSection('favorites.settings', { - label: 'Favorites', - iconName: 'fas fa-star' - }); - - // private settings - let favorites = new Favorites(); - await SETTINGS.registerSetting('favorites', { - value: [], - type: SettingItemType.Array, - section: 'favorites.settings', - public: false, - label: 'Favorites' - }); - await favorites.read(); - - // general settings - let editBeforeAdd: boolean; - await SETTINGS.registerSetting('editBeforeAdd', { - value: true, - type: SettingItemType.Bool, - section: 'favorites.settings', - public: true, - label: 'Edit favorite before add', - description: 'Opens a dialog to edit the favorite before adding it. If disabled, the name can still be changed later.' - }); - - let enableDragAndDrop: boolean; - await SETTINGS.registerSetting('enableDragAndDrop', { - value: true, - type: SettingItemType.Bool, - section: 'favorites.settings', - public: true, - label: 'Enable drag & drop of favorites', - description: 'If enabled, the position of favorites can be change via drag & drop.' - }); - - let showPanelTitle: boolean; - await SETTINGS.registerSetting('showPanelTitle', { - value: true, - type: SettingItemType.Bool, - section: 'favorites.settings', - public: true, - label: 'Show favorites panel title', - description: "Display 'FAVORITES' title in front of the favorites." - }); - - let showTypeIcons: boolean; - await SETTINGS.registerSetting('showTypeIcons', { - value: true, - type: SettingItemType.Bool, - section: 'favorites.settings', - public: true, - label: 'Show type icons for favorites', - description: 'Display icons before favorite titles representing the types (notebook, note, tag, etc.).' - }); - - let lineHeight: number; - await SETTINGS.registerSetting('lineHeight', { - value: "30", - type: SettingItemType.Int, - section: 'favorites.settings', - public: true, - label: 'Line height (px)', - description: 'Line height of the favorites panel.' - }); - - let minWidth: number; - await SETTINGS.registerSetting('minFavoriteWidth', { - value: "15", - type: SettingItemType.Int, - section: 'favorites.settings', - public: true, - label: 'Minimum favorite width (px)', - description: 'Minimum width of one favorite in pixel.' - }); - - let maxWidth: number; - await SETTINGS.registerSetting('maxFavoriteWidth', { - value: "100", - type: 1, - section: 'favorites.settings', - public: true, - label: 'Maximum favorite width (px)', - description: 'Maximum width of one favorite in pixel.' - }); - - // Advanced settings - let fontFamily: string; - await SETTINGS.registerSetting('fontFamily', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'favorites.settings', - public: true, - advanced: true, - label: 'Font family', - description: "Font family used in the panel. Font families other than 'default' must be installed on the system. If the font is incorrect or empty, it might default to a generic sans-serif font. (default: Roboto)" - }); - - let fontSize: string; - await SETTINGS.registerSetting('fontSize', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'favorites.settings', - public: true, - advanced: true, - label: 'Font size', - description: "Font size used in the panel. Values other than 'default' must be specified in valid CSS syntax, e.g. '13px'. (default: App default font size)" - }); - - let background: string; - await SETTINGS.registerSetting('mainBackground', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'favorites.settings', - public: true, - advanced: true, - label: 'Background color', - description: "Main background color of the panel. (default: Note list background color)" - }); - - let hoverBackground: string; - await SETTINGS.registerSetting('hoverBackground', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'favorites.settings', - public: true, - advanced: true, - label: 'Hover Background color', - description: "Background color used when hovering a favorite. (default: Note list hover color)" - }); - - let foreground: string; - await SETTINGS.registerSetting('mainForeground', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'favorites.settings', - public: true, - advanced: true, - label: 'Foreground color', - description: "Foreground color used for text and icons. (default: App faded color)" - }); - - let dividerColor: string; - await SETTINGS.registerSetting('dividerColor', { - value: SettingDefaults.Default, - type: SettingItemType.String, - section: 'favorites.settings', - public: true, - advanced: true, - label: 'Divider color', - description: "Color of the divider between the favorites. (default: App default border color)" - }); - - const regexp: RegExp = new RegExp(SettingDefaults.Default, "i"); - async function getSettingOrDefault(event: ChangeEvent, localVar: any, setting: string, defaultValue?: string): Promise { - const read: boolean = (!event || event.keys.includes(setting)); - if (read) { - const value: string = await SETTINGS.value(setting); - if (defaultValue && value.match(regexp)) { - return defaultValue; - } else { - return value; - } - } - return localVar; - } - - async function readSettingsAndUpdate(event?: ChangeEvent) { - enableDragAndDrop = await getSettingOrDefault(event, enableDragAndDrop, 'enableDragAndDrop'); - showPanelTitle = await getSettingOrDefault(event, showPanelTitle, 'showPanelTitle'); - showTypeIcons = await getSettingOrDefault(event, showTypeIcons, 'showTypeIcons'); - editBeforeAdd = await getSettingOrDefault(event, editBeforeAdd, 'editBeforeAdd'); - lineHeight = await getSettingOrDefault(event, lineHeight, 'lineHeight'); - maxWidth = await getSettingOrDefault(event, maxWidth, 'maxFavoriteWidth'); - minWidth = await getSettingOrDefault(event, minWidth, 'minFavoriteWidth'); - fontFamily = await getSettingOrDefault(event, fontFamily, 'fontFamily', SettingDefaults.FontFamily); - fontSize = await getSettingOrDefault(event, fontSize, 'fontSize', SettingDefaults.FontSize); - background = await getSettingOrDefault(event, background, 'mainBackground', SettingDefaults.Background); - hoverBackground = await getSettingOrDefault(event, hoverBackground, 'hoverBackground', SettingDefaults.HoverBackground); - foreground = await getSettingOrDefault(event, foreground, 'mainForeground', SettingDefaults.Foreground); - dividerColor = await getSettingOrDefault(event, dividerColor, 'dividerColor', SettingDefaults.DividerColor); - await updatePanelView(); - } - - SETTINGS.onChange(async (event: ChangeEvent) => { - await readSettingsAndUpdate(event); - }); - - //#endregion + // settings + const settings: Settings = new Settings(); + await settings.register(); + // favorites + const favorites = new Favorites(settings.favorites); + // panel + const panel = new Panel(favorites, settings); + await panel.register(); + // dialogs + const addDialog = new Dialog('Add'); + await addDialog.register(); + const editDialog = new Dialog('Edit'); + await editDialog.register([ + { id: 'delete', title: 'Delete', }, + { id: 'ok', title: 'OK' }, + { id: 'cancel', title: 'Cancel' } + ]); //#region HELPERS /** - * Check if favorite target still exists - otherwise ask to remove favorite - */ - async function checkAndRemoveFavorite(favorite: any): Promise { + * Check if favorite target still exists - otherwise ask to remove favorite + */ + async function checkAndRemoveFavorite(favorite: IFavorite, index: number): Promise { try { await DATA.get([FavoriteDesc[favorite.type].dataType, favorite.value], { fields: ['id'] }); } catch (err) { - const result: number = await DIALOGS.showMessageBox(`Cannot open favorite. Seems that the target ${FavoriteDesc[favorite.type].name.toLocaleLowerCase()} was deleted.\n\nDo you want to delete the favorite also?`); + const result: number = await Dialog.showMessage(`Cannot open favorite. Seems that the target ${FavoriteDesc[favorite.type].name.toLocaleLowerCase()} was deleted.\n\nDo you want to delete the favorite also?`); if (!result) { - await favorites.delete(favorite.value); - await updatePanelView(); + await favorites.delete(index); + await panel.updateWebview(); return true; } } @@ -228,114 +52,37 @@ joplin.plugins.register({ /** * Check if note/todo is still of the same type - otherwise change type */ - async function checkAndUpdateType(favorite: any) { + async function checkAndUpdateType(favorite: IFavorite, index: number) { let newType: FavoriteType; const note: any = await DATA.get([FavoriteDesc[favorite.type].dataType, favorite.value], { fields: ['id', 'is_todo'] }); if (favorite.type === FavoriteType.Note && note.is_todo) newType = FavoriteType.Todo; if (favorite.type === FavoriteType.Todo && (!note.is_todo)) newType = FavoriteType.Note; if (newType) { - await favorites.changeType(favorite.value, newType); - await updatePanelView(); + await favorites.changeType(index, newType); + await panel.updateWebview(); } } /** - * Gets the full path, tag name or search query for the favorite. + * Add new favorite entry */ - async function getFavoritePath(value: string, type: FavoriteType): Promise { - switch (type) { - case FavoriteType.Folder: - case FavoriteType.Note: - case FavoriteType.Todo: - const item = await DATA.get([FavoriteDesc[type].dataType, value], { fields: ['title', 'parent_id'] }); - if (item) { - let parents: any[] = new Array(); - let parent_id: string = item.parent_id; - - while (parent_id) { - const parent: any = await DATA.get(['folders', parent_id], { fields: ['title', 'parent_id'] }); - if (!parent) break; - parent_id = parent.parent_id; - parents.push(parent.title); - } - parents.reverse().push(item.title); - return parents.join('/'); - } - - case FavoriteType.Tag: - const tag = await DATA.get([FavoriteDesc[type].dataType, value], { fields: ['title'] }); - if (tag) return tag.title; - - case FavoriteType.Search: - return value; - - default: - break; - } - return ''; - } - - async function openFavorite(value: string) { - const favorite: any = await favorites.get(value); - if (!favorite) return; - - switch (favorite.type) { - case FavoriteType.Folder: - if (await checkAndRemoveFavorite(favorite)) return; - COMMANDS.execute('openFolder', value); - break; - - case FavoriteType.Note: - case FavoriteType.Todo: - if (await checkAndRemoveFavorite(favorite)) return; - await checkAndUpdateType(favorite); - COMMANDS.execute('openNote', value); - break; - - case FavoriteType.Tag: - if (await checkAndRemoveFavorite(favorite)) return; - COMMANDS.execute('openTag', value); - break; - - case FavoriteType.Search: - // TODO there is a command `~\app-desktop\gui\MainScreen\commands\search.ts` avaiable, but currently empty - // use this once it is implemented - - // currently there's no command to trigger a global search, so the following workaround is used - // 1. copy saved search to clipboard - const copy = require('../node_modules/copy-to-clipboard'); - copy(favorite.value as string); - // 2. focus global search bar via command - await COMMANDS.execute('focusSearch'); - // 3. paste clipboard content to current cursor position (should be search bar now) - // TODO how? - break; - - default: - break; - } - } - - async function addFavorite(value: string, title: string, type: FavoriteType, showDialog: boolean) { + async function addFavorite(value: string, title: string, type: FavoriteType, showDialog: boolean, targetIdx?: number) { let newValue: string = value; let newTitle: string = title; // check whether a favorite with handled value already exists - if (favorites.hasFavorite(value)) { + const index: number = favorites.indexOf(value); + if (index >= 0) { // if so... open editFavorite dialog - await editFavorite(value); + await COMMANDS.execute('favsEditFavorite', index); } else { // otherwise create new favorite, with or without user interaction if (showDialog) { - // prepare and open dialog - const dialogHtml: string = await prepareDialogHtml('Add', value, newTitle, type); - await DIALOGS.setHtml(dialogAdd, dialogHtml); - const result: any = await DIALOGS.open(dialogAdd); - - // handle result + // open dialog and handle result + const result: any = await addDialog.open(value, newTitle, type); if (result.id == 'ok' && result.formData != null) { newTitle = result.formData.inputForm.title; if (result.formData.inputForm.value) @@ -344,33 +91,10 @@ joplin.plugins.register({ return; } - if (newValue === '' || newTitle === '') - return; - - await favorites.add(newValue, newTitle, type); - await updatePanelView(); - } - } + if (newValue === '' || newTitle === '') return; - async function editFavorite(value: string) { - const favorite: any = await favorites.get(value); - if (!favorite) return; - - // prepare and open dialog - const dialogHtml: string = await prepareDialogHtml('Edit', favorite.value, favorite.title, favorite.type); - await DIALOGS.setHtml(dialogEdit, dialogHtml); - const result: any = await DIALOGS.open(dialogEdit); - - // handle result - if (result.id == "ok" && result.formData != null) { - await favorites.changeTitle(value, result.formData.inputForm.title); - await favorites.changeValue(value, result.formData.inputForm.value); - await updatePanelView(); - } else if (result.id == "delete") { - await favorites.delete(value); - await updatePanelView(); - } else { - return; + await favorites.add(newValue, newTitle, type, targetIdx); + await panel.updateWebview(); } } @@ -378,24 +102,95 @@ joplin.plugins.register({ //#region COMMANDS + // Command: favsOpenFavorite (INTERNAL) + // Desc: Internal command to open a favorite + await COMMANDS.register({ + name: 'favsOpenFavorite', + execute: async (index: number) => { + const favorite: IFavorite = favorites.get(index); + if (!favorite) return; + + switch (favorite.type) { + case FavoriteType.Folder: + if (await checkAndRemoveFavorite(favorite, index)) return; + COMMANDS.execute('openFolder', favorite.value); + break; + + case FavoriteType.Note: + case FavoriteType.Todo: + if (await checkAndRemoveFavorite(favorite, index)) return; + await checkAndUpdateType(favorite, index); + COMMANDS.execute('openNote', favorite.value); + break; + + case FavoriteType.Tag: + if (await checkAndRemoveFavorite(favorite, index)) return; + COMMANDS.execute('openTag', favorite.value); + break; + + case FavoriteType.Search: + // TODO there is a command `~\app-desktop\gui\MainScreen\commands\search.ts` avaiable, but currently empty + // use this once it is implemented + + // currently there's no command to trigger a global search, so the following workaround is used + // 1. copy saved search to clipboard + const copy = require('../node_modules/copy-to-clipboard'); + copy(favorites.getDecodedValue(favorite)); + // 2. focus global search bar via command + await COMMANDS.execute('focusSearch'); + // 3. paste clipboard content to current cursor position (should be search bar now) + // TODO how? + break; + + default: + break; + } + + await panel.updateWebview(); + } + }); + + // Command: favsEditFavorite (INTERNAL) + // Desc: Internal command to edit a favorite + await COMMANDS.register({ + name: 'favsEditFavorite', + execute: async (index: number) => { + const favorite: IFavorite = favorites.get(index); + if (!favorite) return; + + // open dialog and handle result + const result: any = await editDialog.open(favorite.value, favorite.title, favorite.type); + if (result.id == "ok" && result.formData != null) { + await favorites.changeTitle(index, result.formData.inputForm.title); + await favorites.changeValue(index, result.formData.inputForm.value); + } else if (result.id == "delete") { + await favorites.delete(index); + } else { + return; + } + + await panel.updateWebview(); + } + }); + // Command: favsAddFolder // Desc: Add selected folder to favorites await COMMANDS.register({ name: 'favsAddFolder', - label: 'Favorites: Add notebook', + label: 'Add notebook to Favorites', iconName: 'fas fa-book', enabledCondition: 'oneFolderSelected', - execute: async (folderId: string) => { + execute: async (folderId: string, targetIdx?: number) => { if (folderId) { const folder = await DATA.get(['folders', folderId], { fields: ['id', 'title'] }); if (!folder) return; - await addFavorite(folder.id, folder.title, FavoriteType.Folder, editBeforeAdd); + await addFavorite(folder.id, folder.title, FavoriteType.Folder, settings.editBeforeAdd, targetIdx); } else { const selectedFolder: any = await WORKSPACE.selectedFolder(); if (!selectedFolder) return; - await addFavorite(selectedFolder.id, selectedFolder.title, FavoriteType.Folder, editBeforeAdd); + await addFavorite(selectedFolder.id, selectedFolder.title, FavoriteType.Folder, settings.editBeforeAdd, targetIdx); } } }); @@ -404,10 +199,10 @@ joplin.plugins.register({ // Desc: Add selected note to favorites await COMMANDS.register({ name: 'favsAddNote', - label: 'Favorites: Add note', + label: 'Add note to Favorites', iconName: 'fas fa-sticky-note', enabledCondition: "someNotesSelected", - execute: async (noteIds: string[]) => { + execute: async (noteIds: string[], targetIdx?: number) => { if (noteIds) { // in case multiple notes are selected - add them directly without user interaction @@ -418,14 +213,14 @@ joplin.plugins.register({ if (!note) return; // never show dialog for multiple notes - const showDialog: boolean = (editBeforeAdd && noteIds.length == 1); - await addFavorite(note.id, note.title, note.is_todo ? FavoriteType.Todo : FavoriteType.Note, showDialog); + const showDialog: boolean = (settings.editBeforeAdd && noteIds.length == 1); + await addFavorite(note.id, note.title, note.is_todo ? FavoriteType.Todo : FavoriteType.Note, showDialog, targetIdx); } } else { const selectedNote: any = await WORKSPACE.selectedNote(); if (!selectedNote) return; - await addFavorite(selectedNote.id, selectedNote.title, selectedNote.is_todo ? FavoriteType.Todo : FavoriteType.Note, editBeforeAdd); + await addFavorite(selectedNote.id, selectedNote.title, selectedNote.is_todo ? FavoriteType.Todo : FavoriteType.Note, settings.editBeforeAdd, targetIdx); } } }); @@ -434,14 +229,14 @@ joplin.plugins.register({ // Desc: Add tag to favorites await COMMANDS.register({ name: 'favsAddTag', - label: 'Favorites: Add tag', + label: 'Add tag to Favorites', iconName: 'fas fa-tag', execute: async (tagId: string) => { if (tagId) { const tag = await DATA.get(['tags', tagId], { fields: ['id', 'title'] }); if (!tag) return; - await addFavorite(tag.id, tag.title, FavoriteType.Tag, editBeforeAdd); + await addFavorite(tag.id, tag.title, FavoriteType.Tag, settings.editBeforeAdd); } } }); @@ -450,7 +245,7 @@ joplin.plugins.register({ // Desc: Add entered search query to favorites await COMMANDS.register({ name: 'favsAddSearch', - label: 'Favorites: Add Search', + label: 'Add new search to Favorites', iconName: 'fas fa-search', execute: async () => { await addFavorite('', 'New Search', FavoriteType.Search, true); // always add with dialog @@ -461,15 +256,15 @@ joplin.plugins.register({ // Desc: Remove all favorites await COMMANDS.register({ name: 'favsClear', - label: 'Favorites: Remove all favorites', + label: 'Remove all Favorites', iconName: 'fas fa-times', execute: async () => { // ask user before removing favorites - const result: number = await DIALOGS.showMessageBox(`Remove all favorites?`); + const result: number = await Dialog.showMessage('Do you really want to remove all Favorites?'); if (result) return; await favorites.clearAll(); - await updatePanelView(); + await panel.updateWebview(); } }); @@ -477,38 +272,37 @@ joplin.plugins.register({ // Desc: Toggle panel visibility await COMMANDS.register({ name: 'favsToggleVisibility', - label: 'Favorites: Toggle visibility', + label: 'Toggle Favorites panel visibility', iconName: 'fas fa-eye-slash', execute: async () => { - const isVisible: boolean = await PANELS.visible(panel); - await PANELS.show(panel, (!isVisible)); + await panel.toggleVisibility(); } }); - // prepare Tools > Favorites menu + // prepare commands menu const commandsSubMenu: MenuItem[] = [ { - commandName: "favsAddFolder", - label: 'Add selected notebook' + commandName: 'favsAddFolder', + label: 'Add active notebook' }, { - commandName: "favsAddNote", - label: 'Add selected note' + commandName: 'favsAddNote', + label: 'Add selected note(s)' }, { - commandName: "favsAddSearch", - label: 'Add search' + commandName: 'favsAddSearch', + label: 'Add new search' }, // { // commandName: "favsAddActiveSearch", // label: 'Add current active search' // }, { - commandName: "favsClear", - label: 'Remove all favorites' + commandName: 'favsClear', + label: 'Remove all Favorites' }, { - commandName: "favsToggleVisibility", + commandName: 'favsToggleVisibility', label: 'Toggle panel visibility' } ]; @@ -528,125 +322,14 @@ joplin.plugins.register({ //#endregion - //#region DIALOGS - - // prepare dialog objects - const dialogAdd = await DIALOGS.create('dialogAdd'); - await DIALOGS.addScript(dialogAdd, './assets/fontawesome/css/all.min.css'); - await DIALOGS.addScript(dialogAdd, './webview_dialog.css'); - - const dialogEdit = await DIALOGS.create('dialogEdit'); - await DIALOGS.addScript(dialogEdit, './assets/fontawesome/css/all.min.css'); - await DIALOGS.addScript(dialogEdit, './webview_dialog.css'); - await DIALOGS.setButtons(dialogEdit, [ - { id: 'delete', title: 'Delete', }, - { id: 'ok', title: 'OK' }, - { id: 'cancel', title: 'Cancel' } - ]); - - // prepare dialog HTML content - async function prepareDialogHtml(header: string, value: string, title: string, type: FavoriteType): Promise { - const path: string = await getFavoritePath(value, type); - const disabled: string = (type === FavoriteType.Search) ? '' : 'disabled'; - - return ` -
-

${header} ${FavoriteDesc[type].name} Favorite

-
- - - - -
-
- `; - } - - //#endregion - - //#region PANEL VIEW + //#region EVENTS - // prepare panel object - const panel = await PANELS.create('favorites.panel'); - await PANELS.addScript(panel, './assets/fontawesome/css/all.min.css'); - await PANELS.addScript(panel, './webview.css'); - await PANELS.addScript(panel, './webview.js'); - await PANELS.onMessage(panel, async (message: any) => { - if (message.name === 'favsAddFolder') { - await COMMANDS.execute('favsAddFolder', message.id); - } - if (message.name === 'favsAddNote') { - await COMMANDS.execute('favsAddNote', message.id); - } - if (message.name === 'favsEdit') { - editFavorite(message.id); - } - if (message.name === 'favsOpen') { - openFavorite(message.id); - } - if (message.name === 'favsDrag') { - await favorites.moveWithValue(message.sourceId, message.targetId); - await updatePanelView(); - } + SETTINGS.onChange(async (event: ChangeEvent) => { + await settings.read(event); + await panel.updateWebview(); }); - // set init message - await PANELS.setHtml(panel, ` -
-
-

Loading panel...

-
-
- `); - - // update HTML content - async function updatePanelView() { - const favsHtml: any = []; - - // prepare panel title if enabled - let panelTitleHtml: string = ''; - if (showPanelTitle) { - panelTitleHtml = ` -
- - FAVORITES -
- `; - } - - // create HTML for each favorite - for (const favorite of favorites.getAll()) { - const typeIconHtml: string = showTypeIcons ? `` : ''; - - favsHtml.push(` -
- - ${typeIconHtml} - - ${favorite.title} - - -
- `); - } - - // add entries to container and push to panel - await PANELS.setHtml(panel, ` -
-
- ${panelTitleHtml} - ${favsHtml.join('\n')} -
-
- `); - } - //#endregion - await readSettingsAndUpdate(); - }, + } }); diff --git a/src/manifest.json b/src/manifest.json index ed17485..3832d12 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,10 +2,24 @@ "manifest_version": 1, "id": "joplin.plugin.benji.favorites", "app_min_version": "1.6.5", - "version": "1.1.0", + "version": "1.2.0", "name": "Favorites", - "description": "Save any notebook, note, to-do, tag, or search as favorite in an extra panel view for quick access. (v1.1.0)", + "description": "Save any notebook, note, to-do, tag, or search as favorite in an extra panel view for quick access.", "author": "Benji300", "homepage_url": "https://github.com/benji300/joplin-favorites", - "repository_url": "https://github.com/benji300/joplin-favorites" + "repository_url": "https://github.com/benji300/joplin-favorites", + "keywords": [ + "favorite", + "shortcut", + "saved", + "quick", + "folder", + "notebook", + "note", + "todo", + "tag", + "search", + "panel", + "view" + ] } \ No newline at end of file diff --git a/src/panel.ts b/src/panel.ts new file mode 100644 index 0000000..652709f --- /dev/null +++ b/src/panel.ts @@ -0,0 +1,143 @@ +import joplin from 'api'; +import { FavoriteDesc, Favorites, FavoriteType } from './favorites'; +import { Settings } from './settings'; + +export class Panel { + private _panel: any; + private _favs: Favorites; + private _settings: Settings; + + constructor(favs: Favorites, settings: Settings) { + this._favs = favs; + this._settings = settings; + } + + /** + * Register plugin panel and update webview for the first time. + */ + async register() { + this._panel = await joplin.views.panels.create('favorites.panel'); + await joplin.views.panels.addScript(this._panel, './assets/fontawesome/css/all.min.css'); + await joplin.views.panels.addScript(this._panel, './webview.css'); + await joplin.views.panels.addScript(this._panel, './webview.js'); + await joplin.views.panels.onMessage(this._panel, async (message: any) => { + if (message.name === 'favsAddFolder') { + await joplin.commands.execute('favsAddFolder', message.id, message.targetIdx); + } + if (message.name === 'favsAddNote') { + await joplin.commands.execute('favsAddNote', message.id, message.targetIdx); + } + if (message.name === 'favsEdit') { + await joplin.commands.execute('favsEditFavorite', message.index); + } + if (message.name === 'favsOpen') { + await joplin.commands.execute('favsOpenFavorite', message.index); + } + if (message.name === 'favsRename') { + await this._favs.changeTitle(message.index, message.newTitle); + await this.updateWebview(); + } + if (message.name === 'favsDrag') { + await this._favs.moveWithIndex(message.index, message.targetIdx); + await this.updateWebview(); + } + if (message.name === 'favsDelete') { + await this._favs.delete(message.index); + await this.updateWebview(); + } + }); + + // set init message + await joplin.views.panels.setHtml(this._panel, ` +
+
+

Loading panel...

+
+
+ `); + + await this.updateWebview(); + } + + private getPanelTitleHtml(): string { + let panelTitleHtml: string = ''; + + if (this._settings.showPanelTitle) { + const fg = this._settings.foreground; + + panelTitleHtml = ` +
+ + FAVORITES +
+ `; + } + return panelTitleHtml; + } + + // create HTML for each favorite + private getFavoritesHtml(): string { + const favsHtml: any = []; + let index: number = 0; + + for (const favorite of this._favs.all) { + const fg = this._settings.foreground; + const bg = this._settings.background; + const hoverBg = this._settings.hoverBackground; + const dividerColor = this._settings.dividerColor; + + let typeIconHtml: string = ''; + if (this._settings.showTypeIcons) { + typeIconHtml = ``; + } + + favsHtml.push(` +
+ + ${typeIconHtml} + + + + + + +
+ `); + } + return favsHtml.join('\n'); + } + + async updateWebview() { + const panelTitleHtml: string = this.getPanelTitleHtml(); + const favsHtml: string = this.getFavoritesHtml(); + + // add entries to container and push to panel + await joplin.views.panels.setHtml(this._panel, ` +
+ ${panelTitleHtml} +
+ ${favsHtml} +
+
+
+ `); + + // store the current favorites array back to the settings + // - Currently there's no "event" to call store() only on App closing + // - Which would be preferred + await this._settings.storeFavorites(this._favs.all); + } + + /** + * Toggle visibility of the panel. + */ + async toggleVisibility() { + const isVisible: boolean = await joplin.views.panels.visible(this._panel); + await joplin.views.panels.show(this._panel, (!isVisible)); + } +} \ No newline at end of file diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..e9b17a0 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,287 @@ +import joplin from 'api'; +import { SettingItemType } from 'api/types'; +import { ChangeEvent } from 'api/JoplinSettings'; + +/** + * Advanced style setting default values. + * Used when setting is set to 'default'. + */ +enum SettingDefaults { + Default = 'default', + FontFamily = 'Roboto', + FontSize = 'var(--joplin-font-size)', + Background = 'var(--joplin-background-color3)', + HoverBackground = 'var(--joplin-background-color-hover3)', // var(--joplin-background-hover) + Foreground = 'var(--joplin-color-faded)', + DividerColor = 'var(--joplin-divider-color)' +} + +/** + * Definitions of plugin settings. + */ +export class Settings { + // private settings + private _store: any[] = new Array(); + // general settings + private _enableDragAndDrop: boolean = true; + private _editBeforeAdd: boolean = true; + private _showPanelTitle: boolean = true; + private _showTypeIcons: boolean = true; + private _lineHeight: number = 30; + private _minFavoriteWidth: number = 15; + private _maxFavoriteWidth: number = 100; + // advanced settings + private _fontFamily: string = SettingDefaults.Default; + private _fontSize: string = SettingDefaults.Default; + private _background: string = SettingDefaults.Default; + private _hoverBackground: string = SettingDefaults.Default; + private _foreground: string = SettingDefaults.Default; + private _dividerColor: string = SettingDefaults.Default; + // internals + private _defaultRegExp: RegExp = new RegExp(SettingDefaults.Default, "i"); + + constructor() { + } + + //#region GETTER + + get favorites(): any[] { + return this._store; + } + + get enableDragAndDrop(): boolean { + return this._enableDragAndDrop; + } + + get editBeforeAdd(): boolean { + return this._editBeforeAdd; + } + + get showPanelTitle(): boolean { + return this._showPanelTitle; + } + + get showTypeIcons(): boolean { + return this._showTypeIcons; + } + + get lineHeight(): number { + return this._lineHeight; + } + + get minFavWidth(): number { + return this._minFavoriteWidth; + } + + get maxFavWidth(): number { + return this._maxFavoriteWidth; + } + + get fontFamily(): string { + return this._fontFamily; + } + + get fontSize(): string { + return this._fontSize; + } + + get background(): string { + return this._background; + } + + get hoverBackground(): string { + return this._hoverBackground; + } + + get foreground(): string { + return this._foreground; + } + + get dividerColor(): string { + return this._dividerColor; + } + + //#endregion + + //#region GLOBAL VALUES + + //#endregion + + /** + * Register settings section with all options and intially read them at the end. + */ + async register() { + // settings section + await joplin.settings.registerSection('favorites.settings', { + label: 'Favorites', + iconName: 'fas fa-star' + }); + + // private settings + await joplin.settings.registerSetting('favorites', { + value: [], + type: SettingItemType.Array, + section: 'favorites.settings', + public: false, + label: 'Favorites' + }); + this._store = await joplin.settings.value('favorites'); + + // general settings + await joplin.settings.registerSetting('enableDragAndDrop', { + value: this._enableDragAndDrop, + type: SettingItemType.Bool, + section: 'favorites.settings', + public: true, + label: 'Enable drag & drop of favorites', + description: 'If enabled, the position of favorites can be change via drag & drop.' + }); + await joplin.settings.registerSetting('editBeforeAdd', { + value: this._editBeforeAdd, + type: SettingItemType.Bool, + section: 'favorites.settings', + public: true, + label: 'Edit favorite before add', + description: 'Opens a dialog to edit the favorite before adding it. If disabled, the name can still be changed later.' + }); + await joplin.settings.registerSetting('showPanelTitle', { + value: this._showPanelTitle, + type: SettingItemType.Bool, + section: 'favorites.settings', + public: true, + label: 'Show favorites panel title', + description: "Display 'FAVORITES' title in front of the favorites." + }); + await joplin.settings.registerSetting('showTypeIcons', { + value: this._showTypeIcons, + type: SettingItemType.Bool, + section: 'favorites.settings', + public: true, + label: 'Show type icons for favorites', + description: 'Display icons before favorite titles representing the types (notebook, note, tag, etc.).' + }); + await joplin.settings.registerSetting('lineHeight', { + value: this._lineHeight, + type: SettingItemType.Int, + section: 'favorites.settings', + public: true, + minimum: 20, + label: 'Line height (px)', + description: 'Line height of the favorites panel.' + }); + await joplin.settings.registerSetting('minFavoriteWidth', { + value: this._minFavoriteWidth, + type: SettingItemType.Int, + section: 'favorites.settings', + public: true, + label: 'Minimum favorite width (px)', + description: 'Minimum width of one favorite in pixel.' + }); + await joplin.settings.registerSetting('maxFavoriteWidth', { + value: this._maxFavoriteWidth, + type: SettingItemType.Int, + section: 'favorites.settings', + public: true, + label: 'Maximum favorite width (px)', + description: 'Maximum width of one favorite in pixel.' + }); + + // advanced settings + await joplin.settings.registerSetting('fontFamily', { + value: this._fontFamily, + type: SettingItemType.String, + section: 'favorites.settings', + public: true, + advanced: true, + label: 'Font family', + description: "Font family used in the panel. Font families other than 'default' must be installed on the system. If the font is incorrect or empty, it might default to a generic sans-serif font. (default: Roboto)" + }); + await joplin.settings.registerSetting('fontSize', { + value: this._fontSize, + type: SettingItemType.String, + section: 'favorites.settings', + public: true, + advanced: true, + label: 'Font size', + description: "Font size used in the panel. Values other than 'default' must be specified in valid CSS syntax, e.g. '13px'. (default: App default font size)" + }); + await joplin.settings.registerSetting('mainBackground', { + value: this._background, + type: SettingItemType.String, + section: 'favorites.settings', + public: true, + advanced: true, + label: 'Background color', + description: 'Main background color of the panel. (default: Note list background color)' + }); + await joplin.settings.registerSetting('hoverBackground', { + value: this._hoverBackground, + type: SettingItemType.String, + section: 'favorites.settings', + public: true, + advanced: true, + label: 'Hover Background color', + description: 'Background color used when hovering a favorite. (default: Note list hover color)' + }); + await joplin.settings.registerSetting('mainForeground', { + value: this._foreground, + type: SettingItemType.String, + section: 'favorites.settings', + public: true, + advanced: true, + label: 'Foreground color', + description: 'Foreground color used for text and icons. (default: App faded color)' + }); + await joplin.settings.registerSetting('dividerColor', { + value: this._dividerColor, + type: SettingItemType.String, + section: 'favorites.settings', + public: true, + advanced: true, + label: 'Divider color', + description: 'Color of the divider between the favorites. (default: App default border color)' + }); + + // initially read settings + await this.read(); + } + + private async getOrDefault(event: ChangeEvent, localVar: any, setting: string, defaultValue?: string): Promise { + const read: boolean = (!event || event.keys.includes(setting)); + if (read) { + const value: string = await joplin.settings.value(setting); + if (defaultValue && value.match(this._defaultRegExp)) { + return defaultValue; + } else { + return value; + } + } + return localVar; + } + + /** + * Update settings. Either all or only changed ones. + */ + async read(event?: ChangeEvent) { + this._enableDragAndDrop = await this.getOrDefault(event, this._enableDragAndDrop, 'enableDragAndDrop'); + this._editBeforeAdd = await this.getOrDefault(event, this._editBeforeAdd, 'editBeforeAdd'); + this._showPanelTitle = await this.getOrDefault(event, this._showPanelTitle, 'showPanelTitle'); + this._showTypeIcons = await this.getOrDefault(event, this._showTypeIcons, 'showTypeIcons'); + this._lineHeight = await this.getOrDefault(event, this._lineHeight, 'lineHeight'); + this._minFavoriteWidth = await this.getOrDefault(event, this._minFavoriteWidth, 'minFavoriteWidth'); + this._maxFavoriteWidth = await this.getOrDefault(event, this._maxFavoriteWidth, 'maxFavoriteWidth'); + this._fontFamily = await this.getOrDefault(event, this._fontFamily, 'fontFamily', SettingDefaults.FontFamily); + this._fontSize = await this.getOrDefault(event, this._fontSize, 'fontSize', SettingDefaults.FontSize); + this._background = await this.getOrDefault(event, this._background, 'mainBackground', SettingDefaults.Background); + this._hoverBackground = await this.getOrDefault(event, this._hoverBackground, 'hoverBackground', SettingDefaults.HoverBackground); + this._foreground = await this.getOrDefault(event, this._foreground, 'mainForeground', SettingDefaults.Foreground); + this._dividerColor = await this.getOrDefault(event, this._dividerColor, 'dividerColor', SettingDefaults.DividerColor); + } + + /** + * Store the handled favorites array back to the settings. + */ + async storeFavorites(favorites: any[]) { + await joplin.settings.setValue('favorites', favorites); + } +} diff --git a/src/webview.css b/src/webview.css index c3d5913..642d987 100644 --- a/src/webview.css +++ b/src/webview.css @@ -26,18 +26,14 @@ span { } .fas { overflow: initial; + /* Regular does not support all required icons */ + /* font-weight: 400; */ } /* HORIZONTAL LAYOUT */ #container { - height: 100%; - overflow-x: auto; - overflow-y: hidden; - width: 100%; -} -#container-inner { display: flex; - float: left; + height: 100%; width: 100%; } @@ -47,17 +43,28 @@ span { padding: 0 5px; } +#favs-container { + display: flex; + float: left; + overflow-x: overlay; + overflow-y: overlay; + width: 100%; +} +#favs-container:empty { + min-width: 100%; +} #favorite { align-items: center; border-style: solid; border-width: 0; display: flex; - opacity: 0.8; } .favorite-inner { + align-items: center; border-width: 0; border-right-width: 1px; border-style: solid; + display: flex; padding: 0 3px; width: 100%; } @@ -65,6 +72,43 @@ span { border-style: none; } +input.title { + background: none; + border: none; + display: flex; + font-family: inherit; + font-size: inherit; + text-overflow: ellipsis; + width: 100%; +} +input.title:hover { + background: none; + cursor: default; +} +input.title:focus { + border-color: var(--joplin-warning-background-color); + border-radius: 3px; + border-style: solid; + border-width: 1px; + margin-right: 8px; + outline: none; + padding: 3px 1px; +} + +.controls { + border-radius: 3px; + display: none; + opacity: 0; + position: relative; + right: 10px; +} +.controls:hover { + opacity: 1; +} +.controls > .fas { + cursor: pointer; +} + /* DRAG AND DROP */ [draggable="true"] { /* To prevent user selecting inside the drag source */ @@ -73,12 +117,10 @@ span { -webkit-user-select: none; -ms-user-select: none; } -.dragging { - opacity: 0.4 !important; -} ::-webkit-scrollbar { height: 4px; + width: 7px; } ::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); @@ -88,19 +130,21 @@ span { /* VERCTICAL LAYOUT OVERWRITES */ @media screen and (max-width: 400px) { #container { - overflow-x: hidden; - overflow-y: auto; - } - - #container-inner { display: block; - width: 100%; } #panel-title { padding-left: 12px; } + #favs-container { + display: block; + height: 100% !important; + width: 100%; + } + #favs-container:empty { + min-height: 100% !important; + } #favorite { border-bottom-width: 1px; max-width: 100% !important; @@ -111,7 +155,7 @@ span { text-align: left; } - ::-webkit-scrollbar { - width: 7px; + .controls { + display: table; } } diff --git a/src/webview.js b/src/webview.js index 7a1bd41..761a250 100644 --- a/src/webview.js +++ b/src/webview.js @@ -1,72 +1,147 @@ +let editStarted = false; +let sourceIdx = ''; -function getDataId(event) { - if (event.currentTarget.id === 'favorite') { - return event.currentTarget.dataset.id; +function cancelDefault(event) { + event.preventDefault(); + event.stopPropagation(); + return false; +} + +function getDataId(currentTarget) { + if (currentTarget && currentTarget.id === 'favorite') { + return currentTarget.dataset.id; + } else { + return; } - return; } -/* RIGHT CLICK EVENT */ -function favsContext(event) { - const dataId = getDataId(event); - if (dataId) { - webviewApi.postMessage({ name: 'favsEdit', id: dataId }); +/* EVENT HANDLER */ + +function openFav(currentTarget) { + const dataIdx = getDataId(currentTarget); + if (dataIdx) { + webviewApi.postMessage({ name: 'favsOpen', index: dataIdx }); } } -/* CLICK EVENT */ -function favsClick(event) { - const dataId = getDataId(event); - if (dataId) { - webviewApi.postMessage({ name: 'favsOpen', id: dataId }); +function deleteFav(currentTarget) { + const dataIdx = getDataId(currentTarget); + if (dataIdx) { + webviewApi.postMessage({ name: 'favsDelete', index: dataIdx }); } } -/* DRAG AND DROP */ -let sourceId = ''; +function openDialog(event) { + if (!editStarted) { + const dataIdx = getDataId(event.currentTarget); + if (dataIdx) { + webviewApi.postMessage({ name: 'favsEdit', index: dataIdx }); + } + } +} -function cancelDefault(event) { - event.preventDefault(); - event.stopPropagation(); - return false; +function enableEdit(element, value) { + editStarted = value; + element.disabled = (!value); + element.focus(); + element.select(); } -function dragStart(event) { - const dataId = getDataId(event); - if (dataId) { - event.currentTarget.classList.add('dragging'); - event.dataTransfer.setData('text/x-plugin-favorites-id', dataId); - sourceId = dataId; +function editFav(currentTarget) { + const input = currentTarget.getElementsByTagName('input')[0]; + if (input) { + enableEdit(input, true); } } -function dragEnd(event) { +// default click handler +function clickFav(event) { cancelDefault(event); - event.currentTarget.classList.remove('dragging'); - document.querySelectorAll('#favorite').forEach(x => { - x.style.background = 'none'; - }); - sourceId = ''; + if (!editStarted) { + if (event.target.classList.contains('rename')) { + editFav(event.currentTarget); + } else if (event.target.classList.contains('delete')) { + deleteFav(event.currentTarget); + } else { + openFav(event.currentTarget); + } + } } -function dragOver(event, hoverColor) { +// rename finished with changes +document.addEventListener('change', event => { cancelDefault(event); - if (sourceId) { - const dataId = getDataId(event); - if (dataId) { - document.querySelectorAll('#favorite').forEach(x => { - if (x.dataset.id !== dataId) x.style.background = 'none'; - }); - - if (sourceId !== dataId) { - event.currentTarget.style.background = hoverColor; - } + const element = event.target; + if (editStarted && element.className === 'title') { + enableEdit(element, false); + const dataIdx = element.parentElement.parentElement.dataset.id; + if (dataIdx && element.value !== '') { + webviewApi.postMessage({ name: 'favsRename', index: dataIdx, newTitle: element.value }); + } else { + element.value = element.title; } } +}); + +// input lost focus (w/o changes) +document.addEventListener('focusout', (event) => { + cancelDefault(event); + const element = event.target; + if (editStarted && element.className === 'title') { + enableEdit(element, false); + element.value = element.title; + } +}); + +// scroll horizontally without 'shift' key +document.addEventListener('wheel', (event) => { + const element = document.getElementById('favs-container'); + if (element) { + element.scrollLeft -= (-event.deltaY); + } +}); + +/* DRAG AND DROP */ + +function setBackground(event, background) { + event.currentTarget.style.background = background; +} + +function resetBackground(element) { + if (element.dataset.bg) { + element.style.background = element.dataset.bg; + } +} + +function resetTabBackgrounds() { + document.querySelectorAll('#favorite').forEach(x => { resetBackground(x); }); + + container = document.querySelector('#favs-container'); + if (container) { + container.style.background = 'none'; + } +} + +function dragStart(event) { + const dataIdx = getDataId(event.currentTarget); + if (dataIdx) { + event.dataTransfer.setData('text/x-plugin-favorites-id', dataIdx); + sourceIdx = dataIdx; + } +} + +function dragEnd(event) { + resetTabBackgrounds(); + cancelDefault(event); + sourceIdx = ''; } -function dragOverTitle(event) { +function dragOver(event, hoverColor) { + resetTabBackgrounds(); cancelDefault(event); + if (sourceIdx !== getDataId(event.currentTarget)) { + setBackground(event, hoverColor); + } } function dragLeave(event) { @@ -74,42 +149,45 @@ function dragLeave(event) { } function drop(event) { + resetTabBackgrounds(); cancelDefault(event); - const dataSourceId = event.dataTransfer.getData('text/x-plugin-favorites-id'); - if (dataSourceId) { - const dataTargetId = getDataId(event); - if (dataTargetId !== sourceId) { - webviewApi.postMessage({ name: 'favsDrag', targetId: dataTargetId, sourceId: dataSourceId }); + const dataTargetIdx = getDataId(event.currentTarget); + + // check whether plugin tab was dragged - trigger favsDrag message + const dataSourceIdx = event.dataTransfer.getData('text/x-plugin-favorites-id'); + if (dataSourceIdx) { + if (dataTargetIdx !== sourceIdx) { + webviewApi.postMessage({ name: 'favsDrag', index: dataSourceIdx, targetIdx: dataTargetIdx, }); + return; } } -} - -function dropOnTitle(event) { - cancelDefault(event); // check whether folder was dragged from app onto the panel - trigger favsAddFolder then - const appDragFolderIds = event.dataTransfer.getData('text/x-jop-folder-ids'); - if (appDragFolderIds) { - const folderIds = JSON.parse(appDragFolderIds); + const joplinFolderIds = event.dataTransfer.getData('text/x-jop-folder-ids'); + if (joplinFolderIds) { + const folderIds = JSON.parse(joplinFolderIds); if (folderIds.length == 1) { - webviewApi.postMessage({ name: 'favsAddFolder', id: folderIds[0] }); + webviewApi.postMessage({ name: 'favsAddFolder', id: folderIds[0], targetIdx: dataTargetIdx }); + return; } } - // check whether note was dragged from app onto the panel - trigger favsAddNote then - const appDragNoteIds = event.dataTransfer.getData('text/x-jop-note-ids'); - if (appDragNoteIds) { - const ids = new Array(); - for (const noteId of JSON.parse(appDragNoteIds)) { - ids.push(noteId); + // check whether note was dragged from app onto the panel - add new favorite at dropped index + const joplinNoteIds = event.dataTransfer.getData('text/x-jop-note-ids'); + if (joplinNoteIds) { + const noteIds = new Array(); + for (const noteId of JSON.parse(joplinNoteIds)) { + noteIds.push(noteId); } - webviewApi.postMessage({ name: 'favsAddNote', id: ids }); + webviewApi.postMessage({ name: 'favsAddNote', id: noteIds, targetIdx: dataTargetIdx }); + return; } - // check whether tab (from joplin.plugin.note.tabs plugin) was dragged onto the panel - trigger favsAddNote then - const appDragTabId = event.dataTransfer.getData('text/x-plugin-note-tabs-id'); // 'text/plain' - if (appDragTabId) { - const ids = new Array(appDragTabId); - webviewApi.postMessage({ name: 'favsAddNote', id: ids }); + // check whether tab (from joplin.plugin.note.tabs plugin) was dragged onto the panel - add new favorite at dropped index + const noteTabsId = event.dataTransfer.getData('text/x-plugin-note-tabs-id'); + if (noteTabsId) { + const noteIds = new Array(noteTabsId); + webviewApi.postMessage({ name: 'favsAddNote', id: noteIds, targetIdx: dataTargetIdx }); + return; } }