Introduce plugin system supporting theme registration

This commit is contained in:
Aya Morisawa 2019-06-04 18:14:27 +09:00
parent 83fedcff3b
commit 0fd0b4f466
No known key found for this signature in database
GPG Key ID: 3E64865D70D579F2
23 changed files with 141 additions and 48 deletions

View File

@ -47,7 +47,14 @@ gulp.task('build:copy:views', () =>
gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views')) gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
); );
gulp.task('build:copy', gulp.parallel('build:copy:views', () => gulp.task('build:copy:plugins', () =>
gulp.src([
'./src/plugins/**/*',
'!./src/plugins/**/*.ts'
]).pipe(gulp.dest('./built/plugins'))
);
gulp.task('build:copy', gulp.parallel('build:copy:views', 'build:copy:plugins', () =>
gulp.src([ gulp.src([
'./src/const.json', './src/const.json',
'./src/server/web/views/**/*', './src/server/web/views/**/*',

View File

@ -21,11 +21,11 @@ export default async function() {
process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`; process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
if (cluster.isMaster || program.disableClustering) { if (cluster.isMaster || program.disableClustering) {
await masterMain();
if (cluster.isMaster) { if (cluster.isMaster) {
ev.mount(); ev.mount();
} }
await masterMain();
} }
if (cluster.isWorker || program.disableClustering) { if (cluster.isWorker || program.disableClustering) {

View File

@ -1,3 +1,4 @@
import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import * as cluster from 'cluster'; import * as cluster from 'cluster';
import chalk from 'chalk'; import chalk from 'chalk';
@ -12,6 +13,7 @@ import * as pkg from '../../package.json';
import { program } from '../argv'; import { program } from '../argv';
import { showMachineInfo } from '../misc/show-machine-info'; import { showMachineInfo } from '../misc/show-machine-info';
import { initDb } from '../db/postgre'; import { initDb } from '../db/postgre';
import Xev from 'xev';
const logger = new Logger('core', 'cyan'); const logger = new Logger('core', 'cyan');
const bootLogger = logger.createSubLogger('boot', 'magenta', false); const bootLogger = logger.createSubLogger('boot', 'magenta', false);
@ -75,6 +77,10 @@ export async function masterMain() {
await spawnWorkers(config.clusterLimit); await spawnWorkers(config.clusterLimit);
} }
loadPlugins();
bootLogger.succ('All plugins loaded');
if (!program.noDaemons) { if (!program.noDaemons) {
require('../daemons/server-stats').default(); require('../daemons/server-stats').default();
require('../daemons/notes-stats').default(); require('../daemons/notes-stats').default();
@ -109,6 +115,24 @@ function showEnvironment(): void {
logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`); logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
} }
const pluginService = {
registerTheme(theme: any) {
const ev = new Xev();
ev.emit('registerPluginTheme', theme);
}
};
function loadPlugins(): void {
const plugins = [
path.resolve(`${__dirname}/../plugins/featured-themes`)
];
for (const plugin of plugins) {
const pluginMeta = require(`${plugin}/plugin-meta.json`);
bootLogger.info(`Plugin loaded: ${pluginMeta.name} v${pluginMeta.version}`);
require(`${plugin}/main.js`).onActivate(pluginService);
}
}
/** /**
* Init app * Init app
*/ */

View File

@ -1,5 +1,9 @@
import * as cluster from 'cluster'; import * as cluster from 'cluster';
import { initDb } from '../db/postgre'; import { initDb } from '../db/postgre';
import Xev from 'xev';
import { registerTheme } from '../pluginThemes';
const ev = new Xev();
/** /**
* Init worker process * Init worker process
@ -16,5 +20,9 @@ export async function workerMain() {
if (cluster.isWorker) { if (cluster.isWorker) {
// Send a 'ready' message to parent process // Send a 'ready' message to parent process
process.send!('ready'); process.send!('ready');
ev.on('registerPluginTheme', theme => {
registerTheme(theme);
});
} }
} }

View File

@ -93,7 +93,7 @@
<summary><fa icon="folder-open"/> {{ $t('manage-themes') }}</summary> <summary><fa icon="folder-open"/> {{ $t('manage-themes') }}</summary>
<ui-select v-model="selectedThemeId" :placeholder="$t('select-theme')"> <ui-select v-model="selectedThemeId" :placeholder="$t('select-theme')">
<optgroup :label="$t('builtin-themes')"> <optgroup :label="$t('builtin-themes')">
<option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option> <option v-for="x in presetThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup> </optgroup>
<optgroup :label="$t('my-themes')"> <optgroup :label="$t('my-themes')">
<option v-for="x in installedThemes.filter(t => t.author == this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option> <option v-for="x in installedThemes.filter(t => t.author == this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option>
@ -113,7 +113,7 @@
<span>{{ $t('theme-code') }}</span> <span>{{ $t('theme-code') }}</span>
</ui-textarea> </ui-textarea>
<ui-button @click="export_()" link :download="`${selectedTheme.name}.misskeytheme`" ref="export"><fa icon="box"/> {{ $t('export') }}</ui-button> <ui-button @click="export_()" link :download="`${selectedTheme.name}.misskeytheme`" ref="export"><fa icon="box"/> {{ $t('export') }}</ui-button>
<ui-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="['far', 'trash-alt']"/> {{ $t('uninstall') }}</ui-button> <ui-button @click="uninstall()" v-if="!presetThemes.some(t => t.id == selectedTheme.id)"><fa :icon="['far', 'trash-alt']"/> {{ $t('uninstall') }}</ui-button>
</template> </template>
</details> </details>
</section> </section>
@ -123,7 +123,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../../i18n'; import i18n from '../../../../i18n';
import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../../theme'; import { lightTheme, darkTheme, applyTheme, Theme } from '../../../../theme';
import { Chrome } from 'vue-color'; import { Chrome } from 'vue-color';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
import * as tinycolor from 'tinycolor2'; import * as tinycolor from 'tinycolor2';
@ -138,7 +138,8 @@ export default Vue.extend({
data() { data() {
return { return {
builtinThemes: builtinThemes, themes: [],
presetThemes: [],
installThemeCode: null, installThemeCode: null,
selectedThemeId: null, selectedThemeId: null,
myThemeBase: 'light', myThemeBase: 'light',
@ -151,11 +152,17 @@ export default Vue.extend({
}; };
}, },
computed: { created() {
themes(): Theme[] { this.$root.getThemes().then(themes => {
return builtinThemes.concat(this.$store.state.device.themes); this.themes = themes;
}, });
this.$root.getPresetThemes().then(presetThemes => {
this.presetThemes = presetThemes;
});
}
computed: {
darkThemes(): Theme[] { darkThemes(): Theme[] {
return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark'); return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark');
}, },

View File

@ -16,7 +16,7 @@ import App from './app.vue';
import checkForUpdate from './common/scripts/check-for-update'; import checkForUpdate from './common/scripts/check-for-update';
import MiOS from './mios'; import MiOS from './mios';
import { version, codename, lang, locale } from './config'; import { version, codename, lang, locale } from './config';
import { builtinThemes, applyTheme, futureTheme } from './theme'; import { applyTheme, futureTheme } from './theme';
import Dialog from './common/views/components/dialog.vue'; import Dialog from './common/views/components/dialog.vue';
if (localStorage.getItem('theme') == null) { if (localStorage.getItem('theme') == null) {
@ -364,29 +364,32 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS)
os.store.watch(s => { os.store.watch(s => {
return s.device.darkmode; return s.device.darkmode;
}, v => { }, v => {
const themes = os.store.state.device.themes.concat(builtinThemes); os.getThemes().then(themes => {
const dark = themes.find(t => t.id == os.store.state.device.darkTheme); const dark = themes.find(t => t.id == os.store.state.device.darkTheme);
const light = themes.find(t => t.id == os.store.state.device.lightTheme); const light = themes.find(t => t.id == os.store.state.device.lightTheme);
applyTheme(v ? dark : light); applyTheme(v ? dark : light);
}); });
});
os.store.watch(s => { os.store.watch(s => {
return s.device.lightTheme; return s.device.lightTheme;
}, v => { }, v => {
const themes = os.store.state.device.themes.concat(builtinThemes); os.getThemes().then(themes => {
const theme = themes.find(t => t.id == v); const theme = themes.find(t => t.id == v);
if (!os.store.state.device.darkmode) { if (!os.store.state.device.darkmode) {
applyTheme(theme); applyTheme(theme);
} }
}); });
});
os.store.watch(s => { os.store.watch(s => {
return s.device.darkTheme; return s.device.darkTheme;
}, v => { }, v => {
const themes = os.store.state.device.themes.concat(builtinThemes); os.getThemes().then(themes => {
const theme = themes.find(t => t.id == v); const theme = themes.find(t => t.id == v);
if (os.store.state.device.darkmode) { if (os.store.state.device.darkmode) {
applyTheme(theme); applyTheme(theme);
} }
}); });
});
//#endregion //#endregion
/*// Reapply current theme /*// Reapply current theme
@ -447,6 +450,8 @@ export default (callback: (launch: (router: VueRouter) => [Vue, MiOS], os: MiOS)
api: os.api, api: os.api,
getMeta: os.getMeta, getMeta: os.getMeta,
getMetaSync: os.getMetaSync, getMetaSync: os.getMetaSync,
getThemes: os.getThemes,
getPresetThemes: os.getPresetThemes,
signout: os.signout, signout: os.signout,
new(vm, props) { new(vm, props) {
const x = new vm({ const x = new vm({

View File

@ -9,6 +9,8 @@ import Progress from './common/scripts/loading';
import Err from './common/views/components/connect-failed.vue'; import Err from './common/views/components/connect-failed.vue';
import Stream from './common/scripts/stream'; import Stream from './common/scripts/stream';
import { Theme, builtinThemes } from './theme';
import { concat } from '../../prelude/array';
//#region api requests //#region api requests
let spinner = null; let spinner = null;
@ -478,6 +480,28 @@ export default class MiOS extends EventEmitter {
} }
}); });
} }
/**
*
*/
@autobind
public getPresetThemes() {
return new Promise<Theme[]>(async (res, rej) => {
const pluginThemes = await this.api('plugins/themes') as Theme[];
res(concat([builtinThemes, pluginThemes]));
});
}
/**
*
*/
@autobind
public getThemes() {
return new Promise<Theme[]>(async (res, rej) => {
const installedThemes = this.store.state.device.themes as Theme[];
res(concat([await this.getPresetThemes(), installedThemes]));
});
}
} }
class WindowSystem extends EventEmitter { class WindowSystem extends EventEmitter {

View File

@ -12,34 +12,12 @@ export type Theme = {
export const lightTheme: Theme = require('../themes/light.json5'); export const lightTheme: Theme = require('../themes/light.json5');
export const darkTheme: Theme = require('../themes/dark.json5'); export const darkTheme: Theme = require('../themes/dark.json5');
export const lavenderTheme: Theme = require('../themes/lavender.json5');
export const futureTheme: Theme = require('../themes/future.json5'); export const futureTheme: Theme = require('../themes/future.json5');
export const halloweenTheme: Theme = require('../themes/halloween.json5');
export const cafeTheme: Theme = require('../themes/cafe.json5');
export const japaneseSushiSetTheme: Theme = require('../themes/japanese-sushi-set.json5');
export const gruvboxDarkTheme: Theme = require('../themes/gruvbox-dark.json5');
export const monokaiTheme: Theme = require('../themes/monokai.json5');
export const vividTheme: Theme = require('../themes/vivid.json5');
export const rainyTheme: Theme = require('../themes/rainy.json5');
export const mauveTheme: Theme = require('../themes/mauve.json5');
export const grayTheme: Theme = require('../themes/gray.json5');
export const tweetDeckTheme: Theme = require('../themes/tweet-deck.json5');
export const builtinThemes = [ export const builtinThemes = [
lightTheme, lightTheme,
darkTheme, darkTheme,
lavenderTheme,
futureTheme, futureTheme,
halloweenTheme,
cafeTheme,
japaneseSushiSetTheme,
gruvboxDarkTheme,
monokaiTheme,
vividTheme,
rainyTheme,
mauveTheme,
grayTheme,
tweetDeckTheme,
]; ];
export function applyTheme(theme: Theme, persisted = true) { export function applyTheme(theme: Theme, persisted = true) {

9
src/pluginThemes.ts Normal file
View File

@ -0,0 +1,9 @@
const themes: any[] = [];
export function registerTheme(theme: any) {
themes.push(theme);
}
export function getThemes() {
return themes;
}

View File

@ -0,0 +1,12 @@
require('json5/lib/register');
import * as fs from 'fs';
export function onActivate(service: any) {
const fileNames = fs.readdirSync(`${__dirname}/themes`)
.filter(f => fs.statSync(`${__dirname}/themes/${f}`).isFile());
for (const fileName of fileNames) {
const theme = require(`${__dirname}/themes/${fileName}`);
service.registerTheme(theme);
}
}

View File

@ -0,0 +1,5 @@
{
"misskeyVersion": "11",
"name": "featured-themes",
"version": "1.0.0"
}

View File

@ -0,0 +1,14 @@
import define from '../../define';
import { getThemes } from '../../../../pluginThemes';
export const meta = {
desc: {
'ja-JP': 'プラグインによって登録されたテーマを取得します。'
},
tags: ['themes']
};
export default define(meta, async (ps, me) => {
return getThemes();
});