mirror of https://github.com/keeweb/keeweb.git
plugin settings
This commit is contained in:
parent
b572876099
commit
2c386e67e2
|
@ -1,17 +1,17 @@
|
||||||
import * as kdbxweb from 'kdbxweb';
|
import * as kdbxweb from 'kdbxweb';
|
||||||
import { Events } from 'util/events';
|
|
||||||
import { SettingsStore } from 'comp/settings/settings-store';
|
import { SettingsStore } from 'comp/settings/settings-store';
|
||||||
import { Links } from 'const/links';
|
import { Links } from 'const/links';
|
||||||
import { SignatureVerifier } from 'util/data/signature-verifier';
|
import { SignatureVerifier } from 'util/data/signature-verifier';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
import { PluginGalleryData } from 'plugins/types';
|
import { PluginGalleryData } from 'plugins/types';
|
||||||
|
import { Model } from 'util/model';
|
||||||
|
|
||||||
const PluginGallery = {
|
class PluginGallery extends Model {
|
||||||
logger: new Logger('plugin-gallery'),
|
logger = new Logger('plugin-gallery');
|
||||||
|
|
||||||
gallery: undefined as PluginGalleryData | undefined,
|
gallery?: PluginGalleryData;
|
||||||
loading: false,
|
loading = false;
|
||||||
loadError: false,
|
loadError = false;
|
||||||
|
|
||||||
async loadPlugins(): Promise<PluginGalleryData> {
|
async loadPlugins(): Promise<PluginGalleryData> {
|
||||||
if (this.gallery) {
|
if (this.gallery) {
|
||||||
|
@ -49,18 +49,16 @@ const PluginGallery = {
|
||||||
this.gallery = data;
|
this.gallery = data;
|
||||||
await this.saveGallery(data);
|
await this.saveGallery(data);
|
||||||
|
|
||||||
Events.emit('plugin-gallery-load-complete');
|
|
||||||
return this.gallery;
|
return this.gallery;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.loadError = true;
|
this.loadError = true;
|
||||||
this.logger.error('Error loading plugin gallery', e);
|
this.logger.error('Error loading plugin gallery', e);
|
||||||
Events.emit('plugin-gallery-load-complete');
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
async verifySignature(gallery: PluginGalleryData): Promise<void> {
|
private async verifySignature(gallery: PluginGalleryData): Promise<void> {
|
||||||
const dataToVerify = JSON.stringify(gallery, null, 2).replace(gallery.signature, '');
|
const dataToVerify = JSON.stringify(gallery, null, 2).replace(gallery.signature, '');
|
||||||
const valid = await SignatureVerifier.verify(
|
const valid = await SignatureVerifier.verify(
|
||||||
kdbxweb.ByteUtils.stringToBytes(dataToVerify),
|
kdbxweb.ByteUtils.stringToBytes(dataToVerify),
|
||||||
|
@ -70,7 +68,7 @@ const PluginGallery = {
|
||||||
this.logger.error('JSON signature invalid');
|
this.logger.error('JSON signature invalid');
|
||||||
throw new Error('JSON signature invalid');
|
throw new Error('JSON signature invalid');
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
async getCachedGallery(): Promise<PluginGalleryData | undefined> {
|
async getCachedGallery(): Promise<PluginGalleryData | undefined> {
|
||||||
const ts = this.logger.ts();
|
const ts = this.logger.ts();
|
||||||
|
@ -84,11 +82,13 @@ const PluginGallery = {
|
||||||
this.logger.error('Cannot load cached plugin gallery: signature validation error');
|
this.logger.error('Cannot load cached plugin gallery: signature validation error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
saveGallery(data: PluginGalleryData): Promise<void> {
|
private saveGallery(data: PluginGalleryData): Promise<void> {
|
||||||
return SettingsStore.save('plugin-gallery', data);
|
return SettingsStore.save('plugin-gallery', data);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export { PluginGallery };
|
const instance = new PluginGallery();
|
||||||
|
|
||||||
|
export { instance as PluginGallery };
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { SignatureVerifier } from 'util/data/signature-verifier';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
import { PluginGalleryData, PluginManifest, StoredPlugin, StoredPlugins } from 'plugins/types';
|
import { PluginGalleryData, PluginManifest, StoredPlugin, StoredPlugins } from 'plugins/types';
|
||||||
import { Timeouts } from 'const/timeouts';
|
import { Timeouts } from 'const/timeouts';
|
||||||
|
import { errorToString } from 'util/fn';
|
||||||
|
|
||||||
const logger = new Logger('plugin-mgr');
|
const logger = new Logger('plugin-mgr');
|
||||||
|
|
||||||
|
@ -15,6 +16,9 @@ class PluginManager extends Model {
|
||||||
autoUpdateAppVersion?: string;
|
autoUpdateAppVersion?: string;
|
||||||
autoUpdateDate?: Date;
|
autoUpdateDate?: Date;
|
||||||
|
|
||||||
|
installing = new Set<string>();
|
||||||
|
installErrors = new Map<string, string>();
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const ts = logger.ts();
|
const ts = logger.ts();
|
||||||
const storedPlugins = (await SettingsStore.load('plugins')) as StoredPlugins;
|
const storedPlugins = (await SettingsStore.load('plugins')) as StoredPlugins;
|
||||||
|
@ -44,13 +48,25 @@ class PluginManager extends Model {
|
||||||
expectedManifest?: PluginManifest,
|
expectedManifest?: PluginManifest,
|
||||||
skipSignatureValidation?: boolean
|
skipSignatureValidation?: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const plugin = await Plugin.loadFromUrl(url, expectedManifest);
|
this.installErrors.delete(url);
|
||||||
await this.uninstall(plugin.id);
|
this.installing = new Set(this.installing).add(url);
|
||||||
if (skipSignatureValidation) {
|
try {
|
||||||
plugin.skipSignatureValidation = true;
|
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();
|
await this.saveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,35 @@
|
||||||
import { Color } from 'util/data/color';
|
import { Color } from 'util/data/color';
|
||||||
|
|
||||||
const ThemeVarsScss = require('!!raw-loader!../../styles/base/_theme-vars.scss') as string;
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
const ThemeDefaults = require('!!raw-loader!../../styles/themes/_theme-defaults.scss') as string;
|
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;
|
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<string, (...args: StyleVar[]) => StyleVar> = {
|
const ThemeFunctions: Record<string, (...args: StyleVar[]) => StyleVar> = {
|
||||||
'mix'(color1: StyleVar, color2: StyleVar, percent: StyleVar): 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');
|
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 {
|
'semi-mute-percent'(mutePercent: StyleVar): StyleVar {
|
||||||
if (typeof mutePercent !== 'number') throw new TypeError('mutePercent is not a number');
|
if (typeof mutePercent !== 'number') throw new TypeError('mutePercent is not a number');
|
||||||
return mutePercent / 2;
|
return mutePercent / 2;
|
||||||
},
|
},
|
||||||
'rgba'(color: StyleVar, alpha: StyleVar): StyleVar {
|
'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');
|
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;
|
res.a = alpha;
|
||||||
return res.toRgba();
|
return res.toRgba();
|
||||||
},
|
},
|
||||||
|
@ -29,28 +39,23 @@ const ThemeFunctions: Record<string, (...args: StyleVar[]) => StyleVar> = {
|
||||||
thBg: StyleVar,
|
thBg: StyleVar,
|
||||||
thText: StyleVar
|
thText: StyleVar
|
||||||
): 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 (typeof lshift !== 'number') throw new TypeError('lshift is not a number');
|
||||||
if (!(thBg instanceof Color)) throw new TypeError('thBg is not a Color');
|
if (checkColor(color).l - lshift >= checkColor(thBg).l) {
|
||||||
if (!(thText instanceof Color)) throw new TypeError('thText is not a Color');
|
return checkColor(thText).toRgba();
|
||||||
if (color.l - lshift >= thBg.l) {
|
|
||||||
return thText.toRgba();
|
|
||||||
}
|
}
|
||||||
return thBg.toRgba();
|
return checkColor(thBg).toRgba();
|
||||||
},
|
},
|
||||||
'lightness-alpha'(color: StyleVar, lightness: StyleVar, alpha: StyleVar): StyleVar {
|
'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 lightness !== 'number') throw new TypeError('lightness is not a number');
|
||||||
if (typeof alpha !== 'number') throw new TypeError('alpha 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.l += Math.min(0, Math.max(1, lightness));
|
||||||
res.a += Math.min(0, Math.max(1, alpha));
|
res.a += Math.min(0, Math.max(1, alpha));
|
||||||
return res.toHsla();
|
return res.toHsla();
|
||||||
},
|
},
|
||||||
'shade'(color: StyleVar, percent: StyleVar): StyleVar {
|
'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');
|
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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ export interface PluginSetting {
|
||||||
type: 'text' | 'select' | 'checkbox';
|
type: 'text' | 'select' | 'checkbox';
|
||||||
value?: string | boolean;
|
value?: string | boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
maxlength?: string;
|
maxlength?: number;
|
||||||
options?: PluginSettingOption[];
|
options?: PluginSettingOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 };
|
|
|
@ -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
|
||||||
|
});
|
||||||
|
};
|
|
@ -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
|
||||||
|
});
|
||||||
|
};
|
|
@ -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
|
||||||
|
});
|
||||||
|
};
|
|
@ -26,7 +26,6 @@ export interface GlobalEventSpec {
|
||||||
'enter-full-screen': () => void;
|
'enter-full-screen': () => void;
|
||||||
'leave-full-screen': () => void;
|
'leave-full-screen': () => void;
|
||||||
'drag-handle-set': (name: string, size: number | null) => void;
|
'drag-handle-set': (name: string, size: number | null) => void;
|
||||||
'plugin-gallery-load-complete': () => void;
|
|
||||||
'custom-icon-downloaded': (data: string) => void;
|
'custom-icon-downloaded': (data: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ export const SettingsGeneralStorageView: FunctionComponent<{
|
||||||
<label for={`settings__general-prv-check-${prv.name}`}>{prv.locName}</label>
|
<label for={`settings__general-prv-check-${prv.name}`}>{prv.locName}</label>
|
||||||
</h4>
|
</h4>
|
||||||
{prv.enabled && prv.hasConfig ? (
|
{prv.enabled && prv.hasConfig ? (
|
||||||
<div class={`settings__general-prv-wrap settings__general-${prv.name}}`}>
|
<div class={`settings__general-prv-wrap settings__general-${prv.name}`}>
|
||||||
<SettingsGeneralStorageProvider name={prv.name} />
|
<SettingsGeneralStorageProvider name={prv.name} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -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 (
|
||||||
|
<div class="settings__plugins-gallery-plugin">
|
||||||
|
<h4 class="settings__plugins-gallery-plugin-title">
|
||||||
|
<a
|
||||||
|
href={manifest.url}
|
||||||
|
target="_blank"
|
||||||
|
class="settings__plugins-gallery-plugin-title-link"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{manifest.name}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
{hasUnicodeFlags && manifest.locale?.flag ? (
|
||||||
|
<div class="settings__plugins-gallery-plugin-country-flag">
|
||||||
|
{manifest.locale.flag}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div class="settings__plugins-gallery-plugin-desc">{manifest.description}</div>
|
||||||
|
<ul class="settings__plugins-plugin-files">
|
||||||
|
{manifest.resources.js ? (
|
||||||
|
<li class="settings__plugins-plugin-file">
|
||||||
|
<i class="fa fa-code" /> {Locale.setPlJs}
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
{manifest.resources.css ? (
|
||||||
|
<li class="settings__plugins-plugin-file">
|
||||||
|
<i class="fa fa-paint-brush" /> {Locale.setPlCss}
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
{manifest.resources.loc ? (
|
||||||
|
<li class="settings__plugins-plugin-file">
|
||||||
|
<i class="fa fa-language" /> {Locale.setPlLoc}: {manifest.locale?.title}
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
</ul>
|
||||||
|
<div class="settings__plugins-gallery-plugin-author muted-color">
|
||||||
|
{official ? (
|
||||||
|
<>
|
||||||
|
<i class="fa fa-check" /> {Locale.setPlOfficial}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i class="fa fa-at" />{' '}
|
||||||
|
<a href={manifest.author.url} target="_blank" rel="noreferrer">
|
||||||
|
{manifest.author.name}
|
||||||
|
</a>{' '}
|
||||||
|
({manifest.author.email})
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{installError ? <div class="error-color">{installError}</div> : null}
|
||||||
|
<button
|
||||||
|
class="settings__plugins-gallery-plugin-install-btn"
|
||||||
|
disabled={installing}
|
||||||
|
onClick={installClicked}
|
||||||
|
>
|
||||||
|
{installing ? Locale.setPlInstallBtnProgress + '...' : Locale.setPlInstallBtn}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 (
|
||||||
|
<div class="settings__plugins-plugin" id={`settings__plugins-plugin--${id}`}>
|
||||||
|
<h2>{id}</h2>
|
||||||
|
<div>{manifest.description}</div>
|
||||||
|
<div>
|
||||||
|
<ul class="settings__plugins-plugin-files">
|
||||||
|
{manifest.resources.js ? (
|
||||||
|
<li class="settings__plugins-plugin-file">
|
||||||
|
<i class="fa fa-code" /> {Locale.setPlJs}
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
{manifest.resources.css ? (
|
||||||
|
<li class="settings__plugins-plugin-file">
|
||||||
|
<i class="fa fa-paint-brush" /> {Locale.setPlCss}
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
{manifest.resources.loc ? (
|
||||||
|
<li class="settings__plugins-plugin-file">
|
||||||
|
<i class="fa fa-language" />
|
||||||
|
|
||||||
|
{Locale.setPlLoc}: {manifest.locale?.title}
|
||||||
|
{hasUnicodeFlags ? manifest.locale?.flag ?? ' ' : ''}
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="settings__plugins-plugin-desc">
|
||||||
|
<a href={manifest.url} target="_blank" rel="noreferrer">
|
||||||
|
{manifest.url}
|
||||||
|
</a>
|
||||||
|
, v{manifest.version}.{' '}
|
||||||
|
{manifest.author.name === 'KeeWeb' ? (
|
||||||
|
Locale.setPlOfficial
|
||||||
|
) : (
|
||||||
|
<LocalizedWith str={Locale.setPlCreatedBy}>
|
||||||
|
<a href={manifest.author.url} target="_blank" rel="noreferrer">
|
||||||
|
{manifest.author.name}
|
||||||
|
</a>{' '}
|
||||||
|
({manifest.author.email})
|
||||||
|
</LocalizedWith>
|
||||||
|
)}
|
||||||
|
,{' '}
|
||||||
|
{status === 'active' ? (
|
||||||
|
<LocalizedWith str={Locale.setPlLoadTime}>{installTime}ms</LocalizedWith>
|
||||||
|
) : status === 'error' ? (
|
||||||
|
<span class="error-color"> {Locale.setPlLoadError}</span>
|
||||||
|
) : (
|
||||||
|
status
|
||||||
|
)}
|
||||||
|
{updateCheckDate ? (
|
||||||
|
<div>
|
||||||
|
{Locale.setPlLastUpdate}: {updateCheckDate}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{installError ? (
|
||||||
|
<div class="error-color settings__plugins-install-error">
|
||||||
|
<pre>{installError}</pre>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{updateError ? (
|
||||||
|
<div class="error-color settings__plugins-install-error">
|
||||||
|
<pre>{updateError}</pre>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div class="settings__plugins-plugin-updates">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="settings__plugins-plugin-update-check settings__input input-base"
|
||||||
|
id={`plugin-${id}-auto-update`}
|
||||||
|
checked={autoUpdate}
|
||||||
|
onClick={autoUpdateChanged}
|
||||||
|
/>
|
||||||
|
<label for={`plugin-${id}-auto-update`}>{Locale.setPlAutoUpdate}</label>
|
||||||
|
</div>
|
||||||
|
{settings ? (
|
||||||
|
<div class="settings__plugins-plugin-settings">
|
||||||
|
{settings.map((setting) => (
|
||||||
|
<div key={setting.name} class="settings__plugins-plugin-setting">
|
||||||
|
{setting.type === 'checkbox' ? (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="settings__plugins-plugin-input settings__input input-base"
|
||||||
|
id={`plugin-${id}-setting-${setting.name}`}
|
||||||
|
checked={!!setting.value}
|
||||||
|
onClick={(e) =>
|
||||||
|
pluginSettingChanged(
|
||||||
|
setting.name,
|
||||||
|
(e.target as HTMLInputElement).checked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<label
|
||||||
|
class="settings__plugins-plugin-label"
|
||||||
|
for={`plugin-${id}-setting-${setting.name}`}
|
||||||
|
>
|
||||||
|
{setting.label}
|
||||||
|
</label>
|
||||||
|
{setting.type === 'text' ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="settings__plugins-plugin-input settings__input input-base"
|
||||||
|
id={`plugin-${id}-setting-${setting.name}`}
|
||||||
|
placeholder={setting.placeholder}
|
||||||
|
maxLength={setting.maxlength}
|
||||||
|
value={String(setting.value || '')}
|
||||||
|
onInput={(e) =>
|
||||||
|
pluginSettingChanged(
|
||||||
|
setting.name,
|
||||||
|
(e.target as HTMLInputElement).value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{setting.type === 'select' && setting.options ? (
|
||||||
|
<select
|
||||||
|
class="settings__plugins-plugin-input settings__select input-base"
|
||||||
|
id={`plugin-${id}-setting-${setting.name}`}
|
||||||
|
value={String(setting.value || '')}
|
||||||
|
onChange={(e) =>
|
||||||
|
pluginSettingChanged(
|
||||||
|
setting.name,
|
||||||
|
(e.target as HTMLSelectElement).value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{setting.options.map((opt) => (
|
||||||
|
<option key={opt.label} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div class="settings__plugins-plugin-buttons">
|
||||||
|
<button class="settings_plugins-uninstall-btn btn-error" onClick={uninstallClicked}>
|
||||||
|
{Locale.setPlUninstallBtn}
|
||||||
|
</button>
|
||||||
|
{status === 'active' ? (
|
||||||
|
<button
|
||||||
|
class="settings_plugins-disable-btn btn-silent"
|
||||||
|
onClick={disableClicked}
|
||||||
|
>
|
||||||
|
{Locale.setPlDisableBtn}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{status === 'inactive' ? (
|
||||||
|
<button class="settings_plugins-enable-btn btn-silent" onClick={enableClicked}>
|
||||||
|
{Locale.setPlEnableBtn}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<button class="settings_plugins-update-btn btn-silent" onClick={updateClicked}>
|
||||||
|
{Locale.setPlUpdateBtn}
|
||||||
|
</button>
|
||||||
|
{status === 'active' ? (
|
||||||
|
<>
|
||||||
|
{manifest.locale ? (
|
||||||
|
<button
|
||||||
|
class="settings_plugins-use-locale-btn btn-silent"
|
||||||
|
onClick={useLocaleClicked}
|
||||||
|
>
|
||||||
|
{Locale.setPlLocaleBtn}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{manifest.theme ? (
|
||||||
|
<button
|
||||||
|
class="settings_plugins-use-theme-btn btn-silent"
|
||||||
|
onClick={useThemeClicked}
|
||||||
|
>
|
||||||
|
{Locale.setPlThemeBtn}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 (
|
||||||
|
<div class="settings__content">
|
||||||
|
<h1>
|
||||||
|
<i class="fa fa-puzzle-piece settings__head-icon" /> {Locale.plugins}
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
{Locale.setPlDevelop}{' '}
|
||||||
|
<a href={Links.PluginDevelopStart} target="_blank" rel="noreferrer">
|
||||||
|
{Locale.setPlDevelopStart}
|
||||||
|
</a>
|
||||||
|
.{' '}
|
||||||
|
<LocalizedWith str={Locale.setPlTranslate}>
|
||||||
|
<a href={Links.Translation} target="_blank" rel="noreferrer">
|
||||||
|
{Locale.setPlTranslateLink}
|
||||||
|
</a>
|
||||||
|
</LocalizedWith>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings__plugins-list">
|
||||||
|
{plugins.map((plugin) => (
|
||||||
|
<SettingsPlugin key={plugin.id} plugin={plugin} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
{galleryLoading ? Locale.setPlGalleryLoading + '...' : Locale.setPlInstallTitle}
|
||||||
|
</h2>
|
||||||
|
<div class="settings__plugins-install">
|
||||||
|
<div>{Locale.setPlInstallDesc}</div>
|
||||||
|
{galleryLoadError ? (
|
||||||
|
<div class="error-color">{Locale.setPlGalleryLoadError}</div>
|
||||||
|
) : null}
|
||||||
|
{galleryPlugins ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input-base settings__plugins-gallery-search"
|
||||||
|
placeholder={Locale.setPlSearch}
|
||||||
|
value={gallerySearchStr}
|
||||||
|
onInput={(e) =>
|
||||||
|
gallerySearchChanged((e.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div class="settings__plugins-gallery">
|
||||||
|
{galleryPlugins.map((plugin) => (
|
||||||
|
<SettingsGalleryPlugin plugin={plugin} key={plugin.url} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
class="settings__plugins-gallery-load-btn"
|
||||||
|
disabled={galleryLoading}
|
||||||
|
onClick={loadGalleryClicked}
|
||||||
|
>
|
||||||
|
{Locale.setPlLoadGallery}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>{Locale.setPlInstallUrlTitle}</h2>
|
||||||
|
<div class="settings__plugins-install-url">
|
||||||
|
<div>{Locale.setPlInstallUrlDesc}</div>
|
||||||
|
<label for="settings__plugins-install-url">{Locale.setPlInstallLabel}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class={classes({
|
||||||
|
'settings__input input-base': true,
|
||||||
|
'input--error': installUrlMalformed
|
||||||
|
})}
|
||||||
|
id="settings__plugins-install-url"
|
||||||
|
disabled={installingFromUrl}
|
||||||
|
value={installUrl}
|
||||||
|
onInput={(e) => installUrlChanged((e.target as HTMLInputElement).value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="settings_plugins-install-btn"
|
||||||
|
disabled={installingFromUrl || !installUrl || installUrlMalformed}
|
||||||
|
onClick={installFromUrlClicked}
|
||||||
|
>
|
||||||
|
{installingFromUrl ? Locale.setPlInstallBtnProgress : Locale.setPlInstallBtn}
|
||||||
|
</button>
|
||||||
|
<div class="error-color settings__plugins-install-error">{installUrlError}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,6 +4,7 @@ import { Scrollable } from 'views/components/scrollable';
|
||||||
import { Locale } from 'util/locale';
|
import { Locale } from 'util/locale';
|
||||||
import { SettingsGeneral } from 'ui/settings/settings-general';
|
import { SettingsGeneral } from 'ui/settings/settings-general';
|
||||||
import { SettingsShortcuts } from 'ui/settings/settings-shortcuts';
|
import { SettingsShortcuts } from 'ui/settings/settings-shortcuts';
|
||||||
|
import { SettingsPlugins } from 'ui/settings/settings-plugins';
|
||||||
import { SettingsAbout } from 'ui/settings/settings-about';
|
import { SettingsAbout } from 'ui/settings/settings-about';
|
||||||
import { SettingsHelp } from 'ui/settings/settings-help';
|
import { SettingsHelp } from 'ui/settings/settings-help';
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ export const SettingsView: FunctionComponent<{
|
||||||
<Scrollable>
|
<Scrollable>
|
||||||
{page === 'general' ? <SettingsGeneral /> : null}
|
{page === 'general' ? <SettingsGeneral /> : null}
|
||||||
{page === 'shortcuts' ? <SettingsShortcuts /> : null}
|
{page === 'shortcuts' ? <SettingsShortcuts /> : null}
|
||||||
|
{page === 'plugins' ? <SettingsPlugins /> : null}
|
||||||
{page === 'about' ? <SettingsAbout /> : null}
|
{page === 'about' ? <SettingsAbout /> : null}
|
||||||
{page === 'help' ? <SettingsHelp /> : null}
|
{page === 'help' ? <SettingsHelp /> : null}
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
|
|
|
@ -1,166 +0,0 @@
|
||||||
<div class="settings__content">
|
|
||||||
<h1><i class="fa fa-puzzle-piece settings__head-icon"></i> {{res 'plugins'}}</h1>
|
|
||||||
<div>
|
|
||||||
{{res 'setPlDevelop'}} <a href="{{pluginDevLink}}" target="_blank">{{res 'setPlDevelopStart'}}</a>.
|
|
||||||
{{#res 'setPlTranslate'}}<a href="{{translateLink}}" target="_blank">{{res 'setPlTranslateLink'}}</a>{{/res}}.
|
|
||||||
</div>
|
|
||||||
<div class="settings__plugins-list">
|
|
||||||
{{#each plugins as |plugin|}}
|
|
||||||
<div class="settings__plugins-plugin" id="settings__plugins-plugin--{{plugin.id}}">
|
|
||||||
<h2>{{plugin.id}}</h2>
|
|
||||||
<div>{{plugin.manifest.description}}</div>
|
|
||||||
<div>
|
|
||||||
<ul class="settings__plugins-plugin-files">
|
|
||||||
{{#if plugin.manifest.resources.js}}<li class="settings__plugins-plugin-file"><i class="fa fa-code"></i> {{res 'setPlJs'}}</li>{{/if}}
|
|
||||||
{{#if plugin.manifest.resources.css}}<li class="settings__plugins-plugin-file"><i class="fa fa-paint-brush"></i> {{res 'setPlCss'}}</li>{{/if}}
|
|
||||||
{{#if plugin.manifest.resources.loc}}<li class="settings__plugins-plugin-file"><i class="fa fa-language"></i>
|
|
||||||
{{res 'setPlLoc'}}: {{plugin.manifest.locale.title}} {{#if ../hasUnicodeFlags}}{{#if plugin.manifest.locale}}{{#if plugin.manifest.locale.flag}} {{plugin.manifest.locale.flag}}{{/if}}{{/if}}{{/if}}</li>
|
|
||||||
{{/if}}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="settings__plugins-plugin-desc">
|
|
||||||
<a href="{{plugin.manifest.url}}" target="_blank">{{plugin.manifest.url}}</a>, v{{plugin.manifest.version}}.
|
|
||||||
{{#if plugin.official}}
|
|
||||||
{{res 'setPlOfficial'}},
|
|
||||||
{{else}}
|
|
||||||
{{#res 'setPlCreatedBy'}}<a href="{{plugin.manifest.author.url}}" target="_blank">{{plugin.manifest.author.name}}</a> ({{plugin.manifest.author.email}}){{/res}},
|
|
||||||
{{/if}}
|
|
||||||
{{#ifeq plugin.status 'active'}}
|
|
||||||
{{#res 'setPlLoadTime'}}{{plugin.installTime}}ms{{/res}}
|
|
||||||
{{else}}
|
|
||||||
{{#ifeq plugin.status 'error'}}
|
|
||||||
<span class="error-color"> {{res 'setPlLoadError'}}</span>
|
|
||||||
{{else}}
|
|
||||||
{{plugin.status}}
|
|
||||||
{{/ifeq}}
|
|
||||||
{{/ifeq}}
|
|
||||||
{{#if updateCheckDate}}
|
|
||||||
<div>{{res 'setPlLastUpdate'}}: {{updateCheckDate}}</div>
|
|
||||||
{{/if}}
|
|
||||||
{{#if plugin.installError}}<div class="error-color settings__plugins-install-error"><pre>{{plugin.installError}}</pre></div>{{/if}}
|
|
||||||
{{#if plugin.updateError}}<div class="error-color settings__plugins-install-error"><pre>{{plugin.updateError}}</pre></div>{{/if}}
|
|
||||||
</div>
|
|
||||||
<div class="settings__plugins-plugin-updates">
|
|
||||||
<input type="checkbox" class="settings__plugins-plugin-update-check settings__input input-base"
|
|
||||||
id="plugin-{{plugin.id}}-auto-update" data-plugin="{{plugin.id}}"
|
|
||||||
{{#if plugin.autoUpdate}}checked{{/if}} />
|
|
||||||
<label for="plugin-{{plugin.id}}-auto-update">{{res 'setPlAutoUpdate'}}</label>
|
|
||||||
</div>
|
|
||||||
{{#if plugin.settings}}
|
|
||||||
<div class="settings__plugins-plugin-settings">
|
|
||||||
{{#each plugin.settings as |setting|}}
|
|
||||||
<div class="settings__plugins-plugin-setting"
|
|
||||||
data-setting="{{setting.name}}"
|
|
||||||
data-plugin="{{../id}}"
|
|
||||||
>
|
|
||||||
{{#ifeq setting.type 'checkbox'}}
|
|
||||||
<input type="checkbox"
|
|
||||||
class="settings__plugins-plugin-input settings__input input-base"
|
|
||||||
id="plugin-{{../id}}-setting-{{setting.name}}"
|
|
||||||
{{#if setting.value}}checked{{/if}}
|
|
||||||
/>
|
|
||||||
{{/ifeq}}
|
|
||||||
<label
|
|
||||||
class="settings__plugins-plugin-label"
|
|
||||||
for="plugin-{{../id}}-setting-{{setting.name}}"
|
|
||||||
>{{setting.label}}</label>
|
|
||||||
{{#ifeq setting.type 'text'}}
|
|
||||||
<input type="text"
|
|
||||||
class="settings__plugins-plugin-input settings__input input-base"
|
|
||||||
id="plugin-{{../id}}-setting-{{setting.name}}"
|
|
||||||
{{#if setting.placeholder}}placeholder="{{setting.placeholder}}"{{/if}}
|
|
||||||
{{#if setting.maxlength}}maxlength="{{setting.maxlength}}"{{/if}}
|
|
||||||
value="{{setting.value}}"
|
|
||||||
/>
|
|
||||||
{{/ifeq}}
|
|
||||||
{{#ifeq setting.type 'select'}}
|
|
||||||
<select class="settings__plugins-plugin-input settings__select input-base"
|
|
||||||
id="plugin-{{../name}}-setting-{{setting.name}}"
|
|
||||||
>
|
|
||||||
{{#each setting.options as |opt|}}
|
|
||||||
<option value="{{opt.value}}" {{#ifeq opt.value ../value}}selected{{/ifeq}}>{{opt.label}}</option>
|
|
||||||
{{/each}}
|
|
||||||
</select>
|
|
||||||
{{/ifeq}}
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
<div class="settings__plugins-plugin-buttons">
|
|
||||||
<button class="settings_plugins-uninstall-btn btn-error" data-plugin="{{plugin.id}}">{{res 'setPlUninstallBtn'}}</button>
|
|
||||||
{{#ifeq plugin.status 'active'}}<button class="settings_plugins-disable-btn btn-silent" data-plugin="{{plugin.id}}">{{res 'setPlDisableBtn'}}</button>{{/ifeq}}
|
|
||||||
{{#ifeq plugin.status 'inactive'}}<button class="settings_plugins-enable-btn btn-silent" data-plugin="{{plugin.id}}">{{res 'setPlEnableBtn'}}</button>{{/ifeq}}
|
|
||||||
<button class="settings_plugins-update-btn btn-silent" data-plugin="{{plugin.id}}">{{res 'setPlUpdateBtn'}}</button>
|
|
||||||
{{#ifeq plugin.status 'active'}}
|
|
||||||
{{#if plugin.manifest.locale}}<button class="settings_plugins-use-locale-btn btn-silent" data-locale="{{plugin.manifest.locale.name}}">{{res 'setPlLocaleBtn'}}</button>{{/if}}
|
|
||||||
{{#if plugin.manifest.theme}}<button class="settings_plugins-use-theme-btn btn-silent" data-theme="{{plugin.manifest.theme.name}}">{{res 'setPlThemeBtn'}}</button>{{/if}}
|
|
||||||
{{/ifeq}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
<h2>
|
|
||||||
{{#if galleryLoading}}{{res 'setPlGalleryLoading'}}...{{else}}{{res 'setPlInstallTitle'}}{{/if}}
|
|
||||||
</h2>
|
|
||||||
<div class="settings__plugins-install">
|
|
||||||
<div>{{res 'setPlInstallDesc'}}</div>
|
|
||||||
{{#if galleryLoadError}}<div class="error-color">{{res 'setPlGalleryLoadError'}}</div>{{/if}}
|
|
||||||
{{#if galleryPlugins}}
|
|
||||||
<input type="text" class="input-base settings__plugins-gallery-search" placeholder="{{res 'setPlSearch'}}" value="{{searchStr}}" />
|
|
||||||
<div class="settings__plugins-gallery">
|
|
||||||
{{#each galleryPlugins as |plugin|}}
|
|
||||||
<div class="settings__plugins-gallery-plugin" data-plugin="{{plugin.manifest.name}}">
|
|
||||||
<h4 class="settings__plugins-gallery-plugin-title">
|
|
||||||
<a href="{{plugin.url}}" target="_blank" class="settings__plugins-gallery-plugin-title-link">{{plugin.manifest.name}}</a>
|
|
||||||
</h4>
|
|
||||||
{{#if ../hasUnicodeFlags}}
|
|
||||||
{{#if plugin.manifest.locale}}
|
|
||||||
{{#if plugin.manifest.locale.flag}}
|
|
||||||
<div class="settings__plugins-gallery-plugin-country-flag">{{plugin.manifest.locale.flag}}</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
<div class="settings__plugins-gallery-plugin-desc">{{plugin.manifest.description}}</div>
|
|
||||||
<ul class="settings__plugins-plugin-files">
|
|
||||||
{{#if plugin.manifest.resources.js}}<li class="settings__plugins-plugin-file"><i class="fa fa-code"></i> {{res 'setPlJs'}}</li>{{/if}}
|
|
||||||
{{#if plugin.manifest.resources.css}}<li class="settings__plugins-plugin-file"><i class="fa fa-paint-brush"></i> {{res 'setPlCss'}}</li>{{/if}}
|
|
||||||
{{#if plugin.manifest.resources.loc}}<li class="settings__plugins-plugin-file"><i class="fa fa-language"></i> {{res 'setPlLoc'}}: {{plugin.manifest.locale.title}}</li>{{/if}}
|
|
||||||
</ul>
|
|
||||||
<div class="settings__plugins-gallery-plugin-author muted-color">
|
|
||||||
{{#if plugin.official}}
|
|
||||||
<i class="fa fa-check"></i> {{res 'setPlOfficial'}}
|
|
||||||
{{else}}
|
|
||||||
<i class="fa fa-at"></i> <a href="{{plugin.manifest.author.url}}" target="_blank">{{plugin.manifest.author.name}}</a> ({{plugin.manifest.author.email}})
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{#if plugin.installError}}
|
|
||||||
<div class="error-color">{{plugin.installError}}</div>
|
|
||||||
{{/if}}
|
|
||||||
<button class="settings__plugins-gallery-plugin-install-btn"
|
|
||||||
data-plugin="{{plugin.manifest.name}}"
|
|
||||||
{{#if plugin.installing}}disabled{{/if}}
|
|
||||||
>
|
|
||||||
{{#if plugin.installing}}
|
|
||||||
{{res 'setPlInstallBtnProgress'}}...
|
|
||||||
{{else}}
|
|
||||||
{{res 'setPlInstallBtn'}}
|
|
||||||
{{/if}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<button class="settings__plugins-gallery-load-btn" {{#if galleryLoading}}disabled="disabled"{{/if}}>{{res 'setPlLoadGallery'}}</button>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
<h2>{{res 'setPlInstallUrlTitle'}}</h2>
|
|
||||||
<div class="settings__plugins-install-url">
|
|
||||||
<div>{{res 'setPlInstallUrlDesc'}}</div>
|
|
||||||
<label for="settings__plugins-install-url">{{res 'setPlInstallLabel'}}</label>
|
|
||||||
<input type="text" class="settings__input input-base" id="settings__plugins-install-url" value="{{installUrl}}" />
|
|
||||||
<button class="settings_plugins-install-btn" {{#if installingFromUrl}}disabled{{/if}}>
|
|
||||||
{{#if installingFromUrl}}{{res 'setPlInstallBtnProgress'}}{{else}}{{res 'setPlInstallBtn'}}{{/if}}
|
|
||||||
</button>
|
|
||||||
<div class="error-color settings__plugins-install-error">{{installUrlError}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
Loading…
Reference in New Issue