├── .gitignore ├── .run ├── Run-ReactApp.run.xml └── Run-ServerApp.run.xml ├── .xml ├── LICENSE ├── README.md ├── README_EN.md ├── README_TW.md ├── doc └── 模板开发.md ├── server ├── _data │ └── static │ │ └── public │ │ ├── global.d.ts │ │ ├── package.json │ │ ├── scripts │ │ ├── @types │ │ │ ├── template.js │ │ │ └── template.ts │ │ ├── bin │ │ │ ├── BaseTemplateMethods.js │ │ │ └── BaseTemplateMethods.ts │ │ ├── d3.js │ │ ├── echarts.min.js │ │ ├── interact.min.js │ │ ├── lib │ │ │ ├── BaseEventEmitter.js │ │ │ └── BaseEventEmitter.ts │ │ ├── template.js │ │ ├── template.ts │ │ ├── templateDefaultConfig.js │ │ ├── templateDefaultConfig.ts │ │ ├── utils.js │ │ └── utils.ts │ │ ├── styles │ │ ├── animate.min.css │ │ └── default.css │ │ ├── templates │ │ ├── base-bar-chart │ │ │ ├── .initialized.t.bin │ │ │ ├── conf.ts │ │ │ ├── config.json │ │ │ ├── cover.gif │ │ │ ├── cover.png │ │ │ ├── data.json │ │ │ └── index.ts │ │ ├── bump-chart │ │ │ ├── .initialized.t.bin │ │ │ ├── conf.ts │ │ │ ├── config.json │ │ │ ├── cover.gif │ │ │ ├── cover.png │ │ │ ├── data.json │ │ │ ├── index.html │ │ │ └── index.ts │ │ └── pie-chart │ │ │ ├── .initialized.t.bin │ │ │ ├── conf.ts │ │ │ ├── config.json │ │ │ ├── cover.gif │ │ │ ├── cover.png │ │ │ ├── data.json │ │ │ ├── index.html │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── view │ │ └── index.pug ├── bin │ ├── HttpServer.ts │ ├── Player.ts │ ├── Process.ts │ ├── Socket.ts │ ├── TaskScheduler.ts │ ├── TemplateSocket.ts │ ├── WebSocketServer.ts │ ├── controllers │ │ ├── Catch.ts │ │ ├── Color.ts │ │ ├── File.ts │ │ ├── Resources.ts │ │ ├── ServerBase.ts │ │ ├── Task.ts │ │ └── Template.ts │ └── dataBase │ │ ├── defaultData.ts │ │ ├── index.ts │ │ └── init.ts ├── conf │ └── AppConfig.ts ├── const │ ├── ErrorMessage.ts │ └── UpLoadFileTypes.ts ├── global.d.ts ├── gulpfile.js ├── lib │ ├── CheckDirectory.ts │ ├── Cli.ts │ ├── MemoryCache.ts │ └── TSC.ts ├── main.ts ├── nodemon.json ├── package.json ├── router │ └── index.ts ├── sql │ ├── lmo-dv-sp_TSQL.sql │ └── lmo-dv_TSQL.sql ├── tsconfig.json ├── utils │ └── index.ts └── yarn.lock ├── service ├── bin │ ├── Core.ts │ ├── SocketServer.ts │ ├── ffmpeg.ts │ └── wvc │ │ ├── config.ts │ │ └── index.ts ├── config │ └── AppConfig.ts ├── global.d.ts ├── main.ts ├── nodemon.json ├── package.json ├── tsconfig.json └── yarn.lock └── web_app ├── .eslintrc.js ├── .gitignore ├── README.md ├── config-overrides.js ├── craco.config.js ├── package.json ├── public ├── bg.svg ├── favicon.ico ├── favicon48.ico ├── favicon96.ico ├── index.html ├── logo.svg ├── logo192.png ├── manifest.json └── robots.txt ├── src ├── bin │ ├── @types │ │ └── TemplateTypes.ts │ └── Hooks.ts ├── components │ ├── About │ │ ├── OpenSourceComponentLicense │ │ │ ├── content.ts │ │ │ └── index.tsx │ │ ├── icon │ │ │ └── github.ts │ │ ├── index.tsx │ │ └── style.scss │ ├── AudioPreview.tsx │ ├── BackgroundConfig │ │ └── index.tsx │ ├── ColorConfig │ │ ├── GradientColorPicker.tsx │ │ ├── SelectBackground.tsx │ │ ├── SelectBackgroundImage.tsx │ │ ├── SelectTheme.tsx │ │ ├── SingleColorPicker.tsx │ │ └── index.tsx │ ├── ColorPicker │ │ └── index.tsx │ ├── DesignConfigs.tsx │ ├── EditDataTable.tsx │ ├── GlobalComponent │ │ ├── components │ │ │ ├── SwitchLang.tsx │ │ │ ├── SwitchTheme.tsx │ │ │ ├── TDisplayTemplate.tsx │ │ │ └── THTMLTemplate.tsx │ │ └── index.ts │ ├── Header │ │ ├── DesignHeader.tsx │ │ ├── HomeHeader.tsx │ │ └── index.ts │ ├── Preview.tsx │ ├── ProgressBar.tsx │ ├── ResetButton.tsx │ ├── Resources │ │ ├── ResourcesItem.tsx │ │ ├── VideoPlayer.tsx │ │ ├── index.tsx │ │ └── style.scss │ ├── SelectFile │ │ ├── AudioItem.tsx │ │ ├── AudioList.tsx │ │ ├── FileCategoryTree.tsx │ │ ├── ImageList.tsx │ │ ├── LoadingImage.tsx │ │ ├── UploadFile.tsx │ │ └── index.tsx │ ├── ServerInfo.tsx │ ├── SyntheticConfig │ │ ├── config.ts │ │ └── index.tsx │ ├── Task.tsx │ ├── TemplateList │ │ ├── TemplateItem │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── style.scss │ ├── TemplateOtherConfig │ │ └── index.tsx │ ├── TextConfig │ │ └── index.tsx │ ├── YExtendTemplate.tsx │ └── index.ts ├── config │ └── AppConfig.ts ├── const │ ├── AnimateNames.ts │ └── Message.ts ├── global.d.ts ├── i18n │ ├── config │ │ ├── enUk.json │ │ ├── jpJp.json │ │ ├── koKr.json │ │ ├── ruRU.json │ │ ├── zhCn.json │ │ └── zhTw.json │ └── index.ts ├── index.tsx ├── lib │ ├── AudioPlayer │ │ └── index.ts │ ├── Notification.ts │ ├── Nprogress │ │ ├── index.ts │ │ └── style.scss │ ├── PostMessage │ │ └── index.ts │ ├── Request │ │ └── index.ts │ ├── Socket │ │ └── index.ts │ ├── Storage │ │ └── index.ts │ └── Store │ │ ├── AppStore.ts │ │ ├── index.ts │ │ └── interface │ │ └── AppState.ts ├── pages │ ├── AppDesign.tsx │ ├── AppHome.tsx │ ├── Welcome.tsx │ └── style │ │ ├── AppDesign-Dack.scss │ │ ├── AppDesign-Light.scss │ │ ├── AppDesign.scss │ │ ├── AppHome-Dack.scss │ │ ├── AppHome-Light.scss │ │ ├── AppHome.scss │ │ └── Welcome.scss ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts ├── style │ ├── Global.css │ ├── animate.min.css │ ├── fonts │ │ ├── JetbrainsMono.ttf │ │ ├── element-icons.ttf │ │ └── element-icons.woff │ ├── hi.scss │ ├── index.scss │ └── theme.scss ├── svg │ ├── error.ts │ └── lang.ts ├── types │ ├── ReactTypes.ts │ └── TemplateMessage.ts └── utils │ └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .vscode 4 | *.suo 5 | *.ntvs* 6 | *.njsproj 7 | *.sln 8 | *.sw? 9 | 10 | node_modules 11 | /dist 12 | /service/dist 13 | /server/dist 14 | /server/_data/static/public/templates/*/*.js 15 | ffmpeg.exe 16 | \temp 17 | \output* 18 | \uploads* 19 | 20 | service/.bin/* 21 | service/tmp/* 22 | *.mp4* 23 | *.log* 24 | *.t.log* 25 | *.ting 26 | .env.local 27 | .env.*.local 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | pnpm-debug.log* 32 | -------------------------------------------------------------------------------- /.run/Run-ReactApp.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 |
28 | 29 |
30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /server/_data/static/public/templates/pie-chart/.initialized.t.bin: -------------------------------------------------------------------------------- 1 | {"id":"ff194e87-3ed7-47d7-add5-a411f05a7c70","name":"饼图","description":"以面积的形式展示数据比例分配情况","type":1,"cover":"/static/templates/pie-chart/cover.png","gifCover":"/static/templates/pie-chart/cover.gif","path":"/static/templates/pie-chart/index.html","createTime":1719823842649} -------------------------------------------------------------------------------- /server/_data/static/public/templates/pie-chart/conf.ts: -------------------------------------------------------------------------------- 1 | import {ITemplateConfig} from "../../scripts/@types/template.js"; 2 | import TemplateDefaultConfig, {TemplateTextDefaultConfig} from "../../scripts/templateDefaultConfig.js"; 3 | 4 | const config: ITemplateConfig = { 5 | config: { 6 | text: { 7 | ...TemplateTextDefaultConfig 8 | }, 9 | theme: { 10 | type: 'Theme', 11 | configs: ['Gradient', 'Theme'], 12 | value: ['#28C8D5', '#1CA8E3', '#1CA8E3', '#5169CA', '#5837A6', '#BF39A7', '#E54574', '#F47F22'] 13 | }, 14 | ...TemplateDefaultConfig 15 | }, 16 | otherConfig: { 17 | label: '图表其他配置', 18 | configs: [ 19 | { 20 | label: '类型', 21 | key: 'type', 22 | type: 'select', 23 | value: 'round', 24 | options: [ 25 | { 26 | label: '饼图', 27 | value: 'round' 28 | }, 29 | { 30 | label: '玫瑰图', 31 | value: 'radius' 32 | } 33 | ] 34 | }, 35 | { 36 | label: '标签字体大小', 37 | key: 'labelFontSize', 38 | type: 'input-number', 39 | value: 16 40 | }, 41 | { 42 | label: '显示标签', 43 | key: 'showLabel', 44 | type: 'switch', 45 | value: true 46 | }, 47 | { 48 | label: '标签字体颜色', 49 | key: 'labelFontColor', 50 | type: 'color', 51 | value: '#000' 52 | }, 53 | { 54 | label: '边框半径', 55 | key: 'borderRadius', 56 | type: 'input-number', 57 | value: 20 58 | }, 59 | { 60 | label: '中心半径', 61 | key: 'centerRadius', 62 | type: 'input-number', 63 | value: 0 64 | } 65 | ], 66 | values: { 67 | type: 'round', 68 | labelFontSize: 16, 69 | showLabel: true, 70 | labelFontColor: '#000', 71 | borderRadius: 20, 72 | centerRadius: 0 73 | } 74 | } 75 | }; 76 | 77 | export default config; 78 | -------------------------------------------------------------------------------- /server/_data/static/public/templates/pie-chart/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "饼图", 3 | "description": "以面积的形式展示数据比例分配情况", 4 | "type": 1 5 | } -------------------------------------------------------------------------------- /server/_data/static/public/templates/pie-chart/cover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayuanlmo/lmo-data-visualization/119a911ca9ae6e7af0724a6cc2b7939a2d653a26/server/_data/static/public/templates/pie-chart/cover.gif -------------------------------------------------------------------------------- /server/_data/static/public/templates/pie-chart/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayuanlmo/lmo-data-visualization/119a911ca9ae6e7af0724a6cc2b7939a2d653a26/server/_data/static/public/templates/pie-chart/cover.png -------------------------------------------------------------------------------- /server/_data/static/public/templates/pie-chart/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "Apple", 4 | 700 5 | ], 6 | [ 7 | "Banana", 8 | 800 9 | ], 10 | [ 11 | "Cherry", 12 | 700 13 | ], 14 | [ 15 | "Damson", 16 | 520 17 | ], 18 | [ 19 | "Grape", 20 | 650 21 | ], 22 | [ 23 | "Haw", 24 | 420 25 | ], 26 | [ 27 | "Kiwifruit", 28 | 220 29 | ], 30 | [ 31 | "Lemon", 32 | 300 33 | ] 34 | ] 35 | -------------------------------------------------------------------------------- /server/_data/static/public/templates/pie-chart/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 饼图 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 |
27 | 28 |
29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /server/_data/static/public/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "module": "ESNext", 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "strict": true, 13 | "resolveJsonModule": true, 14 | "moduleResolution": "Bundler", 15 | "skipLibCheck": true, 16 | "newLine": "crlf" 17 | }, 18 | "include": [ 19 | "templates", 20 | "scripts", 21 | "global.d.ts" 22 | ], 23 | "exclude": [ 24 | "*.js" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /server/_data/static/public/view/index.pug: -------------------------------------------------------------------------------- 1 | -const TEMPLATE_DEFAULT_STYLES = ["../../styles/animate.min.css", "../../styles/default.css"]; 2 | -const TEMPLATE_DEFAULT_SCRIPTS = ['../../scripts/echarts.min.js', '../../scripts/d3.js', '../../scripts/interact.min.js'] 3 | -const TEMPLATE_DEFAULT_TEXT_IDS = ["main-title", "sub-title", "from-source"]; 4 | 5 | doctype html 6 | html(lang=lang) 7 | head 8 | meta(charset="UTF-8") 9 | meta(content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0", name="viewport") 10 | meta(content="ie=edge", http-equiv="X-UA-Compatible") 11 | title #{title} 12 | each _STYLE_ITEM in TEMPLATE_DEFAULT_STYLES 13 | link(rel="stylesheet", href=_STYLE_ITEM) 14 | each _SCRIPT_ITEM in TEMPLATE_DEFAULT_SCRIPTS 15 | script(src = _SCRIPT_ITEM) 16 | script. 17 | void function (_) { 18 | _.TEMPLATE_ENGINE = "SR"; 19 | _.queryParams = JSON.parse("#{queryParams}".replace(/"/g, '"')); 20 | }(window ?? Object.create(globalThis ?? this)); 21 | body 22 | div(id="template") 23 | each _TEXT_ID in TEMPLATE_DEFAULT_TEXT_IDS 24 | div(class="animated fadeInDown", id=_TEXT_ID) 25 | div(class="text-value", contenteditable) 26 | div(id="logo") 27 | div(id="app", style="width: 100vw;height: calc(100vh - 120px);user-select: none;margin-top: 120px;") 28 | //__LMO_SERVER_AUDIO_RENDER_TAG 29 | script(src="./index.js", type="module") 30 | -------------------------------------------------------------------------------- /server/bin/Player.ts: -------------------------------------------------------------------------------- 1 | import {ReadStream} from "node:fs"; 2 | import fs from "fs"; 3 | import {resolve} from "path"; 4 | import {Request, Response} from "express"; 5 | import {UpLoadFilesModel} from "./dataBase"; 6 | import Utils from "../utils"; 7 | import createErrorMessage = Utils.createErrorMessage; 8 | 9 | export default class Player { 10 | constructor(req: Request, res: Response) { 11 | if (!req.headers.range) { 12 | return res.json({ 13 | code: 500, 14 | message: 'No range header' 15 | }); 16 | } 17 | 18 | UpLoadFilesModel.findOne({ 19 | where: { 20 | id: req.params.id 21 | } 22 | }).then(data => { 23 | if (!data) 24 | return res.json(createErrorMessage('ext004')); 25 | 26 | const {path, type} = data.dataValues; 27 | const filePath: string = resolve(__dirname, '../', '_data/static/public/uploads/' + path.split('/static/uploads')[1]); 28 | if (!fs.existsSync(filePath)) 29 | return void res.json(createErrorMessage('ext001')); 30 | const size: number = fs.statSync(filePath).size; 31 | const p: Array = (req.headers.range ?? '').replace(/bytes=/, '').split('-'); 32 | const start: number = parseInt(p[0], 10); 33 | const end: number = p[1] ? parseInt(p[1], 10) : size - 1; 34 | const stream: ReadStream = fs.createReadStream(filePath); 35 | 36 | res.writeHead(206, { 37 | 'Content-Type': type, 38 | 'Accept-Ranges': 'bytes', 39 | 'Content-Length': (end - start) + 1, 40 | 'Content-Range': `bytes ${start}-${end}/${size}` 41 | }); 42 | stream.pipe(res); 43 | }); 44 | } 45 | } -------------------------------------------------------------------------------- /server/bin/Process.ts: -------------------------------------------------------------------------------- 1 | import AppConfig from "../conf/AppConfig"; 2 | import HttpServer from "./HttpServer"; 3 | import os from "node:os"; 4 | import {close as closeDataBase} from "./dataBase"; 5 | 6 | export namespace Process { 7 | export const sendMessage = (message: string): boolean | undefined => process.send?.(message); 8 | 9 | export const exit = (code?: number): void => process.exit(code || 0); 10 | 11 | export const ready = (): void => void Process.sendMessage('ready'); 12 | 13 | export const onClose = (cb: Function): void => { 14 | if (os.platform() === 'win32') { 15 | process.on('message', async (msg: string): Promise => { 16 | if (msg === 'shutdown') 17 | cb(); 18 | }); 19 | } else { 20 | process.on('SIGINT', (): void => cb()); 21 | process.on('SIGTERM', (): void => cb()); 22 | } 23 | }; 24 | } 25 | 26 | ((): void => { 27 | process.title = AppConfig.__APP_NAME; 28 | new HttpServer(); 29 | })(); 30 | 31 | 32 | ((): void => { 33 | Process.onClose(async (): Promise => { 34 | try { 35 | await closeDataBase(); 36 | } catch (e) { 37 | throw e; 38 | } finally { 39 | Process.exit(); 40 | } 41 | }); 42 | })(); 43 | -------------------------------------------------------------------------------- /server/bin/TaskScheduler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @class TaskScheduler 3 | * @author ayuanlmo 4 | * @description Task scheduler 5 | * **/ 6 | import socketClient from "./Socket"; 7 | import {WebSocketServer} from "./WebSocketServer"; 8 | import MemoryCache from "../lib/MemoryCache"; 9 | 10 | export interface ITask { 11 | readonly id: string; 12 | readonly path: string; 13 | readonly duration: number; 14 | readonly width: number; 15 | readonly height: number; 16 | readonly fps: number; 17 | readonly optPath: number; 18 | readonly fileName: string; 19 | readonly folder: string; 20 | readonly name: string; 21 | readonly audioPath: string; 22 | readonly pAudio: boolean; 23 | readonly audioVolume: number; 24 | } 25 | 26 | class TaskScheduler { 27 | private readonly concurrencyLimit: number; 28 | private taskQueue: Array; 29 | private runningTask: number; 30 | private timer: any; 31 | 32 | constructor() { 33 | this.concurrencyLimit = 1; // 多进程负载均衡 默认允许1个 34 | this.taskQueue = []; 35 | this.runningTask = 0; 36 | this.init(); 37 | } 38 | 39 | public push(task: ITask): void { 40 | this.taskQueue.push(task); 41 | this.init(); 42 | this.initProcessQueue(); 43 | } 44 | 45 | public removeTask(): void { 46 | this.runningTask--; 47 | this.initProcessQueue(); 48 | } 49 | 50 | private init(): void { 51 | clearInterval(this.timer); 52 | this.timer = setInterval((): void => { 53 | if (this.runningTask === 0) 54 | clearInterval(this.timer); 55 | else 56 | this.initProcessQueue(); 57 | }, 3000); 58 | } 59 | 60 | private initProcessQueue(): void { 61 | while (this.taskQueue.length > 0 && this.runningTask < this.concurrencyLimit) { 62 | const task: ITask | undefined = this.taskQueue.shift(); 63 | 64 | if (task) 65 | this.runTask(task); 66 | } 67 | } 68 | 69 | private runTask(task: ITask): void { 70 | const serviceOnLineStatus: boolean = MemoryCache.get('SYNTHESIS_SERVICES_CONNECT_STATUS') ?? false; 71 | 72 | if (serviceOnLineStatus) { 73 | this.runningTask++; 74 | 75 | socketClient.sendMessage({ 76 | type: "COMPOSITE-VIDEO", 77 | data: JSON.stringify({ 78 | ...task 79 | }) 80 | }); 81 | WebSocketServer.sendMessage(JSON.stringify({ 82 | type: 'TASK_READY', 83 | message: { 84 | id: task.id, 85 | name: task.name 86 | } 87 | })); 88 | } else { 89 | setTimeout((): void => { 90 | this.runTask(task); 91 | }, 5000); 92 | } 93 | } 94 | } 95 | 96 | export default new TaskScheduler(); 97 | -------------------------------------------------------------------------------- /server/bin/TemplateSocket.ts: -------------------------------------------------------------------------------- 1 | import {IWsApp} from "./WebSocketServer"; 2 | import Cli from "../lib/Cli"; 3 | import {ResourcesModel, TemplateModel} from "./dataBase"; 4 | import AppConfig from "../conf/AppConfig"; 5 | 6 | class TemplateSocket { 7 | private readonly WsApp: IWsApp; 8 | private isOpen: boolean; 9 | 10 | constructor(Ws: IWsApp) { 11 | this.WsApp = Ws; 12 | this.isOpen = false; 13 | this.WsApp.on("message", async (msg: string): Promise => { 14 | if (msg === AppConfig.__SOCKET_PONG_KEY) 15 | this.WsApp.send(AppConfig.__SOCKET_PONG_MESSAGE); 16 | 17 | if (this.isOpen) return; 18 | else { 19 | try { 20 | const {id, template} = JSON.parse(msg); 21 | 22 | if (!template) { 23 | this.isOpen = false; 24 | this.WsApp.close(); 25 | } else { 26 | const task = await ResourcesModel.findOne({ 27 | where: { 28 | id 29 | } 30 | }); 31 | 32 | const _template = await TemplateModel.findOne({ 33 | where: { 34 | id: template 35 | } 36 | }); 37 | 38 | if (_template && task) { 39 | this.isOpen = true; 40 | this.WsApp.send('open'); 41 | } else 42 | this.WsApp.close(); 43 | } 44 | } catch (e) { 45 | Cli.warn(e); 46 | this.isOpen = false; 47 | this.WsApp.close(); 48 | } 49 | } 50 | }); 51 | setTimeout((): void => { 52 | if (!this.isOpen) 53 | this.WsApp.close(); 54 | }, 3000); 55 | } 56 | 57 | public sendMessage(message: string): void { 58 | this.WsApp.send(message); 59 | } 60 | } 61 | 62 | export default TemplateSocket; 63 | -------------------------------------------------------------------------------- /server/bin/WebSocketServer.ts: -------------------------------------------------------------------------------- 1 | import AppConfig from "../conf/AppConfig"; 2 | import OS from "node:os"; 3 | 4 | const platform: string = OS.platform(); 5 | const cpuName: string = OS.cpus()[0].model; 6 | const arch: string = OS.arch(); 7 | 8 | export interface IWsApp { 9 | send: (msg: string) => void; 10 | close: () => void; 11 | on: (key: string, cb: (msg: string) => void) => void; 12 | } 13 | 14 | export class WebSocketServer { 15 | private readonly WsApp: IWsApp; 16 | private readonly OnLineUsers: number; 17 | private readonly Pool: Array; 18 | 19 | constructor(Ws: IWsApp, OnLineUsers: number, Pool: Array) { 20 | this.WsApp = Ws; 21 | this.OnLineUsers = OnLineUsers; 22 | this.Pool = Pool; 23 | this.initConnect(); 24 | } 25 | 26 | public static sendMessage(message: string): void { 27 | const _: any = global; 28 | 29 | _.WebSocketPool?.getWss()?.clients?.forEach((ws: IWsApp): void => { 30 | ws.send?.(message); 31 | }); 32 | } 33 | 34 | private initConnect(): void { 35 | this.WsApp.send?.(JSON.stringify({ 36 | type: 'connect', 37 | success: true, 38 | onLineUser: this.OnLineUsers, 39 | serverInfo: { 40 | __isX86Windows: platform === 'win32' && arch === 'x64', 41 | __isArmWindows: platform === 'win32' && arch === 'arm64', 42 | __isLinux: platform === 'linux', 43 | __isIntelMac: platform === 'darwin' && arch === 'x64', 44 | __isAppleSiliconMac: platform === 'darwin' && arch === 'arm64' && cpuName.includes('Apple'), 45 | __enCoding: 'CPU.H264', 46 | __cpuEnCode: { 47 | __AMD: cpuName.includes('AMD'), 48 | __Intel: cpuName.includes('Intel'), 49 | __AppleSilicon: cpuName.includes('Apple') 50 | } 51 | } 52 | })); 53 | const _: any = global; 54 | 55 | this.WsApp.on('message', (msg: string): void => { 56 | if (msg === AppConfig.__SOCKET_PONG_KEY) 57 | return this.WsApp.send(AppConfig.__SOCKET_PONG_MESSAGE); 58 | 59 | try { 60 | const data = JSON.parse(msg); 61 | 62 | if ('cmd' in data) { 63 | if (data.cmd === '__ABORT_RENDER') { 64 | _.TemplateWsPool?.clients?.forEach((ws: IWsApp): void => { 65 | ws.send?.(JSON.stringify({ 66 | id: data.id, 67 | _signal: "__ABORT_RENDER" 68 | })); 69 | }); 70 | } 71 | } else 72 | this.WsApp.send(msg); 73 | } catch (e) { 74 | this.WsApp.send(msg); 75 | } 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /server/bin/controllers/Catch.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from "express"; 2 | import MemoryCache from "../../lib/MemoryCache"; 3 | 4 | export default class Catch { 5 | public static clearCatch(_req: Request, res: Response): void { 6 | MemoryCache.clear(); 7 | res.status(204).send(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/bin/controllers/Color.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from "express"; 2 | import {ColorModel} from "../dataBase"; 3 | import Utils from "../../utils"; 4 | import {Op} from "sequelize"; 5 | import createSuccessMessage = Utils.createSuccessMessage; 6 | import MemoryCache from "../../lib/MemoryCache"; 7 | 8 | export type TColorType = 'theme' | 'background'; 9 | 10 | export default class Color { 11 | public static getColors(req: Request, res: Response): void { 12 | const { 13 | type = 'theme' as TColorType 14 | } = req.query ?? {}; 15 | const query: string = type === 'theme' ? '__theme_color' : '__background_color'; 16 | const data = MemoryCache.get(query); 17 | 18 | if (data) 19 | return void res.json(createSuccessMessage(data)); 20 | 21 | ColorModel.findAndCountAll({ 22 | where: { 23 | type: {[Op.like]: `%${type}%`} 24 | } 25 | }).then(({rows, count}): void => { 26 | MemoryCache.set(query, {rows, total: count}); 27 | res.json(createSuccessMessage({ 28 | rows, 29 | total: count 30 | })); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/bin/controllers/Resources.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from "express"; 2 | import {ResourcesModel} from "../dataBase"; 3 | import {Op} from "sequelize"; 4 | import Utils from "../../utils"; 5 | import path from "path"; 6 | import fs from "node:fs"; 7 | import createSuccessMessage = Utils.createSuccessMessage; 8 | import createErrorMessage = Utils.createErrorMessage; 9 | 10 | export default class Resources { 11 | public static getResources(req: Request, res: Response): void { 12 | const { 13 | name = '', 14 | pageIndex = 0, 15 | pageSize = 10, 16 | } = req.query; 17 | 18 | ResourcesModel.findAndCountAll({ 19 | where: { 20 | name: {[Op.like]: `%${name}%`}, 21 | }, 22 | attributes: { 23 | exclude: ['templatePath', 'url', 'template', 'taskConfig'] 24 | }, 25 | offset: (Number(pageIndex) - 1) * Number(pageIndex), 26 | limit: Number(pageSize) 27 | }).then(({rows, count}): void => { 28 | res.json(createSuccessMessage({ 29 | rows, 30 | total: count 31 | })); 32 | }); 33 | } 34 | 35 | public static deleteResources(req: Request, res: Response): void { 36 | const {id = ''} = req.params; 37 | 38 | if (id === '') 39 | res.json(createErrorMessage('ext003')); 40 | 41 | ResourcesModel.findOne({ 42 | where: { 43 | id 44 | } 45 | }).then((resource): void => { 46 | if (!resource) 47 | return void res.json(createErrorMessage('ext005')); 48 | 49 | const videoPath: string = path.resolve(__dirname, `../../_data/static/public/${resource?.dataValues.filePath.replace('/static', '')}`); 50 | const gifPath: string = path.resolve(__dirname, `../../_data/static/public/${resource?.dataValues.gifPath.replace('/static', '')}`); 51 | const videoCover: string = path.resolve(__dirname, `../../_data/static/public/${resource?.dataValues.videoCover.replace('/static', '')}`); 52 | const templatePath: string = resource?.dataValues.templatePath; 53 | 54 | if (fs.existsSync(videoPath)) 55 | fs.unlinkSync(videoPath); 56 | if (fs.existsSync(gifPath)) 57 | fs.unlinkSync(gifPath); 58 | if (fs.existsSync(videoCover)) 59 | fs.unlinkSync(videoCover); 60 | if (fs.existsSync(templatePath)) { 61 | fs.readdirSync(templatePath).forEach((file: string): void => { 62 | fs.unlinkSync(path.resolve(templatePath, file)); 63 | }); 64 | fs.rmdirSync(templatePath); 65 | } 66 | 67 | ResourcesModel.destroy({ 68 | where: { 69 | id 70 | } 71 | }).then((): void => { 72 | res.status(204).send(); 73 | }); 74 | }); 75 | } 76 | } -------------------------------------------------------------------------------- /server/bin/controllers/ServerBase.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from "express"; 2 | import Utils from "../../utils"; 3 | import MemoryCache from "../../lib/MemoryCache"; 4 | import createSuccessMessage = Utils.createSuccessMessage; 5 | 6 | export default class ServerBase { 7 | public static GetServerBaseInfo(req: Request, res: Response): void { 8 | const memoryUsage = process.memoryUsage(); 9 | const uptime = process.uptime(); 10 | const [days, hours, minutes, seconds] = [ 11 | Math.floor(uptime / (24 * 60 * 60)), 12 | Math.floor(uptime / (60 * 60) % 24), 13 | Math.floor(uptime / 60 % 60), 14 | Math.floor(uptime % 60) 15 | ]; 16 | const {node, v8} = process.versions; 17 | 18 | res.json(createSuccessMessage({ 19 | main: { 20 | memory: { 21 | rss: Number((memoryUsage.rss / 1024 / 1024).toFixed(2)), 22 | heapTotal: Number((memoryUsage.heapTotal / 1024 / 1024).toFixed(2)), 23 | heapUsed: Number((memoryUsage.heapUsed / 1024 / 1024).toFixed(2)), 24 | external: Number((memoryUsage.external / 1024 / 1024).toFixed(2)), 25 | arrayBuffers: Number((memoryUsage.arrayBuffers / 1024 / 1024).toFixed(2)) 26 | }, 27 | upTime: { 28 | days, hours, minutes, seconds 29 | }, 30 | synthesisServicesConnectStatus: MemoryCache.get('SYNTHESIS_SERVICES_CONNECT_STATUS') 31 | }, 32 | rtVersion: { 33 | node, 34 | v8 35 | } 36 | })); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /server/bin/dataBase/defaultData.ts: -------------------------------------------------------------------------------- 1 | const DB_COLOR_TABLE_DEFAULT_DATA = [ 2 | { 3 | value: ["#28C8D5", "#1CA8E3", "#1CA8E3", "#5169CA", "#5837A6", "#BF39A7", "#E54574", "#F47F22"], 4 | type: 'theme' 5 | }, 6 | { 7 | value: ["#2659D6", "#7041B6", "#C44DB5", "#E54CA7", "#F56524", "#F9AF2C", "#F9DA44", "#8ACB31"], 8 | type: 'theme' 9 | }, 10 | { 11 | value: ["#F5D84B", "#66BA49", "#00A041", "#00CBC9", "#0086CE", "#8A37C6", "#FFB115", "#FCDD58"], 12 | type: 'theme' 13 | }, 14 | { 15 | value: ["#463CB4", "#0070B5", "#07A582", "#68D759", "#F0CD00", "#FF7B18", "#FF3D26", "#C70232"], 16 | type: 'theme' 17 | }, 18 | { 19 | value: ["#4DBFCD", "#2484AF", "#4790EC", "#F090B8", "#80EBEE", "#4ABDD4", "#6F71E6", "#EFAEB4"], 20 | type: 'theme' 21 | }, 22 | { 23 | value: ["#FCBA23", "#FF8125", "#03BAB3", "#68D759", "#0171DF", "#EF4693", "#9952D2", "#FE395C"], 24 | type: 'theme' 25 | }, 26 | { 27 | value: ["#46E6FE", "#FEF006", "#FD0000", "#FF9001", "#FF3072", "#CB6FFF", "#2784F0", "#4ADEC3"], 28 | type: 'theme' 29 | }, 30 | { 31 | value: ["#ED9909", "#FEDE00", "#FFEA9A", "#79B710", "#40A470", "#98C9AD", "#009BA4", "#C30063"], 32 | type: 'theme' 33 | }, 34 | { 35 | value: ["#FEF6C7", "#C30063", "#744183", "#E2BFD5", "#FF3072", "#DA535A", "#D16C91", "#D95308"], 36 | type: 'theme' 37 | }, 38 | { 39 | value: ["#FF9897", "#F650A0"], 40 | type: 'background' 41 | }, 42 | { 43 | value: ["#FF5894", "#8441A4"], 44 | type: 'background' 45 | }, 46 | { 47 | value: ["#F869D5", "#5650DE"], 48 | type: 'background' 49 | }, 50 | { 51 | value: ["#F00B51", "#73005C"], 52 | type: 'background' 53 | }, 54 | { 55 | value: ["#B65EAB", "#2E8DE1"], 56 | type: 'background' 57 | }, 58 | { 59 | value: ["#64E8DE", "#8A64E8"], 60 | type: 'background' 61 | }, 62 | { 63 | value: ["#78F2E9", "#B65EBA"], 64 | type: 'background' 65 | }, 66 | { 67 | value: ["#FF9482", "#7D77FF"], 68 | type: 'background' 69 | }, 70 | { 71 | value: ["#FFCF1B", "#FF8818"], 72 | type: 'background' 73 | }, 74 | { 75 | value: ["#FFA62E", "#EA4D2C"], 76 | type: 'background' 77 | }, 78 | { 79 | value: ["#00FFED", "#0088BA"], 80 | type: 'background' 81 | }, 82 | { 83 | value: ["#6EE2F5", "#6454F0"], 84 | type: 'background' 85 | }, 86 | { 87 | value: ["#3499FF", "#3A3985"], 88 | type: 'background' 89 | } 90 | ]; 91 | 92 | export {DB_COLOR_TABLE_DEFAULT_DATA}; 93 | -------------------------------------------------------------------------------- /server/conf/AppConfig.ts: -------------------------------------------------------------------------------- 1 | export interface ParsedArgs { 2 | [key: string]: string | any; 3 | } 4 | 5 | export type TAppModeType = 'development' | 'live-server' | 'prod'; 6 | 7 | export interface IArgsParams { 8 | p: string; 9 | mode: TAppModeType; 10 | dbType: 'sqlite' | 'mssql'; 11 | dbHost: string; 12 | dbPort: number; 13 | dbName: string; 14 | dbUserName: string; 15 | dbPassWord: string; 16 | } 17 | 18 | function getArgvParams(): IArgsParams { 19 | const args: string[] = process.argv.slice(2); 20 | const parsedArgs: ParsedArgs = {}; 21 | let currentKey: string | null = null; 22 | 23 | for (let i: number = 0; i < args.length; i++) { 24 | if (args[i].startsWith('-')) { 25 | currentKey = args[i].slice(1); 26 | parsedArgs[currentKey] = true; 27 | } else if (currentKey) { 28 | parsedArgs[currentKey] = args[i]; 29 | currentKey = null; 30 | } 31 | } 32 | 33 | return parsedArgs as unknown as IArgsParams; 34 | } 35 | 36 | const argv: IArgsParams = getArgvParams(); 37 | const AppConfig = { 38 | __APP_NAME: 'lmo-Data-Visualization-Server-Application', 39 | __APP_AUTHOR: 'ayuanlmo', 40 | __SERVER_PORT: Number(argv.p) || 3000, 41 | __STATIC_PATH: '/static', 42 | __SOCKET_CONNECT: '/connect', 43 | __SOCKET_PONG_KEY: 'ping', 44 | __SOCKET_PONG_MESSAGE: 'pong', 45 | __DEV_SERVER: argv.mode === 'development', 46 | __LIVE_SERVER: argv.mode === 'live-server', 47 | __ARGV: argv as IArgsParams, 48 | __PROTECTED_STATIC_FILES: ['.ts', '.bin', 'config.json', 'package.json', 'tsconfig.json', '.pug'], 49 | __PROTECTED_ROUTERS: ['uploadFile', 'template/copy', 'createTask', 'uploadFileCategory', 'createCustomTemplate'] 50 | } as const; 51 | 52 | export default AppConfig; 53 | -------------------------------------------------------------------------------- /server/const/ErrorMessage.ts: -------------------------------------------------------------------------------- 1 | const ErrorMessage = { 2 | ext000: {message: 'No error .'}, 3 | ext00n: {message: 'Not found.'}, 4 | ext00e: {message: 'Server error.'}, 5 | ext00el: {message: 'The live server does not allow this operation.'}, 6 | ext00d: {message: 'Data base error.'}, 7 | ext00d1: {message: 'Failed to update.'}, 8 | ext001: {message: 'None or not a file.'}, 9 | ext002: {message: 'Unsupported file type for upload.'}, 10 | ext003: {message: 'id?'}, 11 | ext004: {message: 'The file corresponding to the id was not found.'}, 12 | ext005: {message: 'Not a valid id or id is empty.'}, 13 | ext006: {message: 'This template does not exist.'}, 14 | ext007: {message: 'This template is not editable.'}, 15 | ext008: {message: 'This template cannot be deleted.'}, 16 | ext009: {message: 'Unable to connect to the synthesis server.'}, 17 | ext0010: {message: 'The field cover is a base64 string'} 18 | }; 19 | 20 | export default ErrorMessage; 21 | -------------------------------------------------------------------------------- /server/const/UpLoadFileTypes.ts: -------------------------------------------------------------------------------- 1 | const UpLoadFileTypes = ['image/jpeg', 'image/png', 'image/svg+xml', 'audio/mp4', 'audio/mpeg', 'video/mp4', 'audio/x-m4a', 'audio/flac', 'audio/aac']; 2 | 3 | export default UpLoadFileTypes; 4 | -------------------------------------------------------------------------------- /server/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface GlobalThis extends global { 2 | WebSocketPool: any; 3 | } 4 | 5 | declare const global: GlobalThis; -------------------------------------------------------------------------------- /server/gulpfile.js: -------------------------------------------------------------------------------- 1 | ((__Gulp, __Uglify) => { 2 | const copy = require('gulp-copy'); 3 | __Gulp.task('cjs', () => { 4 | return __Gulp.src(['dist/**/*.js']) 5 | .pipe(__Uglify({mangle: true})) 6 | .pipe(__Gulp.dest('./dist')); 7 | }); 8 | return __Gulp.src('package.json') 9 | .pipe(copy('dist', {prefix: 1})); 10 | })(require('gulp'), require('gulp-uglify')); 11 | -------------------------------------------------------------------------------- /server/lib/CheckDirectory.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import * as fs from "fs"; 3 | import Cli from "./Cli"; 4 | 5 | ((): void => { 6 | function mkdir(dir: string): void { 7 | if (!fs.existsSync(dir)) 8 | fs.mkdirSync(dir); 9 | else { 10 | try { 11 | fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK); 12 | } catch (e) { 13 | try { 14 | fs.chmodSync(dir, 0o777); 15 | } catch (csErr) { 16 | Cli.warn(`Failed to add permissions to ${dir}: ${csErr}`); 17 | } 18 | } 19 | } 20 | } 21 | 22 | setInterval((): void => { 23 | const currentTime: number = new Date().getTime(); 24 | const origin: string = path.resolve('./_data/static/public/previewTemplate'); 25 | fs.readdir(path.resolve(origin), (err: NodeJS.ErrnoException | null, files: Array): void => { 26 | if (err) return; 27 | 28 | files.map((i: string): void => { 29 | const stats: fs.Stats = fs.statSync(path.resolve(origin, i)); 30 | const creationTime: number = stats.birthtime.getTime(); 31 | const differenceInMinutes: number = (currentTime - creationTime) / (1000 * 60); 32 | 33 | if (differenceInMinutes >= 2) { 34 | if (stats.isDirectory()) { 35 | fs.readdirSync(path.resolve(origin, i)).map((j: string): void => { 36 | fs.unlinkSync(path.resolve(origin, i, j)); 37 | }); 38 | fs.rmdirSync(path.resolve(origin, i)); 39 | } 40 | if (stats.isFile()) 41 | fs.unlinkSync(path.resolve(origin, i)); 42 | } 43 | }); 44 | }); 45 | }, 1000 * 60); 46 | 47 | mkdir(path.resolve('./_data/static')); 48 | mkdir(path.resolve('./_data/static/public/previewTemplate')); 49 | mkdir(path.resolve('./_data/static/public/templates')); 50 | mkdir(path.resolve('./_data/static/public/scripts')); 51 | mkdir(path.resolve('./_data/static/public/styles')); 52 | mkdir(path.resolve('./_data/static/public/output')); 53 | })(); 54 | -------------------------------------------------------------------------------- /server/lib/Cli.ts: -------------------------------------------------------------------------------- 1 | namespace Cli { 2 | const cli_color: any = require('cli-color'); 3 | export const debug = (...Args: any[]): void => console.log(cli_color.bgGreen('Debug:'), ...Args); 4 | export const log = (...Args: any[]): void => console.log(cli_color.bgGreen('Log:'), ...Args); 5 | export const warn = (...Args: any[]): void => console.warn(cli_color.bgYellow('Warn:'), ...Args); 6 | } 7 | 8 | export default Cli; 9 | -------------------------------------------------------------------------------- /server/lib/MemoryCache.ts: -------------------------------------------------------------------------------- 1 | import NC = require('node-cache'); 2 | 3 | class MemoryCache { 4 | private Cache: NC; 5 | 6 | constructor() { 7 | this.Cache = new NC(); 8 | } 9 | 10 | public set(key: string, value: T, ttl: number = 0): boolean { 11 | return this.Cache.set(key, value, ttl); 12 | } 13 | 14 | public get(key: string): T | undefined { 15 | return this.Cache.get(key); 16 | } 17 | 18 | public remove(key: string): number { 19 | return this.Cache.del(key); 20 | } 21 | 22 | public clear(): void { 23 | this.Cache.flushAll(); 24 | } 25 | } 26 | 27 | export default new MemoryCache(); 28 | -------------------------------------------------------------------------------- /server/lib/TSC.ts: -------------------------------------------------------------------------------- 1 | import Cli from "./Cli"; 2 | import {dirname as pathDirname, join as pathJoin, relative as pathRelative, resolve as pathResolve} from "node:path"; 3 | import { 4 | CompilerHost, 5 | createCompilerHost, 6 | createProgram, 7 | Diagnostic, 8 | EmitResult, 9 | formatDiagnosticsWithColorAndContext, 10 | getPreEmitDiagnostics, 11 | ParsedCommandLine, 12 | parseJsonConfigFileContent, 13 | Program, 14 | readConfigFile, 15 | SourceFile, 16 | sys as tsSys 17 | } from "typescript"; 18 | 19 | interface ITSConfig { 20 | config?: any; 21 | error?: Diagnostic; 22 | } 23 | 24 | namespace TSC { 25 | export function compileTemplate(): void { 26 | const cli_color: any = require('cli-color'); 27 | const originDir: string = pathResolve(__dirname, "../_data/static/public/"); 28 | const tsConfigFile: string = pathResolve(originDir, 'tsconfig.json'); 29 | const configJson: ITSConfig = readConfigFile(tsConfigFile, tsSys.readFile); 30 | const compilerOptions: ParsedCommandLine = parseJsonConfigFileContent(configJson.config, tsSys, pathDirname(tsConfigFile)); 31 | const compilerHost: CompilerHost = createCompilerHost(compilerOptions.options); 32 | const program: Program = createProgram(compilerOptions.fileNames, compilerOptions.options, compilerHost); 33 | const emitResult: EmitResult = program.emit(); 34 | const diagnostics: Diagnostic[] = getPreEmitDiagnostics(program).concat(emitResult.diagnostics); 35 | const outDir: string = compilerOptions.options.outDir ?? ''; 36 | 37 | Cli.log('Template compilation start .'); 38 | 39 | if (diagnostics.length > 0) 40 | throw new Error(`${formatDiagnosticsWithColorAndContext(diagnostics, compilerHost)}`); 41 | 42 | program.getSourceFiles().forEach((sourceFile: SourceFile): void => { 43 | if (!sourceFile.isDeclarationFile) { 44 | const relativePath: string = pathRelative(process.cwd(), sourceFile.fileName); 45 | const outputPath: string = pathJoin(outDir, relativePath); 46 | 47 | Cli.log(`Compiled ${cli_color.blue(`"${sourceFile.fileName}"`)} to ${`"${cli_color.yellow(outputPath)}`}"`); 48 | } 49 | }); 50 | 51 | Cli.log('Template compilation succeeded .'); 52 | } 53 | } 54 | 55 | TSC.compileTemplate(); 56 | 57 | export default TSC; 58 | -------------------------------------------------------------------------------- /server/main.ts: -------------------------------------------------------------------------------- 1 | import './bin/dataBase/index'; 2 | import "./lib/CheckDirectory"; 3 | import "./bin/Process"; 4 | import "./bin/Socket"; 5 | import "./bin/TaskScheduler"; 6 | import "./lib/TSC"; 7 | -------------------------------------------------------------------------------- /server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "bin", 4 | "conf", 5 | "lib", 6 | "utils", 7 | "router", 8 | "const", 9 | "main.ts" 10 | ], 11 | "ext": "ts,json", 12 | "ignore": [ 13 | "bin/dataBase/*.db", 14 | "node_modules", 15 | "dist", 16 | "test", 17 | "coverage", 18 | "docs" 19 | ], 20 | "exec": "ts-node ./main.ts -p 3000 -mode development" 21 | } 22 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lmo-data-visualization-server-application", 3 | "version": "1.0.0", 4 | "main": "main.ts", 5 | "author": "ayuanlmo", 6 | "license": "Apache-2.0", 7 | "repository": "https://github.com/ayuanlmo/lmo-data-visualization", 8 | "homepage": "", 9 | "engines": { 10 | "node": ">=14.0.0" 11 | }, 12 | "dependencies": { 13 | "cli-color": "^2.0.3", 14 | "cors": "^2.8.5", 15 | "express": "^4.18.2", 16 | "express-ws": "^5.0.2", 17 | "fs-extra": "^11.2.0", 18 | "multer": "^1.4.5-lts.1", 19 | "net": "^1.0.2", 20 | "node-cache": "^5.1.2", 21 | "pug": "^3.0.3", 22 | "sequelize": "^6.37.1", 23 | "sharp": "^0.33.5", 24 | "sqlite3": "^5.1.7", 25 | "tedious": "^18.6.1", 26 | "uuid": "^9.0.1" 27 | }, 28 | "scripts": { 29 | "start": "nodemon", 30 | "live-server": "tsc", 31 | "prod": "tsc && gulp cjs" 32 | }, 33 | "devDependencies": { 34 | "@types/express": "^4.17.21", 35 | "@types/express-ws": "^3.0.4", 36 | "@types/multer": "^1.4.11", 37 | "@types/node": "^20.11.21", 38 | "@types/uuid": "^9.0.8", 39 | "copy": "^0.3.2", 40 | "gulp": "^4.0.2", 41 | "gulp-copy": "^4.0.1", 42 | "gulp-uglify": "^3.0.2", 43 | "nodemon": "^3.1.0", 44 | "ts-node": "^10.9.2", 45 | "typescript": "^5.3.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/router/index.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response, Router} from "express"; 2 | import TemplateController from "../bin/controllers/Template"; 3 | import File from "../bin/controllers/File"; 4 | import Color from "../bin/controllers/Color"; 5 | import Catch from "../bin/controllers/Catch"; 6 | import Player from "../bin/Player"; 7 | import Task from "../bin/controllers/Task"; 8 | import Resources from "../bin/controllers/Resources"; 9 | import ServerBase from "../bin/controllers/ServerBase"; 10 | import Multer = require("multer"); 11 | 12 | export const multer: Multer.Multer = Multer({ 13 | dest: require('path').resolve('./_data/static/public/uploads'), 14 | }); 15 | const _Router: Router = Router(); 16 | 17 | _Router 18 | .get('/template', (req: Request, res: Response): void => TemplateController.getTemplates(req, res)) 19 | .get('/template/:id', (req: Request, res: Response): void => TemplateController.getTemplate(req, res)) 20 | .put('/template', (req: Request, res: Response): void => TemplateController.editTemplate(req, res)) 21 | .delete('/template/:id', (req: Request, res: Response): void => TemplateController.deleteTemplate(req, res)) 22 | .post('/template/copy', (req: Request, res: Response): void => TemplateController.copyTemplate(req, res)) 23 | .post('/template/refresh', (req: Request, res: Response): void => TemplateController.refreshTemplate(req, res)) 24 | .post('/createCustomTemplate', (req: Request, res: Response): void => Task.createTask(req, res)) 25 | .post('/uploadFile', multer.single('media'), (req: Request, res: Response): void => File.upload(req, res)) 26 | .put('/editFileInfo/:id', (req: Request, res: Response): void => File.edit(req, res)) 27 | .delete('/deleteFile', (req: Request, res: Response): void => File.delete(req, res)) 28 | .get('/uploadFile', (req: Request, res: Response): void => File.getFiles(req, res)) 29 | .get('/uploadFileCategory', (req: Request, res: Response): void => File.getFileCategory(req, res)) 30 | .post('/uploadFileCategory', (req: Request, res: Response): void => File.addFileCategory(req, res)) 31 | .delete('/uploadFileCategory/:id', (req: Request, res: Response): void => File.deleteFileCategory(req, res)) 32 | .get('/color', (req: Request, res: Response): void => Color.getColors(req, res)) 33 | .post('/clearCatch', (req: Request, res: Response): void => Catch.clearCatch(req, res)) 34 | .get('/player/:id', (req: Request, res: Response): void => void new Player(req, res)) 35 | .post('/createTask', (req: Request, res: Response): void => Task.createTask(req, res)) 36 | .get('/resources', (req: Request, res: Response): void => Resources.getResources(req, res)) 37 | .delete('/resources/:id', (req: Request, res: Response): void => Resources.deleteResources(req, res)) 38 | .get('/serverInfo', (req: Request, res: Response): void => ServerBase.GetServerBaseInfo(req, res)) 39 | ; 40 | 41 | export default _Router; 42 | -------------------------------------------------------------------------------- /server/sql/lmo-dv_TSQL.sql: -------------------------------------------------------------------------------- 1 | /** 2 | @author ayuanlmo 3 | @date 2024/10 4 | **/ 5 | 6 | DECLARE @DataBaseName CHAR(10); 7 | SET @DataBaseName = 'lmo-dv'; 8 | 9 | IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = @DataBaseName) 10 | BEGIN 11 | CREATE DATABASE [lmo-dv]; 12 | END; 13 | 14 | USE [lmo-dv]; 15 | GO 16 | 17 | -- Color表 18 | SET ANSI_NULLS ON; 19 | GO 20 | 21 | SET QUOTED_IDENTIFIER ON; 22 | GO 23 | 24 | IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Colors]')AND type in (N'U')) 25 | BEGIN 26 | CREATE TABLE[dbo].[Colors]( 27 | [id][nvarchar](255)NOT NULL, 28 | [value][nvarchar](255)NULL, 29 | [cssCode][nvarchar](255)NULL, 30 | [type][char](255)NULL, 31 | PRIMARY KEY CLUSTERED ([id]ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF)ON[PRIMARY] 32 | )ON[PRIMARY]; 33 | END; 34 | GO 35 | 36 | -- Resources表 37 | SET ANSI_NULLS ON; 38 | GO 39 | 40 | SET QUOTED_IDENTIFIER ON; 41 | GO 42 | 43 | IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Resources]')AND type in (N'U')) 44 | BEGIN 45 | CREATE TABLE[dbo].[Resources]( 46 | [id][nvarchar](36)NOT NULL, 47 | [name][nvarchar](255)NULL, 48 | [template][nvarchar](255)NULL, 49 | [filePath][nvarchar](255)NULL, 50 | [createTime][nvarchar](255)NULL, 51 | [templatePath][nvarchar](255)NULL, 52 | [url][nvarchar](255)NULL, 53 | [gifPath][nvarchar](255)NULL, 54 | [videoCover][nvarchar](255)NULL, 55 | [clarity][nvarchar](255)NULL, 56 | [status][nvarchar](255)NULL, 57 | [taskConfig][nvarchar](255)NULL, 58 | PRIMARY KEY CLUSTERED ([id]ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF)ON[PRIMARY] 59 | ) ON [PRIMARY]; 60 | END; 61 | GO 62 | 63 | -- Templates表 64 | SET ANSI_NULLS ON 65 | GO 66 | 67 | SET QUOTED_IDENTIFIER ON 68 | GO 69 | 70 | IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Templates]')AND type in (N'U')) 71 | BEGIN 72 | CREATE TABLE[dbo].[Templates]( 73 | [id][nvarchar](36)NOT NULL, 74 | [name][nvarchar](255)NULL, 75 | [description][nvarchar](255)NULL, 76 | [path][nvarchar](255)NULL, 77 | [cover][nvarchar](255)NULL, 78 | [gifCover][nvarchar](255)NULL, 79 | [createTime][nvarchar](255)NULL, 80 | [type][int]NULL, 81 | PRIMARY KEY CLUSTERED ([id]ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF)ON[PRIMARY] 82 | ) ON [PRIMARY]; 83 | END; 84 | GO 85 | 86 | -- UpLoadFiles表 87 | SET ANSI_NULLS ON; 88 | GO 89 | 90 | SET QUOTED_IDENTIFIER ON; 91 | GO 92 | IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[UpLoadFiles]')AND type in (N'U')) 93 | BEGIN 94 | CREATE TABLE[dbo].[UpLoadFiles]( 95 | [id][nvarchar](36)NOT NULL, 96 | [name][nvarchar](255)NULL, 97 | [path][nvarchar](255)NULL, 98 | [cover][nvarchar](255)NULL, 99 | [createTime][nvarchar](255)NULL, 100 | [type][nvarchar](255)NULL, 101 | [hash][nvarchar](255)NULL, 102 | [categoryId][nvarchar](255)NULL, 103 | PRIMARY KEY CLUSTERED ([id]ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF)ON[PRIMARY] 104 | ) ON [PRIMARY]; 105 | END; 106 | GO 107 | 108 | -- UpLoadFilesCategories表 109 | SET ANSI_NULLS ON; 110 | GO 111 | 112 | SET QUOTED_IDENTIFIER ON; 113 | GO 114 | 115 | IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[UpLoadFilesCategories]')AND type in (N'U')) 116 | BEGIN 117 | CREATE TABLE[dbo].[UpLoadFilesCategories]( 118 | [id][nvarchar](36)NOT NULL, 119 | [name][nvarchar](255)NULL, 120 | [parentId][nvarchar](255)NULL, 121 | PRIMARY KEY CLUSTERED ([id]ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF)ON[PRIMARY] 122 | ) ON [PRIMARY]; 123 | END; 124 | GO 125 | 126 | -- 键 127 | 128 | ALTER TABLE [dbo].[UpLoadFiles] WITH CHECK ADD FOREIGN KEY([categoryId]) 129 | REFERENCES [dbo].[UpLoadFilesCategories] ([id]) 130 | ON DELETE NO ACTION; 131 | GO 132 | 133 | ALTER TABLE [dbo].[UpLoadFilesCategories] WITH CHECK ADD FOREIGN KEY([parentId]) 134 | REFERENCES [dbo].[UpLoadFilesCategories] ([id]) 135 | ON DELETE NO ACTION; 136 | GO 137 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "tsBuildInfoFile": "./dist/lmo.t", 5 | "target": "es5", 6 | "lib": [ 7 | "dom", 8 | "dom.iterable", 9 | "esnext" 10 | ], 11 | "outDir": "dist", 12 | "allowJs": true, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "strict": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "module": "commonjs", 20 | "moduleResolution": "node", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "noEmit": false 24 | }, 25 | "exclude": ["gulpfile.js"] 26 | } 27 | -------------------------------------------------------------------------------- /server/utils/index.ts: -------------------------------------------------------------------------------- 1 | import errorMessage from "../const/ErrorMessage"; 2 | import {existsSync, readdirSync, rmdirSync, statSync, unlinkSync} from "node:fs"; 3 | import path from "path"; 4 | 5 | export interface IResponseMessage { 6 | code: number; 7 | message: string; 8 | errorCode?: string; 9 | success: boolean; 10 | data: any; 11 | _t: number; 12 | _app: string; 13 | } 14 | 15 | export type ErrorMessage = typeof errorMessage; 16 | 17 | export type ErrorCode = 500 | 404 | 401; 18 | 19 | namespace Utils { 20 | export const createSuccessMessage = (data = {}, msg: string = 'success', code = 200): IResponseMessage => { 21 | return { 22 | code: code, 23 | message: msg, 24 | success: true, 25 | data: data, 26 | _t: new Date().getTime(), 27 | _app: 'lmo_dv_sa_t' 28 | } 29 | } 30 | 31 | export const createErrorMessage = (errCode: T, code: ErrorCode = 500, data: object | Array = {}): IResponseMessage => { 32 | return { 33 | code: code, 34 | message: errorMessage[errCode].message, 35 | errorCode: errCode, 36 | data: data, 37 | success: false, 38 | _t: new Date().getTime(), 39 | _app: 'lmo_dv_sa_t' 40 | } 41 | } 42 | 43 | export const deleteFolderRecursive = (dir: string): boolean => { 44 | if (!existsSync(dir)) return false; 45 | const files = readdirSync(dir); 46 | 47 | for (let file of files) { 48 | const filePath = path.join(dir, file); 49 | const stats = statSync(filePath); 50 | 51 | if (stats.isDirectory()) 52 | deleteFolderRecursive(file); 53 | else 54 | unlinkSync(filePath); 55 | } 56 | 57 | rmdirSync(dir); 58 | return true; 59 | } 60 | } 61 | 62 | 63 | export default Utils 64 | -------------------------------------------------------------------------------- /service/bin/Core.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import WebVideoCreator, {VIDEO_ENCODER} from "web-video-creator"; 3 | // @ts-ignore 4 | import SingleVideo from 'web-video-creator/api/SingleVideo'; 5 | import SocketServer from "./SocketServer"; 6 | 7 | interface ICreateTaskConfig { 8 | id: string; 9 | path: string; 10 | duration: number; 11 | width?: number; 12 | height?: number; 13 | fps?: number; 14 | optPath: string; 15 | } 16 | 17 | class CreateTask { 18 | private config: ICreateTaskConfig | undefined; 19 | private readonly wvc: WebVideoCreator; 20 | 21 | constructor(conf: ICreateTaskConfig) { 22 | this.wvc = new WebVideoCreator(); 23 | this.wvc.config({ 24 | mp4Encoder: VIDEO_ENCODER.CPU.H264 25 | }); 26 | } 27 | 28 | public init(conf: ICreateTaskConfig): Promise { 29 | this.config = conf; 30 | const {config} = this; 31 | 32 | return new Promise((resolve, reject) => { 33 | const video: SingleVideo = this.wvc.createSingleVideo({ 34 | url: config.path, 35 | width: config.width, 36 | height: config.height, 37 | fps: config.fps, 38 | duration: config.duration, 39 | outputPath: this.config?.optPath, 40 | showProgress: false 41 | }); 42 | 43 | video.once('completed', (data: { 44 | takes: number; 45 | duration: number; 46 | outputPath: string; 47 | rtf: number; 48 | }): void => { 49 | resolve({ 50 | ...data 51 | }); 52 | }); 53 | video.once('progress', (data: { 54 | progress: number; 55 | }): void => { 56 | SocketServer.sendMessage(JSON.stringify({ 57 | type: 'progress', 58 | data: { 59 | id: this.config?.id, 60 | progress: data 61 | } 62 | })); 63 | }); 64 | video.start(); 65 | }); 66 | } 67 | } 68 | 69 | export default CreateTask; 70 | -------------------------------------------------------------------------------- /service/bin/SocketServer.ts: -------------------------------------------------------------------------------- 1 | import {createServer, Server, Socket} from 'node:net'; 2 | import AppConfig from "../config/AppConfig"; 3 | import Cli from "cli-color"; 4 | import wvc, {ICreateTaskConfig} from "./wvc"; 5 | import ffmpeg from "./ffmpeg"; 6 | 7 | class SocketServer { 8 | private readonly server: Server; 9 | private socket: Socket | undefined; 10 | 11 | constructor() { 12 | this.server = createServer((socket: Socket): void => { 13 | this.socket = socket; 14 | }); 15 | this.server.on('connection', (socket: Socket): void => { 16 | this.onConnection(socket); 17 | }); 18 | this.server.listen(AppConfig.__SOCKET_SERVER_PORT, '0.0.0.0', (): void => { 19 | console.log(Cli.bgBlue('Synthetic-Service-Socket-Server'), Cli.yellow('started on port'), Cli.red(AppConfig.__SOCKET_SERVER_PORT)); 20 | }); 21 | } 22 | 23 | public sendMessage(message: string): void { 24 | this.socket && this.socket.write(message); 25 | } 26 | 27 | private onConnection(socket: Socket): void { 28 | socket.on('data', (data: Buffer): void => { 29 | const _data: string = data.toString(); 30 | 31 | if (_data === AppConfig.__SOCKET_PING_KEY) 32 | return this.sendMessage(AppConfig.__SOCKET_PONG_KEY); 33 | 34 | try { 35 | const message: { 36 | type: string; 37 | data: string; 38 | } = JSON.parse(_data); 39 | 40 | if (message.type === 'GENERATING-AUDIO-VISUALIZATIONS') { 41 | const _ = JSON.parse(message.data); 42 | 43 | ffmpeg.getAudioVisualizationDiagram(_?.audioPath ?? '', _?.optPath ?? '').then((r: string): void => { 44 | this.sendMessage(JSON.stringify({ 45 | type: "GENERATING_AUDIO_VISUALIZATIONS", 46 | data: { 47 | path: r 48 | } 49 | })); 50 | }); 51 | } 52 | if (message.type === 'COMPOSITE-VIDEO') { 53 | wvc.init({ 54 | ...JSON.parse(message.data) as unknown as ICreateTaskConfig 55 | }, ({progress, id}): void => { 56 | this.sendMessage(JSON.stringify({ 57 | type: "TASK_PROGRESS_CHANGE", 58 | data: { 59 | id, progress 60 | } 61 | })); 62 | }).then((res): void => { 63 | this.sendMessage(JSON.stringify({ 64 | type: "TASK_END", 65 | data: { 66 | id: res.id, 67 | ...res 68 | } 69 | })); 70 | }); 71 | } 72 | 73 | this.sendMessage(AppConfig.__SOCKET_PONG_KEY); 74 | } catch (e) { 75 | this.sendMessage('ERROR'); 76 | } 77 | }); 78 | socket.on('error', () => { 79 | this.socket?.pause(); 80 | }); 81 | } 82 | } 83 | 84 | export default new SocketServer(); 85 | -------------------------------------------------------------------------------- /service/bin/ffmpeg.ts: -------------------------------------------------------------------------------- 1 | import {ChildProcessWithoutNullStreams, spawn} from "node:child_process"; 2 | import ffmpegPath from "ffmpeg-static"; 3 | 4 | namespace FFMPEG { 5 | export const getAudioVisualizationDiagram = (audioPath: string, optPath: string): Promise => { 6 | const args: string[] = [ 7 | '-i', 8 | audioPath, 9 | '-filter_complex', 10 | 'showwavespic=s=640x120:colors=#407DF2', 11 | '-frames:v', '1', 12 | optPath, 13 | '-y' 14 | ]; 15 | 16 | return new Promise((resolve, reject): void => { 17 | const ffmpegProcess: ChildProcessWithoutNullStreams = spawn(ffmpegPath ?? '', args); 18 | 19 | ffmpegProcess.on('exit', (code: number | null): void => { 20 | if (code !== 0) 21 | reject(`FFMPEG exited with code ${code}`); 22 | else 23 | resolve(optPath); 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | export default FFMPEG; 30 | -------------------------------------------------------------------------------- /service/bin/wvc/config.ts: -------------------------------------------------------------------------------- 1 | export const DECODES = { 2 | CPU: { 3 | H264: 'libx264', 4 | H265: 'libx265', 5 | VP8: 'libvpx', 6 | VP9: 'libvpx-vp9' 7 | }, 8 | INTEL: { 9 | H264: 'h264_qsv', 10 | H265: 'hevc_qsv', 11 | VP8: 'vp8_qsv', 12 | VP9: 'vp9_qsv' 13 | }, 14 | VAAPI: { 15 | H264: 'h264_vaapi', 16 | H265: 'hevc_vaapi', 17 | VP8: 'vp8_vaapi', 18 | VP9: 'vp9_vaapi' 19 | }, 20 | AMD: { 21 | H264: 'h264_amf', 22 | H265: 'h265_amf' 23 | }, 24 | NVIDIA: { 25 | H264: 'h264_nvenc', 26 | H265: 'hevc_nvenc' 27 | }, 28 | OMX: { 29 | H264: 'h264_videotoolbox', 30 | H265: 'hevc_videotoolbox' 31 | }, 32 | VIDEOTOOLBOX: { 33 | H264: 'h264_omx' 34 | }, 35 | V4L2: { 36 | H264: 'h264_v4l2m2m' 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /service/config/AppConfig.ts: -------------------------------------------------------------------------------- 1 | import process from 'process'; 2 | 3 | const AppConfig = { 4 | __APP_NAME: 'lmo-Data-Visualization-Synthesis-Service-Application', 5 | __HTTP_SERVER_PORT: 3002, 6 | __SOCKET_SERVER_PORT: 3002, 7 | __SOCKET_PONG_KEY: 'pong', 8 | __SOCKET_PING_KEY: 'ping', 9 | __OPEN_BROWSER: false 10 | } as const; 11 | 12 | export default AppConfig; 13 | 14 | ((): void => { 15 | process.title = AppConfig.__APP_NAME; 16 | })(); 17 | -------------------------------------------------------------------------------- /service/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'cli-color' { 2 | export function black(...args: any[]): string; 3 | 4 | export function red(...args: any[]): string; 5 | 6 | export function green(...args: any[]): string; 7 | 8 | export function yellow(...args: any[]): string; 9 | 10 | export function blue(...args: any[]): string; 11 | 12 | export function magenta(...args: any[]): string; 13 | 14 | export function cyan(...args: any[]): string; 15 | 16 | export function white(...args: any[]): string; 17 | 18 | export function bgBlack(...args: any[]): string; 19 | 20 | export function bgRed(...args: any[]): string; 21 | 22 | export function bgGreen(...args: any[]): string; 23 | 24 | export function bgYellow(...args: any[]): string; 25 | 26 | export function bgBlue(...args: any[]): string; 27 | 28 | export function bgMagenta(...args: any[]): string; 29 | 30 | export function bgCyan(...args: any[]): string; 31 | 32 | export function bgWhite(...args: any[]): string; 33 | } 34 | -------------------------------------------------------------------------------- /service/main.ts: -------------------------------------------------------------------------------- 1 | import "./bin/SocketServer"; 2 | -------------------------------------------------------------------------------- /service/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "bin", 4 | "main.ts" 5 | ], 6 | "ext": "ts,json", 7 | "exec": "tsc && node --experimental-modules --es-module-specifier-resolution=node dist/main.js" 8 | } 9 | -------------------------------------------------------------------------------- /service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lmo-dv-service", 3 | "version": "1.0.0", 4 | "main": "main.ts", 5 | "type": "module", 6 | "author": "ayuanlmo", 7 | "license": "Apache-2.0", 8 | "dependencies": { 9 | "cli-color": "^2.0.4", 10 | "ffmpeg-static": "^5.2.0", 11 | "fluent-ffmpeg": "^2.1.3", 12 | "typescript": "^5.4.5", 13 | "web-video-creator": "^0.0.34" 14 | }, 15 | "scripts": { 16 | "start": "nodemon", 17 | "live-server": "tsc", 18 | "c": "tsc" 19 | }, 20 | "devDependencies": { 21 | "@types/fluent-ffmpeg": "^2.1.24", 22 | "@types/node": "^20.12.7", 23 | "nodemon": "^3.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "tsBuildInfoFile": "./dist/lmo.t", 5 | "module": "esnext", 6 | "target": "esnext", 7 | "sourceMap": false, 8 | "outDir": "dist", 9 | "baseUrl": "/", 10 | "paths": { 11 | "*": [ 12 | "*.js", 13 | "node_modules/*" 14 | ] 15 | }, 16 | "allowJs": true, 17 | "skipLibCheck": true, 18 | "esModuleInterop": true, 19 | "allowSyntheticDefaultImports": true, 20 | "strict": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "resolveJsonModule": false, 24 | "isolatedModules": false, 25 | "noEmit": false, 26 | "moduleResolution": "node" 27 | }, 28 | "ts-node": { 29 | "esm": true 30 | }, 31 | "include": ["**/*.ts"], 32 | "exclude": [ 33 | "node_modules" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /web_app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "parserOptions": { 4 | "parser": "@typescript-eslint/parser", 5 | "ecmaVersion": 2020, 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "jsx": true 9 | } 10 | }, 11 | "env": { 12 | "browser": true 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint" 16 | ], 17 | "extends": [ 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "plugin:react/recommended" 21 | ], 22 | "rules": { 23 | "no-restricted-globals": "off", 24 | "semi": 2, 25 | "no-var": 2, 26 | "no-alert": 0, 27 | "no-const-assign": 2, 28 | "no-eval": 2, 29 | "no-duplicate-case": 2, 30 | "func-names": 0, 31 | "no-unused-vars": 0, 32 | "no-prototype-builtins": 0, 33 | "use-isnan": 2, 34 | "vars-on-top": 2, 35 | "valid-typeof": 2, 36 | "no-delete-var": 2, 37 | "prefer-const": 2, 38 | "no-extra-parens": 2, 39 | "no-extra-semi": 2, 40 | "prefer-spread": 2, 41 | "guard-for-in": 2, 42 | "no-constant-condition": 2, 43 | "no-else-return": 2, 44 | "no-dupe-keys": 2, 45 | "no-empty": 2, 46 | "linebreak-style": [ 47 | 0, 48 | "windows" 49 | ], 50 | "no-multi-spaces": 2, 51 | "arrow-spacing": 2, 52 | "no-mixed-spaces-and-tabs": [ 53 | 2, 54 | false 55 | ], 56 | "no-new-func": 2, 57 | "no-new-object": 2, 58 | "no-new-require": 2, 59 | "no-new-wrappers": 2, 60 | "no-plusplus": 2, 61 | "no-undef": 1, 62 | "no-undef-init": 2, 63 | "no-unreachable": 2, 64 | "comma-dangle": [ 65 | 2, 66 | "never" 67 | ], 68 | "@typescript-eslint/no-namespace": "off", 69 | "@typescript-eslint/no-explicit-any": "off", 70 | "@typescript-eslint/no-explicit-function": "off", 71 | "init-declarations": 2, 72 | "newline-after-var": 2, 73 | "no-unneeded-ternary": 2, 74 | "warn": "off", 75 | "@typescript-eslint/ban-types": "off", 76 | "@typescript-eslint/ban-ts-comment": [ 77 | "error", 78 | { 79 | "ts-expect-error": "allow-with-description", 80 | "minimumDescriptionLength": 1 81 | } 82 | ] 83 | // "@typescript-eslint/consistent-type-assertions": "error", 84 | // "@typescript-eslint/parser": [ 85 | // "error", 86 | // { 87 | // "types": {}, 88 | // "extendDefaults": false 89 | // } 90 | // ] 91 | }, 92 | "globals": { 93 | "WindowEventMap": "readonly", 94 | "HTMLMediaElementEventMap": "readonly" 95 | } 96 | }; -------------------------------------------------------------------------------- /web_app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /web_app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /web_app/config-overrides.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = function override(config, env) { 4 | config.module.rules.push({ 5 | test: /\.less$/, 6 | use: [ 7 | require.resolve('style-loader'), 8 | { 9 | loader: require.resolve('css-loader'), 10 | options: { 11 | importLoaders: 1, 12 | }, 13 | }, 14 | { 15 | loader: require.resolve('less-loader'), 16 | options: { 17 | lessOptions: { 18 | javascriptEnabled: true, 19 | }, 20 | }, 21 | }, 22 | ], 23 | include: path.resolve(__dirname, 'src'), // 只在 src 目录下寻找 LESS 文件 24 | }); 25 | 26 | return config; 27 | }; -------------------------------------------------------------------------------- /web_app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lmo-data-visualization_web_app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^7.1.0", 7 | "@handsontable/react": "^14.2.0", 8 | "@hi-ui/core": "^4.0.5", 9 | "@hi-ui/hiui": "^4.3.3", 10 | "@reduxjs/toolkit": "^2.2.1", 11 | "axios": "^1.6.7", 12 | "handsontable": "^14.2.0", 13 | "html2canvas": "^1.4.1", 14 | "i18next": "^23.11.5", 15 | "less-loader": "^12.2.0", 16 | "nprogress": "^0.2.0", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-i18next": "^14.1.2", 20 | "react-redux": "^9.1.0", 21 | "react-router-dom": "^6.22.1", 22 | "react-scripts": "5.0.1", 23 | "sass": "^1.71.0", 24 | "sass-loader": "^13.3.2", 25 | "web-vitals": "^2.1.4" 26 | }, 27 | "devDependencies": { 28 | "@testing-library/jest-dom": "^5.17.0", 29 | "@testing-library/react": "^13.4.0", 30 | "@testing-library/user-event": "^13.5.0", 31 | "@types/jest": "^27.5.2", 32 | "@types/node": "^16.18.82", 33 | "@types/react": "^18.2.56", 34 | "@types/react-dom": "^18.2.19", 35 | "@typescript-eslint/eslint-plugin": "^7.1.1", 36 | "@typescript-eslint/parser": "^7.1.1", 37 | "eslint": "^8.57.0", 38 | "eslint-config-airbnb": "^19.0.4", 39 | "eslint-config-prettier": "^8.3.0", 40 | "eslint-config-react-app": "^7.0.1", 41 | "eslint-loader": "^4.0.2", 42 | "eslint-plugin-import": "^2.25.4", 43 | "eslint-plugin-jsx-a11y": "^6.5.1", 44 | "eslint-plugin-prettier": "^4.0.0", 45 | "eslint-plugin-react": "^7.28.0", 46 | "eslint-plugin-react-hooks": "^4.3.0", 47 | "prettier": "^2.5.1", 48 | "typescript": "^4.9.5" 49 | }, 50 | "scripts": { 51 | "start": "craco start", 52 | "build": "craco build", 53 | "test": "react-scripts test", 54 | "eject": "react-scripts eject" 55 | }, 56 | "eslintConfig": { 57 | "extends": [ 58 | "react-app", 59 | "react-app/jest" 60 | ] 61 | }, 62 | "browserslist": { 63 | "production": [ 64 | ">0.2%", 65 | "not dead", 66 | "not op_mini all" 67 | ], 68 | "development": [ 69 | "last 1 chrome version", 70 | "last 1 firefox version", 71 | "last 1 safari version" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /web_app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayuanlmo/lmo-data-visualization/119a911ca9ae6e7af0724a6cc2b7939a2d653a26/web_app/public/favicon.ico -------------------------------------------------------------------------------- /web_app/public/favicon48.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayuanlmo/lmo-data-visualization/119a911ca9ae6e7af0724a6cc2b7939a2d653a26/web_app/public/favicon48.ico -------------------------------------------------------------------------------- /web_app/public/favicon96.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayuanlmo/lmo-data-visualization/119a911ca9ae6e7af0724a6cc2b7939a2d653a26/web_app/public/favicon96.ico -------------------------------------------------------------------------------- /web_app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | <%= __LMO_BUILD_CONFIG.__LMO_APP_NAME %> 15 | 16 | 17 | 18 |
19 | 20 | 37 | 38 | -------------------------------------------------------------------------------- /web_app/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /web_app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayuanlmo/lmo-data-visualization/119a911ca9ae6e7af0724a6cc2b7939a2d653a26/web_app/public/logo192.png -------------------------------------------------------------------------------- /web_app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "lmo-dv", 3 | "name": "lmo-Data-Visualization", 4 | "icons": [ 5 | { 6 | "src": "favicon48.ico", 7 | "sizes": "48x48", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "favicon96.ico", 12 | "sizes": "96x96", 13 | "type": "image/x-icon" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /web_app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web_app/src/bin/@types/TemplateTypes.ts: -------------------------------------------------------------------------------- 1 | export type TThemeTypes = Readonly<'Gradient' | 'Single' | 'Theme'>; 2 | export type TClarityTypes = Readonly<'1080P' | '2K' | '4K'>; 3 | export type TConfigOtherConfigItemValueType = boolean | number | string; 4 | export type TOtherConfigItemComponentType = "switch" | "input" | "input-number" | "color" | "select"; 5 | 6 | export type TConfigTextType = { 7 | color: string; 8 | value: string; 9 | display: boolean; 10 | fontSize: number; 11 | align: string; 12 | width: number; 13 | height: number; 14 | x: number; 15 | y: number; 16 | readonly key?: string; 17 | }; 18 | 19 | export type TConfigOtherConfigItemType = Readonly<{ 20 | label: string; 21 | key: string; 22 | type: TOtherConfigItemComponentType; 23 | value: TConfigOtherConfigItemValueType; 24 | }> 25 | & (Readonly<{ 26 | type: "select"; 27 | options: Array<{ 28 | label: string; 29 | value: string; 30 | }>; 31 | }> 32 | | 33 | Readonly<{ 34 | type: Exclude, "select">; 35 | options?: never; 36 | }>); 37 | 38 | export type TThemeConfig = Readonly<{ 39 | type: TThemeTypes; 40 | configs: Array; 41 | value: Array; 42 | }>; 43 | 44 | export type TOtherConfigGroup = Readonly; 47 | }>>; 48 | 49 | export type TOtherConfig = { 50 | readonly label?: string; 51 | readonly configs?: Array; 52 | readonly group?: TOtherConfigGroup; 53 | values: { 54 | [key: string]: TConfigOtherConfigItemValueType; 55 | }; 56 | } & ( 57 | Readonly<{ 58 | configs: Array; 59 | group?: never; 60 | }> 61 | | 62 | Readonly<{ 63 | group: TOtherConfigGroup; 64 | configs?: never; 65 | label?: never; 66 | }>); 67 | 68 | export interface IConfig { 69 | text: { 70 | mainTitle: TConfigTextType; 71 | subTitle: TConfigTextType; 72 | fromSource: TConfigTextType; 73 | [key: string]: TConfigTextType; 74 | }; 75 | theme: TThemeConfig; 76 | background: { 77 | type: string; 78 | color: string; 79 | image: string; 80 | arrangement: string; 81 | }; 82 | animation?: { 83 | chatAnimationIsControllable: boolean; 84 | }; 85 | video: { 86 | duration: number; 87 | fps: number; 88 | clarity: TClarityTypes; 89 | }; 90 | audio: { 91 | path: string; 92 | full: boolean; 93 | volume: number; 94 | }; 95 | } 96 | 97 | export interface ITemplateConfig { 98 | data?: Array; 99 | config: IConfig; 100 | otherConfig: TOtherConfig; 101 | readonly id?: string; 102 | readonly template?: string; 103 | } 104 | -------------------------------------------------------------------------------- /web_app/src/bin/Hooks.ts: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useEffect, useRef, useState} from "react"; 2 | import MyStorage from "../lib/Storage"; 3 | import Utils from "../utils"; 4 | import {TTemplateMessageType} from "../types/TemplateMessage"; 5 | import Socket from "../lib/Socket"; 6 | 7 | export namespace Hooks { 8 | const __UNDEF: undefined = void false; 9 | 10 | export const useThrottle = (cb: Function, delay: number = 100) => { 11 | const timer: React.MutableRefObject = useRef(__UNDEF); 12 | 13 | return useCallback((...args: any): void => { 14 | if (timer.current) return; 15 | 16 | timer.current = setTimeout((): void => { 17 | cb(...args); 18 | timer.current = __UNDEF; 19 | }, delay) as unknown as number; 20 | }, [cb, delay]); 21 | }; 22 | 23 | export const useEventListener = (type: T, listener: (...args: Array) => any): void => { 24 | useEffect(() => { 25 | const handler = (...args: Array): void => { 26 | listener(...args); 27 | }; 28 | 29 | addEventListener(type, handler); 30 | 31 | return (): void => removeEventListener(type, handler); 32 | }, []); 33 | }; 34 | 35 | export const useMount = (cb: () => void): void => { 36 | useEffect((): void => { 37 | cb(); 38 | }, [cb]); 39 | }; 40 | 41 | export const useUnmount = (cb: () => void): void => { 42 | useEffect((): void => { 43 | return cb(); 44 | }, [cb]); 45 | }; 46 | 47 | export const useTimeout = (timeout: number, cb: () => void): void => { 48 | useEffect(() => { 49 | const _ = setTimeout(cb, timeout); 50 | 51 | return (): void => clearTimeout(_); 52 | }, [timeout, cb]); 53 | }; 54 | 55 | export const useStorage = (k: string, v: any): [any, (value: any) => void] => { 56 | const [value, setValue] = useState(v); 57 | 58 | useEffect((): void => { 59 | MyStorage.set(k, Utils.toString(v)); 60 | }, [value]); 61 | 62 | return [value, setValue]; 63 | }; 64 | 65 | export const useTemplateMessageListener = (type: TTemplateMessageType, cb: (msg: T) => void): void => { 66 | useEffect(() => { 67 | const handler = (e: MessageEvent): void => { 68 | if (e.origin !== location.origin) return; 69 | if (typeof e.data !== 'object') return; 70 | 71 | const {data} = e; 72 | 73 | if ('type' in data && 'message' in data) 74 | if (data.type === type) 75 | cb(data.message); 76 | }; 77 | 78 | addEventListener('message', handler); 79 | 80 | return (): void => removeEventListener('message', handler); 81 | }, [cb]); 82 | }; 83 | 84 | export const useWebSocketMessageListener = (type: M, cb: (msg: T) => void): void => { 85 | useEffect((): () => void => { 86 | const handler = (e: MessageEvent): void => { 87 | if (e.data.includes('pong')) return; 88 | 89 | const data = JSON.parse(e.data); 90 | 91 | data.type === type && cb(data.message); 92 | }; 93 | 94 | Socket.addMessageEventListener(handler); 95 | 96 | return (): void => { 97 | Socket.removeMessageEventListener(handler); 98 | }; 99 | 100 | }, [cb]); 101 | }; 102 | } 103 | 104 | export const { 105 | useMount, 106 | useUnmount, 107 | useTimeout, 108 | useThrottle, 109 | useEventListener, 110 | useStorage, 111 | useTemplateMessageListener 112 | } = Hooks; 113 | 114 | export default Hooks; 115 | -------------------------------------------------------------------------------- /web_app/src/components/About/OpenSourceComponentLicense/content.ts: -------------------------------------------------------------------------------- 1 | export default `-------------------- 2 | FFMPEG 3 | -------------------- 4 | https://www.ffmpeg.org/ 5 | 6 | Copyright (c) 2000-2022 the ffmpeg developers 7 | 8 | Licensed under LGPL license 9 | 10 | 11 | -------------------- 12 | typescript 13 | -------------------- 14 | https://github.com/Microsoft/TypeScript 15 | 16 | Copyright (c) 2012-2024 Microsoft Corporation 17 | 18 | Licensed under Apache License 2.0 license 19 | 20 | 21 | -------------------- 22 | react 23 | -------------------- 24 | https://github.com/facebook/react 25 | 26 | Copyright (c) Meta Platforms, Inc. and affiliates. 27 | 28 | Licensed under MIT license 29 | 30 | 31 | -------------------- 32 | react-redux 33 | -------------------- 34 | https://github.com/reduxjs/react-redux 35 | 36 | Copyright (c) 2015-present Dan Abramov 37 | 38 | Licensed under MIT license 39 | 40 | 41 | -------------------- 42 | react-i18next 43 | -------------------- 44 | https://github.com/i18next/react-i18next 45 | 46 | Licensed under MIT license 47 | 48 | 49 | -------------------- 50 | react-router-dom 51 | -------------------- 52 | https://github.com/i18next/react-i18next 53 | 54 | Copyright (c) Remix Software, Inc. 55 | 56 | Licensed under MIT license 57 | 58 | 59 | -------------------- 60 | axios 61 | -------------------- 62 | https://github.com/axios/axios 63 | 64 | Copyright © 2014-present Matt Zabriskie and contributors 65 | 66 | Licensed under MIT license 67 | 68 | 69 | -------------------- 70 | nprogress 71 | -------------------- 72 | https://github.com/rstacruz/nprogress 73 | 74 | NProgress © 2013-2017, Rico Sta. 75 | 76 | Licensed under MIT license 77 | 78 | -------------------- 79 | handsontable 80 | -------------------- 81 | https://github.com/rstacruz/nprogress 82 | 83 | Copyright (c) HANDSONCODE sp. z o. o. 84 | 85 | Licensed under HANDSONCODE license 86 | 87 | 88 | -------------------- 89 | cli-color 90 | -------------------- 91 | https://github.com/medikoo/cli-color 92 | 93 | Mariusz Nowak (http://www.medikoo.com/) 94 | 95 | Licensed under ISC license 96 | 97 | 98 | -------------------- 99 | express 100 | -------------------- 101 | https://github.com/expressjs/express 102 | 103 | Copyright © 2017 StrongLoop, IBM, and other expressjs.com contributors. 104 | 105 | Licensed under MIT license 106 | 107 | 108 | -------------------- 109 | express-ws 110 | -------------------- 111 | https://github.com/HenningM/express-ws 112 | 113 | Copyright (c) 2016, Henning Morud 114 | 115 | Licensed under BSD-2-Clause license 116 | 117 | 118 | -------------------- 119 | node-catch 120 | -------------------- 121 | https://github.com/node-cache/node-cache 122 | 123 | Copyright (c) 2019 mpneuried 124 | 125 | Licensed under MIT license 126 | 127 | 128 | -------------------- 129 | Sequelize 130 | -------------------- 131 | https://github.com/node-cache/node-cache 132 | 133 | Copyright (c) 2014-present Sequelize Authors 134 | 135 | Licensed under MIT license 136 | 137 | 138 | -------------------- 139 | node-sqlite3 140 | -------------------- 141 | https://github.com/TryGhost/node-sqlite3 142 | 143 | Copyright (c) MapBox 144 | 145 | Licensed under BSD-3-Clause license 146 | 147 | 148 | -------------------- 149 | uuid 150 | -------------------- 151 | https://github.com/TryGhost/node-sqlite3 152 | 153 | Copyright (c) 2010-2020 Robert Kieffer and other contributors 154 | 155 | Licensed under MIT license 156 | 157 | 158 | -------------------- 159 | ffmpeg-static 160 | -------------------- 161 | https://github.com/eugeneware/ffmpeg-static 162 | 163 | Copyright (C) 2007 Free Software Foundation, Inc. 164 | 165 | Licensed under GPL-3.0 license 166 | 167 | 168 | -------------------- 169 | fluent-ffmpeg 170 | -------------------- 171 | https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/ 172 | 173 | Copyright (c) 2011-2015 The fluent-ffmpeg contributors 174 | 175 | Licensed under MIT license 176 | 177 | -------------------- 178 | WebVideoCreator 179 | -------------------- 180 | https://github.com/Vinlic/WebVideoCreator 181 | 182 | Copyright (c) Vinlic 183 | 184 | Licensed under Apache-2.0 license` as string; -------------------------------------------------------------------------------- /web_app/src/components/About/OpenSourceComponentLicense/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useImperativeHandle, useState} from "react"; 2 | import {useTranslation} from "react-i18next"; 3 | import {Modal, TextArea} from "@hi-ui/hiui"; 4 | import {ReactState} from "../../../types/ReactTypes"; 5 | import Content from "./content"; 6 | 7 | export interface IOpenSourceComponentLicenseRef { 8 | open: () => void; 9 | } 10 | 11 | const OpenSourceComponentLicense: React.ForwardRefExoticComponent & React.RefAttributes> = React.forwardRef((_props, ref: React.ForwardedRef) => { 12 | const [visible, setVisible]: ReactState = useState(false); 13 | const {t} = useTranslation(); 14 | 15 | const open = (): void => { 16 | setVisible(!visible); 17 | }; 18 | 19 | useImperativeHandle(ref, (): IOpenSourceComponentLicenseRef => ({ 20 | open 21 | })); 22 | 23 | return ( 24 | { 31 | open(); 32 | }} 33 | onCancel={(): void => { 34 | open(); 35 | }} 36 | > 37 |
38 |