Rewrite frontend as single self-contained HTML file — all CSS/JS inline, no external files to fail loading
This commit is contained in:
37
dist/server/services/jobManager.d.ts
vendored
Normal file
37
dist/server/services/jobManager.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Job, OutputOptions } from '../db/jobStore';
|
||||
import { Config } from '../../config/config';
|
||||
import { AudioSegment } from '../../interfaces';
|
||||
interface ProgressData {
|
||||
id: string;
|
||||
status: string;
|
||||
progress: number;
|
||||
currentIndex: number;
|
||||
totalUnits: number;
|
||||
segments: AudioSegment[];
|
||||
error: string | null;
|
||||
output_audio: string | null;
|
||||
output_subtitles_srt: string | null;
|
||||
output_subtitles_vtt: string | null;
|
||||
output_muxed: string | null;
|
||||
}
|
||||
export declare class JobManager {
|
||||
private queue;
|
||||
private processing;
|
||||
private pausedJobs;
|
||||
private emitter;
|
||||
private pollInterval;
|
||||
constructor();
|
||||
private recoverStuckJobs;
|
||||
createJob(videoPath: string, configOverride?: Partial<Config>, outputOptions?: Partial<OutputOptions>): Job;
|
||||
startJob(jobId: string): Promise<void>;
|
||||
pauseJob(jobId: string): Promise<void>;
|
||||
restartJob(jobId: string): Promise<void>;
|
||||
cancelJob(jobId: string): Promise<void>;
|
||||
deleteJob(jobId: string): void;
|
||||
listJobs(): Job[];
|
||||
onJobProgress(jobId: string, callback: (data: ProgressData) => void): () => void;
|
||||
private emitProgress;
|
||||
private processNext;
|
||||
private processJob;
|
||||
}
|
||||
export {};
|
||||
239
dist/server/services/jobManager.js
vendored
Normal file
239
dist/server/services/jobManager.js
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.JobManager = void 0;
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const jobStore_1 = require("../db/jobStore");
|
||||
const processor_1 = require("../../utils/processor");
|
||||
const subtitleGenerator_1 = require("./subtitleGenerator");
|
||||
const muxer_1 = require("./muxer");
|
||||
const config_1 = require("../../config/config");
|
||||
const mediaUtils_1 = require("../../utils/mediaUtils");
|
||||
const events_1 = require("events");
|
||||
class JobManager {
|
||||
constructor() {
|
||||
this.queue = [];
|
||||
this.processing = false;
|
||||
this.pausedJobs = new Set();
|
||||
this.emitter = new events_1.EventEmitter();
|
||||
this.pollInterval = null;
|
||||
this.recoverStuckJobs();
|
||||
this.emitter.setMaxListeners(100);
|
||||
}
|
||||
recoverStuckJobs() {
|
||||
const jobs = (0, jobStore_1.getAllJobs)();
|
||||
for (const job of jobs) {
|
||||
if (job.status === 'processing') {
|
||||
(0, jobStore_1.updateJobStatus)(job.id, 'failed', 'Server restarted while job was in progress. Click Restart to resume from the last checkpoint.');
|
||||
}
|
||||
}
|
||||
}
|
||||
createJob(videoPath, configOverride = {}, outputOptions = {}) {
|
||||
const baseConfig = (0, config_1.getDefaultConfig)();
|
||||
const mergedConfig = { ...baseConfig, ...configOverride };
|
||||
const filename = path_1.default.basename(videoPath);
|
||||
const opts = {
|
||||
audio: outputOptions.audio !== false,
|
||||
subtitles: outputOptions.subtitles !== false,
|
||||
muxed: outputOptions.muxed || false
|
||||
};
|
||||
return (0, jobStore_1.createJob)(videoPath, filename, mergedConfig, opts);
|
||||
}
|
||||
async startJob(jobId) {
|
||||
const job = (0, jobStore_1.getJob)(jobId);
|
||||
if (!job)
|
||||
throw new Error('Job not found');
|
||||
if (job.status === 'processing')
|
||||
throw new Error('Job is already processing');
|
||||
if (job.status === 'completed')
|
||||
throw new Error('Job is already completed');
|
||||
(0, jobStore_1.updateJobStatus)(jobId, 'queued');
|
||||
this.queue.push(jobId);
|
||||
this.processNext();
|
||||
}
|
||||
async pauseJob(jobId) {
|
||||
const job = (0, jobStore_1.getJob)(jobId);
|
||||
if (!job)
|
||||
throw new Error('Job not found');
|
||||
if (job.status !== 'processing')
|
||||
throw new Error('Only processing jobs can be paused');
|
||||
this.pausedJobs.add(jobId);
|
||||
(0, jobStore_1.updateJobStatus)(jobId, 'paused');
|
||||
this.emitProgress(jobId);
|
||||
}
|
||||
async restartJob(jobId) {
|
||||
const job = (0, jobStore_1.getJob)(jobId);
|
||||
if (!job)
|
||||
throw new Error('Job not found');
|
||||
if (job.status !== 'failed' && job.status !== 'paused' && job.status !== 'cancelled') {
|
||||
throw new Error('Only failed, paused, or cancelled jobs can be restarted');
|
||||
}
|
||||
this.pausedJobs.delete(jobId);
|
||||
(0, jobStore_1.updateJobStatus)(jobId, 'queued');
|
||||
this.queue.push(jobId);
|
||||
this.processNext();
|
||||
}
|
||||
async cancelJob(jobId) {
|
||||
const job = (0, jobStore_1.getJob)(jobId);
|
||||
if (!job)
|
||||
throw new Error('Job not found');
|
||||
if (job.status === 'processing') {
|
||||
this.pausedJobs.add(jobId);
|
||||
}
|
||||
(0, jobStore_1.updateJobStatus)(jobId, 'cancelled');
|
||||
this.emitProgress(jobId);
|
||||
}
|
||||
deleteJob(jobId) {
|
||||
const job = (0, jobStore_1.getJob)(jobId);
|
||||
if (!job)
|
||||
throw new Error('Job not found');
|
||||
if (job.status === 'processing')
|
||||
throw new Error('Cannot delete a running job');
|
||||
(0, jobStore_1.deleteJob)(jobId);
|
||||
}
|
||||
listJobs() {
|
||||
return (0, jobStore_1.getAllJobs)();
|
||||
}
|
||||
onJobProgress(jobId, callback) {
|
||||
this.emitter.on(`progress:${jobId}`, callback);
|
||||
if (!this.pollInterval) {
|
||||
this.pollInterval = setInterval(() => {
|
||||
for (const id of this.emitter.eventNames()) {
|
||||
const eventName = String(id);
|
||||
if (eventName.startsWith('progress:')) {
|
||||
const jId = eventName.replace('progress:', '');
|
||||
this.emitProgress(jId);
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
return () => {
|
||||
this.emitter.off(`progress:${jobId}`, callback);
|
||||
};
|
||||
}
|
||||
emitProgress(jobId) {
|
||||
const job = (0, jobStore_1.getJob)(jobId);
|
||||
if (!job)
|
||||
return;
|
||||
const data = {
|
||||
id: job.id,
|
||||
status: job.status,
|
||||
progress: job.progress,
|
||||
currentIndex: job.current_index,
|
||||
totalUnits: job.total_units,
|
||||
segments: JSON.parse(job.segments || '[]'),
|
||||
error: job.error,
|
||||
output_audio: job.output_audio,
|
||||
output_subtitles_srt: job.output_subtitles_srt,
|
||||
output_subtitles_vtt: job.output_subtitles_vtt,
|
||||
output_muxed: job.output_muxed
|
||||
};
|
||||
this.emitter.emit(`progress:${jobId}`, data);
|
||||
}
|
||||
async processNext() {
|
||||
if (this.processing)
|
||||
return;
|
||||
while (this.queue.length > 0) {
|
||||
this.processing = true;
|
||||
const jobId = this.queue.shift();
|
||||
const job = (0, jobStore_1.getJob)(jobId);
|
||||
if (!job || job.status !== 'queued')
|
||||
continue;
|
||||
try {
|
||||
await this.processJob(job);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`Job ${jobId} failed:`, err.message);
|
||||
}
|
||||
}
|
||||
this.processing = false;
|
||||
}
|
||||
async processJob(job) {
|
||||
(0, jobStore_1.updateJobStatus)(job.id, 'processing');
|
||||
this.emitProgress(job.id);
|
||||
const config = JSON.parse(job.config);
|
||||
const outputOptions = JSON.parse(job.output_options);
|
||||
const existingSegments = JSON.parse(job.segments || '[]');
|
||||
const lastContext = JSON.parse(job.last_context || '{}');
|
||||
const startIndex = existingSegments.length > 0 ? job.current_index : 0;
|
||||
const startTimePosition = job.current_time_position || 0;
|
||||
const videoDuration = (0, mediaUtils_1.getVideoDuration)(job.video_path);
|
||||
const totalUnits = config.batchTimeMode
|
||||
? Math.floor(videoDuration / config.batchWindowDuration)
|
||||
: Math.floor(videoDuration / config.captureIntervalSeconds);
|
||||
(0, jobStore_1.saveCheckpoint)(job.id, JSON.stringify(existingSegments), startIndex, totalUnits, startTimePosition, JSON.stringify(lastContext), 0);
|
||||
this.emitProgress(job.id);
|
||||
try {
|
||||
const result = await (0, processor_1.generateAudioDescriptionFromOptions)(job.video_path, config, {
|
||||
startIndex,
|
||||
existingSegments,
|
||||
lastContext,
|
||||
currentTimePosition: startTimePosition,
|
||||
onProgress: (info) => {
|
||||
if (this.pausedJobs.has(job.id)) {
|
||||
throw new Error('JOB_PAUSED');
|
||||
}
|
||||
const allSegments = existingSegments.length > 0 && info.index === startIndex
|
||||
? [...existingSegments, info.segment]
|
||||
: (() => {
|
||||
const currentJob = (0, jobStore_1.getJob)(job.id);
|
||||
if (!currentJob)
|
||||
return [info.segment];
|
||||
const segs = JSON.parse(currentJob.segments || '[]');
|
||||
segs.push(info.segment);
|
||||
return segs;
|
||||
})();
|
||||
const progress = totalUnits > 0 ? Math.min(((info.index + 1) / totalUnits) * 100, 99) : 50;
|
||||
(0, jobStore_1.saveCheckpoint)(job.id, JSON.stringify(allSegments), info.index + 1, totalUnits, info.segment.startTime + info.segment.duration + (config.batchTimeMode ? 0.5 : 0.25), JSON.stringify(lastContext), progress);
|
||||
this.emitProgress(job.id);
|
||||
}
|
||||
});
|
||||
// All segments from the result
|
||||
const segments = result.segments || [];
|
||||
// Combine audio segments into final audio (use the result's pre-combined file)
|
||||
const outputAudio = result.audioDescriptionFile;
|
||||
let outputSubtitlesSrt = null;
|
||||
let outputSubtitlesVtt = null;
|
||||
let outputMuxed = null;
|
||||
const baseName = path_1.default.basename(job.video_path, path_1.default.extname(job.video_path));
|
||||
const outputDir = config.outputDir;
|
||||
if (outputOptions.subtitles && segments.length > 0) {
|
||||
const srtPath = path_1.default.join(outputDir, `${baseName}_description.srt`);
|
||||
const vttPath = path_1.default.join(outputDir, `${baseName}_description.vtt`);
|
||||
fs_1.default.writeFileSync(srtPath, (0, subtitleGenerator_1.generateSRT)(segments, videoDuration));
|
||||
fs_1.default.writeFileSync(vttPath, (0, subtitleGenerator_1.generateVTT)(segments, videoDuration));
|
||||
outputSubtitlesSrt = srtPath;
|
||||
outputSubtitlesVtt = vttPath;
|
||||
}
|
||||
if (outputOptions.muxed && fs_1.default.existsSync(outputAudio)) {
|
||||
const muxedPath = path_1.default.join(outputDir, `${baseName}_described.mkv`);
|
||||
(0, muxer_1.muxAudioDescription)(job.video_path, outputAudio, muxedPath);
|
||||
outputMuxed = muxedPath;
|
||||
}
|
||||
(0, jobStore_1.saveJobOutputs)(job.id, {
|
||||
audio: outputAudio,
|
||||
subtitlesSrt: outputSubtitlesSrt || undefined,
|
||||
subtitlesVtt: outputSubtitlesVtt || undefined,
|
||||
muxed: outputMuxed || undefined
|
||||
});
|
||||
(0, jobStore_1.saveCheckpoint)(job.id, JSON.stringify(segments), totalUnits, totalUnits, 0, '{}', 100);
|
||||
(0, jobStore_1.updateJobStatus)(job.id, 'completed');
|
||||
this.emitProgress(job.id);
|
||||
}
|
||||
catch (err) {
|
||||
if (err.message === 'JOB_PAUSED') {
|
||||
(0, jobStore_1.updateJobStatus)(job.id, 'paused');
|
||||
this.emitProgress(job.id);
|
||||
return;
|
||||
}
|
||||
const errorMsg = err.message || 'Unknown error';
|
||||
(0, jobStore_1.updateJobStatus)(job.id, 'failed', errorMsg);
|
||||
this.emitProgress(job.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.JobManager = JobManager;
|
||||
//# sourceMappingURL=jobManager.js.map
|
||||
1
dist/server/services/jobManager.js.map
vendored
Normal file
1
dist/server/services/jobManager.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/server/services/muxer.d.ts
vendored
Normal file
1
dist/server/services/muxer.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export declare function muxAudioDescription(videoPath: string, audioPath: string, outputPath: string): void;
|
||||
29
dist/server/services/muxer.js
vendored
Normal file
29
dist/server/services/muxer.js
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.muxAudioDescription = muxAudioDescription;
|
||||
const child_process_1 = require("child_process");
|
||||
const path_1 = __importDefault(require("path"));
|
||||
function muxAudioDescription(videoPath, audioPath, outputPath) {
|
||||
const ext = path_1.default.extname(outputPath).toLowerCase();
|
||||
const isMkv = ext === '.mkv';
|
||||
const cmd = [
|
||||
'ffmpeg -v error',
|
||||
`-i "${videoPath}"`,
|
||||
`-i "${audioPath}"`,
|
||||
'-map 0:v',
|
||||
'-map 0:a?',
|
||||
'-map 1:a',
|
||||
'-c:v copy',
|
||||
'-c:a copy',
|
||||
isMkv
|
||||
? '-metadata:s:a:1 title="Audio Description"'
|
||||
: '-metadata:s:a:1 title="Audio Description"',
|
||||
`"${outputPath}"`,
|
||||
'-y'
|
||||
].join(' ');
|
||||
(0, child_process_1.execSync)(cmd);
|
||||
}
|
||||
//# sourceMappingURL=muxer.js.map
|
||||
1
dist/server/services/muxer.js.map
vendored
Normal file
1
dist/server/services/muxer.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"muxer.js","sourceRoot":"","sources":["../../../src/server/services/muxer.ts"],"names":[],"mappings":";;;;;AAGA,kDAyBC;AA5BD,iDAAyC;AACzC,gDAAwB;AAExB,SAAgB,mBAAmB,CACjC,SAAiB,EACjB,SAAiB,EACjB,UAAkB;IAElB,MAAM,GAAG,GAAG,cAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC;IACnD,MAAM,KAAK,GAAG,GAAG,KAAK,MAAM,CAAC;IAE7B,MAAM,GAAG,GAAG;QACV,iBAAiB;QACjB,OAAO,SAAS,GAAG;QACnB,OAAO,SAAS,GAAG;QACnB,UAAU;QACV,WAAW;QACX,UAAU;QACV,WAAW;QACX,WAAW;QACX,KAAK;YACH,CAAC,CAAC,2CAA2C;YAC7C,CAAC,CAAC,2CAA2C;QAC/C,IAAI,UAAU,GAAG;QACjB,IAAI;KACL,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEZ,IAAA,wBAAQ,EAAC,GAAG,CAAC,CAAC;AAChB,CAAC"}
|
||||
3
dist/server/services/subtitleGenerator.d.ts
vendored
Normal file
3
dist/server/services/subtitleGenerator.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { AudioSegment } from '../../interfaces';
|
||||
export declare function generateSRT(segments: AudioSegment[], videoDuration: number): string;
|
||||
export declare function generateVTT(segments: AudioSegment[], videoDuration: number): string;
|
||||
65
dist/server/services/subtitleGenerator.js
vendored
Normal file
65
dist/server/services/subtitleGenerator.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.generateSRT = generateSRT;
|
||||
exports.generateVTT = generateVTT;
|
||||
function formatSrtTime(seconds) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
const ms = Math.floor((seconds % 1) * 1000);
|
||||
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`;
|
||||
}
|
||||
function formatVttTime(seconds) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
const ms = Math.floor((seconds % 1) * 1000);
|
||||
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
|
||||
}
|
||||
function cleanDescription(text) {
|
||||
return text.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
function generateSRT(segments, videoDuration) {
|
||||
if (segments.length === 0)
|
||||
return '';
|
||||
const sorted = [...segments].sort((a, b) => a.startTime - b.startTime);
|
||||
const lines = [];
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const seg = sorted[i];
|
||||
const startTime = seg.startTime;
|
||||
let endTime;
|
||||
if (i < sorted.length - 1) {
|
||||
endTime = sorted[i + 1].startTime;
|
||||
}
|
||||
else {
|
||||
endTime = Math.min(seg.startTime + seg.duration + 0.5, videoDuration);
|
||||
}
|
||||
lines.push((i + 1).toString());
|
||||
lines.push(`${formatSrtTime(startTime)} --> ${formatSrtTime(endTime)}`);
|
||||
lines.push(cleanDescription(seg.description));
|
||||
lines.push('');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
function generateVTT(segments, videoDuration) {
|
||||
if (segments.length === 0)
|
||||
return '';
|
||||
const sorted = [...segments].sort((a, b) => a.startTime - b.startTime);
|
||||
const lines = ['WEBVTT', ''];
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const seg = sorted[i];
|
||||
const startTime = seg.startTime;
|
||||
let endTime;
|
||||
if (i < sorted.length - 1) {
|
||||
endTime = sorted[i + 1].startTime;
|
||||
}
|
||||
else {
|
||||
endTime = Math.min(seg.startTime + seg.duration + 0.5, videoDuration);
|
||||
}
|
||||
lines.push(`${formatVttTime(startTime)} --> ${formatVttTime(endTime)}`);
|
||||
lines.push(cleanDescription(seg.description));
|
||||
lines.push('');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
//# sourceMappingURL=subtitleGenerator.js.map
|
||||
1
dist/server/services/subtitleGenerator.js.map
vendored
Normal file
1
dist/server/services/subtitleGenerator.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subtitleGenerator.js","sourceRoot":"","sources":["../../../src/server/services/subtitleGenerator.ts"],"names":[],"mappings":"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"}
|
||||
7
dist/server/services/ytDlp.d.ts
vendored
Normal file
7
dist/server/services/ytDlp.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface YtDlpResult {
|
||||
filePath: string;
|
||||
filename: string;
|
||||
title: string;
|
||||
}
|
||||
export declare function isYtDlpAvailable(): boolean;
|
||||
export declare function downloadVideo(url: string, outputDir: string): YtDlpResult;
|
||||
38
dist/server/services/ytDlp.js
vendored
Normal file
38
dist/server/services/ytDlp.js
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.isYtDlpAvailable = isYtDlpAvailable;
|
||||
exports.downloadVideo = downloadVideo;
|
||||
const child_process_1 = require("child_process");
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
function isYtDlpAvailable() {
|
||||
try {
|
||||
(0, child_process_1.execSync)('yt-dlp --version', { stdio: 'pipe' });
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function downloadVideo(url, outputDir) {
|
||||
if (!fs_1.default.existsSync(outputDir)) {
|
||||
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
const outputTemplate = path_1.default.join(outputDir, '%(title)s.%(ext)s');
|
||||
const result = (0, child_process_1.execSync)(`yt-dlp -f "best[ext=mp4]/best" -o "${outputTemplate}" --print filename --print title "${url}"`, { encoding: 'utf-8', timeout: 600000 });
|
||||
const lines = result.trim().split('\n');
|
||||
const filename = lines[0]?.trim();
|
||||
const title = lines[1]?.trim() || filename;
|
||||
if (!filename) {
|
||||
throw new Error('yt-dlp: Failed to parse downloaded filename');
|
||||
}
|
||||
const filePath = path_1.default.resolve(outputDir, filename);
|
||||
if (!fs_1.default.existsSync(filePath)) {
|
||||
throw new Error(`yt-dlp: Downloaded file not found at ${filePath}`);
|
||||
}
|
||||
return { filePath, filename, title };
|
||||
}
|
||||
//# sourceMappingURL=ytDlp.js.map
|
||||
1
dist/server/services/ytDlp.js.map
vendored
Normal file
1
dist/server/services/ytDlp.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"ytDlp.js","sourceRoot":"","sources":["../../../src/server/services/ytDlp.ts"],"names":[],"mappings":";;;;;AAUA,4CAOC;AAED,sCA2BC;AA9CD,iDAAyC;AACzC,gDAAwB;AACxB,4CAAoB;AAQpB,SAAgB,gBAAgB;IAC9B,IAAI,CAAC;QACH,IAAA,wBAAQ,EAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAgB,aAAa,CAAC,GAAW,EAAE,SAAiB;IAC1D,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,YAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,cAAc,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;IAEjE,MAAM,MAAM,GAAG,IAAA,wBAAQ,EACrB,sCAAsC,cAAc,qCAAqC,GAAG,GAAG,EAC/F,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CACvC,CAAC;IAEF,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IAClC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,QAAQ,CAAC;IAE3C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEnD,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,wCAAwC,QAAQ,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AACvC,CAAC"}
|
||||
Reference in New Issue
Block a user