Introduce plugin system supporting theme registration
This commit is contained in:
		
							parent
							
								
									83fedcff3b
								
							
						
					
					
						commit
						0fd0b4f466
					
				
					 23 changed files with 141 additions and 48 deletions
				
			
		|  | @ -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/**/*', | ||||||
|  |  | ||||||
|  | @ -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) { | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
|  | @ -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); | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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'); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | @ -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,28 +364,31 @@ 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
 | ||||||
| 
 | 
 | ||||||
|  | @ -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({ | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -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
									
								
							
							
						
						
									
										9
									
								
								src/pluginThemes.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | const themes: any[] = []; | ||||||
|  | 
 | ||||||
|  | export function registerTheme(theme: any) { | ||||||
|  | 	themes.push(theme); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getThemes() { | ||||||
|  | 	return themes; | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								src/plugins/featured-themes/main.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/plugins/featured-themes/main.ts
									
										
									
									
									
										Normal 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); | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								src/plugins/featured-themes/plugin-meta.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/plugins/featured-themes/plugin-meta.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | { | ||||||
|  | 	"misskeyVersion": "11", | ||||||
|  | 	"name": "featured-themes", | ||||||
|  | 	"version": "1.0.0" | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								src/server/api/endpoints/plugins/themes.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/server/api/endpoints/plugins/themes.ts
									
										
									
									
									
										Normal 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(); | ||||||
|  | }); | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue