mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Example of potential plugin implementation
This commit is contained in:
		
							parent
							
								
									71b943f434
								
							
						
					
					
						commit
						62bd7e73f4
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -7,6 +7,7 @@ | ||||
| /podcasts/ | ||||
| /media/ | ||||
| /metadata/ | ||||
| /plugins/ | ||||
| /client/.nuxt/ | ||||
| /client/dist/ | ||||
| /dist/ | ||||
|  | ||||
| @ -364,6 +364,9 @@ export default { | ||||
|     showCollectionsButton() { | ||||
|       return this.isBook && this.userCanUpdate | ||||
|     }, | ||||
|     pluginExtensions() { | ||||
|       return this.$store.getters['getPluginExtensions']('item.detail.actions') | ||||
|     }, | ||||
|     contextMenuItems() { | ||||
|       const items = [] | ||||
| 
 | ||||
| @ -429,6 +432,18 @@ export default { | ||||
|         }) | ||||
|       } | ||||
| 
 | ||||
|       if (this.pluginExtensions.length) { | ||||
|         this.pluginExtensions.forEach((plugin) => { | ||||
|           const pluginSlug = plugin.slug | ||||
|           plugin.extensions.forEach((pext) => { | ||||
|             items.push({ | ||||
|               text: pext.label, | ||||
|               action: `plugin-${pluginSlug}-action-${pext.name}` | ||||
|             }) | ||||
|           }) | ||||
|         }) | ||||
|       } | ||||
| 
 | ||||
