Merge branch 'next-gen' into toolkit-update-dev
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
/.nyc_output
|
||||
/coverage
|
||||
/node_modules
|
||||
**/node_modules/
|
||||
/sign
|
||||
/dist
|
||||
/build
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.13",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"semver": "^7.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
import * as fs from 'fs';
|
||||
import * as jpeg from 'jpeg-js';
|
||||
|
||||
/**
|
||||
* 用于表示像素的 RGB 颜色
|
||||
* [r, g, b] 每个范围 0~255
|
||||
*/
|
||||
type Color = [number, number, number];
|
||||
|
||||
/*************************************************************************
|
||||
* 1. 使用 jpeg-js 解码 JPG,并抽取像素用于 K-means
|
||||
*************************************************************************/
|
||||
|
||||
/**
|
||||
* 从指定文件读取并解码 JPG 得到原始 RGBA 像素数据。
|
||||
* @param filePath
|
||||
* @returns { data: Uint8Array; width: number; height: number }
|
||||
*/
|
||||
function decodeJpg(filePath: string) {
|
||||
const jpgBuffer = fs.readFileSync(filePath);
|
||||
// jpeg.decode 返回 { data: Buffer, width, height }
|
||||
// data 是 RGBA (width * height * 4)
|
||||
const decoded = jpeg.decode(jpgBuffer, { useTArray: true });
|
||||
return {
|
||||
data: decoded.data, // Uint8Array,顺序 RGBA
|
||||
width: decoded.width,
|
||||
height: decoded.height,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两种颜色(RGB)的欧几里得距离
|
||||
*/
|
||||
function distance(c1: Color, c2: Color): number {
|
||||
const dr = c1[0] - c2[0];
|
||||
const dg = c1[1] - c2[1];
|
||||
const db = c1[2] - c2[2];
|
||||
return Math.sqrt(dr * dr + dg * dg + db * db);
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
* 2. K-means 聚类核心
|
||||
*************************************************************************/
|
||||
|
||||
/**
|
||||
* 对给定像素集合执行 K-means 聚类,返回聚类中心(颜色)。
|
||||
* @param pixels 所有像素的 RGB 数组
|
||||
* @param k 聚类数量
|
||||
* @param maxIterations 迭代次数上限
|
||||
* @param tolerance 判断收敛的距离阈值
|
||||
* @returns {Color[]} k 个聚类中心(RGB)
|
||||
*/
|
||||
function kMeansCluster(
|
||||
pixels: Color[],
|
||||
k: number,
|
||||
maxIterations = 50,
|
||||
tolerance = 1.0
|
||||
): Color[] {
|
||||
// 1. 随机初始化 k 个聚类中心 (从像素中随机选 k 个)
|
||||
const centers: Color[] = [];
|
||||
for (let i = 0; i < k; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * pixels.length);
|
||||
centers.push([...pixels[randomIndex]] as Color);
|
||||
}
|
||||
|
||||
// 用于存储每次迭代时每个像素的所属聚类
|
||||
let assignments: number[] = new Array(pixels.length).fill(-1);
|
||||
|
||||
for (let iter = 0; iter < maxIterations; iter++) {
|
||||
// 记录旧中心,用于判断收敛
|
||||
const oldCenters = centers.map((c) => [...c]) as Color[];
|
||||
|
||||
// 2.1 为每个像素找到离自己最近的中心
|
||||
for (let pIndex = 0; pIndex < pixels.length; pIndex++) {
|
||||
let minDist = Infinity;
|
||||
let clusterIndex = 0;
|
||||
for (let cIndex = 0; cIndex < k; cIndex++) {
|
||||
const dist = distance(pixels[pIndex], centers[cIndex]);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
clusterIndex = cIndex;
|
||||
}
|
||||
}
|
||||
assignments[pIndex] = clusterIndex;
|
||||
}
|
||||
|
||||
// 2.2 重新计算每个聚类的中心(将同一个 cluster 里的所有像素平均值作为新的中心)
|
||||
const sums: Color[] = new Array(k).fill([0, 0, 0]).map(() => [0, 0, 0]);
|
||||
const counts: number[] = new Array(k).fill(0);
|
||||
|
||||
for (let pIndex = 0; pIndex < pixels.length; pIndex++) {
|
||||
const clusterIndex = assignments[pIndex];
|
||||
sums[clusterIndex][0] += pixels[pIndex][0];
|
||||
sums[clusterIndex][1] += pixels[pIndex][1];
|
||||
sums[clusterIndex][2] += pixels[pIndex][2];
|
||||
counts[clusterIndex]++;
|
||||
}
|
||||
|
||||
for (let cIndex = 0; cIndex < k; cIndex++) {
|
||||
if (counts[cIndex] > 0) {
|
||||
centers[cIndex][0] = sums[cIndex][0] / counts[cIndex];
|
||||
centers[cIndex][1] = sums[cIndex][1] / counts[cIndex];
|
||||
centers[cIndex][2] = sums[cIndex][2] / counts[cIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// 2.3 判断是否收敛:新旧中心之间的最大移动距离 < tolerance
|
||||
let maxShift = 0;
|
||||
for (let cIndex = 0; cIndex < k; cIndex++) {
|
||||
const shift = distance(centers[cIndex], oldCenters[cIndex]);
|
||||
if (shift > maxShift) {
|
||||
maxShift = shift;
|
||||
}
|
||||
}
|
||||
if (maxShift < tolerance) {
|
||||
// 收敛
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 返回最终的 k 个聚类中心(颜色分量取整)
|
||||
return centers.map(c => [
|
||||
Math.round(c[0]),
|
||||
Math.round(c[1]),
|
||||
Math.round(c[2])
|
||||
] as Color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取 3 个主要颜色主流程:
|
||||
* 1. 读取并解码图片 -> RGBA
|
||||
* 2. 抽取 (R,G,B) 像素
|
||||
* 3. 用 K-means 求得 3 个主要颜色
|
||||
*/
|
||||
function extractTop3Colors(filePath: string): Color[] {
|
||||
console.log(`[INFO] decoding JPG: ${filePath}`);
|
||||
const { data, width, height } = decodeJpg(filePath);
|
||||
|
||||
// data: RGBA,(width * height * 4)
|
||||
const sampleStep = Math.max(1, Math.floor((width * height) / 20000));
|
||||
|
||||
const pixels: Color[] = [];
|
||||
for (let i = 0; i < width * height; i += sampleStep) {
|
||||
const r = data[i * 4 + 0];
|
||||
const g = data[i * 4 + 1];
|
||||
const b = data[i * 4 + 2];
|
||||
// alpha 通常忽略
|
||||
pixels.push([r, g, b]);
|
||||
}
|
||||
|
||||
console.log(`[INFO] Running K-means for 3 clusters (sample size = ${pixels.length})...`);
|
||||
const k = 3;
|
||||
const centers = kMeansCluster(pixels, k);
|
||||
|
||||
return centers;
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
* 3. 在新图上绘制 3 个「从圆心色 -> 背景透明」的圆,并保存为 JPG
|
||||
*************************************************************************/
|
||||
|
||||
/** 画布大小 */
|
||||
const CANVAS_WIDTH = 430;
|
||||
const CANVAS_HEIGHT = 250;
|
||||
|
||||
/** 背景颜色 */
|
||||
const BACKGROUND_COLOR: [number, number, number, number] = [0, 0, 0, 255];
|
||||
|
||||
/**
|
||||
* 圆心/半径的范围,可自行修改:
|
||||
* - 圆心距离画布边缘至少 MARGIN
|
||||
* - 半径范围 [MIN_RADIUS, MAX_RADIUS]
|
||||
*/
|
||||
const MARGIN = 2;
|
||||
const MIN_RADIUS = 80;
|
||||
const MAX_RADIUS = 110;
|
||||
|
||||
/**
|
||||
* 圆与圆之间的最小距离(基于圆心与圆心之间的距离减去半径之和)。
|
||||
* 如果希望**完全不重叠**,可令:
|
||||
* distanceBetweenCenters >= (r1 + r2 + MIN_GAP_BETWEEN_CIRCLES).
|
||||
*/
|
||||
const MIN_GAP_BETWEEN_CIRCLES = 20;
|
||||
|
||||
/**
|
||||
* 新增:圆心颜色亮度因子 (0~1),用于降低圆心过亮
|
||||
* 例如:0.8 -> 稍微暗一点;0.5 -> 明显暗
|
||||
*/
|
||||
const COLOR_BRIGHTNESS_FACTOR = 0.62;
|
||||
|
||||
/**
|
||||
* 对 RGBA 像素进行 alpha 混合:
|
||||
* outA = srcA + dstA * (1 - srcA)
|
||||
* outRGB = (srcRGB * srcA + dstRGB * dstA * (1 - srcA)) / outA
|
||||
*
|
||||
* data 数组中每像素 4 通道:R, G, B, A,值范围 [0,255]
|
||||
* alpha 范围 [0,1]
|
||||
*/
|
||||
function alphaBlendPixel(
|
||||
data: Uint8Array,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
color: Color,
|
||||
alpha: number
|
||||
) {
|
||||
const offset = (y * width + x) * 4;
|
||||
const dstR = data[offset + 0];
|
||||
const dstG = data[offset + 1];
|
||||
const dstB = data[offset + 2];
|
||||
const dstA = data[offset + 3] / 255; // 转为 0~1
|
||||
|
||||
const srcR = color[0];
|
||||
const srcG = color[1];
|
||||
const srcB = color[2];
|
||||
const srcA = alpha;
|
||||
|
||||
const outA = srcA + dstA * (1 - srcA);
|
||||
if (outA > 0) {
|
||||
const outR = (srcR * srcA + dstR * dstA * (1 - srcA)) / outA;
|
||||
const outG = (srcG * srcA + dstG * dstA * (1 - srcA)) / outA;
|
||||
const outB = (srcB * srcA + dstB * dstA * (1 - srcA)) / outA;
|
||||
|
||||
data[offset + 0] = Math.round(outR);
|
||||
data[offset + 1] = Math.round(outG);
|
||||
data[offset + 2] = Math.round(outB);
|
||||
data[offset + 3] = Math.round(outA * 255);
|
||||
} else {
|
||||
// outA=0 -> 全透明
|
||||
data[offset + 0] = 0;
|
||||
data[offset + 1] = 0;
|
||||
data[offset + 2] = 0;
|
||||
data[offset + 3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 data 上绘制一个「从 centerColor->背景透明」的径向渐变圆
|
||||
* @param data 画布(RGBA)
|
||||
* @param width, height 画布尺寸
|
||||
* @param cx, cy 圆心坐标
|
||||
* @param radius 半径
|
||||
* @param centerColor 圆心处的颜色 (RGB)
|
||||
*/
|
||||
function drawRadialGradientCircle(
|
||||
data: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
radius: number,
|
||||
centerColor: Color
|
||||
) {
|
||||
// 仅在圆的外接矩形范围内绘制
|
||||
const minX = Math.max(0, cx - radius);
|
||||
const maxX = Math.min(width - 1, cx + radius);
|
||||
const minY = Math.max(0, cy - radius);
|
||||
const maxY = Math.min(height - 1, cy + radius);
|
||||
|
||||
for (let y = minY; y <= maxY; y++) {
|
||||
for (let x = minX; x <= maxX; x++) {
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist <= radius) {
|
||||
// 根据距离,计算该点的 alpha:
|
||||
// 圆心 dist=0 -> alpha=1
|
||||
// 半径 dist=radius -> alpha=0
|
||||
const alpha = 1 - dist / radius;
|
||||
alphaBlendPixel(data, x, y, width, centerColor, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数:判断新圆 (cx, cy, r) 与已放置的圆数组 placedCircles
|
||||
* 是否满足“互不重叠”,即:
|
||||
* distanceBetweenCenters >= (r + oldCircle.r + MIN_GAP_BETWEEN_CIRCLES)
|
||||
*/
|
||||
function canPlaceCircle(
|
||||
cx: number,
|
||||
cy: number,
|
||||
r: number,
|
||||
placedCircles: { cx: number, cy: number, r: number }[],
|
||||
minGap: number
|
||||
): boolean {
|
||||
for (const c of placedCircles) {
|
||||
const dist = Math.sqrt((cx - c.cx) ** 2 + (cy - c.cy) ** 2);
|
||||
// 如果距离 < 两个半径和 + minGap,就判定为重叠
|
||||
if (dist < (r + c.r + minGap)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成画布并绘制 3 个不会彼此重叠的渐变圆
|
||||
* @param colors 3 个原始颜色 (RGB)
|
||||
*/
|
||||
function generateCirclesImage(colors: Color[]): { data: Uint8Array; width: number; height: number } {
|
||||
const width = CANVAS_WIDTH;
|
||||
const height = CANVAS_HEIGHT;
|
||||
|
||||
// 1. 创建画布并填充背景
|
||||
const data = new Uint8Array(width * height * 4);
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
data[i * 4 + 0] = BACKGROUND_COLOR[0];
|
||||
data[i * 4 + 1] = BACKGROUND_COLOR[1];
|
||||
data[i * 4 + 2] = BACKGROUND_COLOR[2];
|
||||
data[i * 4 + 3] = BACKGROUND_COLOR[3];
|
||||
}
|
||||
|
||||
// 2. 依次为 3 个颜色生成圆,确保不重叠
|
||||
const placedCircles: { cx: number; cy: number; r: number }[] = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
let circlePlaced = false;
|
||||
|
||||
// 最多尝试多次,若都无法放置,就跳过
|
||||
const MAX_TRIES = 100;
|
||||
|
||||
const radius = randInt(MIN_RADIUS, MAX_RADIUS);
|
||||
var cx = 0;
|
||||
var cy = 0;
|
||||
for (let attempt = 0; attempt < MAX_TRIES; attempt++) {
|
||||
cx = randInt(radius + MARGIN, width - radius - MARGIN);
|
||||
cy = randInt(radius + MARGIN, height - radius - MARGIN);
|
||||
|
||||
if (canPlaceCircle(cx, cy, radius, placedCircles, MIN_GAP_BETWEEN_CIRCLES)) {
|
||||
// 找到不重叠的位置了
|
||||
placedCircles.push({ cx, cy, r: radius });
|
||||
circlePlaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!circlePlaced) {
|
||||
console.warn(
|
||||
`[WARN] 无法在 ${MAX_TRIES} 次随机尝试中给第${i + 1}个圆找到合适位置,但仍然绘制。`
|
||||
);
|
||||
placedCircles.push({ cx, cy, r: radius });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 把成功放置的圆绘制到画布
|
||||
// 在这里应用 COLOR_BRIGHTNESS_FACTOR,降低每个圆心的RGB
|
||||
placedCircles.forEach((c, idx) => {
|
||||
// 取对应颜色(若 idx>=colors.length,可加保护)
|
||||
const origColor = colors[idx] || [255, 255, 255];
|
||||
// 调整亮度
|
||||
const adjustedColor: Color = [
|
||||
Math.min(255, Math.max(0, origColor[0] * COLOR_BRIGHTNESS_FACTOR)),
|
||||
Math.min(255, Math.max(0, origColor[1] * COLOR_BRIGHTNESS_FACTOR)),
|
||||
Math.min(255, Math.max(0, origColor[2] * COLOR_BRIGHTNESS_FACTOR)),
|
||||
];
|
||||
|
||||
console.log(
|
||||
`[INFO] Circle #${idx + 1}: center=(${c.cx},${c.cy}), radius=${c.r}, color=${adjustedColor}`
|
||||
);
|
||||
drawRadialGradientCircle(data, width, height, c.cx, c.cy, c.r, adjustedColor);
|
||||
});
|
||||
|
||||
return { data, width, height };
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 RGBA 数据保存为 JPG 文件
|
||||
*/
|
||||
function saveAsJpeg(data: Uint8Array, width: number, height: number, outPath: string) {
|
||||
const rawImageData = {
|
||||
data: Buffer.from(data), // jpeg-js 需要 Buffer 类型
|
||||
width,
|
||||
height,
|
||||
};
|
||||
// 设定 jpg 质量
|
||||
const jpegImageData = jpeg.encode(rawImageData, 80);
|
||||
fs.writeFileSync(outPath, jpegImageData.data);
|
||||
console.log(`[INFO] Saved: ${outPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数:在 [min, max] 范围内随机整数
|
||||
*/
|
||||
function randInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
* 4. 主函数:读取命令行参数并执行
|
||||
*************************************************************************/
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 1) {
|
||||
console.log(`用法: node kmeans-colors.js <jpg文件路径>`);
|
||||
process.exit(1);
|
||||
}
|
||||
const filePath = args[0];
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`[ERROR] 文件不存在: ${filePath}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// 1) 提取 3 个主色
|
||||
const top3Colors = extractTop3Colors(filePath);
|
||||
console.log(`\n[RESULT] 主要颜色 (RGB):`);
|
||||
top3Colors.forEach((c, idx) => {
|
||||
console.log(` #${idx + 1}: R=${c[0]}, G=${c[1]}, B=${c[2]}`);
|
||||
});
|
||||
|
||||
// 2) 用这 3 个颜色在 400×400 的画布上绘制不重叠的渐变圆
|
||||
const { data, width, height } = generateCirclesImage(top3Colors);
|
||||
|
||||
// 3) 保存结果为 JPG
|
||||
const outPath = 'output-circles.jpg';
|
||||
saveAsJpeg(data, width, height, outPath);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "image-color-pick-gen",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "image-color-pick-gen",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"jpeg-js": "^0.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/jpeg-js": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "image-color-pick-gen",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"jpeg-js": "^0.4.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
console.log("1")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
@@ -33,6 +33,7 @@ module.exports = {
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@src': path.resolve(__dirname, 'src'),
|
||||
'@components': path.resolve(__dirname, 'src/components'),
|
||||
'@less': path.resolve(__dirname, 'src/less'),
|
||||
'@protobuf': path.resolve(__dirname, 'src/protobuf'),
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as jumpcheck from "./jumpcheck"
|
||||
import * as htmlparser from "./htmlparser"
|
||||
import * as savedcontent from "./savedcontent"
|
||||
import * as bgimg from "./bgimg"
|
||||
import * as usertracker from "./usertracker"
|
||||
import logger from "./logger/logger"
|
||||
export default {
|
||||
dayjs: dayjs,
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { file } from '../tsimports'
|
||||
|
||||
interface FileMoveOptions {
|
||||
srcUri: string;
|
||||
dstUri: string;
|
||||
}
|
||||
|
||||
interface FileCopyOptions {
|
||||
srcUri: string;
|
||||
dstUri: string;
|
||||
}
|
||||
|
||||
interface FileListOptions {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
interface FileGetOptions {
|
||||
uri: string;
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
interface FileDeleteOptions {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
interface FileWriteTextOptions {
|
||||
uri: string;
|
||||
text: string;
|
||||
encoding?: string;
|
||||
append?: boolean;
|
||||
}
|
||||
|
||||
interface FileWriteArrayBufferOptions {
|
||||
uri: string;
|
||||
buffer: Uint8Array;
|
||||
position?: number;
|
||||
append?: boolean;
|
||||
}
|
||||
|
||||
interface FileReadTextOptions {
|
||||
uri: string;
|
||||
encoding?: string;
|
||||
}
|
||||
|
||||
interface FileReadArrayBufferOptions {
|
||||
uri: string;
|
||||
position?: number;
|
||||
length?: number;
|
||||
}
|
||||
|
||||
interface FileAccessOptions {
|
||||
uri: string;
|
||||
}
|
||||
|
||||
interface FileMkdirOptions {
|
||||
uri: string;
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
interface FileRmdirOptions {
|
||||
uri: string;
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
class FileAPI {
|
||||
async move(options: FileMoveOptions): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.move({
|
||||
...options,
|
||||
success: (uri: string) => resolve(uri),
|
||||
fail: (_, code: number) => reject(new Error(`Move failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async copy(options: FileCopyOptions): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.copy({
|
||||
...options,
|
||||
success: (uri: string) => resolve(uri),
|
||||
fail: (_, code: number) => reject(new Error(`Copy failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async list(options: FileListOptions): Promise<{ fileList: { uri: string; lastModifiedTime: number; length: number }[] }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.list({
|
||||
...options,
|
||||
success: (data: { fileList: { uri: string; lastModifiedTime: number; length: number }[] }) => resolve(data),
|
||||
fail: (_, code: number) => reject(new Error(`List failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async get(options: FileGetOptions): Promise<{ uri: string; length: number; lastModifiedTime: number; type: string; subFiles?: any[] }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.get({
|
||||
...options,
|
||||
success: (data: any) => resolve(data),
|
||||
fail: (_, code: number) => reject(new Error(`Get failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async delete(options: FileDeleteOptions): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.delete({
|
||||
...options,
|
||||
success: () => resolve(),
|
||||
fail: (_, code: number) => reject(new Error(`Delete failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async writeText(options: FileWriteTextOptions): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.writeText({
|
||||
...options,
|
||||
success: () => resolve(),
|
||||
fail: (_, code: number) => reject(new Error(`WriteText failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async writeArrayBuffer(options: FileWriteArrayBufferOptions): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.writeArrayBuffer({
|
||||
...options,
|
||||
success: () => resolve(),
|
||||
fail: (_, code: number) => reject(new Error(`WriteArrayBuffer failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async readText(options: FileReadTextOptions): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.readText({
|
||||
...options,
|
||||
success: (data: { text: string }) => resolve(data.text),
|
||||
fail: (_, code: number) => reject(new Error(`ReadText failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async readArrayBuffer(options: FileReadArrayBufferOptions): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.readArrayBuffer({
|
||||
...options,
|
||||
success: (data: { buffer: Uint8Array }) => resolve(data.buffer),
|
||||
fail: (_, code: number) => reject(new Error(`ReadArrayBuffer failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async access(options: FileAccessOptions): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.access({
|
||||
...options,
|
||||
success: () => resolve(true),
|
||||
fail: (_, code: number) => reject(new Error(`Access failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async mkdir(options: FileMkdirOptions): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.mkdir({
|
||||
...options,
|
||||
success: () => resolve(),
|
||||
fail: (_, code: number) => reject(new Error(`Mkdir failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async rmdir(options: FileRmdirOptions): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.rmdir({
|
||||
...options,
|
||||
success: () => resolve(),
|
||||
fail: (_, code: number) => reject(new Error(`Rmdir failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const asyncFile = new FileAPI();
|
||||
@@ -0,0 +1,58 @@
|
||||
import { storage } from "../tsimports";
|
||||
|
||||
interface StorageGetOptions {
|
||||
key: string;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
interface StorageSetOptions {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface StorageDeleteOptions {
|
||||
key: string;
|
||||
}
|
||||
|
||||
class StorageAPI {
|
||||
async get(options: StorageGetOptions): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
storage.get({
|
||||
...options,
|
||||
success: (data: string) => resolve(data),
|
||||
fail: (_, code: number) => reject(new Error(`Get failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async set(options: StorageSetOptions): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
storage.set({
|
||||
...options,
|
||||
success: () => resolve(),
|
||||
fail: (_, code: number) => reject(new Error(`Set failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
storage.clear({
|
||||
success: () => resolve(),
|
||||
fail: (_, code: number) => reject(new Error(`Clear failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async delete(options: StorageDeleteOptions): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
storage.delete({
|
||||
...options,
|
||||
success: () => resolve(),
|
||||
fail: (_, code: number) => reject(new Error(`Delete failed with code ${code}`)),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const asyncStorage = new StorageAPI();
|
||||
@@ -1,4 +1,14 @@
|
||||
export const BilibiliClientUserMethods = {
|
||||
// 获取单个用户的信息
|
||||
async getUserInfoByUID(this: any, uid: String) {
|
||||
const url = `https://api.bilibili.com/x/space/wbi/acc/info`;
|
||||
const response = await this.getRequestWbi(url, {
|
||||
mid: uid
|
||||
})
|
||||
|
||||
return response.data.data
|
||||
},
|
||||
|
||||
// 根据UID批量获取用户信息
|
||||
async getMultiUserInfoByUID(this: any, uids: Array<String>) {
|
||||
const url = "https://api.bilibili.com/x/polymer/pc-electron/v1/user/cards";
|
||||
|
||||
@@ -85,6 +85,9 @@ export default {
|
||||
token: data.token,
|
||||
success: (data) => {
|
||||
global.logger.log("[BetterOnlineImage] Image Saved in: " + data.uri)
|
||||
this.$emit('loaded', {
|
||||
uri: data.uri
|
||||
})
|
||||
this.localsrc = data.uri
|
||||
this.show_loading = false
|
||||
this.show_image = true
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="reply">
|
||||
<div class="userinfo" onclick="ShowTools(reply)">
|
||||
<image class="userimg" style="lines: 1" src="{{reply.member.avatar}}@44w_44h" alt="/common/fullgray.png"></image>
|
||||
<div style="flex-direction: column; justify-content: center">
|
||||
<div class="userinfo">
|
||||
<image class="userimg" onclick="OpenUser(reply.member.mid)" style="lines: 1" src="{{reply.member.avatar}}@44w_44h" alt="/common/fullgray.png"></image>
|
||||
<div style="flex-direction: column; justify-content: center" onclick="ShowTools(reply)">
|
||||
<div class="rowbar">
|
||||
<text class="username" style="margin-left: 10px">{{ reply.member.uname }}</text>
|
||||
<image style="width: 24px; height: 14px; object-fit: contain; margin-left: 6px"
|
||||
@@ -34,8 +34,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from "@system.router"
|
||||
export default {
|
||||
props: ["reply", "secmode"],
|
||||
OpenUser(id) {
|
||||
router.push({
|
||||
uri: "pages/user",
|
||||
params: {
|
||||
uid: id
|
||||
}
|
||||
})
|
||||
},
|
||||
ShowTools(reply) {
|
||||
this.$emit("showTools", {
|
||||
reply: reply
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
.profdiv {
|
||||
width: 176px;
|
||||
/* 设置为与 profpendantimg 一致 */
|
||||
height: 176px;
|
||||
/* 设置为与 profpendantimg 一致 */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profimg {
|
||||
width: 106px;
|
||||
height: 106px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.profpendantimg {
|
||||
width: 176px;
|
||||
height: 176px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vipname {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #ff7da8;
|
||||
}
|
||||
|
||||
.tagbar {
|
||||
flex-direction: row;
|
||||
width: 40%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tagimg {
|
||||
height: 30px;
|
||||
object-fit: contain;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div style="flex-direction: column; align-items: center">
|
||||
<div class="profdiv">
|
||||
<image src="{{accountInfo.face}}@106w_106h" alt="/common/fullgray.png" class="profimg"></image>
|
||||
<image src="{{accountInfo.pendant.image}}@176w_176h" alt="/common/alphaimg.png" class="profpendantimg"></image>
|
||||
</div>
|
||||
<text class="{{name_text_class}}">{{ name }}</text>
|
||||
<div class="tagbar" style="margin-top: 5px">
|
||||
<image class="tagimg" src="/common/bililevel/lv{{level}}.png" style="height: 20px"></image>
|
||||
<image class="tagimg" if="{{is_vip}}" style="margin-left: 10px"
|
||||
src="/common/bilivip/{{accountInfo.vip_label.label_theme}}.png"></image>
|
||||
<image class="tagimg" if="{{is_senior_member}}" style="margin-left: 10px" src="/common/senior_member.png">
|
||||
</image>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["accountInfo"],
|
||||
data: {
|
||||
name_text_class: "name",
|
||||
is_vip: false,
|
||||
is_senior_member: false,
|
||||
name: "",
|
||||
level: ""
|
||||
},
|
||||
UpdateShow() {
|
||||
try{
|
||||
this.is_vip = !!this.accountInfo.vip_pay_type
|
||||
this.name_text_class = this.is_vip ? "vipname" : this.name_text_class
|
||||
|
||||
this.is_senior_member = !!this.accountInfo.is_senior_member
|
||||
|
||||
|
||||
console.log(this.accountInfo.level_info?.current_level)
|
||||
console.log(this.accountInfo.level)
|
||||
this.level = this.accountInfo.level_info?.current_level || this.accountInfo.level
|
||||
|
||||
this.name = this.accountInfo.uname || this.accountInfo.name
|
||||
}
|
||||
catch(error){
|
||||
global.logger.error("Error when updating show:", error.toString())
|
||||
}
|
||||
},
|
||||
onInit() {
|
||||
this.UpdateShow()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import "@less/global.less";
|
||||
@import "./UserInfo.less";
|
||||
</style>
|
||||
@@ -0,0 +1,419 @@
|
||||
import * as jpeg from 'jpeg-js';
|
||||
import { asyncFile } from '../asyncapi/file';
|
||||
|
||||
/**
|
||||
* 用于表示像素的 RGB 颜色
|
||||
* [r, g, b] 每个范围 0~255
|
||||
*/
|
||||
type Color = [number, number, number];
|
||||
|
||||
/*************************************************************************
|
||||
* 1. 使用 jpeg-js 解码 JPG,并抽取像素用于 K-means
|
||||
*************************************************************************/
|
||||
|
||||
/**
|
||||
* 从指定文件读取并解码 JPG 得到原始 RGBA 像素数据。
|
||||
* @param filePath
|
||||
* @returns { data: Uint8Array; width: number; height: number }
|
||||
*/
|
||||
async function decodeJpg(filePath: string) {
|
||||
global.logger.log(`[INFO] JPG读取中`);
|
||||
const jpgBuffer = await asyncFile.readArrayBuffer({ uri: filePath });
|
||||
// jpeg.decode 返回 { data: Buffer, width, height }
|
||||
// data 是 RGBA (width * height * 4)
|
||||
global.logger.log(`[INFO] 调用JPEG库解码JPG`);
|
||||
const decoded = jpeg.decode(jpgBuffer, { useTArray: true });
|
||||
return {
|
||||
data: decoded.data, // Uint8Array,顺序 RGBA
|
||||
width: decoded.width,
|
||||
height: decoded.height,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两种颜色(RGB)的欧几里得距离
|
||||
*/
|
||||
function distance(c1: Color, c2: Color): number {
|
||||
const dr = c1[0] - c2[0];
|
||||
const dg = c1[1] - c2[1];
|
||||
const db = c1[2] - c2[2];
|
||||
return Math.sqrt(dr * dr + dg * dg + db * db);
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
* 2. K-means 聚类核心
|
||||
*************************************************************************/
|
||||
|
||||
/**
|
||||
* 对给定像素集合执行 K-means 聚类,返回聚类中心(颜色)。
|
||||
* @param pixels 所有像素的 RGB 数组
|
||||
* @param k 聚类数量
|
||||
* @param maxIterations 迭代次数上限
|
||||
* @param tolerance 判断收敛的距离阈值
|
||||
* @returns {Color[]} k 个聚类中心(RGB)
|
||||
*/
|
||||
function kMeansCluster(
|
||||
pixels: Color[],
|
||||
k: number,
|
||||
maxIterations = 50,
|
||||
tolerance = 1.0
|
||||
): Color[] {
|
||||
// 1. 随机初始化 k 个聚类中心 (从像素中随机选 k 个)
|
||||
const centers: Color[] = [];
|
||||
for (let i = 0; i < k; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * pixels.length);
|
||||
centers.push([...pixels[randomIndex]] as Color);
|
||||
}
|
||||
|
||||
// 用于存储每次迭代时每个像素的所属聚类
|
||||
let assignments: number[] = new Array(pixels.length).fill(-1);
|
||||
|
||||
for (let iter = 0; iter < maxIterations; iter++) {
|
||||
// 记录旧中心,用于判断收敛
|
||||
const oldCenters = centers.map((c) => [...c]) as Color[];
|
||||
|
||||
// 2.1 为每个像素找到离自己最近的中心
|
||||
for (let pIndex = 0; pIndex < pixels.length; pIndex++) {
|
||||
let minDist = Infinity;
|
||||
let clusterIndex = 0;
|
||||
for (let cIndex = 0; cIndex < k; cIndex++) {
|
||||
const dist = distance(pixels[pIndex], centers[cIndex]);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
clusterIndex = cIndex;
|
||||
}
|
||||
}
|
||||
assignments[pIndex] = clusterIndex;
|
||||
}
|
||||
|
||||
// 2.2 重新计算每个聚类的中心(将同一个 cluster 里的所有像素平均值作为新的中心)
|
||||
const sums: Color[] = new Array(k).fill([0, 0, 0]).map(() => [0, 0, 0]);
|
||||
const counts: number[] = new Array(k).fill(0);
|
||||
|
||||
for (let pIndex = 0; pIndex < pixels.length; pIndex++) {
|
||||
const clusterIndex = assignments[pIndex];
|
||||
sums[clusterIndex][0] += pixels[pIndex][0];
|
||||
sums[clusterIndex][1] += pixels[pIndex][1];
|
||||
sums[clusterIndex][2] += pixels[pIndex][2];
|
||||
counts[clusterIndex]++;
|
||||
}
|
||||
|
||||
for (let cIndex = 0; cIndex < k; cIndex++) {
|
||||
if (counts[cIndex] > 0) {
|
||||
centers[cIndex][0] = sums[cIndex][0] / counts[cIndex];
|
||||
centers[cIndex][1] = sums[cIndex][1] / counts[cIndex];
|
||||
centers[cIndex][2] = sums[cIndex][2] / counts[cIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// 2.3 判断是否收敛:新旧中心之间的最大移动距离 < tolerance
|
||||
let maxShift = 0;
|
||||
for (let cIndex = 0; cIndex < k; cIndex++) {
|
||||
const shift = distance(centers[cIndex], oldCenters[cIndex]);
|
||||
if (shift > maxShift) {
|
||||
maxShift = shift;
|
||||
}
|
||||
}
|
||||
if (maxShift < tolerance) {
|
||||
// 收敛
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 返回最终的 k 个聚类中心(颜色分量取整)
|
||||
return centers.map(c => [
|
||||
Math.round(c[0]),
|
||||
Math.round(c[1]),
|
||||
Math.round(c[2])
|
||||
] as Color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取 3 个主要颜色主流程:
|
||||
* 1. 读取并解码图片 -> RGBA
|
||||
* 2. 抽取 (R,G,B) 像素
|
||||
* 3. 用 K-means 求得 3 个主要颜色
|
||||
*/
|
||||
async function extractTop3Colors(filePath: string): Promise<Color[]> {
|
||||
global.logger.log(`[INFO] decoding JPG: ${filePath}`);
|
||||
const { data, width, height } = await decodeJpg(filePath);
|
||||
|
||||
// data: RGBA,(width * height * 4)
|
||||
const sampleStep = Math.max(1, Math.floor((width * height) / 20000));
|
||||
|
||||
const pixels: Color[] = [];
|
||||
for (let i = 0; i < width * height; i += sampleStep) {
|
||||
const r = data[i * 4 + 0];
|
||||
const g = data[i * 4 + 1];
|
||||
const b = data[i * 4 + 2];
|
||||
// alpha 通常忽略
|
||||
pixels.push([r, g, b]);
|
||||
}
|
||||
|
||||
global.logger.log(`[INFO] Running K-means for 3 clusters (sample size = ${pixels.length})...`);
|
||||
const k = 3;
|
||||
const centers = kMeansCluster(pixels, k);
|
||||
|
||||
return centers;
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
* 3. 在新图上绘制 3 个「从圆心色 -> 背景透明」的圆,并保存为 JPG
|
||||
*************************************************************************/
|
||||
|
||||
/** 画布大小 */
|
||||
const CANVAS_WIDTH = 430;
|
||||
const CANVAS_HEIGHT = 250;
|
||||
|
||||
/** 背景颜色 */
|
||||
const BACKGROUND_COLOR: [number, number, number, number] = [0, 0, 0, 255];
|
||||
|
||||
/**
|
||||
* 圆心/半径的范围,可自行修改:
|
||||
* - 圆心距离画布边缘至少 MARGIN
|
||||
* - 半径范围 [MIN_RADIUS, MAX_RADIUS]
|
||||
*/
|
||||
const MARGIN = 2;
|
||||
const MIN_RADIUS = 80;
|
||||
const MAX_RADIUS = 110;
|
||||
|
||||
/**
|
||||
* 圆与圆之间的最小距离(基于圆心与圆心之间的距离减去半径之和)。
|
||||
* 如果希望**完全不重叠**,可令:
|
||||
* distanceBetweenCenters >= (r1 + r2 + MIN_GAP_BETWEEN_CIRCLES).
|
||||
*/
|
||||
const MIN_GAP_BETWEEN_CIRCLES = 20;
|
||||
|
||||
/**
|
||||
* 新增:圆心颜色亮度因子 (0~1),用于降低圆心过亮
|
||||
* 例如:0.8 -> 稍微暗一点;0.5 -> 明显暗
|
||||
*/
|
||||
const COLOR_BRIGHTNESS_FACTOR = 0.62;
|
||||
|
||||
/**
|
||||
* 对 RGBA 像素进行 alpha 混合:
|
||||
* outA = srcA + dstA * (1 - srcA)
|
||||
* outRGB = (srcRGB * srcA + dstRGB * dstA * (1 - srcA)) / outA
|
||||
*
|
||||
* data 数组中每像素 4 通道:R, G, B, A,值范围 [0,255]
|
||||
* alpha 范围 [0,1]
|
||||
*/
|
||||
function alphaBlendPixel(
|
||||
data: Uint8Array,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
color: Color,
|
||||
alpha: number
|
||||
) {
|
||||
const offset = (y * width + x) * 4;
|
||||
const dstR = data[offset + 0];
|
||||
const dstG = data[offset + 1];
|
||||
const dstB = data[offset + 2];
|
||||
const dstA = data[offset + 3] / 255; // 转为 0~1
|
||||
|
||||
const srcR = color[0];
|
||||
const srcG = color[1];
|
||||
const srcB = color[2];
|
||||
const srcA = alpha;
|
||||
|
||||
const outA = srcA + dstA * (1 - srcA);
|
||||
if (outA > 0) {
|
||||
const outR = (srcR * srcA + dstR * dstA * (1 - srcA)) / outA;
|
||||
const outG = (srcG * srcA + dstG * dstA * (1 - srcA)) / outA;
|
||||
const outB = (srcB * srcA + dstB * dstA * (1 - srcA)) / outA;
|
||||
|
||||
data[offset + 0] = Math.round(outR);
|
||||
data[offset + 1] = Math.round(outG);
|
||||
data[offset + 2] = Math.round(outB);
|
||||
data[offset + 3] = Math.round(outA * 255);
|
||||
} else {
|
||||
// outA=0 -> 全透明
|
||||
data[offset + 0] = 0;
|
||||
data[offset + 1] = 0;
|
||||
data[offset + 2] = 0;
|
||||
data[offset + 3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 data 上绘制一个「从 centerColor->背景透明」的径向渐变圆
|
||||
* @param data 画布(RGBA)
|
||||
* @param width, height 画布尺寸
|
||||
* @param cx, cy 圆心坐标
|
||||
* @param radius 半径
|
||||
* @param centerColor 圆心处的颜色 (RGB)
|
||||
*/
|
||||
function drawRadialGradientCircle(
|
||||
data: Uint8Array,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
radius: number,
|
||||
centerColor: Color
|
||||
) {
|
||||
// 仅在圆的外接矩形范围内绘制
|
||||
const minX = Math.max(0, cx - radius);
|
||||
const maxX = Math.min(width - 1, cx + radius);
|
||||
const minY = Math.max(0, cy - radius);
|
||||
const maxY = Math.min(height - 1, cy + radius);
|
||||
|
||||
for (let y = minY; y <= maxY; y++) {
|
||||
for (let x = minX; x <= maxX; x++) {
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist <= radius) {
|
||||
// 根据距离,计算该点的 alpha:
|
||||
// 圆心 dist=0 -> alpha=1
|
||||
// 半径 dist=radius -> alpha=0
|
||||
const alpha = 1 - dist / radius;
|
||||
alphaBlendPixel(data, x, y, width, centerColor, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数:判断新圆 (cx, cy, r) 与已放置的圆数组 placedCircles
|
||||
* 是否满足“互不重叠”,即:
|
||||
* distanceBetweenCenters >= (r + oldCircle.r + MIN_GAP_BETWEEN_CIRCLES)
|
||||
*/
|
||||
function canPlaceCircle(
|
||||
cx: number,
|
||||
cy: number,
|
||||
r: number,
|
||||
placedCircles: { cx: number, cy: number, r: number }[],
|
||||
minGap: number
|
||||
): boolean {
|
||||
for (const c of placedCircles) {
|
||||
const dist = Math.sqrt((cx - c.cx) ** 2 + (cy - c.cy) ** 2);
|
||||
// 如果距离 < 两个半径和 + minGap,就判定为重叠
|
||||
if (dist < (r + c.r + minGap)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成画布并绘制 3 个不会彼此重叠的渐变圆
|
||||
* @param colors 3 个原始颜色 (RGB)
|
||||
*/
|
||||
function generateCirclesImage(colors: Color[]): { data: Uint8Array; width: number; height: number } {
|
||||
const width = CANVAS_WIDTH;
|
||||
const height = CANVAS_HEIGHT;
|
||||
|
||||
// 1. 创建画布并填充背景
|
||||
const data = new Uint8Array(width * height * 4);
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
data[i * 4 + 0] = BACKGROUND_COLOR[0];
|
||||
data[i * 4 + 1] = BACKGROUND_COLOR[1];
|
||||
data[i * 4 + 2] = BACKGROUND_COLOR[2];
|
||||
data[i * 4 + 3] = BACKGROUND_COLOR[3];
|
||||
}
|
||||
|
||||
// 2. 依次为 3 个颜色生成圆,确保不重叠
|
||||
const placedCircles: { cx: number; cy: number; r: number }[] = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
let circlePlaced = false;
|
||||
|
||||
// 最多尝试多次,若都无法放置,就跳过
|
||||
const MAX_TRIES = 100;
|
||||
|
||||
const radius = randInt(MIN_RADIUS, MAX_RADIUS);
|
||||
var cx = 0;
|
||||
var cy = 0;
|
||||
for (let attempt = 0; attempt < MAX_TRIES; attempt++) {
|
||||
cx = randInt(radius + MARGIN, width - radius - MARGIN);
|
||||
cy = randInt(radius + MARGIN, height - radius - MARGIN);
|
||||
|
||||
if (canPlaceCircle(cx, cy, radius, placedCircles, MIN_GAP_BETWEEN_CIRCLES)) {
|
||||
// 找到不重叠的位置了
|
||||
placedCircles.push({ cx, cy, r: radius });
|
||||
circlePlaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!circlePlaced) {
|
||||
global.logger.warn(
|
||||
`[WARN] 无法在 ${MAX_TRIES} 次随机尝试中给第${i + 1}个圆找到合适位置,但仍然绘制。`
|
||||
);
|
||||
placedCircles.push({ cx, cy, r: radius });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 把成功放置的圆绘制到画布
|
||||
// 在这里应用 COLOR_BRIGHTNESS_FACTOR,降低每个圆心的RGB
|
||||
placedCircles.forEach((c, idx) => {
|
||||
// 取对应颜色(若 idx>=colors.length,可加保护)
|
||||
const origColor = colors[idx] || [255, 255, 255];
|
||||
// 调整亮度
|
||||
const adjustedColor: Color = [
|
||||
Math.min(255, Math.max(0, origColor[0] * COLOR_BRIGHTNESS_FACTOR)),
|
||||
Math.min(255, Math.max(0, origColor[1] * COLOR_BRIGHTNESS_FACTOR)),
|
||||
Math.min(255, Math.max(0, origColor[2] * COLOR_BRIGHTNESS_FACTOR)),
|
||||
];
|
||||
|
||||
global.logger.log(
|
||||
`[INFO] Circle #${idx + 1}: center=(${c.cx},${c.cy}), radius=${c.r}, color=${adjustedColor}`
|
||||
);
|
||||
drawRadialGradientCircle(data, width, height, c.cx, c.cy, c.r, adjustedColor);
|
||||
});
|
||||
|
||||
return { data, width, height };
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 RGBA 数据保存为 JPG 文件
|
||||
*/
|
||||
async function saveAsJpeg(data: Uint8Array, width: number, height: number, outPath: string) {
|
||||
const rawImageData = {
|
||||
data: Buffer.from(data), // jpeg-js 需要 Buffer 类型
|
||||
width,
|
||||
height,
|
||||
};
|
||||
// 设定 jpg 质量
|
||||
const jpegImageData = jpeg.encode(rawImageData, 80);
|
||||
await asyncFile.writeArrayBuffer({
|
||||
uri: outPath,
|
||||
buffer: jpegImageData.data
|
||||
});
|
||||
global.logger.log(`[INFO] Saved: ${outPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具函数:在 [min, max] 范围内随机整数
|
||||
*/
|
||||
function randInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用Kmeans取色并生成最多三个渐变色圆形的blur背景图
|
||||
*/
|
||||
export async function kmeansCircleGradientImage(imgPath: string) {
|
||||
global.logger.log(`[kmeansCircleGradientImage] 开始处理图片${imgPath}`)
|
||||
|
||||
// 提取 3 个主色
|
||||
const top3Colors = await extractTop3Colors(imgPath);
|
||||
global.logger.log(`\n[kmeansCircleGradientImage] 主要颜色 (RGB):`);
|
||||
top3Colors.forEach((c, idx) => {
|
||||
global.logger.log(` #${idx + 1}: R=${c[0]}, G=${c[1]}, B=${c[2]}`);
|
||||
});
|
||||
|
||||
global.logger.log(`[kmeansCircleGradientImage] 绘制图片取色模糊...`)
|
||||
|
||||
// 用这 3 个颜色在 400×400 的画布上绘制不重叠的渐变圆
|
||||
const { data, width, height } = generateCirclesImage(top3Colors);
|
||||
|
||||
global.logger.log(`[kmeansCircleGradientImage] 输出result图片...`)
|
||||
|
||||
// 保存结果为 JPG
|
||||
const outPath = 'internal://files/km-gen-tmp.jpg';
|
||||
await saveAsJpeg(data, width, height, outPath);
|
||||
|
||||
global.logger.log(`[kmeansCircleGradientImage] 完成工作`)
|
||||
}
|
||||
@@ -121,6 +121,9 @@
|
||||
},
|
||||
"pages/app/features/settings/opensoftware": {
|
||||
"component": "opensoftware"
|
||||
},
|
||||
"pages/user": {
|
||||
"component": "user"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,3 @@
|
||||
.profdiv {
|
||||
width: 176px;
|
||||
/* 设置为与 profpendantimg 一致 */
|
||||
height: 176px;
|
||||
/* 设置为与 profpendantimg 一致 */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profimg {
|
||||
width: 106px;
|
||||
height: 106px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.profpendantimg {
|
||||
width: 176px;
|
||||
height: 176px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vipname {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #ff7da8;
|
||||
}
|
||||
|
||||
.tagbar {
|
||||
flex-direction: row;
|
||||
width: 40%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tagimg {
|
||||
height: 30px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.btnbase {
|
||||
width: 314px;
|
||||
height: 93px;
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
<import name="switch-bar" src="@components/SwitchBar/SwitchBar.ux"></import>
|
||||
<import name="user-info" src="@components/UserInfo/UserInfo.ux"></import>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<switch-bar title="mypage.title"></switch-bar>
|
||||
<div class="profdiv">
|
||||
<image src="{{accountInfo.face}}@106w_106h" alt="/common/fullgray.png" class="profimg"></image>
|
||||
<image src="{{accountInfo.pendant.image}}@176w_176h" alt="/common/alphaimg.png" class="profpendantimg"></image>
|
||||
</div>
|
||||
<text class="{{name_text_class}}">{{ accountInfo.uname }}</text>
|
||||
<div class="tagbar" style="margin-top: 5px">
|
||||
<image class="tagimg" src="/common/bililevel/lv{{accountInfo.level_info.current_level}}.png" style="height: 20px">
|
||||
</image>
|
||||
<image class="tagimg" if="{{is_vip}}" style="margin-left: 10px"
|
||||
src="/common/bilivip/{{accountInfo.vip_label.label_theme}}.png"></image>
|
||||
<image class="tagimg" if="{{is_senior_member}}" style="margin-left: 10px" src="/common/senior_member.png"></image>
|
||||
</div>
|
||||
<user-info account-info="{{accountInfo}}"></user-info>
|
||||
<div class="btnbase" style="margin-top: 40px" @click="GoMyFavFolders()">
|
||||
<image class="btnimg" src="/common/mypage_star.png"></image>
|
||||
<text class="btntext">{{ $t("mypage.favourite") }}</text>
|
||||
@@ -44,9 +34,6 @@ import router from "@system.router"
|
||||
import prompt from "@system.prompt"
|
||||
export default {
|
||||
private: {
|
||||
name_text_class: "name",
|
||||
is_vip: false,
|
||||
is_senior_member: false,
|
||||
accountInfo: {}
|
||||
},
|
||||
GoHistory(){
|
||||
@@ -72,18 +59,8 @@ export default {
|
||||
message: "该功能还在开发中"
|
||||
})
|
||||
},
|
||||
UpdateShow() {
|
||||
if (this.accountInfo.vip_pay_type) {
|
||||
this.is_vip = true
|
||||
this.name_text_class = "vipname"
|
||||
}
|
||||
if (this.accountInfo.is_senior_member) {
|
||||
this.is_senior_member = true
|
||||
}
|
||||
},
|
||||
onInit() {
|
||||
this.accountInfo = global.biliclient.accountInfo
|
||||
this.UpdateShow()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
}
|
||||
|
||||
.user {
|
||||
margin-top: 10px;
|
||||
height: 119px;
|
||||
border-radius: 33px;
|
||||
background-color: #222222;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.replylist {
|
||||
width: 77%;
|
||||
height: 75%;
|
||||
height: 77%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
.swipermain {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 97%;
|
||||
}
|
||||
|
||||
.tiplabel {
|
||||
|
||||
@@ -25,14 +25,14 @@
|
||||
<div class="tiplabel" if="{{comprehensive_status.has_users_result}}">
|
||||
<text class="listtip">用户</text>
|
||||
</div>
|
||||
<div style="width: 100%; height: 99px; justify-content: center"
|
||||
<div style="width: 100%; justify-content: center"
|
||||
if="{{comprehensive_status.has_users_result}}">
|
||||
<user user="{{search.users[0]}}"></user>
|
||||
</div>
|
||||
<div class="tiplabel" style="margin-top: 10px" if="{{comprehensive_status.has_video_result}}">
|
||||
<text class="listtip">视频</text>
|
||||
</div>
|
||||
<div style="width: 100%; height: 170px; justify-content: center"
|
||||
<div style="width: 100%; justify-content: center"
|
||||
if="{{comprehensive_status.has_video_result}}" for="{{search.comprehensive_videos}}">
|
||||
<div style="width: 77%; justify-content: center; margin-top: 5px" onclick="OpenVideo($item.bvid)">
|
||||
<videoshow video="{{$item}}"></videoshow>
|
||||
@@ -43,7 +43,7 @@
|
||||
</div>
|
||||
<div class="swiper_box">
|
||||
<scroll if="{{typebar_if.c2}}" class="swiper_container" scroll-y="true" @scroll="ScrollOverviewCheck">
|
||||
<div style="width: 100%; height: 170px; justify-content: center" for="{{search.videos}}">
|
||||
<div style="width: 100%; justify-content: center" for="{{search.videos}}">
|
||||
<div style="width: 77%; justify-content: center; margin-top: 5px" onclick="OpenVideo($item.bvid)">
|
||||
<videoshow video="{{$item}}"></videoshow>
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
<div class="swiper_box">
|
||||
<scroll if="{{typebar_if.c3}}" class="swiper_container" scroll-y="true" @scroll="ScrollOverviewCheck">
|
||||
<div style="width: 100%; height: 99px; justify-content: center; margin-top: 5px" for="{{search.users}}">
|
||||
<div style="width: 100%; justify-content: center; margin-top: 5px" for="{{search.users}}">
|
||||
<user user="{{$item}}"></user>
|
||||
</div>
|
||||
<text style="margin-top: 30px"></text>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<import name="title-bar" src="@components/TitleBar/TitleBar.ux"></import>
|
||||
<import name="user-info" src="@components/UserInfo/UserInfo.ux"></import>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<title-bar title="user.title"></title-bar>
|
||||
<user-info account-info="{{user}}" if="{{loaded}}"></user-info>
|
||||
<image style="position: absolute; margin-top: 245px" if="{{anims.show_loading}}" src="{{anims.loading_src.value}}"></image>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
public: {
|
||||
uid: 1
|
||||
},
|
||||
private: {
|
||||
user: {},
|
||||
anims: {
|
||||
show_loading: false
|
||||
},
|
||||
loaded: false
|
||||
},
|
||||
async LoadUserInfo(){
|
||||
this.anims.loading.start()
|
||||
this.user = await global.biliclient.getUserInfoByUID(this.uid)
|
||||
this.anims.loading.stop()
|
||||
this.anims.show_loading = false
|
||||
this.loaded = true
|
||||
},
|
||||
onInit(){
|
||||
global.animengine.defaults.loading.CreateLoadingAnimation(this)
|
||||
this.LoadUserInfo()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import '@less/global.less';
|
||||
@import './user.less';
|
||||
</style>
|
||||
@@ -1,10 +1,3 @@
|
||||
.vidpic {
|
||||
border-radius: 20px;
|
||||
width: 320px;
|
||||
height: 150px;
|
||||
margin-top: 31px;
|
||||
}
|
||||
|
||||
.vidtitle {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<import name="title-bar" src="@components/TitleBar/TitleBar.ux"></import>
|
||||
<import name="full-screen-input" src="@components/FullScreenInput/FullScreenInput.ux"></import>
|
||||
<import name="default-button" src="@components/DefaultButton/DefaultButton.ux"></import>
|
||||
<import name="online-image" src="@components/BetterOnlineImage/BetterOnlineImage.ux"></import>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<scroll scroll-y="true" class="scroll" bounces="true">
|
||||
<title-bar title="video.title"></title-bar>
|
||||
<image src="{{vid.pic}}@319w_150h" alt="/common/grayloading_cn.png" class="vidpic"></image>
|
||||
<online-image if="{{renderingCover}}" src="{{vid.pic}}@319w_150h" margin-top="30px" lock-size="true" width="320px" height="150px" loading-anim="true" @loaded="GenBlur"></online-image>
|
||||
<text class="vidtitle">{{ vid.title }}</text>
|
||||
<div class="statbar">
|
||||
<div class="stat">
|
||||
@@ -62,6 +63,8 @@
|
||||
import router from "@system.router"
|
||||
import prompt from "@system.prompt"
|
||||
|
||||
import { kmeansCircleGradientImage } from "@src/image/jpegkmeans.ts"
|
||||
|
||||
export default {
|
||||
public: {
|
||||
bvid: 1 // 视频BVID
|
||||
@@ -74,7 +77,8 @@ export default {
|
||||
likesrc: "/common/vidtool_like.png",
|
||||
coinsrc: "/common/vidtool_coin.png",
|
||||
starsrc: "/common/vidtool_star.png",
|
||||
replymode: false
|
||||
replymode: false,
|
||||
renderingCover: false
|
||||
},
|
||||
onInit() {
|
||||
global.logger.log("[videodetail] load bvid: " + this.bvid);
|
||||
@@ -88,6 +92,7 @@ export default {
|
||||
},
|
||||
async GetVideoDetail() {
|
||||
this.vid = await global.biliclient.getVideoInfoByBVID(this.bvid)
|
||||
this.renderingCover = true
|
||||
},
|
||||
async UpdateVideoToolbarStatus() {
|
||||
this.stared = await global.biliclient.isVideoStaredByBVID(this.bvid)
|
||||
@@ -176,6 +181,13 @@ export default {
|
||||
prompt.showToast({message})
|
||||
this.UpdateVideoToolbarStatus()
|
||||
},
|
||||
async GenBlur(evt){
|
||||
// 该Feature在现在任何设备上运行都会卡死,故注释
|
||||
/*
|
||||
global.logger.log(`\n[GenBlur] 开始处理封面取色模糊`);
|
||||
await kmeansCircleGradientImage(evt.detail.uri)
|
||||
*/
|
||||
},
|
||||
GoReplyArea() {
|
||||
router.push({
|
||||
uri: "pages/reply/replys",
|
||||
|
||||
+11
-60
@@ -1,4 +1,4 @@
|
||||
import { file } from "./tsimports";
|
||||
import { asyncFile } from "./asyncapi/file";
|
||||
|
||||
// 定义文件存储的基本结构
|
||||
interface StoredContent {
|
||||
@@ -15,64 +15,15 @@ let storageIndex: StoredContent[] = [];
|
||||
const baseUri = 'internal://files/bilisavedcontent/';
|
||||
const indexFileUri = `${baseUri}index.json`; // 存储 storageIndex 的文件
|
||||
|
||||
// 封装 file 接口的 Promise 方法
|
||||
function fileWrite(uri: string, data: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.writeText({
|
||||
uri,
|
||||
text: data,
|
||||
success: () => resolve(),
|
||||
fail: (data, code) => reject(`Failed to write to ${uri}: ${code}`)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fileRead(uri: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.readText({
|
||||
uri,
|
||||
success: (data) => resolve(data.text),
|
||||
fail: (data, code) => reject(`Failed to read from ${uri}: ${code}`)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fileList(dirUri: string): Promise<any[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.list({
|
||||
uri: dirUri,
|
||||
success: (data) => resolve(data.fileList),
|
||||
fail: (data, code) => reject(`Failed to list files in ${dirUri}: ${code}`)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fileAccess(uri: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
file.access({
|
||||
uri,
|
||||
success: () => resolve(true),
|
||||
fail: () => resolve(false)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fileDelete(uri: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
file.delete({
|
||||
uri,
|
||||
success: () => resolve(),
|
||||
fail: (data, code) => reject(`Failed to delete ${uri}: ${code}`)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 同步 storageIndex 到本地文件
|
||||
async function saveStorageIndex(): Promise<void> {
|
||||
try {
|
||||
global.logger.log(storageIndex)
|
||||
const indexData = JSON.stringify(storageIndex);
|
||||
await fileWrite(indexFileUri, indexData);
|
||||
await asyncFile.writeText({
|
||||
uri: indexFileUri,
|
||||
text: indexData
|
||||
});
|
||||
} catch (e) {
|
||||
global.logger.error(`[SavedContentManager] saveStorageIndex Error: ${e.toString()}`);
|
||||
}
|
||||
@@ -81,9 +32,9 @@ async function saveStorageIndex(): Promise<void> {
|
||||
// 从本地文件加载 storageIndex
|
||||
async function loadStorageIndex(): Promise<void> {
|
||||
try {
|
||||
const fileExists = await fileAccess(indexFileUri);
|
||||
const fileExists = await asyncFile.access({ uri: indexFileUri });
|
||||
if (fileExists) {
|
||||
const indexData = await fileRead(indexFileUri);
|
||||
const indexData = await asyncFile.readText({ uri: indexFileUri });
|
||||
storageIndex = JSON.parse(indexData);
|
||||
} else {
|
||||
storageIndex = [];
|
||||
@@ -116,7 +67,7 @@ export class SavedContentManager {
|
||||
const fileUri = `${baseUri}${id}.txt`;
|
||||
|
||||
// 写入文件
|
||||
await fileWrite(fileUri, data);
|
||||
await asyncFile.writeText({ uri: fileUri, text: data });
|
||||
|
||||
// 更新 storageIndex 并保存
|
||||
storageIndex.push({ id, title, type, fileUri });
|
||||
@@ -135,10 +86,10 @@ export class SavedContentManager {
|
||||
if (!content) return null;
|
||||
|
||||
// 读取文件内容
|
||||
const fileExists = await fileAccess(content.fileUri);
|
||||
const fileExists = await asyncFile.access({ uri: content.fileUri });
|
||||
if (!fileExists) throw new Error(`File does not exist: ${content.fileUri}`);
|
||||
|
||||
return await fileRead(content.fileUri);
|
||||
return await asyncFile.readText({ uri: content.fileUri });
|
||||
} catch (e) {
|
||||
global.logger.error(`[SavedContentManager] getContent Error: ${e.toString()}`);
|
||||
return null;
|
||||
@@ -158,7 +109,7 @@ export class SavedContentManager {
|
||||
|
||||
// 删除文件
|
||||
const fileUri = storageIndex[contentIndex].fileUri;
|
||||
await fileDelete(fileUri);
|
||||
await asyncFile.delete({ uri: fileUri });
|
||||
|
||||
// 从 storageIndex 中删除记录并保存
|
||||
storageIndex.splice(contentIndex, 1);
|
||||
|
||||
+16
-4
@@ -1,6 +1,8 @@
|
||||
import dayjs from "dayjs"
|
||||
import { fetch } from "./tsimports"
|
||||
|
||||
const TRACKER_URL: string = "https://tracker.hyperbili.astralsight.space/trackreport"
|
||||
//const TRACKER_URL: string = "https://tracker.hyperbili.astralsight.space/trackreport"
|
||||
const TRACKER_URL: string = "http://192.168.1.247:4080/trackreport"
|
||||
const TRACKER_SERVER_PROTOCOL_VERSION = "v1"
|
||||
|
||||
interface TrackedInfoUploadPacket {
|
||||
@@ -36,7 +38,7 @@ interface TIUPStartupPayload {
|
||||
interface TIUPRouterPayload {
|
||||
currentPage: string,
|
||||
nextPage: string,
|
||||
routerStack: string
|
||||
routerStack: Array<String>
|
||||
}
|
||||
|
||||
interface TIUPLoginPayload {
|
||||
@@ -77,7 +79,17 @@ function uploadTrack(event: TIUPEvents, payload) {
|
||||
if (canUploadTrack()) {
|
||||
fetch.fetch({
|
||||
url: `${TRACKER_URL}/${TRACKER_SERVER_PROTOCOL_VERSION}/fetch`,
|
||||
data
|
||||
method: "POST",
|
||||
data: JSON.stringify(data),
|
||||
header: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
uploadTrack(TIUPEvents.ON_STARTUP, {
|
||||
startTime: dayjs().format()
|
||||
})
|
||||
}, 3000)
|
||||
@@ -4957,6 +4957,11 @@ jackspeak@^3.1.2:
|
||||
optionalDependencies:
|
||||
"@pkgjs/parseargs" "^0.11.0"
|
||||
|
||||
jpeg-js@^0.4.4:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
|
||||
integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
||||
Reference in New Issue
Block a user