first commit

This commit is contained in:
Fang_Zhijian 2025-09-24 18:35:28 +08:00
commit 34249f5a34
28 changed files with 6074 additions and 0 deletions

19
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,21 @@
# Coyote Pulse Viewer
![License](https://img.shields.io/badge/license-MIT-blue.svg)
[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
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
export * from './src/common';
export * from './src/admin';

2
js/dist/admin.js generated vendored Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

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
View File

@ -0,0 +1,2 @@
export * from './src/common';
export * from './src/forum';

5428
js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
js/package.json Normal file
View 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
View 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
View 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
View 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});
});

View 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;

View 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;

View 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
View 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
View File

@ -0,0 +1 @@
module.exports = require('flarum-webpack-config')();

39
less/forum.less Normal file
View 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
View 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:

View 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>'
);
}
}