|       return items | ||||
|     } | ||||
|   }, | ||||
| @ -763,7 +778,31 @@ export default { | ||||
|       } else if (action === 'share') { | ||||
|         this.$store.commit('setSelectedLibraryItem', this.libraryItem) | ||||
|         this.$store.commit('globals/setShareModal', this.mediaItemShare) | ||||
|       } else if (action.startsWith('plugin-')) { | ||||
|         const actionStrSplit = action.replace('plugin-', '').split('-action-') | ||||
|         const pluginSlug = actionStrSplit[0] | ||||
|         const pluginAction = actionStrSplit[1] | ||||
|         console.log('Plugin action for', pluginSlug, 'with action', pluginAction) | ||||
|         this.onPluginAction(pluginSlug, pluginAction) | ||||
|       } | ||||
|     }, | ||||
|     onPluginAction(pluginSlug, pluginAction) { | ||||
|       this.$axios | ||||
|         .$post(`/api/plugins/action`, { | ||||
|           pluginSlug, | ||||
|           pluginAction, | ||||
|           target: 'item.detail.actions', | ||||
|           data: { | ||||
|             entityId: this.libraryItemId, | ||||
|             entityType: 'libraryItem' | ||||
|           } | ||||
|         }) | ||||
|         .then((data) => { | ||||
|           console.log('Plugin action response', data) | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Plugin action failed', error) | ||||
|         }) | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|  | ||||
| @ -166,10 +166,11 @@ export default { | ||||
| 
 | ||||
|       location.reload() | ||||
|     }, | ||||
|     setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices }) { | ||||
|     setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices, pluginExtensions }) { | ||||
|       this.$store.commit('setServerSettings', serverSettings) | ||||
|       this.$store.commit('setSource', Source) | ||||
|       this.$store.commit('libraries/setEReaderDevices', ereaderDevices) | ||||
|       this.$store.commit('setPluginExtensions', pluginExtensions) | ||||
|       this.$setServerLanguageCode(serverSettings.language) | ||||
| 
 | ||||
|       if (serverSettings.chromecastEnabled) { | ||||
|  | ||||
| @ -28,7 +28,8 @@ export const state = () => ({ | ||||
|   openModal: null, | ||||
|   innerModalOpen: false, | ||||
|   lastBookshelfScrollData: {}, | ||||
|   routerBasePath: '/' | ||||
|   routerBasePath: '/', | ||||
|   pluginExtensions: [] | ||||
| }) | ||||
| 
 | ||||
| export const getters = { | ||||
| @ -61,6 +62,19 @@ export const getters = { | ||||
|   getHomeBookshelfView: (state) => { | ||||
|     if (!state.serverSettings || isNaN(state.serverSettings.homeBookshelfView)) return Constants.BookshelfView.STANDARD | ||||
|     return state.serverSettings.homeBookshelfView | ||||
|   }, | ||||
|   getPluginExtensions: (state) => (target) => { | ||||
|     return state.pluginExtensions | ||||
|       .map((pext) => { | ||||
|         const extensionsMatchingTarget = pext.extensions.filter((ext) => ext.target === target) | ||||
|         if (!extensionsMatchingTarget.length) return null | ||||
|         return { | ||||
|           name: pext.name, | ||||
|           slug: pext.slug, | ||||
|           extensions: extensionsMatchingTarget | ||||
|         } | ||||
|       }) | ||||
|       .filter(Boolean) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -239,5 +253,8 @@ export const mutations = { | ||||
|   }, | ||||
|   setInnerModalOpen(state, val) { | ||||
|     state.innerModalOpen = val | ||||
|   }, | ||||
|   setPluginExtensions(state, val) { | ||||
|     state.pluginExtensions = val | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										4
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								index.js
									
									
									
									
									
								
							| @ -13,6 +13,7 @@ if (isDev) { | ||||
|   if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1' | ||||
|   if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1' | ||||
|   if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath | ||||
|   if (devEnv.PluginsPath) process.env.PLUGINS_PATH = devEnv.PluginsPath | ||||
|   process.env.SOURCE = 'local' | ||||
|   process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || '' | ||||
| } | ||||
| @ -21,10 +22,11 @@ const PORT = process.env.PORT || 80 | ||||
| const HOST = process.env.HOST | ||||
| const CONFIG_PATH = process.env.CONFIG_PATH || '/config' | ||||
| const METADATA_PATH = process.env.METADATA_PATH || '/metadata' | ||||
| const PLUGINS_PATH = process.env.PLUGINS_PATH || '/plugins' | ||||
| const SOURCE = process.env.SOURCE || 'docker' | ||||
| const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '' | ||||
| 
 | ||||
| console.log('Config', CONFIG_PATH, METADATA_PATH) | ||||
| 
 | ||||
| const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) | ||||
| const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, PLUGINS_PATH, ROUTER_BASE_PATH) | ||||
| Server.start() | ||||
|  | ||||
							
								
								
									
										9
									
								
								prod.js
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								prod.js
									
									
									
									
									
								
							| @ -1,6 +1,7 @@ | ||||
| const optionDefinitions = [ | ||||
|   { name: 'config', alias: 'c', type: String }, | ||||
|   { name: 'metadata', alias: 'm', type: String }, | ||||
|   { name: 'plugins', alias: 'l', type: String }, | ||||
|   { name: 'port', alias: 'p', type: String }, | ||||
|   { name: 'host', alias: 'h', type: String }, | ||||
|   { name: 'source', alias: 's', type: String } | ||||
| @ -16,18 +17,20 @@ const server = require('./server/Server') | ||||
| 
 | ||||
| global.appRoot = __dirname | ||||
| 
 | ||||
| var inputConfig = options.config ? Path.resolve(options.config) : null | ||||
| var inputMetadata = options.metadata ? Path.resolve(options.metadata) : null | ||||
| const inputConfig = options.config ? Path.resolve(options.config) : null | ||||
| const inputMetadata = options.metadata ? Path.resolve(options.metadata) : null | ||||
| const inputPlugins = options.plugins ? Path.resolve(options.plugins) : null | ||||
| 
 | ||||
| const PORT = options.port || process.env.PORT || 3333 | ||||
| const HOST = options.host || process.env.HOST | ||||
| const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config') | ||||
| const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata') | ||||
| const PLUGINS_PATH = inputPlugins || process.env.PLUGINS_PATH || Path.resolve('plugins') | ||||
| const SOURCE = options.source || process.env.SOURCE || 'debian' | ||||
| 
 | ||||
| const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '' | ||||
| 
 | ||||
| console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH) | ||||
| 
 | ||||
| const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) | ||||
| const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, PLUGINS_PATH, ROUTER_BASE_PATH) | ||||
| Server.start() | ||||
|  | ||||
| @ -10,6 +10,7 @@ const ExtractJwt = require('passport-jwt').ExtractJwt | ||||
| const OpenIDClient = require('openid-client') | ||||
| const Database = require('./Database') | ||||
| const Logger = require('./Logger') | ||||
| const PluginManager = require('./managers/PluginManager') | ||||
| 
 | ||||
| /** | ||||
|  * @class Class for handling all the authentication related functionality. | ||||
| @ -938,6 +939,7 @@ class Auth { | ||||
|       userDefaultLibraryId: user.getDefaultLibraryId(libraryIds), | ||||
|       serverSettings: Database.serverSettings.toJSONForBrowser(), | ||||
|       ereaderDevices: Database.emailSettings.getEReaderDevices(user), | ||||
|       pluginExtensions: PluginManager.pluginExtensions, | ||||
|       Source: global.Source | ||||
|     } | ||||
|   } | ||||
|  | ||||
							
								
								
									
										20
									
								
								server/PluginAbstract.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								server/PluginAbstract.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| class PluginAbstract { | ||||
|   constructor() { | ||||
|     if (this.constructor === PluginAbstract) { | ||||
|       throw new Error('Cannot instantiate abstract class') | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   init() { | ||||
|     throw new Error('Method "init()" not implemented') | ||||
|   } | ||||
| 
 | ||||
|   onAction() { | ||||
|     throw new Error('Method "onAction()" not implemented') | ||||
|   } | ||||
| 
 | ||||
|   onDestroy() { | ||||
|     throw new Error('Method "onDestroy()" not implemented') | ||||
|   } | ||||
| } | ||||
| module.exports = PluginAbstract | ||||
| @ -36,6 +36,7 @@ const ApiCacheManager = require('./managers/ApiCacheManager') | ||||
| const BinaryManager = require('./managers/BinaryManager') | ||||
| const ShareManager = require('./managers/ShareManager') | ||||
| const LibraryScanner = require('./scanner/LibraryScanner') | ||||
| const PluginManager = require('./managers/PluginManager') | ||||
| 
 | ||||
| //Import the main Passport and Express-Session library
 | ||||
| const passport = require('passport') | ||||
| @ -43,7 +44,7 @@ const expressSession = require('express-session') | ||||
| const MemoryStore = require('./libs/memorystore') | ||||
| 
 | ||||
| class Server { | ||||
|   constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { | ||||
|   constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, PLUGINS_PATH, ROUTER_BASE_PATH) { | ||||
|     this.Port = PORT | ||||
|     this.Host = HOST | ||||
|     global.Source = SOURCE | ||||
| @ -51,6 +52,7 @@ class Server { | ||||
|     global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH)) | ||||
|     global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH)) | ||||
|     global.RouterBasePath = ROUTER_BASE_PATH | ||||
|     global.PluginsPath = fileUtils.filePathToPOSIX(Path.normalize(PLUGINS_PATH)) | ||||
|     global.XAccel = process.env.USE_X_ACCEL | ||||
|     global.AllowCors = process.env.ALLOW_CORS === '1' | ||||
|     global.DisableSsrfRequestFilter = process.env.DISABLE_SSRF_REQUEST_FILTER === '1' | ||||
| @ -151,6 +153,9 @@ class Server { | ||||
|         LibraryScanner.scanFilesChanged(pendingFileUpdates, pendingTask) | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     // Initialize plugins
 | ||||
|     PluginManager.init() | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
							
								
								
									
										24
									
								
								server/controllers/PluginController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								server/controllers/PluginController.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| const { Request, Response } = require('express') | ||||
| const PluginManager = require('../managers/PluginManager') | ||||
| const Logger = require('../Logger') | ||||
| 
 | ||||
| class PluginController { | ||||
|   constructor() {} | ||||
| 
 | ||||
|   /** | ||||
|    * POST: /api/plugins/action | ||||
|    * | ||||
|    * @param {Request} req | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   handleAction(req, res) { | ||||
|     const pluginSlug = req.body.pluginSlug | ||||
|     const actionName = req.body.pluginAction | ||||
|     const target = req.body.target | ||||
|     const data = req.body.data | ||||
|     Logger.info(`[PluginController] Handle plugin action ${pluginSlug} ${actionName} ${target}`, data) | ||||
|     PluginManager.onAction(pluginSlug, actionName, target, data) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| } | ||||
| module.exports = new PluginController() | ||||
							
								
								
									
										145
									
								
								server/managers/PluginManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								server/managers/PluginManager.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,145 @@ | ||||
| const Path = require('path') | ||||
| const Logger = require('../Logger') | ||||
| const PluginAbstract = require('../PluginAbstract') | ||||
| const fs = require('fs').promises | ||||
| 
 | ||||
| class PluginManager { | ||||
|   constructor() { | ||||
|     this.plugins = [] | ||||
|   } | ||||
| 
 | ||||
|   get pluginExtensions() { | ||||
|     return this.plugins | ||||
|       .filter((plugin) => plugin.manifest.extensions?.length) | ||||
|       .map((plugin) => { | ||||
|         return { | ||||
|           name: plugin.manifest.name, | ||||
|           slug: plugin.manifest.slug, | ||||
|           extensions: plugin.manifest.extensions | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
| 
 | ||||
|   get pluginContext() { | ||||
|     return { | ||||
|       Logger | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {string} pluginPath | ||||
|    * @returns {Promise<{manifest: Object, contents: PluginAbstract}>} | ||||
|    */ | ||||
|   async loadPlugin(pluginPath) { | ||||
|     const pluginFiles = await fs.readdir(pluginPath, { withFileTypes: true }).then((files) => files.filter((file) => !file.isDirectory())) | ||||
| 
 | ||||
|     if (!pluginFiles.length) { | ||||
|       Logger.error(`No files found in plugin ${pluginPath}`) | ||||
|       return null | ||||
|     } | ||||
|     const manifestFile = pluginFiles.find((file) => file.name === 'manifest.json') | ||||
|     if (!manifestFile) { | ||||
|       Logger.error(`No manifest found for plugin ${pluginPath}`) | ||||
|       return null | ||||
|     } | ||||
|     const indexFile = pluginFiles.find((file) => file.name === 'index.js') | ||||
|     if (!indexFile) { | ||||
|       Logger.error(`No index file found for plugin ${pluginPath}`) | ||||
|       return null | ||||
|     } | ||||
| 
 | ||||
|     let manifestJson = null | ||||
|     try { | ||||
|       manifestJson = await fs.readFile(Path.join(pluginPath, manifestFile.name), 'utf8').then((data) => JSON.parse(data)) | ||||
|     } catch (error) { | ||||
|       Logger.error(`Error parsing manifest file for plugin ${pluginPath}`, error) | ||||
|       return null | ||||
|     } | ||||
| 
 | ||||
|     // TODO: Validate manifest json
 | ||||
| 
 | ||||
|     let pluginContents = null | ||||
|     try { | ||||
|       pluginContents = require(Path.join(pluginPath, indexFile.name)) | ||||
|     } catch (error) { | ||||
|       Logger.error(`Error loading plugin ${pluginPath}`, error) | ||||
|       return null | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       manifest: manifestJson, | ||||
|       contents: pluginContents | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async loadPlugins() { | ||||
|     const pluginDirs = await fs.readdir(global.PluginsPath, { withFileTypes: true, recursive: true }).then((files) => files.filter((file) => file.isDirectory())) | ||||
|     console.log('pluginDirs', pluginDirs) | ||||
| 
 | ||||
|     for (const pluginDir of pluginDirs) { | ||||
|       Logger.info(`[PluginManager] Loading plugin ${pluginDir.name}`) | ||||
|       const plugin = await this.loadPlugin(Path.join(global.PluginsPath, pluginDir.name)) | ||||
|       if (plugin) { | ||||
|         Logger.info(`[PluginManager] Loaded plugin ${plugin.manifest.name}`) | ||||
|         this.plugins.push(plugin) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async init() { | ||||
|     await this.loadPlugins() | ||||
| 
 | ||||
|     for (const plugin of this.plugins) { | ||||
|       if (plugin.contents.init) { | ||||
|         Logger.info(`[PluginManager] Initializing plugin ${plugin.manifest.name}`) | ||||
|         plugin.contents.init(this.pluginContext) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onAction(pluginSlug, actionName, target, data) { | ||||
|     const plugin = this.plugins.find((plugin) => plugin.manifest.slug === pluginSlug) | ||||
|     if (!plugin) { | ||||
|       Logger.error(`[PluginManager] Plugin ${pluginSlug} not found`) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     const pluginExtension = plugin.manifest.extensions.find((extension) => extension.name === actionName) | ||||
|     if (!pluginExtension) { | ||||
|       Logger.error(`[PluginManager] Extension ${actionName} not found for plugin ${plugin.manifest.name}`) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     if (plugin.contents.onAction) { | ||||
|       Logger.info(`[PluginManager] Calling onAction for plugin ${plugin.manifest.name}`) | ||||
|       plugin.contents.onAction(this.pluginContext, actionName, target, data) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   pluginExists(name) { | ||||
|     return this.plugins.some((plugin) => plugin.name === name) | ||||
|   } | ||||
| 
 | ||||
|   registerPlugin(plugin) { | ||||
|     if (!plugin.name) { | ||||
|       throw new Error('The plugin name and package are required') | ||||
|     } | ||||
| 
 | ||||
|     if (this.pluginExists(plugin.name)) { | ||||
|       throw new Error(`Cannot add existing plugin ${plugin.name}`) | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       // Try to load the plugin
 | ||||
|       const pluginPath = Path.join(global.PluginsPath, plugin.name) | ||||
|       const packageContents = require(pluginPath) | ||||
|       console.log('packageContents', packageContents) | ||||
|       packageContents.init() | ||||
|       this.plugins.push(packageContents) | ||||
|     } catch (error) { | ||||
|       console.log(`Cannot load plugin ${plugin.name}`, error) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| module.exports = new PluginManager() | ||||
| @ -33,6 +33,7 @@ const RSSFeedController = require('../controllers/RSSFeedController') | ||||
| const CustomMetadataProviderController = require('../controllers/CustomMetadataProviderController') | ||||
| const MiscController = require('../controllers/MiscController') | ||||
| const ShareController = require('../controllers/ShareController') | ||||
| const PluginController = require('../controllers/PluginController') | ||||
| 
 | ||||
| const { getTitleIgnorePrefix } = require('../utils/index') | ||||
| 
 | ||||
| @ -320,6 +321,11 @@ class ApiRouter { | ||||
|     this.router.post('/share/mediaitem', ShareController.createMediaItemShare.bind(this)) | ||||
|     this.router.delete('/share/mediaitem/:id', ShareController.deleteMediaItemShare.bind(this)) | ||||
| 
 | ||||
|     //
 | ||||
|     // Plugin routes
 | ||||
|     //
 | ||||
|     this.router.post('/plugins/action', PluginController.handleAction.bind(this)) | ||||
| 
 | ||||
|     //
 | ||||
|     // Misc Routes
 | ||||
|     //
 | ||||
|  | ||||
							
								
								
									
										5
									
								
								test/server/managers/PluginManager.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								test/server/managers/PluginManager.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| describe('PluginManager', () => { | ||||
|   it('should register a plugin', () => { | ||||
|     // Test implementation
 | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										18
									
								
								test/server/managers/plugins/Example/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								test/server/managers/plugins/Example/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| const PluginAbstract = require('../../../../../server/PluginAbstract') | ||||
| 
 | ||||
| class ExamplePlugin extends PluginAbstract { | ||||
|   constructor() { | ||||
|     super() | ||||
| 
 | ||||
|     this.name = 'Example' | ||||
|   } | ||||
| 
 | ||||
|   init(context) { | ||||
|     context.Logger.info('[ExamplePlugin] Example plugin loaded successfully') | ||||
|   } | ||||
| 
 | ||||
|   async onAction(context, actionName, target, data) { | ||||
|     context.Logger.info('[ExamplePlugin] Example plugin onAction', actionName, target, data) | ||||
|   } | ||||
| } | ||||
| module.exports = new ExamplePlugin() | ||||
							
								
								
									
										52
									
								
								test/server/managers/plugins/Example/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								test/server/managers/plugins/Example/manifest.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| { | ||||
|   "name": "Example Plugin", | ||||
|   "slug": "example", | ||||
|   "version": "1.0.0", | ||||
|   "description": "This is an example plugin", | ||||
|   "extensions": [ | ||||
|     { | ||||
|       "target": "item.detail.actions", | ||||
|       "name": "itemActionExample", | ||||
|       "label": "Item Example Action", | ||||
|       "labelKey": "ItemExampleAction" | ||||
|     } | ||||
|   ], | ||||
|   "config": { | ||||
|     "title": "Example Plugin Configuration", | ||||
|     "titleKey": "ExamplePluginConfiguration", | ||||
|     "description": "This is an example plugin", | ||||
|     "descriptionKey": "ExamplePluginConfigurationDescription", | ||||
|     "formFields": [ | ||||
|       { | ||||
|         "name": "apiKey", | ||||
|         "label": "API Key", | ||||
|         "labelKey": "LabelApiKey", | ||||
|         "type": "text", | ||||
|         "required": false | ||||
|       }, | ||||
|       { | ||||
|         "name": "requestAddress", | ||||
|         "label": "Request Address", | ||||
|         "labelKey": "LabelRequestAddress", | ||||
|         "type": "text", | ||||
|         "required": true | ||||
|       }, | ||||
|       { | ||||
|         "name": "enable", | ||||
|         "label": "Enable", | ||||
|         "labelKey": "LabelEnable", | ||||
|         "type": "checkbox" | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "localization": { | ||||
|     "en-us": { | ||||
|       "ItemExampleAction": "Item Example Action", | ||||
|       "LabelApiKey": "API Key", | ||||
|       "LabelEnable": "Enable", | ||||
|       "ExamplePluginConfiguration": "Example Plugin Configuration", | ||||
|       "ExamplePluginConfigurationDescription": "This is an example plugin", | ||||
|       "LabelRequestAddress": "Request Address" | ||||
|     } | ||||
|   } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user