新增tracker&视频封面实时模糊(未启用)&搜索页面改进

This commit is contained in:
Searchstars
2025-01-01 13:54:21 +08:00
parent f3a5d15ca9
commit 5c00120074
22 changed files with 1176 additions and 80 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
/.nyc_output /.nyc_output
/coverage /coverage
/node_modules **/node_modules/
/sign /sign
/dist /dist
/build /build
+1
View File
@@ -55,6 +55,7 @@
}, },
"dependencies": { "dependencies": {
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"jpeg-js": "^0.4.4",
"semver": "^7.6.3" "semver": "^7.6.3"
} }
} }
+422
View File
@@ -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
View File
@@ -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"
}
}
+1
View File
@@ -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

+1
View File
@@ -33,6 +33,7 @@ module.exports = {
}, },
resolve: { resolve: {
alias: { alias: {
'@src': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'), '@components': path.resolve(__dirname, 'src/components'),
'@less': path.resolve(__dirname, 'src/less'), '@less': path.resolve(__dirname, 'src/less'),
'@protobuf': path.resolve(__dirname, 'src/protobuf'), '@protobuf': path.resolve(__dirname, 'src/protobuf'),
+1
View File
@@ -13,6 +13,7 @@ import * as jumpcheck from "./jumpcheck"
import * as htmlparser from "./htmlparser" import * as htmlparser from "./htmlparser"
import * as savedcontent from "./savedcontent" import * as savedcontent from "./savedcontent"
import * as bgimg from "./bgimg" import * as bgimg from "./bgimg"
import * as usertracker from "./usertracker"
import logger from "./logger/logger" import logger from "./logger/logger"
export default { export default {
dayjs: dayjs, dayjs: dayjs,
+187
View File
@@ -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();
+58
View File
@@ -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();
@@ -85,6 +85,9 @@ export default {
token: data.token, token: data.token,
success: (data) => { success: (data) => {
global.logger.log("[BetterOnlineImage] Image Saved in: " + data.uri) global.logger.log("[BetterOnlineImage] Image Saved in: " + data.uri)
this.$emit('loaded', {
uri: data.uri
})
this.localsrc = data.uri this.localsrc = data.uri
this.show_loading = false this.show_loading = false
this.show_image = true this.show_image = true
+419
View File
@@ -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] 完成工作`)
}
@@ -20,7 +20,7 @@
.swipermain { .swipermain {
width: 100%; width: 100%;
height: 100%; height: 97%;
} }
.tiplabel { .tiplabel {
@@ -25,14 +25,14 @@
<div class="tiplabel" if="{{comprehensive_status.has_users_result}}"> <div class="tiplabel" if="{{comprehensive_status.has_users_result}}">
<text class="listtip">用户</text> <text class="listtip">用户</text>
</div> </div>
<div style="width: 100%; height: 99px; justify-content: center" <div style="width: 100%; justify-content: center"
if="{{comprehensive_status.has_users_result}}"> if="{{comprehensive_status.has_users_result}}">
<user user="{{search.users[0]}}"></user> <user user="{{search.users[0]}}"></user>
</div> </div>
<div class="tiplabel" style="margin-top: 10px" if="{{comprehensive_status.has_video_result}}"> <div class="tiplabel" style="margin-top: 10px" if="{{comprehensive_status.has_video_result}}">
<text class="listtip">视频</text> <text class="listtip">视频</text>
</div> </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}}"> if="{{comprehensive_status.has_video_result}}" for="{{search.comprehensive_videos}}">
<div style="width: 77%; justify-content: center; margin-top: 5px" onclick="OpenVideo({{$item.bvid}})"> <div style="width: 77%; justify-content: center; margin-top: 5px" onclick="OpenVideo({{$item.bvid}})">
<videoshow video="{{$item}}"></videoshow> <videoshow video="{{$item}}"></videoshow>
@@ -43,7 +43,7 @@
</div> </div>
<div class="swiper_box"> <div class="swiper_box">
<scroll if="{{typebar_if.c2}}" class="swiper_container" scroll-y="true" @scroll="ScrollOverviewCheck"> <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}})"> <div style="width: 77%; justify-content: center; margin-top: 5px" onclick="OpenVideo({{$item.bvid}})">
<videoshow video="{{$item}}"></videoshow> <videoshow video="{{$item}}"></videoshow>
</div> </div>
@@ -53,7 +53,7 @@
</div> </div>
<div class="swiper_box"> <div class="swiper_box">
<scroll if="{{typebar_if.c3}}" class="swiper_container" scroll-y="true" @scroll="ScrollOverviewCheck"> <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> <user user="{{$item}}"></user>
</div> </div>
<text style="margin-top: 30px"></text> <text style="margin-top: 30px"></text>
@@ -1,10 +1,3 @@
.vidpic {
border-radius: 20px;
width: 320px;
height: 150px;
margin-top: 31px;
}
.vidtitle { .vidtitle {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
+14 -2
View File
@@ -1,12 +1,13 @@
<import name="title-bar" src="@components/TitleBar/TitleBar.ux"></import> <import name="title-bar" src="@components/TitleBar/TitleBar.ux"></import>
<import name="full-screen-input" src="@components/FullScreenInput/FullScreenInput.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="default-button" src="@components/DefaultButton/DefaultButton.ux"></import>
<import name="online-image" src="@components/BetterOnlineImage/BetterOnlineImage.ux"></import>
<template> <template>
<div class="page"> <div class="page">
<scroll scroll-y="true" class="scroll" bounces="true"> <scroll scroll-y="true" class="scroll" bounces="true">
<title-bar title="video.title"></title-bar> <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> <text class="vidtitle">{{ vid.title }}</text>
<div class="statbar"> <div class="statbar">
<div class="stat"> <div class="stat">
@@ -62,6 +63,8 @@
import router from "@system.router" import router from "@system.router"
import prompt from "@system.prompt" import prompt from "@system.prompt"
import { kmeansCircleGradientImage } from "@src/image/jpegkmeans.ts"
export default { export default {
public: { public: {
bvid: 1 // 视频BVID bvid: 1 // 视频BVID
@@ -74,7 +77,8 @@ export default {
likesrc: "/common/vidtool_like.png", likesrc: "/common/vidtool_like.png",
coinsrc: "/common/vidtool_coin.png", coinsrc: "/common/vidtool_coin.png",
starsrc: "/common/vidtool_star.png", starsrc: "/common/vidtool_star.png",
replymode: false replymode: false,
renderingCover: false
}, },
onInit() { onInit() {
global.logger.log("[videodetail] load bvid: " + this.bvid); global.logger.log("[videodetail] load bvid: " + this.bvid);
@@ -88,6 +92,7 @@ export default {
}, },
async GetVideoDetail() { async GetVideoDetail() {
this.vid = await global.biliclient.getVideoInfoByBVID(this.bvid) this.vid = await global.biliclient.getVideoInfoByBVID(this.bvid)
this.renderingCover = true
}, },
async UpdateVideoToolbarStatus() { async UpdateVideoToolbarStatus() {
this.stared = await global.biliclient.isVideoStaredByBVID(this.bvid) this.stared = await global.biliclient.isVideoStaredByBVID(this.bvid)
@@ -176,6 +181,13 @@ export default {
prompt.showToast({message}) prompt.showToast({message})
this.UpdateVideoToolbarStatus() this.UpdateVideoToolbarStatus()
}, },
async GenBlur(evt){
// 该Feature在现在任何设备上运行都会卡死,故注释
/*
global.logger.log(`\n[GenBlur] 开始处理封面取色模糊`);
await kmeansCircleGradientImage(evt.detail.uri)
*/
},
GoReplyArea() { GoReplyArea() {
router.push({ router.push({
uri: "pages/reply/replys", uri: "pages/reply/replys",
+11 -60
View File
@@ -1,4 +1,4 @@
import { file } from "./tsimports"; import { asyncFile } from "./asyncapi/file";
// 定义文件存储的基本结构 // 定义文件存储的基本结构
interface StoredContent { interface StoredContent {
@@ -15,64 +15,15 @@ let storageIndex: StoredContent[] = [];
const baseUri = 'internal://files/bilisavedcontent/'; const baseUri = 'internal://files/bilisavedcontent/';
const indexFileUri = `${baseUri}index.json`; // 存储 storageIndex 的文件 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 到本地文件 // 同步 storageIndex 到本地文件
async function saveStorageIndex(): Promise<void> { async function saveStorageIndex(): Promise<void> {
try { try {
global.logger.log(storageIndex) global.logger.log(storageIndex)
const indexData = JSON.stringify(storageIndex); const indexData = JSON.stringify(storageIndex);
await fileWrite(indexFileUri, indexData); await asyncFile.writeText({
uri: indexFileUri,
text: indexData
});
} catch (e) { } catch (e) {
global.logger.error(`[SavedContentManager] saveStorageIndex Error: ${e.toString()}`); global.logger.error(`[SavedContentManager] saveStorageIndex Error: ${e.toString()}`);
} }
@@ -81,9 +32,9 @@ async function saveStorageIndex(): Promise<void> {
// 从本地文件加载 storageIndex // 从本地文件加载 storageIndex
async function loadStorageIndex(): Promise<void> { async function loadStorageIndex(): Promise<void> {
try { try {
const fileExists = await fileAccess(indexFileUri); const fileExists = await asyncFile.access({ uri: indexFileUri });
if (fileExists) { if (fileExists) {
const indexData = await fileRead(indexFileUri); const indexData = await asyncFile.readText({ uri: indexFileUri });
storageIndex = JSON.parse(indexData); storageIndex = JSON.parse(indexData);
} else { } else {
storageIndex = []; storageIndex = [];
@@ -116,7 +67,7 @@ export class SavedContentManager {
const fileUri = `${baseUri}${id}.txt`; const fileUri = `${baseUri}${id}.txt`;
// 写入文件 // 写入文件
await fileWrite(fileUri, data); await asyncFile.writeText({ uri: fileUri, text: data });
// 更新 storageIndex 并保存 // 更新 storageIndex 并保存
storageIndex.push({ id, title, type, fileUri }); storageIndex.push({ id, title, type, fileUri });
@@ -135,10 +86,10 @@ export class SavedContentManager {
if (!content) return null; 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}`); 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) { } catch (e) {
global.logger.error(`[SavedContentManager] getContent Error: ${e.toString()}`); global.logger.error(`[SavedContentManager] getContent Error: ${e.toString()}`);
return null; return null;
@@ -158,7 +109,7 @@ export class SavedContentManager {
// 删除文件 // 删除文件
const fileUri = storageIndex[contentIndex].fileUri; const fileUri = storageIndex[contentIndex].fileUri;
await fileDelete(fileUri); await asyncFile.delete({ uri: fileUri });
// 从 storageIndex 中删除记录并保存 // 从 storageIndex 中删除记录并保存
storageIndex.splice(contentIndex, 1); storageIndex.splice(contentIndex, 1);
+9 -2
View File
@@ -1,7 +1,8 @@
import dayjs from "dayjs" import dayjs from "dayjs"
import { fetch } from "./tsimports" 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" const TRACKER_SERVER_PROTOCOL_VERSION = "v1"
interface TrackedInfoUploadPacket { interface TrackedInfoUploadPacket {
@@ -78,11 +79,17 @@ function uploadTrack(event: TIUPEvents, payload) {
if (canUploadTrack()) { if (canUploadTrack()) {
fetch.fetch({ fetch.fetch({
url: `${TRACKER_URL}/${TRACKER_SERVER_PROTOCOL_VERSION}/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, { uploadTrack(TIUPEvents.ON_STARTUP, {
startTime: dayjs().format() startTime: dayjs().format()
}) })
}, 3000)
+5
View File
@@ -5066,6 +5066,11 @@ jest-worker@^27.4.5:
merge-stream "^2.0.0" merge-stream "^2.0.0"
supports-color "^8.0.0" supports-color "^8.0.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: js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"