first commit
This commit is contained in:
commit
34249f5a34
19
.editorconfig
Normal file
19
.editorconfig
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{diff,md}]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{php,xml,json}]
|
||||||
|
indent_size = 4
|
||||||
20
.gitattributes
vendored
Normal file
20
.gitattributes
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
**/.gitattributes export-ignore
|
||||||
|
**/.gitignore export-ignore
|
||||||
|
**/.gitmodules export-ignore
|
||||||
|
**/.github export-ignore
|
||||||
|
**/.travis export-ignore
|
||||||
|
**/.travis.yml export-ignore
|
||||||
|
**/.editorconfig export-ignore
|
||||||
|
**/.styleci.yml export-ignore
|
||||||
|
|
||||||
|
**/phpunit.xml export-ignore
|
||||||
|
**/tests export-ignore
|
||||||
|
|
||||||
|
**/js/dist/**/* -diff
|
||||||
|
**/js/dist/**/* linguist-generated
|
||||||
|
**/js/dist-typings/**/* -diff
|
||||||
|
**/js/dist-typings/**/* linguist-generated
|
||||||
|
**/js/yarn.lock -diff
|
||||||
|
**/js/package-lock.json -diff
|
||||||
|
|
||||||
|
* text=auto eol=lf
|
||||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/vendor
|
||||||
|
composer.lock
|
||||||
|
composer.phar
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
tests/.phpunit.result.cache
|
||||||
|
/tests/integration/tmp
|
||||||
|
.vagrant
|
||||||
|
.idea/*
|
||||||
|
.vscode
|
||||||
|
js/coverage-ts
|
||||||
14
.styleci.yml
Normal file
14
.styleci.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
preset: recommended
|
||||||
|
|
||||||
|
enabled:
|
||||||
|
- logical_not_operators_with_successor_space
|
||||||
|
|
||||||
|
disabled:
|
||||||
|
- align_double_arrow
|
||||||
|
- blank_line_after_opening_tag
|
||||||
|
- multiline_array_trailing_comma
|
||||||
|
- new_with_braces
|
||||||
|
- phpdoc_align
|
||||||
|
- phpdoc_order
|
||||||
|
- phpdoc_separation
|
||||||
|
- phpdoc_types
|
||||||
18
LICENSE.md
Normal file
18
LICENSE.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) <year> <copyright holders>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Coyote Pulse Viewer
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[Flarum](https://flarum.org) 扩展:郊狼波形渲染器 —— 允许用户分享郊狼波形并在论坛内可视化查看
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install with composer:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
composer config repositories.klxf/flarum-coyote-pulse-viewer vcs https://gitea.miri.site/DG-BBS/flarum-coyote-pulse-viewer.git
|
||||||
|
composer require klxf/flarum-coyote-pulse-viewer:dev-master
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
```sh
|
||||||
|
composer update klxf/flarum-coyote-pulse-viewer:dev-master
|
||||||
|
php flarum cache:clear
|
||||||
|
```
|
||||||
36
composer.json
Normal file
36
composer.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "klxf/flarum-coyote-pulse-viewer",
|
||||||
|
"description": "Coyote pulse viewer",
|
||||||
|
"keywords": [
|
||||||
|
"flarum"
|
||||||
|
],
|
||||||
|
"type": "flarum-extension",
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"flarum/core": "^1.8.0",
|
||||||
|
"ext-dom": "*"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fang_Zhijian",
|
||||||
|
"email": "klxf@vip.qq.com",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Klxf\\CoyotePulseViewer\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"flarum-extension": {
|
||||||
|
"title": "Coyote Pulse Viewer",
|
||||||
|
"category": "",
|
||||||
|
"icon": {
|
||||||
|
"name": "",
|
||||||
|
"color": "",
|
||||||
|
"backgroundColor": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
extend.php
Normal file
23
extend.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of klxf/flarum-coyote-pulse-viewer.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fang_Zhijian.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE.md
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Klxf\CoyotePulseViewer;
|
||||||
|
|
||||||
|
use Flarum\Extend;
|
||||||
|
|
||||||
|
return [
|
||||||
|
(new Extend\Frontend('forum'))
|
||||||
|
->js(__DIR__.'/js/dist/forum.js'),
|
||||||
|
new Extend\Locales(__DIR__.'/locale'),
|
||||||
|
|
||||||
|
(new Extend\Formatter())
|
||||||
|
->configure(FormatterConfigure::class),
|
||||||
|
];
|
||||||
9
js/.gitignore
vendored
Normal file
9
js/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
node_modules
|
||||||
2
js/admin.ts
Normal file
2
js/admin.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './src/common';
|
||||||
|
export * from './src/admin';
|
||||||
2
js/dist/admin.js
generated
vendored
Normal file
2
js/dist/admin.js
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
(()=>{var e={n:o=>{var l=o&&o.__esModule?()=>o.default:()=>o;return e.d(l,{a:l}),l},d:(o,l)=>{for(var r in l)e.o(l,r)&&!e.o(o,r)&&Object.defineProperty(o,r,{enumerable:!0,get:l[r]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o)};(()=>{"use strict";const o=flarum.core.compat["common/app"];e.n(o)().initializers.add("klxf/flarum-coyote-pulse-viewer",function(){console.log("[klxf/flarum-coyote-pulse-viewer] Hello, forum and admin!")});const l=flarum.core.compat["admin/app"];e.n(l)().initializers.add("klxf/flarum-coyote-pulse-viewer",function(){console.log("[klxf/flarum-coyote-pulse-viewer] Hello, admin!")})})(),module.exports={}})();
|
||||||
|
//# sourceMappingURL=admin.js.map
|
||||||
1
js/dist/admin.js.map
generated
vendored
Normal file
1
js/dist/admin.js.map
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"admin.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,GCLRF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,MCJ3ER,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,I,mBCAlF,MAAM,EAA+BI,OAAOC,KAAKC,OAAO,c,MCExDC,GAAAA,aAAiBC,IAAI,kCAAmC,WACtDC,QAAQC,IAAI,4DACd,GCJA,MAAM,EAA+BN,OAAOC,KAAKC,OAAO,a,MCExDC,GAAAA,aAAiBC,IAAI,kCAAmC,WACtDC,QAAQC,IAAI,kDACd,E","sources":["webpack://@klxf/flarum-coyote-pulse-viewer/webpack/bootstrap","webpack://@klxf/flarum-coyote-pulse-viewer/webpack/runtime/compat get default export","webpack://@klxf/flarum-coyote-pulse-viewer/webpack/runtime/define property getters","webpack://@klxf/flarum-coyote-pulse-viewer/webpack/runtime/hasOwnProperty shorthand","webpack://@klxf/flarum-coyote-pulse-viewer/external root \"flarum.core.compat['common/app']\"","webpack://@klxf/flarum-coyote-pulse-viewer/./src/common/index.ts","webpack://@klxf/flarum-coyote-pulse-viewer/external root \"flarum.core.compat['admin/app']\"","webpack://@klxf/flarum-coyote-pulse-viewer/./src/admin/index.ts"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/app'];","import app from 'flarum/common/app';\n\napp.initializers.add('klxf/flarum-coyote-pulse-viewer', () => {\n console.log('[klxf/flarum-coyote-pulse-viewer] Hello, forum and admin!');\n});\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/app'];","import app from 'flarum/admin/app';\n\napp.initializers.add('klxf/flarum-coyote-pulse-viewer', () => {\n console.log('[klxf/flarum-coyote-pulse-viewer] Hello, admin!');\n});\n"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","flarum","core","compat","app","add","console","log"],"sourceRoot":""}
|
||||||
2
js/dist/forum.js
generated
vendored
Normal file
2
js/dist/forum.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
js/dist/forum.js.map
generated
vendored
Normal file
1
js/dist/forum.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2
js/forum.ts
Normal file
2
js/forum.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './src/common';
|
||||||
|
export * from './src/forum';
|
||||||
5428
js/package-lock.json
generated
Normal file
5428
js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
js/package.json
Normal file
28
js/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@klxf/flarum-coyote-pulse-viewer",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"flarum-webpack-config": "^2.0.0",
|
||||||
|
"webpack": "^5.65.0",
|
||||||
|
"webpack-cli": "^4.9.1",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"@flarum/prettier-config": "^1.0.0",
|
||||||
|
"flarum-tsconfig": "^1.0.2",
|
||||||
|
"typescript": "^4.5.4",
|
||||||
|
"typescript-coverage-report": "^0.6.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "webpack --mode development --watch",
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"analyze": "cross-env ANALYZER=true npm run build",
|
||||||
|
"format": "prettier --write src",
|
||||||
|
"format-check": "prettier --check src",
|
||||||
|
"clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
|
||||||
|
"build-typings": "npm run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && npm run post-build-typings",
|
||||||
|
"post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'",
|
||||||
|
"check-typings": "tsc --noEmit --emitDeclarationOnly false",
|
||||||
|
"check-typings-coverage": "typescript-coverage-report"
|
||||||
|
},
|
||||||
|
"prettier": "@flarum/prettier-config"
|
||||||
|
}
|
||||||
5
js/src/admin/index.ts
Normal file
5
js/src/admin/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import app from 'flarum/admin/app';
|
||||||
|
|
||||||
|
app.initializers.add('klxf/flarum-coyote-pulse-viewer', () => {
|
||||||
|
console.log('[klxf/flarum-coyote-pulse-viewer] Hello, admin!');
|
||||||
|
});
|
||||||
5
js/src/common/index.ts
Normal file
5
js/src/common/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import app from 'flarum/common/app';
|
||||||
|
|
||||||
|
app.initializers.add('klxf/flarum-coyote-pulse-viewer', () => {
|
||||||
|
console.log('[klxf/flarum-coyote-pulse-viewer] Hello, forum and admin!');
|
||||||
|
});
|
||||||
56
js/src/forum/index.ts
Normal file
56
js/src/forum/index.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import app from 'flarum/forum/app';
|
||||||
|
import {extend} from "flarum/common/extend";
|
||||||
|
import TextEditor from "flarum/common/components/TextEditor";
|
||||||
|
import TextEditorButton from "flarum/common/components/TextEditorButton";
|
||||||
|
import renderPulseViewers from "./util/renderPulseViewers";
|
||||||
|
|
||||||
|
app.initializers.add('klxf-coyote-pulse-viewer', () => {
|
||||||
|
extend(TextEditor.prototype, 'toolbarItems', function (items) {
|
||||||
|
items.add(
|
||||||
|
'pulse-file-upload',
|
||||||
|
m(TextEditorButton, {
|
||||||
|
icon: 'fas fa-file-import',
|
||||||
|
title: '波形分享',
|
||||||
|
onclick: () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.pulse';
|
||||||
|
input.style.display = 'none';
|
||||||
|
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const fileNameWithoutExtension = file.name.replace(/\.pulse$/, '');
|
||||||
|
const match = fileNameWithoutExtension.match(/^pulse-(.+?)-\d+$/);
|
||||||
|
|
||||||
|
let title: string;
|
||||||
|
|
||||||
|
if (match) title = match[1];
|
||||||
|
else title = fileNameWithoutExtension;
|
||||||
|
|
||||||
|
const pulseText = `[pulse title="${title}"]${reader.result}[/pulse]`;
|
||||||
|
// @ts-ignore
|
||||||
|
this.attrs?.composer?.editor.insertAtCursor(pulseText);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(input);
|
||||||
|
input.click();
|
||||||
|
document.body.removeChild(input);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
81
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
renderPulseViewers();
|
||||||
|
document.body.addEventListener('contentupdated', renderPulseViewers);
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
renderPulseViewers();
|
||||||
|
});
|
||||||
|
observer.observe(document.body, {childList: true, subtree: true});
|
||||||
|
});
|
||||||
151
js/src/forum/util/generateSVG.ts
Normal file
151
js/src/forum/util/generateSVG.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
const FREQ_SLIDER_VALUE_MAP = [
|
||||||
|
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
|
||||||
|
37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
|
||||||
|
50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78,
|
||||||
|
80, 85, 90, 95,
|
||||||
|
100, 110, 120, 130, 140, 150, 160, 170, 180, 190,
|
||||||
|
200, 233, 266, 300, 333, 366,
|
||||||
|
400, 450, 500, 550,
|
||||||
|
600, 700, 800, 900, 1000
|
||||||
|
];
|
||||||
|
|
||||||
|
const SECTION_TIME_MAP = [
|
||||||
|
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2, 2.1, 2.2,
|
||||||
|
2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 3, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 4, 4.1, 4.2, 4.3, 4.4,
|
||||||
|
4.5, 4.6, 4.7, 4.8, 4.9,
|
||||||
|
5, 5.2, 5.4, 5.6, 5.8, 6, 6.2, 6.4, 6.6, 6.8, 7, 7.2, 7.4, 7.6, 7.8,
|
||||||
|
8, 8.5, 9, 9.5,
|
||||||
|
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
||||||
|
20, 23.4, 26.6, 30, 33.4, 36.6,
|
||||||
|
40, 45, 50, 55,
|
||||||
|
60, 70, 80, 90,
|
||||||
|
100, 120, 140, 160, 180,
|
||||||
|
200, 250, 300
|
||||||
|
];
|
||||||
|
|
||||||
|
function generateSVG(data: string) {
|
||||||
|
const sections = data.replace('Dungeonlab+pulse:', '').split('+section+');
|
||||||
|
|
||||||
|
let svgWidth = 0;
|
||||||
|
let svgHeight = 100;
|
||||||
|
|
||||||
|
let svgContent = '';
|
||||||
|
|
||||||
|
const startPadding = 0;
|
||||||
|
const endPadding = 0;
|
||||||
|
svgWidth += startPadding;
|
||||||
|
|
||||||
|
let restDuration = 0;
|
||||||
|
let speedRate = 1;
|
||||||
|
|
||||||
|
// 只在最开始解析新的数据格式
|
||||||
|
const firstSectionParts = sections[0].split('=');
|
||||||
|
if (firstSectionParts.length === 2) {
|
||||||
|
const [prefix, data] = firstSectionParts;
|
||||||
|
const [rest, speed, num] = prefix.split(',').map(Number);
|
||||||
|
restDuration = rest || 0;
|
||||||
|
speedRate = speed || 1;
|
||||||
|
sections[0] = data; // 将 sections[0] 替换为原始数据部分
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.forEach(section => {
|
||||||
|
const [header, pulsesStr] = section.split('/');
|
||||||
|
const [minFreq, maxFreq, durationIndex, mode, isOn] = header.split(',').map(Number);
|
||||||
|
const duration = SECTION_TIME_MAP[durationIndex];
|
||||||
|
|
||||||
|
if (isOn === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pulses = pulsesStr.split(',');
|
||||||
|
const pulsesDuration = pulses.length * 10; // 计算pulses总宽度, 每个 pulse 宽度为 10
|
||||||
|
const repeatTimes = Math.ceil(duration / (pulsesDuration /
|
||||||
|
100)); // pulsesDuration 单位是 0.1 秒,duration 单位是秒,需要转换单位, pulsesDuration / 100 得到秒
|
||||||
|
|
||||||
|
for (let j = 0; j < repeatTimes; j++) {
|
||||||
|
const sectionWidth = pulses.length * 10;
|
||||||
|
svgWidth += sectionWidth;
|
||||||
|
|
||||||
|
pulses.forEach((pulseStr, index) => {
|
||||||
|
const [value, type] = pulseStr.split('-').map(Number);
|
||||||
|
const pulseHeight = value;
|
||||||
|
|
||||||
|
const xPos = svgWidth - sectionWidth + index * 10;
|
||||||
|
const yPos = svgHeight - pulseHeight;
|
||||||
|
|
||||||
|
const pulseWidth = 10; // 每个 pulse 宽度固定为 10
|
||||||
|
let freq;
|
||||||
|
|
||||||
|
if (mode === 1) {
|
||||||
|
// 频率恒定
|
||||||
|
freq = FREQ_SLIDER_VALUE_MAP[minFreq];
|
||||||
|
} else if (mode === 2) {
|
||||||
|
// 频率从 minFreq 增加到 maxFreq
|
||||||
|
freq = FREQ_SLIDER_VALUE_MAP[minFreq + Math.floor((maxFreq - minFreq) * ((pulses
|
||||||
|
.length * j + index) / (pulses.length * repeatTimes)))];
|
||||||
|
} else if (mode === 3) {
|
||||||
|
// 每个脉冲元内频率从 minFreq 增加到 maxFreq
|
||||||
|
freq = FREQ_SLIDER_VALUE_MAP[minFreq + Math.floor((maxFreq - minFreq) * index /
|
||||||
|
pulses.length)];
|
||||||
|
} else if (mode === 4) {
|
||||||
|
// 频率变化仅发生在脉冲元之间
|
||||||
|
freq = FREQ_SLIDER_VALUE_MAP[minFreq + Math.floor((maxFreq - minFreq) * (j /
|
||||||
|
repeatTimes))];
|
||||||
|
} else {
|
||||||
|
freq = FREQ_SLIDER_VALUE_MAP[minFreq];
|
||||||
|
}
|
||||||
|
|
||||||
|
const blackBarWidth = 0.5;
|
||||||
|
const pulseDurationInSecond = pulseWidth / 100; // 假设 10 宽度对应 0.1 秒,需要根据实际情况调整
|
||||||
|
const numBars = Math.floor(pulseDurationInSecond * 1000 / freq); // 频率单位是毫秒,pulseDurationInSecond 单位是秒,需要转换单位
|
||||||
|
|
||||||
|
if (numBars > 0) {
|
||||||
|
let whiteBarWidth = (pulseWidth - numBars * blackBarWidth) / numBars;
|
||||||
|
if (whiteBarWidth < 0) whiteBarWidth = 0; // 避免条纹宽度为负数的情况
|
||||||
|
|
||||||
|
for (let i = 0; i < numBars; i++) {
|
||||||
|
const barXPos = xPos + i * (blackBarWidth + whiteBarWidth);
|
||||||
|
svgContent +=
|
||||||
|
`<rect x="${barXPos}" y="${yPos}" width="${blackBarWidth}" height="${pulseHeight}" fill="#ffe99d" />`;
|
||||||
|
const whiteBarXPos = barXPos + blackBarWidth;
|
||||||
|
svgContent +=
|
||||||
|
`<rect x="${whiteBarXPos}" y="${yPos}" width="${whiteBarWidth}" height="${pulseHeight}" fill="#1a1a1a" />`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果 numBars 为 0, 则填充整个 pulse
|
||||||
|
svgContent +=
|
||||||
|
`<rect x="${xPos}" y="${yPos}" width="${pulseWidth}" height="${pulseHeight}" fill="#1a1a1a" />`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svgWidth += endPadding;
|
||||||
|
|
||||||
|
const scrollSpeed = 50 * speedRate; // 应用 speedRate
|
||||||
|
const totalScrollDistance = svgWidth + 100;
|
||||||
|
const animationDuration = totalScrollDistance / scrollSpeed;
|
||||||
|
|
||||||
|
const animationContent = `
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
attributeType="XML"
|
||||||
|
type="translate"
|
||||||
|
from="${-startPadding + 200}"
|
||||||
|
to="${-svgWidth - 100}"
|
||||||
|
dur="${animationDuration}s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<svg width="300" height="100" viewBox="${-startPadding} 0 100 ${svgHeight}">
|
||||||
|
<g>
|
||||||
|
${svgContent}
|
||||||
|
${animationContent}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default generateSVG;
|
||||||
51
js/src/forum/util/renderPulseViewers.ts
Normal file
51
js/src/forum/util/renderPulseViewers.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import generateSVG from './generateSVG';
|
||||||
|
import validatePulseData from "./validatePulseData";
|
||||||
|
|
||||||
|
function renderPulseViewers() {
|
||||||
|
document.querySelectorAll('.coyote-pulse-viewer').forEach(el => {
|
||||||
|
if (el.getAttribute('data-pulse-rendered')) return;
|
||||||
|
let error = false;
|
||||||
|
let errorArray: string[] = [];
|
||||||
|
const pulseData = el.getAttribute('data-pulse');
|
||||||
|
const pulseTitle = el.getAttribute('data-title') ? el.getAttribute('data-title'): '自定义波形';
|
||||||
|
const pulseVersion = el.getAttribute('data-version') ? el.getAttribute('data-version') : '3';
|
||||||
|
if (pulseData) {
|
||||||
|
el.innerHTML += `<div class="pulse-title-box"><span class="pulse-title">波形: ${pulseTitle && pulseTitle.length > 12 ? pulseTitle.slice(0, 12) + '...' : pulseTitle}</span><span class="pulse-version">v${pulseVersion}</span><button class="Button Button--primary hasIcon pulse-download-btn"><i class="fas fa-file-download"></i></button></div>`;
|
||||||
|
|
||||||
|
if (pulseVersion != '3') {
|
||||||
|
error = true;
|
||||||
|
errorArray.push('目前仅支持 v3 版本的波形数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validatePulseData(pulseData)) {
|
||||||
|
error = true;
|
||||||
|
errorArray.push('解析失败,请检查数据格式是否正确');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
el.innerHTML += generateSVG(pulseData);
|
||||||
|
} else {
|
||||||
|
el.querySelector('.pulse-download-btn')?.remove();
|
||||||
|
el.innerHTML += `<div class="pulse-warning"><i class="fas fa-exclamation-circle"></i> 错误:${errorArray.join('; ')}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.setAttribute('data-pulse-rendered', '1');
|
||||||
|
|
||||||
|
const downloadBtn = el.querySelector('.pulse-download-btn');
|
||||||
|
if (downloadBtn) {
|
||||||
|
downloadBtn.addEventListener('click', () => {
|
||||||
|
const blob = new Blob([pulseData], { type: 'text/plain' });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = `${pulseTitle}.pulse`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default renderPulseViewers;
|
||||||
81
js/src/forum/util/validatePulseData.ts
Normal file
81
js/src/forum/util/validatePulseData.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
function validatePulseData(decodedData: string) {
|
||||||
|
// 1. 检查开头是否为 "Dungeonlab+pulse:"
|
||||||
|
if (!decodedData.startsWith("Dungeonlab+pulse:")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 移除开头标识
|
||||||
|
const data = decodedData.substring("Dungeonlab+pulse:".length);
|
||||||
|
|
||||||
|
// 3. 分割数据段
|
||||||
|
const sections = data.split("+section+");
|
||||||
|
|
||||||
|
// 4. 校验第一段数据(兼容新格式)
|
||||||
|
const firstSection = sections[0];
|
||||||
|
const firstSectionParts = firstSection.split("/");
|
||||||
|
|
||||||
|
// 检查是否存在前缀
|
||||||
|
const prefixRegex = /^(\d+,\d+,\d+=)?/; // 匹配 "数字,数字,数字=" 的可选前缀
|
||||||
|
const prefixMatch = firstSection.match(prefixRegex);
|
||||||
|
const hasPrefix = prefixMatch && prefixMatch[1];
|
||||||
|
|
||||||
|
let intCounts, floatCounts;
|
||||||
|
|
||||||
|
if (hasPrefix) {
|
||||||
|
// 新格式:包含前缀
|
||||||
|
const dataPart = firstSection.substring(prefixMatch[0].length); // 移除前缀
|
||||||
|
const dataParts = dataPart.split("/");
|
||||||
|
|
||||||
|
if (dataParts.length !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
intCounts = dataParts[0].split(",");
|
||||||
|
floatCounts = dataParts[1].split(",");
|
||||||
|
} else {
|
||||||
|
// 旧格式:不包含前缀
|
||||||
|
if (firstSectionParts.length !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
intCounts = firstSectionParts[0].split(",");
|
||||||
|
floatCounts = firstSectionParts[1].split(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intCounts.length !== 5) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!intCounts.every(count => /^\d+$/.test(count))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!floatCounts.every(count => /^-?\d+(\.\d+)?-\d+$/.test(count))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 校验后续数据段(与旧格式相同)
|
||||||
|
for (let i = 1; i < sections.length; i++) {
|
||||||
|
const section = sections[i];
|
||||||
|
const sectionParts = section.split("/");
|
||||||
|
if (sectionParts.length !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intCounts = sectionParts[0].split(",");
|
||||||
|
if (intCounts.length !== 5) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!intCounts.every(count => /^\d+$/.test(count))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const floatCounts = sectionParts[1].split(",");
|
||||||
|
if (!floatCounts.every(count => /^-?\d+(\.\d+)?-\d+$/.test(count))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default validatePulseData;
|
||||||
24
js/tsconfig.json
Normal file
24
js/tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
// Use Flarum's tsconfig as a starting point
|
||||||
|
"extends": "flarum-tsconfig",
|
||||||
|
// This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder
|
||||||
|
// and also tells your Typescript server to read core's global typings for
|
||||||
|
// access to `dayjs` and `$` in the global namespace.
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"../vendor/*/*/js/dist-typings/@types/**/*",
|
||||||
|
// <CUSTOM-1>
|
||||||
|
// </CUSTOM-1>
|
||||||
|
"@types/**/*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
// This will output typings to `dist-typings`
|
||||||
|
"declarationDir": "./dist-typings",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"flarum/*": ["../vendor/flarum/core/js/dist-typings/*"],
|
||||||
|
// <CUSTOM-2>
|
||||||
|
// </CUSTOM-2>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
js/webpack.config.js
Normal file
1
js/webpack.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('flarum-webpack-config')();
|
||||||
39
less/forum.less
Normal file
39
less/forum.less
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
.coyote-pulse-viewer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.pulse-title-box {
|
||||||
|
width: 300px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
color: var(--text-on-light);
|
||||||
|
background: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-download-btn {
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
top: 32px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-warning {
|
||||||
|
margin: 6px;
|
||||||
|
padding: 2px 16px;
|
||||||
|
background: var(--alert-error-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-version {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: solid 1px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
border: solid var(--primary-color) 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
locale/en.yml
Normal file
7
locale/en.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
klxf-coyote-pulse-viewer:
|
||||||
|
# For more details on the format
|
||||||
|
# Checkout https://docs.flarum.org/extend/i18n/#appendix-a-standard-key-format
|
||||||
|
admin:
|
||||||
|
my_cool_key: My Cool Key
|
||||||
|
|
||||||
|
forum:
|
||||||
16
src/FormatterConfigure.php
Normal file
16
src/FormatterConfigure.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Klxf\CoyotePulseViewer;
|
||||||
|
|
||||||
|
use s9e\TextFormatter\Configurator;
|
||||||
|
|
||||||
|
class FormatterConfigure
|
||||||
|
{
|
||||||
|
public function __invoke(Configurator $config): void
|
||||||
|
{
|
||||||
|
$config->BBCodes->addCustom(
|
||||||
|
'[pulse title={TEXT1;optional} ver={TEXT2;optional}]{TEXT3}[/pulse]',
|
||||||
|
'<div class="coyote-pulse-viewer" data-title="{TEXT1}" data-version="{TEXT2}" data-pulse="{TEXT3}"></div>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user