- // ==UserScript==
- // @name Appinn Forum Upload Enhancer
- // @name:zh-CN 小众软件论坛上传优化
- // @license AGPL-3.0
- // @version 0.4.0
- // @author xymoryn
- // @namespace https://github.com/xymoryn
- // @icon https://h1.appinn.me/logo.png
- // @description 小众软件论坛发帖或回复时,粘贴、拖曳或上传按钮选择图片/文件,自动上传到 h1.appinn.me 并转为对应的 Markdown 格式输出。
- // @homepage https://github.com/xymoryn/user-scripts
- // @supportURL https://github.com/xymoryn/user-scripts/issues
- // @run-at document-idle
- // @match https://meta.appinn.net/*
- // @grant GM_xmlhttpRequest
- // ==/UserScript==
-
- (function () {
- 'use strict';
-
- /**
- * 全局配置对象
- * @type {Object}
- */
- const CONFIG = {
- /** 是否开启调试模式,控制控制台输出 */
- DEBUG: false,
-
- /** 文件大小限制 (20MB) */
- MAX_FILE_SIZE: 20 * 1024 * 1024,
-
- /** 上传端点 */
- UPLOAD_ENDPOINT: 'https://h1.appinn.me/upload',
-
- /**
- * 上传配置参数
- * @property {string} authCode - 认证码(必填)
- * @property {boolean} serverCompress - 是否启用 Telegram 图片压缩:
- * - 启用丢失透明度
- * - 对大于 10MB 的文件无效
- * @property {'telegram'|'cfr2'|'s3'} uploadChannel - 文件上传渠道:
- * - 'telegram': Telegram。当前小众图床唯一可用的上传渠道。
- * - 'cfr2': Cloudflare R2
- * - 's3': Amazon S3
- * @property {'default'|'index'|'origin'|'short'} uploadNameType - 文件命名方式:
- * - 'default': 时间戳_原始文件名
- * - 'index': 仅时间戳
- * - 'origin': 原始文件名
- * - 'short': 类似短链接的随机字母数字
- * @property {boolean} autoRetry - 上传失败时是否自动切换到其他渠道
- */
- UPLOAD_PARAMS: {
- authCode: 'appinn2',
- serverCompress: false,
- uploadChannel: 'telegram',
- uploadNameType: 'default',
- autoRetry: true,
- },
-
- /** 资源访问 URL 前缀 */
- ASSETS_URL_PREFIX: 'https://h1.appinn.me',
-
- /**
- * 支持的文件类型
- * @type {Object.<string, {test: Function, format: Function, acceptString: string}>}
- */
- SUPPORTED_MIME_TYPES: {
- 'image': {
- test: (type) => type.startsWith('image/'),
- format: (filename, url) => ``,
- acceptString: 'image/*',
- },
- 'video': {
- test: (type) => type.startsWith('video/'),
- format: (filename, url) => ``,
- acceptString: 'video/*',
- },
- 'audio': {
- test: (type) => type.startsWith('audio/'),
- format: (filename, url) => ``,
- acceptString: 'audio/*',
- },
- 'pdf': {
- test: (type) => type === 'application/pdf',
- format: (filename, url) => `[${filename}|attachment](${url})`,
- acceptString: '.pdf',
- },
- },
-
- /** 内容格式配置 */
- CONTENT_FORMAT: {
- /** 内容前面的换行符 */
- BEFORE: '\n',
- /** 内容后面的换行符 */
- AFTER: '\n\n',
- },
-
- /** DOM选择器 */
- SELECTORS: {
- REPLY_CONTROL: '#reply-control', // 回复框
- EDITOR_CONTROLS: '.toolbar-visible.wmd-controls', // 编辑器控件
- EDITOR_INPUT: '.d-editor-input', // 编辑区域
- UPLOAD_BUTTON: '.btn.upload', // 上传按钮
- },
-
- /** 错误类型 */
- ERROR_TYPES: {
- NETWORK: 'network', // 网络连接问题
- SERVER: 'server', // 服务器错误
- PERMISSION: 'permission', // 权限问题
- FORMAT: 'format', // 响应格式错误
- FILETYPE: 'filetype', // 文件类型不支持
- FILESIZE: 'filesize', // 文件大小超限
- UNKNOWN: 'unknown', // 未知错误
- },
- };
-
- /**
- * 日志工具
- * @namespace
- */
- const Logger = {
- /**
- * 输出普通日志
- * @param {...any} args - 日志参数
- */
- log(...args) {
- if (CONFIG.DEBUG) {
- console.log('[小众论坛上传]', ...args);
- }
- },
-
- /**
- * 输出错误日志
- * @param {...any} args - 日志参数
- */
- error(...args) {
- if (CONFIG.DEBUG) {
- console.error('[小众论坛上传]', ...args);
- }
- },
- };
-
- /**
- * 应用状态管理
- * @namespace
- */
- const AppState = {
- /**
- * 保存所有上传状态
- * @type {Object.<string, {insertPosition: number, placeholderText: string, active: boolean}>}
- */
- uploads: {},
-
- /** 上传计数器 */
- uploadCounter: 0,
-
- /** DOM元素缓存 */
- elements: {
- replyControl: null,
- editorInput: null,
- editorControls: null,
- uploadButton: null,
- },
-
- /**
- * 生成唯一上传ID
- * @returns {string} 唯一ID
- */
- generateUploadId() {
- return `${Date.now()}-${++this.uploadCounter}`;
- },
-
- /**
- * 添加上传状态
- * @param {string} uploadId - 上传ID
- * @param {number} position - 插入位置
- * @param {string} placeholderText - 占位符文本
- */
- addUpload(uploadId, position, placeholderText) {
- this.uploads[uploadId] = {
- insertPosition: position,
- placeholderText,
- active: true,
- timestamp: Date.now(),
- };
- },
-
- /**
- * 获取上传状态
- * @param {string} uploadId - 上传ID
- * @returns {Object|null} 上传状态对象
- */
- getUpload(uploadId) {
- return this.uploads[uploadId] ?? null;
- },
-
- /**
- * 移除上传状态
- * @param {string} uploadId - 上传ID
- */
- removeUpload(uploadId) {
- delete this.uploads[uploadId];
- },
- };
-
- /**
- * DOM操作工具
- * @namespace
- */
- const DOMUtils = {
- /**
- * 判断回复控制面板是否处于打开状态
- * @param {HTMLElement} element - 回复框元素
- * @returns {boolean} 是否处于打开状态
- */
- isReplyControlOpen(element) {
- return element && element.id === 'reply-control' && !element.classList.contains('closed');
- },
-
- /**
- * 保存编辑器状态
- * @param {HTMLTextAreaElement} editor - 编辑器元素
- * @returns {Object} 编辑器状态
- */
- saveEditorState(editor) {
- const { selectionStart, selectionEnd, scrollTop, value } = editor;
- return { selectionStart, selectionEnd, scrollTop, value };
- },
-
- /**
- * 触发输入事件
- * @param {HTMLTextAreaElement} editor - 编辑器元素
- */
- triggerInputEvent(editor) {
- editor.dispatchEvent(new Event('input', { bubbles: true }));
- },
- };
-
- /**
- * 文件工具
- * @namespace
- */
- const FileUtils = {
- /**
- * 检查文件类型
- * @param {File} file - 文件对象
- * @returns {string|null} 文件类型或null
- */
- getFileType(file) {
- const { type: mimeType } = file;
- const entry = Object.entries(CONFIG.SUPPORTED_MIME_TYPES).find(([_, info]) =>
- info.test(mimeType),
- );
- return entry ? entry[0] : null;
- },
-
- /**
- * 检查文件是否合法
- * @param {File} file - 文件对象
- * @returns {{valid: boolean, error: string|null}} 检查结果及错误类型
- */
- validateFile(file) {
- // 检查类型
- const fileType = this.getFileType(file);
- if (fileType === null) {
- return {
- valid: false,
- error: CONFIG.ERROR_TYPES.FILETYPE,
- };
- }
-
- // 检查大小
- if (file.size > CONFIG.MAX_FILE_SIZE) {
- return {
- valid: false,
- error: CONFIG.ERROR_TYPES.FILESIZE,
- };
- }
-
- return { valid: true, error: null };
- },
-
- /**
- * 检查是否有文件在拖放数据中
- * @param {DataTransfer} dataTransfer - 数据传输对象
- * @returns {boolean} 是否有文件
- */
- hasFileInDataTransfer(dataTransfer) {
- if (!dataTransfer) return false;
-
- // 通过items检查
- if (dataTransfer.items?.length) {
- return [...dataTransfer.items].some((item) => item.kind === 'file');
- }
-
- // 通过types检查
- if (dataTransfer.types?.includes('Files')) {
- return true;
- }
-
- // 通过files检查
- return dataTransfer.files?.length > 0;
- },
-
- /**
- * 获取剪贴板中的文件
- * @param {ClipboardData} clipboardData - 剪贴板数据
- * @returns {File|null} 文件或null
- */
- getFileFromClipboard(clipboardData) {
- if (!clipboardData?.items) return null;
-
- for (const item of clipboardData.items) {
- if (item.kind === 'file') {
- return item.getAsFile();
- }
- }
-
- return null;
- },
-
- /**
- * 生成文件选择器的accept属性
- * @returns {string} accept属性值
- */
- generateAcceptString() {
- return Object.values(CONFIG.SUPPORTED_MIME_TYPES)
- .map((type) => type.acceptString)
- .join(',');
- },
-
- /**
- * 显示文件错误消息
- * @param {File} file - 文件对象
- * @param {string} errorType - 错误类型
- */
- showFileError(file, errorType) {
- const { name, type } = file;
- let message;
-
- switch (errorType) {
- case CONFIG.ERROR_TYPES.FILETYPE:
- message = `不支持的文件类型: ${type}`;
- break;
- case CONFIG.ERROR_TYPES.FILESIZE:
- message = `文件"${name}"超过${
- CONFIG.MAX_FILE_SIZE / (1024 * 1024)
- }MB大小限制,无法上传。`;
- break;
- default:
- message = `文件"${name}"无法上传: 未知错误`;
- }
-
- alert(message);
- Logger.log(message);
- },
- };
-
- /**
- * Markdown格式化工具
- * @namespace
- */
- const MarkdownFormatter = {
- /**
- * 获取文件对应的Markdown链接
- * @param {File} file - 文件对象
- * @param {string} url - 文件URL
- * @returns {string} Markdown格式文本
- */
- getMarkdownLink(file, url) {
- const { name: filename = `file_${Date.now()}` } = file;
- const fileType = FileUtils.getFileType(file);
-
- if (fileType && CONFIG.SUPPORTED_MIME_TYPES[fileType]) {
- return CONFIG.SUPPORTED_MIME_TYPES[fileType].format(filename, url);
- }
-
- // 默认格式
- return `[${filename}](${url})`;
- },
-
- /**
- * 获取占位符文本
- * @param {File} file - 文件对象
- * @param {string} uploadId - 上传ID
- * @returns {string} 占位符文本
- */
- getPlaceholderText(file, uploadId) {
- const fileType = FileUtils.getFileType(file);
- const prefix =
- fileType === 'image' || fileType === 'video' || fileType === 'audio' ? '!' : '';
- const suffix =
- fileType === 'video'
- ? '|video'
- : fileType === 'audio'
- ? '|audio'
- : fileType === 'pdf'
- ? '|attachment'
- : '';
-
- return `${prefix}[上传中...${uploadId}${suffix}]`;
- },
-
- /**
- * 获取上传失败的Markdown文本
- * @param {string} uploadId - 上传ID
- * @param {string} errorType - 错误类型
- * @returns {string} 失败提示文本
- */
- getFailureText(uploadId, errorType) {
- const errorMessages = {
- [CONFIG.ERROR_TYPES.NETWORK]: '网络错误',
- [CONFIG.ERROR_TYPES.SERVER]: '服务器错误',
- [CONFIG.ERROR_TYPES.PERMISSION]: '权限错误',
- [CONFIG.ERROR_TYPES.FORMAT]: '格式错误',
- [CONFIG.ERROR_TYPES.FILETYPE]: '类型不支持',
- [CONFIG.ERROR_TYPES.FILESIZE]: '文件过大',
- [CONFIG.ERROR_TYPES.UNKNOWN]: '未知错误',
- };
-
- const errorMessage = errorMessages[errorType] || '未知错误';
- return `[上传失败(${errorMessage})-${uploadId}]`;
- },
-
- /**
- * 格式化内容,添加配置的前后换行符
- * @param {string} content - 原始内容
- * @returns {string} 格式化后的内容
- */
- formatContent(content) {
- return CONFIG.CONTENT_FORMAT.BEFORE + content + CONFIG.CONTENT_FORMAT.AFTER;
- },
- };
-
- /**
- * 占位符管理器
- * @namespace
- */
- const PlaceholderManager = {
- /**
- * 插入占位符到编辑器
- * @param {HTMLTextAreaElement} editor - 编辑器元素
- * @param {Object} editorState - 编辑器状态
- * @param {string} placeholderText - 占位符文本
- * @param {string} uploadId - 上传ID
- * @param {Function} onInserted - 占位符插入完成后的回调
- */
- insertPlaceholder(editor, editorState, placeholderText, uploadId, onInserted) {
- const { selectionStart: position, scrollTop } = editorState;
- const currentText = editor.value;
-
- // 使用配置的格式化占位符
- const completeText = MarkdownFormatter.formatContent(placeholderText);
-
- // 插入带换行的占位符
- editor.value =
- currentText.substring(0, position) + completeText + currentText.substring(position);
-
- // 触发输入事件
- DOMUtils.triggerInputEvent(editor);
-
- // 将光标移动到占位符后面
- const newCursorPosition = position + completeText.length;
- editor.selectionStart = newCursorPosition;
- editor.selectionEnd = newCursorPosition;
- editor.scrollTop = scrollTop;
- editor.focus();
-
- // 保存上传状态
- AppState.addUpload(uploadId, position, placeholderText);
-
- // 调用回调函数
- onInserted?.(uploadId, position);
- },
-
- /**
- * 查找占位符在文本中的位置
- * @param {string} text - 编辑器文本内容
- * @param {string} uploadId - 上传ID
- * @returns {Object|null} 占位符位置信息或null
- */
- findPlaceholder(text, uploadId) {
- const regex = new RegExp(`(!)?\\[上传中...${uploadId}(\\|[a-z]+)?\\]`);
- const match = regex.exec(text);
-
- if (match) {
- // 仅找到占位符文本本身
- return {
- start: match.index,
- end: match.index + match[0].length,
- text: match[0],
- };
- }
-
- return null;
- },
-
- /**
- * 替换编辑器中的占位符
- * @param {HTMLTextAreaElement} editor - 编辑器元素
- * @param {string} uploadId - 上传ID
- * @param {string} markdownLink - Markdown链接文本
- * @param {Function} onReplaced - 替换完成后的回调
- */
- replacePlaceholder(editor, uploadId, markdownLink, onReplaced) {
- // 保存当前用户光标状态
- const currentState = DOMUtils.saveEditorState(editor);
- const { value: currentText } = currentState;
-
- // 查找占位符位置
- const placeholder = this.findPlaceholder(currentText, uploadId);
-
- // 如果找不到占位符,使用备选策略
- if (!placeholder) {
- Logger.error('找不到占位符,将添加到编辑器末尾');
- this._appendToEditor(editor, markdownLink, onReplaced);
- return;
- }
-
- // 计算长度变化
- const originalLength = placeholder.end - placeholder.start;
- const newLength = markdownLink.length;
- const lengthDiff = newLength - originalLength;
-
- // 替换内容
- const newText =
- currentText.substring(0, placeholder.start) +
- markdownLink +
- currentText.substring(placeholder.end);
-
- // 更新编辑器内容
- editor.value = newText;
- DOMUtils.triggerInputEvent(editor);
-
- // 调整光标位置
- this._adjustCursorPosition(editor, currentState, placeholder, lengthDiff);
-
- // 调用回调函数
- onReplaced?.(true);
- },
-
- /**
- * 调整光标位置
- * @private
- * @param {HTMLTextAreaElement} editor - 编辑器元素
- * @param {Object} currentState - 当前编辑器状态
- * @param {Object} placeholder - 占位符信息
- * @param {number} lengthDiff - 长度变化
- */
- _adjustCursorPosition(editor, currentState, placeholder, lengthDiff) {
- const { selectionStart, selectionEnd, scrollTop } = currentState;
- let newSelectionStart = selectionStart;
- let newSelectionEnd = selectionEnd;
-
- // 情况1:光标在占位符之前 - 不需要调整
- if (selectionStart < placeholder.start) {
- // 不调整光标位置
- }
- // 情况2:光标在占位符范围内 - 移动到替换内容之后
- else if (selectionStart >= placeholder.start && selectionStart <= placeholder.end) {
- newSelectionStart = placeholder.start + (placeholder.end - placeholder.start) + lengthDiff;
- newSelectionEnd = newSelectionStart;
- }
- // 情况3:光标在占位符之后 - 根据内容长度变化调整
- else if (selectionStart > placeholder.end) {
- newSelectionStart = selectionStart + lengthDiff;
- newSelectionEnd = selectionEnd + lengthDiff;
- }
-
- // 设置新的光标位置
- editor.selectionStart = newSelectionStart;
- editor.selectionEnd = newSelectionEnd;
- editor.scrollTop = scrollTop;
- editor.focus();
- },
-
- /**
- * 将内容添加到编辑器末尾
- * @private
- * @param {HTMLTextAreaElement} editor - 编辑器元素
- * @param {string} content - 要添加的内容
- * @param {Function} onAppended - 添加完成后的回调
- */
- _appendToEditor(editor, content, onAppended) {
- const currentText = editor.value;
-
- // 确保有换行分隔
- let newText = currentText;
- if (newText.length > 0 && !newText.endsWith('\n')) {
- newText += '\n';
- }
-
- // 添加新内容,使用配置的格式
- newText += MarkdownFormatter.formatContent(content);
-
- // 更新编辑器内容
- editor.value = newText;
- DOMUtils.triggerInputEvent(editor);
-
- // 移动光标到末尾
- editor.selectionStart = newText.length;
- editor.selectionEnd = newText.length;
- editor.focus();
-
- // 调用回调
- onAppended?.(false);
- },
- };
-
- /**
- * 上传服务
- * @namespace
- */
- const UploadService = {
- /**
- * 处理文件上传
- * @param {FileList|Array<File>} files - 文件列表
- * @param {HTMLTextAreaElement} editor - 编辑器元素
- */
- processFiles(files, editor) {
- if (!files?.length) return;
-
- [...files].forEach((file) => {
- const validation = FileUtils.validateFile(file);
-
- if (validation.valid) {
- this.uploadFile(file, editor);
- } else {
- FileUtils.showFileError(file, validation.error);
- }
- });
- },
-
- /**
- * 上传单个文件
- * @param {File} file - 文件对象
- * @param {HTMLTextAreaElement} editor - 编辑器元素
- */
- uploadFile(file, editor) {
- // 生成唯一ID
- const uploadId = AppState.generateUploadId();
-
- // 获取占位符文本
- const placeholderText = MarkdownFormatter.getPlaceholderText(file, uploadId);
-
- // 保存当前编辑器状态
- const editorState = DOMUtils.saveEditorState(editor);
-
- // 插入占位符
- PlaceholderManager.insertPlaceholder(editor, editorState, placeholderText, uploadId, () => {
- // 占位符插入后执行上传
- this._executeUpload(file, editor, uploadId);
- });
- },
-
- /**
- * 执行文件上传
- * @private
- * @param {File} file - 文件对象
- * @param {HTMLTextAreaElement} editor - 编辑器元素
- * @param {string} uploadId - 上传ID
- */
- async _executeUpload(file, editor, uploadId) {
- try {
- const result = await this.performUpload(file);
-
- // 生成Markdown链接
- const markdownLink = MarkdownFormatter.getMarkdownLink(file, result.url);
-
- // 替换占位符
- PlaceholderManager.replacePlaceholder(editor, uploadId, markdownLink, (success) => {
- if (success) {
- Logger.log('占位符替换成功:', uploadId);
- } else {
- Logger.log('占位符未找到,已添加到编辑器末尾:', uploadId);
- }
- // 清理上传状态
- AppState.removeUpload(uploadId);
- });
- } catch (error) {
- // 处理上传失败
- Logger.error('上传文件失败:', error);
-
- // 确定错误类型
- const errorType = this._categorizeError(error);
-
- // 生成错误文本
- const failureText = MarkdownFormatter.getFailureText(uploadId, errorType);
-
- // 替换占位符
- PlaceholderManager.replacePlaceholder(editor, uploadId, failureText, () => {
- // 清理上传状态
- AppState.removeUpload(uploadId);
- });
- }
- },
-
- /**
- * 分类错误类型
- * @private
- * @param {string|Error} error - 错误信息
- * @returns {string} 错误类型
- */
- _categorizeError(error) {
- const errorStr = error.toString().toLowerCase();
-
- if (
- errorStr.includes('network') ||
- errorStr.includes('failed to fetch') ||
- errorStr.includes('网络请求失败')
- ) {
- return CONFIG.ERROR_TYPES.NETWORK;
- }
-
- if (errorStr.includes('401') || errorStr.includes('403') || errorStr.includes('permission')) {
- return CONFIG.ERROR_TYPES.PERMISSION;
- }
-
- if (errorStr.includes('500') || errorStr.includes('503') || errorStr.includes('服务器')) {
- return CONFIG.ERROR_TYPES.SERVER;
- }
-
- if (errorStr.includes('解析') || errorStr.includes('parse') || errorStr.includes('format')) {
- return CONFIG.ERROR_TYPES.FORMAT;
- }
-
- return CONFIG.ERROR_TYPES.UNKNOWN;
- },
-
- /**
- * 执行文件上传到服务器
- * @param {File} file - 文件对象
- * @returns {Promise<{url: string, filename: string}>} 上传结果
- */
- performUpload(file) {
- return new Promise((resolve, reject) => {
- const { name: filename = `file_${Date.now()}` } = file;
- const formData = new FormData();
- formData.append('filename', filename);
- formData.append('file', file);
-
- const params = new URLSearchParams();
- Object.entries(CONFIG.UPLOAD_PARAMS).forEach(([key, val]) => {
- params.append(key, val);
- });
-
- const uploadUrl = `${CONFIG.UPLOAD_ENDPOINT}?${params.toString()}`;
-
- GM_xmlhttpRequest({
- method: 'POST',
- url: uploadUrl,
- data: formData,
- responseType: 'json',
- onload: (response) => {
- if (response.status !== 200) {
- return reject(`HTTP错误: ${response.status}`);
- }
-
- try {
- const data = response.response;
- if (!data?.[0]?.src) {
- return reject('无效的响应数据');
- }
-
- const fileUrl = CONFIG.ASSETS_URL_PREFIX + data[0].src;
- resolve({
- url: fileUrl,
- filename,
- });
- } catch (error) {
- reject('解析响应数据失败');
- }
- },
- onerror: () => reject('网络请求失败'),
- });
- });
- },
- };
-
- /**
- * 事件处理
- * @namespace
- */
- const EventHandlers = {
- /**
- * 粘贴事件处理
- * @param {ClipboardEvent} e - 粘贴事件
- */
- pasteHandler(e) {
- const editor = e.target;
- const file = FileUtils.getFileFromClipboard(e.clipboardData);
-
- // 如果没有文件,不干预原有处理
- if (!file) return;
-
- // 拦截事件,自己处理
- e.preventDefault();
- e.stopPropagation();
-
- // 验证文件
- const validation = FileUtils.validateFile(file);
-
- if (validation.valid) {
- // 文件有效,上传
- UploadService.uploadFile(file, editor);
- } else {
- // 文件无效,显示错误
- FileUtils.showFileError(file, validation.error);
- }
- },
-
- /**
- * 拖放处理
- * @param {DragEvent} e - 拖放事件
- */
- dropHandler(e) {
- // 检查是否有文件被拖放
- if (e.dataTransfer?.files?.length > 0) {
- // 阻止默认行为
- e.preventDefault();
- e.stopPropagation();
-
- // 查找编辑器元素
- const { editorInput: editor } = AppState.elements;
- if (editor) {
- // 处理所有拖放文件
- UploadService.processFiles(e.dataTransfer.files, editor);
- }
- }
- },
-
- /**
- * 上传按钮点击事件
- * @param {MouseEvent} e - 点击事件
- */
- uploadButtonClickHandler(e) {
- e.preventDefault();
- e.stopPropagation();
-
- // 创建文件选择器
- const fileInput = document.createElement('input');
- fileInput.type = 'file';
- fileInput.multiple = true;
- fileInput.accept = FileUtils.generateAcceptString();
-
- // 文件选择处理
- fileInput.addEventListener('change', function () {
- if (this.files?.length > 0) {
- const { editorInput: editor } = AppState.elements;
- if (!editor) return;
-
- // 处理所有选中的文件
- UploadService.processFiles(this.files, editor);
- }
- });
-
- // 触发文件选择对话框
- fileInput.click();
- },
- };
-
- /**
- * 初始化管理
- * @namespace
- */
- const Initializer = {
- /**
- * 查找并缓存DOM元素
- */
- findElements() {
- const replyControl = document.querySelector(CONFIG.SELECTORS.REPLY_CONTROL);
- if (!DOMUtils.isReplyControlOpen(replyControl)) return false;
-
- const editorControls = replyControl.querySelector(CONFIG.SELECTORS.EDITOR_CONTROLS);
- if (!editorControls) return false;
-
- // 一次性更新所有元素缓存
- Object.assign(AppState.elements, {
- replyControl,
- editorControls,
- editorInput: editorControls.querySelector(CONFIG.SELECTORS.EDITOR_INPUT),
- uploadButton: editorControls.querySelector(CONFIG.SELECTORS.UPLOAD_BUTTON),
- });
-
- return !!AppState.elements.editorInput;
- },
-
- /**
- * 设置事件处理器
- */
- setupEventHandlers() {
- const { editorInput: editor, editorControls, uploadButton } = AppState.elements;
-
- if (!editor || !editorControls) return false;
-
- // 设置粘贴处理
- editor.removeEventListener('paste', EventHandlers.pasteHandler);
- editor.addEventListener('paste', EventHandlers.pasteHandler, { capture: true });
-
- // 设置拖放处理
- editorControls.removeEventListener('drop', EventHandlers.dropHandler, true);
- editorControls.addEventListener('drop', EventHandlers.dropHandler, { capture: true });
-
- // 设置上传按钮(如果存在)
- if (uploadButton) {
- // 检查按钮是否隐藏
- const computedStyle = window.getComputedStyle(uploadButton);
- if (computedStyle.display === 'none' || uploadButton.style.display === 'none') {
- // 设置按钮为可见
- uploadButton.style.display = 'inline-flex';
-
- // 清除现有的事件处理器
- const newBtn = uploadButton.cloneNode(true);
- uploadButton.parentNode.replaceChild(newBtn, uploadButton);
-
- // 更新缓存引用
- AppState.elements.uploadButton = newBtn;
-
- // 添加新的点击事件
- newBtn.addEventListener('click', EventHandlers.uploadButtonClickHandler);
-
- Logger.log('上传按钮已设置为可见并添加事件监听器');
- }
- }
-
- Logger.log('事件处理器设置完成');
- return true;
- },
-
- /**
- * 初始化函数
- */
- init() {
- Logger.log('初始化小众软件论坛上传优化脚本...');
-
- // 查找元素
- if (this.findElements()) {
- this.setupEventHandlers();
- }
-
- // 观察编辑器的出现
- const observer = new MutationObserver((mutations) => {
- let needsUpdate = false;
-
- for (const mutation of mutations) {
- if (
- mutation.type === 'attributes' &&
- mutation.attributeName === 'class' &&
- DOMUtils.isReplyControlOpen(mutation.target)
- ) {
- needsUpdate = true;
- break;
- }
- }
-
- if (needsUpdate && this.findElements()) {
- this.setupEventHandlers();
- }
- });
-
- // MutationObserver 配置
- const replyControl = document.querySelector('#reply-control');
- if (replyControl) {
- observer.observe(replyControl, {
- attributes: true,
- attributeFilter: ['class'],
- childList: false,
- subtree: false,
- });
- } else {
- // 如果尚未找到目标元素,监听body以等待其创建
- observer.observe(document.body, {
- childList: true,
- subtree: true,
- });
- }
-
- Logger.log('初始化完成。');
- },
- };
-
- // 启动脚本
- Initializer.init();
- })();