From 2c386e67e210a111f5cac95a51b6e19f0fe56416 Mon Sep 17 00:00:00 2001 From: antelle Date: Sun, 4 Jul 2021 20:54:59 +0200 Subject: [PATCH] plugin settings --- app/scripts/plugins/plugin-gallery.ts | 30 +- app/scripts/plugins/plugin-manager.ts | 28 +- app/scripts/plugins/theme-vars.ts | 39 +-- app/scripts/plugins/types.ts | 2 +- .../ui-old/settings/settings-plugins-view.js | 266 ------------------ .../plugins/settings-gallery-plugin.ts | 24 ++ .../ui/settings/plugins/settings-plugin.ts | 76 +++++ app/scripts/ui/settings/settings-plugins.ts | 108 +++++++ app/scripts/util/events.ts | 1 - .../general/settings-general-storage-view.tsx | 2 +- .../plugins/settings-gallery-plugin-view.tsx | 74 +++++ .../settings/plugins/settings-plugin-view.tsx | 229 +++++++++++++++ .../views/settings/settings-plugins-view.tsx | 129 +++++++++ app/scripts/views/settings/settings-view.tsx | 2 + app/templates/settings/settings-plugins.hbs | 166 ----------- 15 files changed, 703 insertions(+), 473 deletions(-) delete mode 100644 app/scripts/ui-old/settings/settings-plugins-view.js create mode 100644 app/scripts/ui/settings/plugins/settings-gallery-plugin.ts create mode 100644 app/scripts/ui/settings/plugins/settings-plugin.ts create mode 100644 app/scripts/ui/settings/settings-plugins.ts create mode 100644 app/scripts/views/settings/plugins/settings-gallery-plugin-view.tsx create mode 100644 app/scripts/views/settings/plugins/settings-plugin-view.tsx create mode 100644 app/scripts/views/settings/settings-plugins-view.tsx delete mode 100644 app/templates/settings/settings-plugins.hbs diff --git a/app/scripts/plugins/plugin-gallery.ts b/app/scripts/plugins/plugin-gallery.ts index 09576f4f..75e0eeef 100644 --- a/app/scripts/plugins/plugin-gallery.ts +++ b/app/scripts/plugins/plugin-gallery.ts @@ -1,17 +1,17 @@ import * as kdbxweb from 'kdbxweb'; -import { Events } from 'util/events'; import { SettingsStore } from 'comp/settings/settings-store'; import { Links } from 'const/links'; import { SignatureVerifier } from 'util/data/signature-verifier'; import { Logger } from 'util/logger'; import { PluginGalleryData } from 'plugins/types'; +import { Model } from 'util/model'; -const PluginGallery = { - logger: new Logger('plugin-gallery'), +class PluginGallery extends Model { + logger = new Logger('plugin-gallery'); - gallery: undefined as PluginGalleryData | undefined, - loading: false, - loadError: false, + gallery?: PluginGalleryData; + loading = false; + loadError = false; async loadPlugins(): Promise { if (this.gallery) { @@ -49,18 +49,16 @@ const PluginGallery = { this.gallery = data; await this.saveGallery(data); - Events.emit('plugin-gallery-load-complete'); return this.gallery; } catch (e) { this.loading = false; this.loadError = true; this.logger.error('Error loading plugin gallery', e); - Events.emit('plugin-gallery-load-complete'); throw e; } - }, + } - async verifySignature(gallery: PluginGalleryData): Promise { + private async verifySignature(gallery: PluginGalleryData): Promise { const dataToVerify = JSON.stringify(gallery, null, 2).replace(gallery.signature, ''); const valid = await SignatureVerifier.verify( kdbxweb.ByteUtils.stringToBytes(dataToVerify), @@ -70,7 +68,7 @@ const PluginGallery = { this.logger.error('JSON signature invalid'); throw new Error('JSON signature invalid'); } - }, + } async getCachedGallery(): Promise { const ts = this.logger.ts(); @@ -84,11 +82,13 @@ const PluginGallery = { this.logger.error('Cannot load cached plugin gallery: signature validation error'); } } - }, + } - saveGallery(data: PluginGalleryData): Promise { + private saveGallery(data: PluginGalleryData): Promise { return SettingsStore.save('plugin-gallery', data); } -}; +} -export { PluginGallery }; +const instance = new PluginGallery(); + +export { instance as PluginGallery }; diff --git a/app/scripts/plugins/plugin-manager.ts b/app/scripts/plugins/plugin-manager.ts index 919afe7a..35fdc8b9 100644 --- a/app/scripts/plugins/plugin-manager.ts +++ b/app/scripts/plugins/plugin-manager.ts @@ -7,6 +7,7 @@ import { SignatureVerifier } from 'util/data/signature-verifier'; import { Logger } from 'util/logger'; import { PluginGalleryData, PluginManifest, StoredPlugin, StoredPlugins } from 'plugins/types'; import { Timeouts } from 'const/timeouts'; +import { errorToString } from 'util/fn'; const logger = new Logger('plugin-mgr'); @@ -15,6 +16,9 @@ class PluginManager extends Model { autoUpdateAppVersion?: string; autoUpdateDate?: Date; + installing = new Set(); + installErrors = new Map(); + async init() { const ts = logger.ts(); const storedPlugins = (await SettingsStore.load('plugins')) as StoredPlugins; @@ -44,13 +48,25 @@ class PluginManager extends Model { expectedManifest?: PluginManifest, skipSignatureValidation?: boolean ): Promise { - const plugin = await Plugin.loadFromUrl(url, expectedManifest); - await this.uninstall(plugin.id); - if (skipSignatureValidation) { - plugin.skipSignatureValidation = true; + this.installErrors.delete(url); + this.installing = new Set(this.installing).add(url); + try { + const plugin = await Plugin.loadFromUrl(url, expectedManifest); + await this.uninstall(plugin.id); + if (skipSignatureValidation) { + plugin.skipSignatureValidation = true; + } + await plugin.install(true, false); + this.plugins = this.plugins.concat(plugin); + + this.installErrors.delete(url); + } catch (e) { + this.installErrors.set(url, errorToString(e)); + } finally { + const installing = new Set(this.installing); + installing.delete(url); + this.installing = installing; } - await plugin.install(true, false); - this.plugins = this.plugins.concat(plugin); await this.saveState(); } diff --git a/app/scripts/plugins/theme-vars.ts b/app/scripts/plugins/theme-vars.ts index 01b4645d..756e5932 100644 --- a/app/scripts/plugins/theme-vars.ts +++ b/app/scripts/plugins/theme-vars.ts @@ -1,25 +1,35 @@ import { Color } from 'util/data/color'; -const ThemeVarsScss = require('!!raw-loader!../../styles/base/_theme-vars.scss') as string; -const ThemeDefaults = require('!!raw-loader!../../styles/themes/_theme-defaults.scss') as string; +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +const ThemeVarsScss = require('!!raw-loader!../../styles/base/_theme-vars.scss').default as string; +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +const ThemeDefaults = require('!!raw-loader!../../styles/themes/_theme-defaults.scss') + .default as string; type StyleVar = Color | string | number; +function checkColor(colorVar: StyleVar): Color { + if (colorVar instanceof Color) { + return colorVar; + } else if (typeof colorVar === 'string') { + return new Color(colorVar); + } else { + throw new TypeError(`Not a color: ${colorVar}`); + } +} + const ThemeFunctions: Record StyleVar> = { 'mix'(color1: StyleVar, color2: StyleVar, percent: StyleVar): StyleVar { - if (!(color1 instanceof Color)) throw new TypeError('color1 is not a Color'); - if (!(color2 instanceof Color)) throw new TypeError('color2 is not a Color'); if (typeof percent !== 'number') throw new TypeError('percent is not a number'); - return color1.mix(color2, percent).toRgba(); + return checkColor(color1).mix(checkColor(color2), percent).toRgba(); }, 'semi-mute-percent'(mutePercent: StyleVar): StyleVar { if (typeof mutePercent !== 'number') throw new TypeError('mutePercent is not a number'); return mutePercent / 2; }, 'rgba'(color: StyleVar, alpha: StyleVar): StyleVar { - if (!(color instanceof Color)) throw new TypeError('color is not a Color'); if (typeof alpha !== 'number') throw new TypeError('alpha is not a number'); - const res = new Color(color); + const res = new Color(checkColor(color)); res.a = alpha; return res.toRgba(); }, @@ -29,28 +39,23 @@ const ThemeFunctions: Record StyleVar> = { thBg: StyleVar, thText: StyleVar ): StyleVar { - if (!(color instanceof Color)) throw new TypeError('color is not a Color'); if (typeof lshift !== 'number') throw new TypeError('lshift is not a number'); - if (!(thBg instanceof Color)) throw new TypeError('thBg is not a Color'); - if (!(thText instanceof Color)) throw new TypeError('thText is not a Color'); - if (color.l - lshift >= thBg.l) { - return thText.toRgba(); + if (checkColor(color).l - lshift >= checkColor(thBg).l) { + return checkColor(thText).toRgba(); } - return thBg.toRgba(); + return checkColor(thBg).toRgba(); }, 'lightness-alpha'(color: StyleVar, lightness: StyleVar, alpha: StyleVar): StyleVar { - if (!(color instanceof Color)) throw new TypeError('color is not a Color'); if (typeof lightness !== 'number') throw new TypeError('lightness is not a number'); if (typeof alpha !== 'number') throw new TypeError('alpha is not a number'); - const res = new Color(color); + const res = new Color(checkColor(color)); res.l += Math.min(0, Math.max(1, lightness)); res.a += Math.min(0, Math.max(1, alpha)); return res.toHsla(); }, 'shade'(color: StyleVar, percent: StyleVar): StyleVar { - if (!(color instanceof Color)) throw new TypeError('color is not a Color'); if (typeof percent !== 'number') throw new TypeError('percent is not a number'); - return Color.black.mix(color, percent).toRgba(); + return Color.black.mix(checkColor(color), percent).toRgba(); } }; diff --git a/app/scripts/plugins/types.ts b/app/scripts/plugins/types.ts index bc5c823b..ff61b95a 100644 --- a/app/scripts/plugins/types.ts +++ b/app/scripts/plugins/types.ts @@ -61,7 +61,7 @@ export interface PluginSetting { type: 'text' | 'select' | 'checkbox'; value?: string | boolean; placeholder?: string; - maxlength?: string; + maxlength?: number; options?: PluginSettingOption[]; } diff --git a/app/scripts/ui-old/settings/settings-plugins-view.js b/app/scripts/ui-old/settings/settings-plugins-view.js deleted file mode 100644 index 54ac1a10..00000000 --- a/app/scripts/ui-old/settings/settings-plugins-view.js +++ /dev/null @@ -1,266 +0,0 @@ -import { View } from 'framework/views/view'; -import { Events } from 'framework/events'; -import { RuntimeInfo } from 'const/runtime-info'; -import { Launcher } from 'comp/launcher'; -import { SettingsManager } from 'comp/settings/settings-manager'; -import { Links } from 'const/links'; -import { AppSettingsModel } from 'models/app-settings-model'; -import { PluginGallery } from 'plugins/plugin-gallery'; -import { PluginManager } from 'plugins/plugin-manager'; -import { Comparators } from 'util/data/comparators'; -import { SemVer } from 'util/data/semver'; -import { Features } from 'util/features'; -import { DateFormat } from 'comp/i18n/date-format'; -import { Locale } from 'util/locale'; -import template from 'templates/settings/settings-plugins.hbs'; - -class SettingsPluginsView extends View { - template = template; - - events = { - 'click .settings_plugins-install-btn': 'installClick', - 'click .settings_plugins-uninstall-btn': 'uninstallClick', - 'click .settings_plugins-disable-btn': 'disableClick', - 'click .settings_plugins-enable-btn': 'enableClick', - 'click .settings_plugins-update-btn': 'updateClick', - 'click .settings_plugins-use-locale-btn': 'useLocaleClick', - 'click .settings_plugins-use-theme-btn': 'useThemeClick', - 'click .settings__plugins-gallery-plugin-install-btn': 'galleryInstallClick', - 'input .settings__plugins-gallery-search': 'gallerySearchInput', - 'change select.settings__plugins-plugin-input': 'pluginSettingChange', - 'change input[type=checkbox].settings__plugins-plugin-input': 'pluginSettingChange', - 'input input[type=text].settings__plugins-plugin-input': 'pluginSettingChange', - 'change .settings__plugins-plugin-updates': 'autoUpdateChange', - 'click .settings__plugins-gallery-load-btn': 'loadPluginGalleryClick' - }; - - searchStr = null; - installFromUrl = null; - installing = {}; - installErrors = {}; - - constructor(model, options) { - super(model, options); - this.listenTo(PluginManager, 'change', this.render.bind(this)); - this.listenTo( - Events, - 'plugin-gallery-load-complete', - this.pluginGalleryLoadComplete.bind(this) - ); - } - - render() { - super.render({ - plugins: PluginManager.plugins - .map((plugin) => ({ - id: plugin.id, - manifest: plugin.manifest, - status: plugin.status, - installTime: Math.round(plugin.installTime), - updateError: plugin.updateError, - updateCheckDate: DateFormat.dtStr(plugin.updateCheckDate), - installError: plugin.installError, - official: plugin.official, - autoUpdate: plugin.autoUpdate, - settings: plugin.getSettings() - })) - .sort(Comparators.stringComparator('id', true)), - installingFromUrl: this.installFromUrl && !this.installFromUrl.error, - installUrl: this.installFromUrl ? this.installFromUrl.url : null, - installUrlError: this.installFromUrl ? this.installFromUrl.error : null, - galleryLoading: PluginGallery.loading, - galleryLoadError: PluginGallery.loadError, - galleryPlugins: this.getGalleryPlugins(), - searchStr: this.searchStr, - hasUnicodeFlags: Features.hasUnicodeFlags, - pluginDevLink: Links.PluginDevelopStart, - translateLink: Links.Translation - }); - if (this.searchStr) { - this.showFilterResults(); - } - } - - pluginGalleryLoadComplete() { - this.render(); - Events.emit('page-geometry', { source: 'view' }); - } - - getGalleryPlugins() { - if (!PluginGallery.gallery) { - return null; - } - const plugins = PluginManager.plugins; - return PluginGallery.gallery.plugins - .map((pl) => ({ - url: pl.url, - manifest: pl.manifest, - installing: this.installing[pl.url], - installError: this.installErrors[pl.url], - official: pl.official - })) - .filter((pl) => !plugins.get(pl.manifest.name) && this.canInstallPlugin(pl)) - .sort((x, y) => x.manifest.name.localeCompare(y.manifest.name)); - } - - canInstallPlugin(plugin) { - if (plugin.manifest.locale && SettingsManager.allLocales[plugin.manifest.locale.name]) { - return false; - } - if (plugin.manifest.desktop && !Launcher) { - return false; - } - if ( - plugin.manifest.versionMin && - SemVer.compareVersions(plugin.manifest.versionMin, RuntimeInfo.version) > 0 - ) { - return false; - } - if ( - plugin.manifest.versionMax && - SemVer.compareVersions(plugin.manifest.versionMax, RuntimeInfo.version) > 0 - ) { - return false; - } - return true; - } - - loadPluginGalleryClick() { - if (PluginGallery.loading) { - return; - } - PluginGallery.loadPlugins(); - this.render(); - } - - installClick() { - const installBtn = this.$el.find('.settings_plugins-install-btn'); - const urlTextBox = this.$el.find('#settings__plugins-install-url'); - const errorBox = this.$el.find('.settings__plugins-install-error'); - errorBox.empty(); - const url = urlTextBox.val().trim(); - if (!url) { - return; - } - urlTextBox.prop('disabled', true); - installBtn.text(Locale.setPlInstallBtnProgress + '...').prop('disabled', true); - this.installFromUrl = { url }; - PluginManager.install(url, undefined, true) - .then(() => { - this.installFinished(); - this.installFromUrl = null; - this.render(); - this.$el.closest('.scroller').scrollTop(0); - }) - .catch((e) => { - this.installFinished(); - this.installFromUrl.error = e; - this.$el.find('.settings__plugins-install-error').text(e.toString()); - this.$el.closest('.scroller').scrollTop(this.$el.height()); - }); - } - - installFinished() { - const installBtn = this.$el.find('.settings_plugins-install-btn'); - const urlTextBox = this.$el.find('#settings__plugins-install-url'); - urlTextBox.prop('disabled', false); - installBtn.text(Locale.setPlInstallBtn).prop('disabled', false); - } - - uninstallClick(e) { - const pluginId = $(e.target).data('plugin'); - PluginManager.uninstall(pluginId); - } - - disableClick(e) { - const pluginId = $(e.target).data('plugin'); - PluginManager.disable(pluginId); - } - - enableClick(e) { - const pluginId = $(e.target).data('plugin'); - PluginManager.activate(pluginId); - } - - updateClick(e) { - const pluginId = $(e.target).data('plugin'); - PluginManager.update(pluginId); - } - - useLocaleClick(e) { - const locale = $(e.target).data('locale'); - AppSettingsModel.locale = locale; - } - - useThemeClick(e) { - const theme = $(e.target).data('theme'); - AppSettingsModel.theme = theme; - } - - galleryInstallClick(e) { - const installBtn = $(e.target); - const pluginId = installBtn.data('plugin'); - const plugin = PluginGallery.gallery.plugins.find((pl) => pl.manifest.name === pluginId); - installBtn.text(Locale.setPlInstallBtnProgress + '...').prop('disabled', true); - this.installing[plugin.url] = true; - delete this.installErrors[plugin.url]; - PluginManager.install(plugin.url, plugin.manifest) - .catch((e) => { - this.installErrors[plugin.url] = e; - delete this.installing[plugin.url]; - this.render(); - }) - .then(() => { - installBtn.prop('disabled', true); - delete this.installing[plugin.url]; - }); - } - - gallerySearchInput(e) { - this.searchStr = e.target.value.toLowerCase(); - this.showFilterResults(); - } - - showFilterResults() { - const pluginsById = {}; - for (const plugin of PluginGallery.gallery.plugins) { - pluginsById[plugin.manifest.name] = plugin; - } - for (const pluginEl of $('.settings__plugins-gallery-plugin', this.$el)) { - const pluginId = pluginEl.dataset.plugin; - const visible = this.pluginMatchesFilter(pluginsById[pluginId]); - $(pluginEl).toggle(visible); - } - } - - pluginMatchesFilter(plugin) { - const searchStr = this.searchStr; - const manifest = plugin.manifest; - return !!( - !searchStr || - manifest.name.toLowerCase().indexOf(searchStr) >= 0 || - (manifest.description && manifest.description.toLowerCase().indexOf(searchStr) >= 0) || - (manifest.locale && - (manifest.locale.name.toLowerCase().indexOf(searchStr) >= 0 || - manifest.locale.title.toLowerCase().indexOf(searchStr) >= 0)) - ); - } - - pluginSettingChange(e) { - const el = e.target; - const settingEl = $(el).closest('.settings__plugins-plugin-setting'); - const setting = settingEl.data('setting'); - const pluginId = settingEl.data('plugin'); - const val = el.type === 'checkbox' ? el.checked : el.value; - const plugin = PluginManager.getPlugin(pluginId); - plugin.setSettings({ [setting]: val }); - } - - autoUpdateChange(e) { - const pluginId = $(e.target).data('plugin'); - const enabled = e.target.checked; - PluginManager.setAutoUpdate(pluginId, enabled); - } -} - -export { SettingsPluginsView }; diff --git a/app/scripts/ui/settings/plugins/settings-gallery-plugin.ts b/app/scripts/ui/settings/plugins/settings-gallery-plugin.ts new file mode 100644 index 00000000..7bb64473 --- /dev/null +++ b/app/scripts/ui/settings/plugins/settings-gallery-plugin.ts @@ -0,0 +1,24 @@ +import { FunctionComponent, h } from 'preact'; +import { PluginGalleryPlugin } from 'plugins/types'; +import { SettingsGalleryPluginView } from 'views/settings/plugins/settings-gallery-plugin-view'; +import { Features } from 'util/features'; +import { PluginManager } from 'plugins/plugin-manager'; +import { noop } from 'util/fn'; + +export const SettingsGalleryPlugin: FunctionComponent<{ plugin: PluginGalleryPlugin }> = ({ + plugin +}) => { + const installClicked = () => { + PluginManager.install(plugin.url, plugin.manifest).catch(noop); + }; + + return h(SettingsGalleryPluginView, { + hasUnicodeFlags: Features.hasUnicodeFlags, + manifest: plugin.manifest, + official: plugin.official, + installing: PluginManager.installing.has(plugin.url), + installError: PluginManager.installErrors.get(plugin.url), + + installClicked + }); +}; diff --git a/app/scripts/ui/settings/plugins/settings-plugin.ts b/app/scripts/ui/settings/plugins/settings-plugin.ts new file mode 100644 index 00000000..6b4fc1d8 --- /dev/null +++ b/app/scripts/ui/settings/plugins/settings-plugin.ts @@ -0,0 +1,76 @@ +import { FunctionComponent, h } from 'preact'; +import { useModelWatcher } from 'util/ui/hooks'; +import { Plugin } from 'plugins/plugin'; +import { SettingsPluginView } from 'views/settings/plugins/settings-plugin-view'; +import { DateFormat } from 'util/formatting/date-format'; +import { Features } from 'util/features'; +import { PluginManager } from 'plugins/plugin-manager'; +import { noop } from 'util/fn'; +import { AppSettings } from 'models/app-settings'; +import { SettingsManager } from 'comp/settings/settings-manager'; + +export const SettingsPlugin: FunctionComponent<{ plugin: Plugin }> = ({ plugin }) => { + useModelWatcher(plugin); + + const uninstallClicked = () => { + PluginManager.uninstall(plugin.id).catch(noop); + }; + + const disableClicked = () => { + PluginManager.disable(plugin.id).catch(noop); + }; + + const enableClicked = () => { + PluginManager.activate(plugin.id).catch(noop); + }; + + const updateClicked = () => { + PluginManager.update(plugin.id).catch(noop); + }; + + const autoUpdateChanged = () => { + PluginManager.setAutoUpdate(plugin.id, !plugin.autoUpdate).catch(noop); + }; + + const useLocaleClicked = () => { + if (plugin.manifest.locale?.name) { + AppSettings.locale = plugin.manifest.locale.name; + SettingsManager.setLocale(plugin.manifest.locale.name); + } + }; + + const useThemeClicked = () => { + if (plugin.manifest.theme?.name) { + AppSettings.theme = plugin.manifest.theme.name; + SettingsManager.setTheme(plugin.manifest.theme.name); + } + }; + + const pluginSettingChanged = (key: string, value: string | boolean) => { + plugin.setSettings({ [key]: value }); + }; + + return h(SettingsPluginView, { + hasUnicodeFlags: Features.hasUnicodeFlags, + id: plugin.id, + manifest: plugin.manifest, + status: plugin.status, + installTime: plugin.installTime ? Math.round(plugin.installTime) : undefined, + updateError: plugin.updateError, + updateCheckDate: plugin.updateCheckDate + ? DateFormat.dtStr(plugin.updateCheckDate) + : undefined, + installError: plugin.installError, + autoUpdate: plugin.autoUpdate, + settings: plugin.getSettings(), + + uninstallClicked, + disableClicked, + enableClicked, + updateClicked, + autoUpdateChanged, + useLocaleClicked, + useThemeClicked, + pluginSettingChanged + }); +}; diff --git a/app/scripts/ui/settings/settings-plugins.ts b/app/scripts/ui/settings/settings-plugins.ts new file mode 100644 index 00000000..5871aeed --- /dev/null +++ b/app/scripts/ui/settings/settings-plugins.ts @@ -0,0 +1,108 @@ +import { FunctionComponent, h } from 'preact'; +import { SettingsPluginsView } from 'views/settings/settings-plugins-view'; +import { useModelWatcher } from 'util/ui/hooks'; +import { AppSettings } from 'models/app-settings'; +import { PluginManager } from 'plugins/plugin-manager'; +import { useState } from 'preact/hooks'; +import { PluginGallery } from 'plugins/plugin-gallery'; +import { noop } from 'util/fn'; +import { PluginGalleryPlugin } from 'plugins/types'; +import { SettingsManager } from 'comp/settings/settings-manager'; +import { Launcher } from 'comp/launcher'; +import { SemVer } from 'util/data/semver'; +import { RuntimeInfo } from 'const/runtime-info'; + +function pluginMatchesFilter(searchStr: string, plugin: PluginGalleryPlugin): boolean { + const manifest = plugin.manifest; + return !!( + !searchStr || + manifest.name.toLowerCase().indexOf(searchStr) >= 0 || + (manifest.description && manifest.description.toLowerCase().indexOf(searchStr) >= 0) || + (manifest.locale && + (manifest.locale.name.toLowerCase().indexOf(searchStr) >= 0 || + manifest.locale.title.toLowerCase().indexOf(searchStr) >= 0)) + ); +} + +function canInstallPlugin(plugin: PluginGalleryPlugin): boolean { + if (plugin.manifest.locale && SettingsManager.allLocales[plugin.manifest.locale.name]) { + return false; + } + if (plugin.manifest.desktop && !Launcher) { + return false; + } + if ( + plugin.manifest.versionMin && + SemVer.compareVersions(plugin.manifest.versionMin, RuntimeInfo.version) > 0 + ) { + return false; + } + if ( + plugin.manifest.versionMax && + SemVer.compareVersions(plugin.manifest.versionMax, RuntimeInfo.version) > 0 + ) { + return false; + } + return true; +} + +export const SettingsPlugins: FunctionComponent = () => { + useModelWatcher(AppSettings); + useModelWatcher(PluginManager); + useModelWatcher(PluginGallery); + + const [gallerySearchStr, setGallerySearchStr] = useState(''); + const [installUrl, setInstallUrl] = useState(''); + const [installUrlMalformed, setInstallUrlMalformed] = useState(false); + + const loadGalleryClicked = () => { + if (!PluginGallery.loading) { + PluginGallery.loadPlugins().catch(noop); + } + }; + + const gallerySearchChanged = (text: string) => { + setGallerySearchStr(text); + }; + + const filter = gallerySearchStr.toLowerCase(); + const galleryPlugins = PluginGallery.gallery?.plugins + ?.filter((plugin) => canInstallPlugin(plugin)) + ?.filter((plugin) => pluginMatchesFilter(filter, plugin)) + ?.sort((x, y) => x.manifest.name.localeCompare(y.manifest.name)); + + const installUrlChanged = (url: string) => { + setInstallUrl(url); + if (url) { + try { + const isHTTPS = new URL(url).protocol === 'https:'; + setInstallUrlMalformed(!isHTTPS); + } catch { + setInstallUrlMalformed(true); + } + } else { + setInstallUrlMalformed(false); + } + }; + + const installFromUrlClicked = () => { + PluginManager.install(installUrl).catch(noop); + }; + + return h(SettingsPluginsView, { + plugins: PluginManager.plugins.sort((x, y) => x.id.localeCompare(y.id)), + galleryLoading: PluginGallery.loading, + galleryLoadError: PluginGallery.loadError, + galleryPlugins, + gallerySearchStr, + installUrl, + installingFromUrl: PluginManager.installing.has(installUrl), + installUrlError: PluginManager.installErrors.get(installUrl), + installUrlMalformed, + + loadGalleryClicked, + gallerySearchChanged, + installUrlChanged, + installFromUrlClicked + }); +}; diff --git a/app/scripts/util/events.ts b/app/scripts/util/events.ts index 99a2a073..a5b9b274 100644 --- a/app/scripts/util/events.ts +++ b/app/scripts/util/events.ts @@ -26,7 +26,6 @@ export interface GlobalEventSpec { 'enter-full-screen': () => void; 'leave-full-screen': () => void; 'drag-handle-set': (name: string, size: number | null) => void; - 'plugin-gallery-load-complete': () => void; 'custom-icon-downloaded': (data: string) => void; } diff --git a/app/scripts/views/settings/general/settings-general-storage-view.tsx b/app/scripts/views/settings/general/settings-general-storage-view.tsx index a0c0fc36..d5c7ef38 100644 --- a/app/scripts/views/settings/general/settings-general-storage-view.tsx +++ b/app/scripts/views/settings/general/settings-general-storage-view.tsx @@ -70,7 +70,7 @@ export const SettingsGeneralStorageView: FunctionComponent<{ {prv.enabled && prv.hasConfig ? ( -
+
) : null} diff --git a/app/scripts/views/settings/plugins/settings-gallery-plugin-view.tsx b/app/scripts/views/settings/plugins/settings-gallery-plugin-view.tsx new file mode 100644 index 00000000..fd02366b --- /dev/null +++ b/app/scripts/views/settings/plugins/settings-gallery-plugin-view.tsx @@ -0,0 +1,74 @@ +import { FunctionComponent } from 'preact'; +import { PluginManifest } from 'plugins/types'; +import { Locale } from 'util/locale'; + +export const SettingsGalleryPluginView: FunctionComponent<{ + hasUnicodeFlags: boolean; + manifest: PluginManifest; + official: boolean; + installing: boolean; + installError?: string; + + installClicked: () => void; +}> = ({ hasUnicodeFlags, manifest, official, installing, installError, installClicked }) => { + return ( + + ); +}; diff --git a/app/scripts/views/settings/plugins/settings-plugin-view.tsx b/app/scripts/views/settings/plugins/settings-plugin-view.tsx new file mode 100644 index 00000000..656405e9 --- /dev/null +++ b/app/scripts/views/settings/plugins/settings-plugin-view.tsx @@ -0,0 +1,229 @@ +import { FunctionComponent } from 'preact'; +import { PluginManifest, PluginSetting } from 'plugins/types'; +import { PluginStatus } from 'plugins/plugin'; +import { Locale } from 'util/locale'; +import { LocalizedWith } from 'views/components/localized-with'; + +export const SettingsPluginView: FunctionComponent<{ + hasUnicodeFlags: boolean; + id: string; + manifest: PluginManifest; + status?: PluginStatus; + installTime?: number; + updateError?: string; + updateCheckDate?: string; + installError?: string; + autoUpdate: boolean; + settings: PluginSetting[]; + + uninstallClicked: () => void; + disableClicked: () => void; + enableClicked: () => void; + updateClicked: () => void; + autoUpdateChanged: () => void; + useLocaleClicked: () => void; + useThemeClicked: () => void; + pluginSettingChanged: (key: string, value: string | boolean) => void; +}> = ({ + hasUnicodeFlags, + id, + manifest, + status, + installTime, + updateError, + updateCheckDate, + installError, + autoUpdate, + settings, + + uninstallClicked, + disableClicked, + enableClicked, + updateClicked, + autoUpdateChanged, + useLocaleClicked, + useThemeClicked, + pluginSettingChanged +}) => { + return ( +
+

{id}

+
{manifest.description}
+
+
    + {manifest.resources.js ? ( +
  • + {Locale.setPlJs} +
  • + ) : null} + {manifest.resources.css ? ( +
  • + {Locale.setPlCss} +
  • + ) : null} + {manifest.resources.loc ? ( +
  • + +   + {Locale.setPlLoc}: {manifest.locale?.title} + {hasUnicodeFlags ? manifest.locale?.flag ?? ' ' : ''} +
  • + ) : null} +
+
+
+ + {manifest.url} + + , v{manifest.version}.{' '} + {manifest.author.name === 'KeeWeb' ? ( + Locale.setPlOfficial + ) : ( + + + {manifest.author.name} + {' '} + ({manifest.author.email}) + + )} + ,{' '} + {status === 'active' ? ( + {installTime}ms + ) : status === 'error' ? ( +  {Locale.setPlLoadError} + ) : ( + status + )} + {updateCheckDate ? ( +
+ {Locale.setPlLastUpdate}: {updateCheckDate} +
+ ) : null} + {installError ? ( +
+
{installError}
+
+ ) : null} + {updateError ? ( +
+
{updateError}
+
+ ) : null} +
+
+ + +
+ {settings ? ( +
+ {settings.map((setting) => ( +
+ {setting.type === 'checkbox' ? ( + + pluginSettingChanged( + setting.name, + (e.target as HTMLInputElement).checked + ) + } + /> + ) : null} + + {setting.type === 'text' ? ( + + pluginSettingChanged( + setting.name, + (e.target as HTMLInputElement).value + ) + } + /> + ) : null} + {setting.type === 'select' && setting.options ? ( + + ) : null} +
+ ))} +
+ ) : null} +
+ + {status === 'active' ? ( + + ) : null} + {status === 'inactive' ? ( + + ) : null} + + {status === 'active' ? ( + <> + {manifest.locale ? ( + + ) : null} + {manifest.theme ? ( + + ) : null} + + ) : null} +
+
+ ); +}; diff --git a/app/scripts/views/settings/settings-plugins-view.tsx b/app/scripts/views/settings/settings-plugins-view.tsx new file mode 100644 index 00000000..008ebdac --- /dev/null +++ b/app/scripts/views/settings/settings-plugins-view.tsx @@ -0,0 +1,129 @@ +import { FunctionComponent } from 'preact'; +import { Locale } from 'util/locale'; +import { Links } from 'const/links'; +import { LocalizedWith } from 'views/components/localized-with'; +import { SettingsPlugin } from 'ui/settings/plugins/settings-plugin'; +import { SettingsGalleryPlugin } from 'ui/settings/plugins/settings-gallery-plugin'; +import { Plugin } from 'plugins/plugin'; +import { PluginGalleryPlugin } from 'plugins/types'; +import { classes } from 'util/ui/classes'; + +export const SettingsPluginsView: FunctionComponent<{ + plugins: Plugin[]; + galleryLoading: boolean; + galleryLoadError: boolean; + galleryPlugins?: PluginGalleryPlugin[]; + gallerySearchStr: string; + installingFromUrl: boolean; + installUrl?: string; + installUrlError?: string; + installUrlMalformed: boolean; + + loadGalleryClicked: () => void; + gallerySearchChanged: (text: string) => void; + installUrlChanged: (url: string) => void; + installFromUrlClicked: () => void; +}> = ({ + plugins, + galleryLoading, + galleryLoadError, + galleryPlugins, + gallerySearchStr, + installingFromUrl, + installUrl, + installUrlError, + installUrlMalformed, + + loadGalleryClicked, + gallerySearchChanged, + installUrlChanged, + installFromUrlClicked +}) => { + return ( +
+

+ {Locale.plugins} +

+
+ {Locale.setPlDevelop}{' '} + + {Locale.setPlDevelopStart} + + .{' '} + + + {Locale.setPlTranslateLink} + + + . +
+ +
+ {plugins.map((plugin) => ( + + ))} +
+ +

+ {galleryLoading ? Locale.setPlGalleryLoading + '...' : Locale.setPlInstallTitle} +

+
+
{Locale.setPlInstallDesc}
+ {galleryLoadError ? ( +
{Locale.setPlGalleryLoadError}
+ ) : null} + {galleryPlugins ? ( + <> + + gallerySearchChanged((e.target as HTMLInputElement).value) + } + /> + + + ) : ( + + )} +
+ +

{Locale.setPlInstallUrlTitle}

+
+
{Locale.setPlInstallUrlDesc}
+ + installUrlChanged((e.target as HTMLInputElement).value)} + /> + +
{installUrlError}
+
+
+ ); +}; diff --git a/app/scripts/views/settings/settings-view.tsx b/app/scripts/views/settings/settings-view.tsx index dd7474c1..0932f285 100644 --- a/app/scripts/views/settings/settings-view.tsx +++ b/app/scripts/views/settings/settings-view.tsx @@ -4,6 +4,7 @@ import { Scrollable } from 'views/components/scrollable'; import { Locale } from 'util/locale'; import { SettingsGeneral } from 'ui/settings/settings-general'; import { SettingsShortcuts } from 'ui/settings/settings-shortcuts'; +import { SettingsPlugins } from 'ui/settings/settings-plugins'; import { SettingsAbout } from 'ui/settings/settings-about'; import { SettingsHelp } from 'ui/settings/settings-help'; @@ -21,6 +22,7 @@ export const SettingsView: FunctionComponent<{ {page === 'general' ? : null} {page === 'shortcuts' ? : null} + {page === 'plugins' ? : null} {page === 'about' ? : null} {page === 'help' ? : null} diff --git a/app/templates/settings/settings-plugins.hbs b/app/templates/settings/settings-plugins.hbs deleted file mode 100644 index 5bd8cefe..00000000 --- a/app/templates/settings/settings-plugins.hbs +++ /dev/null @@ -1,166 +0,0 @@ -
-

{{res 'plugins'}}

-
- {{res 'setPlDevelop'}} {{res 'setPlDevelopStart'}}. - {{#res 'setPlTranslate'}}{{res 'setPlTranslateLink'}}{{/res}}. -
-
- {{#each plugins as |plugin|}} -
-

{{plugin.id}}

-
{{plugin.manifest.description}}
-
-
    - {{#if plugin.manifest.resources.js}}
  • {{res 'setPlJs'}}
  • {{/if}} - {{#if plugin.manifest.resources.css}}
  • {{res 'setPlCss'}}
  • {{/if}} - {{#if plugin.manifest.resources.loc}}
  •   - {{res 'setPlLoc'}}: {{plugin.manifest.locale.title}} {{#if ../hasUnicodeFlags}}{{#if plugin.manifest.locale}}{{#if plugin.manifest.locale.flag}} {{plugin.manifest.locale.flag}}{{/if}}{{/if}}{{/if}}
  • - {{/if}} -
-
-
- {{plugin.manifest.url}}, v{{plugin.manifest.version}}. - {{#if plugin.official}} - {{res 'setPlOfficial'}}, - {{else}} - {{#res 'setPlCreatedBy'}}{{plugin.manifest.author.name}} ({{plugin.manifest.author.email}}){{/res}}, - {{/if}} - {{#ifeq plugin.status 'active'}} - {{#res 'setPlLoadTime'}}{{plugin.installTime}}ms{{/res}} - {{else}} - {{#ifeq plugin.status 'error'}} -  {{res 'setPlLoadError'}} - {{else}} - {{plugin.status}} - {{/ifeq}} - {{/ifeq}} - {{#if updateCheckDate}} -
{{res 'setPlLastUpdate'}}: {{updateCheckDate}}
- {{/if}} - {{#if plugin.installError}}
{{plugin.installError}}
{{/if}} - {{#if plugin.updateError}}
{{plugin.updateError}}
{{/if}} -
-
- - -
- {{#if plugin.settings}} -
- {{#each plugin.settings as |setting|}} -
- {{#ifeq setting.type 'checkbox'}} - - {{/ifeq}} - - {{#ifeq setting.type 'text'}} - - {{/ifeq}} - {{#ifeq setting.type 'select'}} - - {{/ifeq}} -
- {{/each}} -
- {{/if}} -
- - {{#ifeq plugin.status 'active'}}{{/ifeq}} - {{#ifeq plugin.status 'inactive'}}{{/ifeq}} - - {{#ifeq plugin.status 'active'}} - {{#if plugin.manifest.locale}}{{/if}} - {{#if plugin.manifest.theme}}{{/if}} - {{/ifeq}} -
-
- {{/each}} -
-

- {{#if galleryLoading}}{{res 'setPlGalleryLoading'}}...{{else}}{{res 'setPlInstallTitle'}}{{/if}} -

-
-
{{res 'setPlInstallDesc'}}
- {{#if galleryLoadError}}
{{res 'setPlGalleryLoadError'}}
{{/if}} - {{#if galleryPlugins}} - - - {{else}} - - {{/if}} -
-

{{res 'setPlInstallUrlTitle'}}

-
-
{{res 'setPlInstallUrlDesc'}}
- - - -
{{installUrlError}}
-
-