diff --git a/README.md b/README.md index 155e479..1e55ef3 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ - **查重逻辑**:支持优先通过历史转存记录查重,对于有转存记录的文件,即使删除网盘文件,也不会重复转存。 - **Aria2**:支持成功添加 Aria2 下载任务后自动删除夸克网盘内对应的文件,清理网盘空间。 - **文件整理**:支持浏览和管理多个夸克账号的网盘文件,支持单项/批量重命名(支持应用完整的命名、过滤规则和撤销重命名等操作)、移动文件、删除文件、新建文件夹等操作。 +- **更新状态**:支持在任务列表页面显示任务的最近更新日期、最近转存文件,支持在任务列表、转存记录、文件整理页面显示当日更新标识(对于当日更新的内容)。 本项目修改后的版本为个人需求定制版,目的是满足我自己的使用需求,某些(我不用的)功能可能会因为修改而出现 BUG,不一定会被修复。若你要使用本项目,请知晓本人不是程序员,我无法保证本项目的稳定性,如果你在使用过程中发现了 BUG,可以在 Issues 中提交,但不保证每个 BUG 都能被修复,请谨慎使用,风险自担。 diff --git a/app/run.py b/app/run.py index be2f470..d63b749 100644 --- a/app/run.py +++ b/app/run.py @@ -40,6 +40,35 @@ from quark_auto_save import Config, format_bytes sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from quark_auto_save import extract_episode_number, sort_file_by_name, chinese_to_arabic, is_date_format + +def process_season_episode_info(filename): + """ + 处理文件名中的季数和集数信息 + + Args: + filename: 文件名(不含扩展名) + + Returns: + 处理后的显示名称 + """ + # 匹配 SxxExx 格式(不区分大小写) + # 支持 S1E1, S01E01, s13e10 等格式 + season_episode_match = re.search(r'[Ss](\d{1,2})[Ee](\d{1,3})', filename) + if season_episode_match: + season = season_episode_match.group(1).zfill(2) # 确保两位数 + episode = season_episode_match.group(2).zfill(2) # 确保两位数 + return f"S{season}E{episode}" + + # 匹配只有 Exx 或 EPxx 格式(不区分大小写) + # 支持 E1, E01, EP1, EP01, e10, ep10 等格式 + episode_only_match = re.search(r'[Ee][Pp]?(\d{1,3})', filename) + if episode_only_match: + episode = episode_only_match.group(1).zfill(2) # 确保两位数 + return f"E{episode}" + + # 如果没有匹配到季数集数信息,返回原文件名 + return filename + # 导入拼音排序工具 try: from utils.pinyin_sort import get_filename_pinyin_sort_key @@ -1358,12 +1387,118 @@ def delete_history_records(): deleted_count += db.delete_record(record_id) return jsonify({ - "success": True, + "success": True, "message": f"成功删除 {deleted_count} 条记录", "deleted_count": deleted_count }) +# 获取任务最新转存信息(包括日期和文件) +@app.route("/task_latest_info") +def get_task_latest_info(): + if not is_login(): + return jsonify({"success": False, "message": "未登录"}) + + try: + # 初始化数据库 + db = RecordDB() + cursor = db.conn.cursor() + + # 获取所有任务的最新转存时间 + query = """ + SELECT task_name, MAX(transfer_time) as latest_transfer_time + FROM transfer_records + WHERE task_name != 'rename' + GROUP BY task_name + """ + cursor.execute(query) + latest_times = cursor.fetchall() + + task_latest_records = {} # 存储最新转存日期 + task_latest_files = {} # 存储最新转存文件 + + for task_name, latest_time in latest_times: + if latest_time: + # 1. 处理最新转存日期 + try: + # 确保时间戳在合理范围内 + timestamp = int(latest_time) + if timestamp > 9999999999: # 检测是否为毫秒级时间戳(13位) + timestamp = timestamp / 1000 # 转换为秒级时间戳 + + if 0 < timestamp < 4102444800: # 从1970年到2100年的合理时间戳范围 + # 格式化为月-日格式(用于显示)和完整日期(用于今日判断) + date_obj = datetime.fromtimestamp(timestamp) + formatted_date = date_obj.strftime("%m-%d") + full_date = date_obj.strftime("%Y-%m-%d") + task_latest_records[task_name] = { + "display": formatted_date, # 显示用的 MM-DD 格式 + "full": full_date # 比较用的 YYYY-MM-DD 格式 + } + except (ValueError, TypeError, OverflowError): + pass # 忽略无效的时间戳 + + # 2. 处理最新转存文件 + # 获取该任务在最新转存时间附近(同一分钟内)的所有文件 + # 这样可以处理同时转存多个文件但时间戳略有差异的情况 + time_window = 60000 # 60秒的时间窗口(毫秒) + query = """ + SELECT renamed_to, original_name, transfer_time, modify_date + FROM transfer_records + WHERE task_name = ? AND transfer_time >= ? AND transfer_time <= ? + ORDER BY id DESC + """ + cursor.execute(query, (task_name, latest_time - time_window, latest_time + time_window)) + files = cursor.fetchall() + + if files: + if len(files) == 1: + # 如果只有一个文件,直接使用 + best_file = files[0][0] # renamed_to + else: + # 如果有多个文件,使用全局排序函数进行排序 + file_list = [] + for renamed_to, original_name, transfer_time, modify_date in files: + # 构造文件信息字典,模拟全局排序函数需要的格式 + file_info = { + 'file_name': renamed_to, + 'original_name': original_name, + 'updated_at': transfer_time # 使用转存时间而不是文件修改时间 + } + file_list.append(file_info) + + # 使用全局排序函数进行正向排序,最后一个就是最新的 + try: + sorted_files = sorted(file_list, key=sort_file_by_name) + best_file = sorted_files[-1]['file_name'] # 取排序后的最后一个文件 + except Exception as e: + # 如果排序失败,使用第一个文件作为备选 + best_file = files[0][0] + + # 去除扩展名并处理季数集数信息 + if best_file: + file_name_without_ext = os.path.splitext(best_file)[0] + processed_name = process_season_episode_info(file_name_without_ext) + task_latest_files[task_name] = processed_name + + db.close() + + return jsonify({ + "success": True, + "data": { + "latest_records": task_latest_records, + "latest_files": task_latest_files + } + }) + + except Exception as e: + return jsonify({ + "success": False, + "message": f"获取任务最新信息失败: {str(e)}" + }) + + + # 删除单条转存记录 @app.route("/delete_history_record", methods=["POST"]) def delete_history_record(): diff --git a/app/static/css/main.css b/app/static/css/main.css index e2d2561..99bc8db 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -1678,7 +1678,7 @@ button.close:focus, } #fileSelectModal .table .text-warning { - color: #ffc107 !important; + color: #098eff !important; } /* 弹窗内表格行悬停效果 */ @@ -1687,7 +1687,20 @@ button.close:focus, cursor: pointer; } -#fileSelectModal .bi-file-earmark { +#fileSelectModal .bi-file-earmark, +#fileSelectModal .bi-file-earmark-play, +#fileSelectModal .bi-file-earmark-music, +#fileSelectModal .bi-file-earmark-image, +#fileSelectModal .bi-file-earmark-text, +#fileSelectModal .bi-file-earmark-richtext, +#fileSelectModal .bi-file-earmark-zip, +#fileSelectModal .bi-file-earmark-font, +#fileSelectModal .bi-file-earmark-code, +#fileSelectModal .bi-file-earmark-pdf, +#fileSelectModal .bi-file-earmark-word, +#fileSelectModal .bi-file-earmark-excel, +#fileSelectModal .bi-file-earmark-ppt, +#fileSelectModal .bi-file-earmark-medical { color: var(--dark-text-color); font-size: 0.9rem; margin-right: 5px; @@ -3915,7 +3928,7 @@ table.selectable-records .expand-button:hover { /* 模态框通用文件夹图标样式 */ #fileSelectModal .bi-folder-fill { - color: #ffc107; + color: #098eff; font-size: 0.95rem; margin-right: 4px !important; position: relative; @@ -3924,7 +3937,20 @@ table.selectable-records .expand-button:hover { } /* 模态框通用文件图标样式 */ -#fileSelectModal .bi-file-earmark { +#fileSelectModal .bi-file-earmark, +#fileSelectModal .bi-file-earmark-play, +#fileSelectModal .bi-file-earmark-music, +#fileSelectModal .bi-file-earmark-image, +#fileSelectModal .bi-file-earmark-text, +#fileSelectModal .bi-file-earmark-richtext, +#fileSelectModal .bi-file-earmark-zip, +#fileSelectModal .bi-file-earmark-font, +#fileSelectModal .bi-file-earmark-code, +#fileSelectModal .bi-file-earmark-pdf, +#fileSelectModal .bi-file-earmark-word, +#fileSelectModal .bi-file-earmark-excel, +#fileSelectModal .bi-file-earmark-ppt, +#fileSelectModal .bi-file-earmark-medical { color: var(--dark-text-color); font-size: 0.95rem; margin-right: 4px !important; @@ -3994,7 +4020,6 @@ table.selectable-records .expand-button:hover { /* 任务按钮悬停显示样式 */ .task-buttons .hover-only { opacity: 0; - transition: opacity 0.2s ease-in-out; position: absolute; right: 0; visibility: hidden; @@ -4029,24 +4054,92 @@ table.selectable-records .expand-button:hover { } } +/* 任务最近更新日期样式 */ +.task-latest-date { + color: var(--dark-text-color); + font-size: 0.95rem; + font-weight: normal; +} + +/* 任务最近转存文件样式 */ +.task-latest-file { + color: var(--dark-text-color); + font-size: 0.95rem; + font-weight: normal; +} + +/* 悬停显示模式 - 使用display来避免占用空间,同时保持平滑的悬停效果 */ +.task-latest-date.hover-only, +.task-latest-file.hover-only { + display: none; + transition: all 0.2s ease; +} + +.task .btn:hover .task-latest-date.hover-only, +.task .btn:hover .task-latest-file.hover-only { + display: inline; +} + +/* 显示设置行样式 */ +.display-setting-row > [class*='col-'] { + padding-left: 4px !important; + padding-right: 4px !important; +} + +/* 当天更新指示器样式 */ +.task-today-indicator { + display: inline-block; + font-size: 0.92rem; + font-weight: bold; +} + +.task-today-indicator i { + background: linear-gradient(45deg, #03d5ff 25%, var(--focus-border-color) 60%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + color: var(--focus-border-color); /* 备用颜色,以防渐变不支持 */ +} + +/* 当日更新图标悬停显示样式 */ +.task-today-indicator.hover-only { + opacity: 0; + visibility: hidden; +} + +/* 任务列表页面悬停显示 */ +.task:hover .task-today-indicator.hover-only { + opacity: 1; + visibility: visible; +} + +/* 转存记录页面悬停显示 */ +.table-hover tbody tr:hover .task-today-indicator.hover-only { + opacity: 1; + visibility: visible; +} + +/* 文件整理页面悬停显示 */ +.selectable-files tbody tr:hover .task-today-indicator.hover-only { + opacity: 1; + visibility: visible; +} + +.display-setting-row { + margin-left: -4px !important; + margin-right: -4px !important; +} + @media (min-width: 992px) { .display-setting-row > .col-lg-3 { padding-left: 4px !important; padding-right: 4px !important; } - .display-setting-row { - margin-left: -4px !important; - margin-right: -4px !important; - } } -.display-setting-row > [class*='col-'] { - padding-left: 4px !important; - padding-right: 4px !important; -} -.display-setting-row { - margin-left: -4px !important; - margin-right: -4px !important; +/* 调整显示设置第二行的上边距,使其与第一行保持8px间距 */ +.display-setting-row + .display-setting-row { + margin-top: -8px !important; } /* 文件整理性能设置样式 */ @@ -4075,22 +4168,6 @@ table.selectable-records .expand-button:hover { cursor: default; } -/* 任务按钮悬停显示样式 */ -.task-buttons .hover-only { - opacity: 0; - transition: opacity 0.2s ease-in-out; - position: absolute; - right: 0; - visibility: hidden; -} - -/* 修改悬停触发范围到整个任务单元 */ -.task:hover .task-buttons .hover-only { - opacity: 1; - visibility: visible; - position: static; -} - /* 确保按钮容器在悬停时保持宽度 */ .task-buttons { position: relative; @@ -5228,7 +5305,20 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil } /* 文件整理页面的文件图标样式 */ -.selectable-files .bi-file-earmark { +.selectable-files .bi-file-earmark, +.selectable-files .bi-file-earmark-play, +.selectable-files .bi-file-earmark-music, +.selectable-files .bi-file-earmark-image, +.selectable-files .bi-file-earmark-text, +.selectable-files .bi-file-earmark-richtext, +.selectable-files .bi-file-earmark-zip, +.selectable-files .bi-file-earmark-font, +.selectable-files .bi-file-earmark-code, +.selectable-files .bi-file-earmark-pdf, +.selectable-files .bi-file-earmark-word, +.selectable-files .bi-file-earmark-excel, +.selectable-files .bi-file-earmark-ppt, +.selectable-files .bi-file-earmark-medical { font-size: 1.06rem; /* 比模态框的0.95rem大一些 */ margin-right: 7px !important; /* 图标距离文本的距离 */ position: relative; @@ -5243,7 +5333,7 @@ body .selectable-files tr.selected-file:has([style*="white-space: normal"]) .fil position: relative; top: 1px; /* 可微调垂直对齐 */ left: -1px; /* 可微调水平对齐 */ - color: #ffc107; /* 保持黄色 */ + color: #098eff; /* 55%接近深蓝色 */ } /* 文件整理页面无法识别剧集编号样式 */ diff --git a/app/templates/index.html b/app/templates/index.html index 347eede..643f4b7 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -69,18 +69,109 @@ } }); + // 根据文件扩展名获取对应的Bootstrap图标类名 + function getFileIconClass(fileName, isDir = false) { + // 如果是文件夹,返回文件夹图标 + if (isDir) { + return 'bi-folder-fill'; + } + + // 获取文件扩展名(转为小写) + const ext = fileName.toLowerCase().split('.').pop(); + + // 视频文件 + const videoExts = ['mp4', 'mkv', 'avi', 'mov', 'rmvb', 'flv', 'wmv', 'm4v', 'ts', 'webm', '3gp', 'f4v']; + if (videoExts.includes(ext)) { + return 'bi-file-earmark-play'; + } + + // 音频文件 + const audioExts = ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a', 'wma', 'ape', 'ac3', 'dts']; + if (audioExts.includes(ext)) { + return 'bi-file-earmark-music'; + } + + // 图片文件 + const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'svg', 'ico', 'raw']; + if (imageExts.includes(ext)) { + return 'bi-file-earmark-image'; + } + + // 文本文件(包括歌词文件和字幕文件) + const textExts = ['txt', 'md', 'rtf', 'log', 'ini', 'cfg', 'conf', 'lrc', 'srt', 'ass', 'ssa', 'vtt', 'sup']; + if (textExts.includes(ext)) { + return 'bi-file-earmark-text'; + } + + // 富文本文件 + const richtextExts = ['rtf', 'odt']; + if (richtextExts.includes(ext)) { + return 'bi-file-earmark-richtext'; + } + + // 压缩文件 + const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'lzma', 'cab', 'iso']; + if (archiveExts.includes(ext)) { + return 'bi-file-earmark-zip'; + } + + // 字体文件 + const fontExts = ['ttf', 'otf', 'woff', 'woff2', 'eot']; + if (fontExts.includes(ext)) { + return 'bi-file-earmark-font'; + } + + // 代码文件 + const codeExts = ['js', 'html', 'css', 'py', 'java', 'c', 'cpp', 'php', 'go', 'json', 'xml', 'yml', 'yaml', 'sql', 'sh', 'bat', 'ps1', 'rb', 'swift', 'kt', 'ts', 'jsx', 'tsx', 'vue', 'scss', 'sass', 'less']; + if (codeExts.includes(ext)) { + return 'bi-file-earmark-code'; + } + + // PDF文件 + if (ext === 'pdf') { + return 'bi-file-earmark-pdf'; + } + + // Word文档 + const wordExts = ['doc', 'docx']; + if (wordExts.includes(ext)) { + return 'bi-file-earmark-word'; + } + + // Excel文档 + const excelExts = ['xls', 'xlsx', 'csv']; + if (excelExts.includes(ext)) { + return 'bi-file-earmark-excel'; + } + + // PowerPoint文档 + const pptExts = ['ppt', 'pptx']; + if (pptExts.includes(ext)) { + return 'bi-file-earmark-ppt'; + } + + // 医疗/健康相关文件 + const medicalExts = ['dcm', 'dicom', 'hl7']; + if (medicalExts.includes(ext)) { + return 'bi-file-earmark-medical'; + } + + // 默认文件图标 + return 'bi-file-earmark'; + } + // 添加检测文件整理页面文件名溢出的自定义指令 Vue.directive('check-file-overflow', { inserted: function(el, binding, vnode) { // 检查元素是否溢出 const isOverflowing = el.scrollWidth > el.clientWidth; - + // 如果绑定了值,则绑定到该值对应的文件属性上 if (binding.value) { const indexAndField = binding.value.split('|'); const index = parseInt(indexAndField[0]); const field = indexAndField[1]; - + // 设置文件的_isOverflowing属性 const files = vnode.context.fileManager.fileList; if (files && files[index]) { @@ -97,13 +188,13 @@ update: function(el, binding, vnode) { // 检查元素是否溢出 const isOverflowing = el.scrollWidth > el.clientWidth; - + // 如果绑定了值,则绑定到该值对应的文件属性上 if (binding.value) { const indexAndField = binding.value.split('|'); const index = parseInt(indexAndField[0]); const field = indexAndField[1]; - + // 设置文件的_isOverflowing属性 const files = vnode.context.fileManager.fileList; if (files && files[index]) { @@ -614,6 +705,44 @@ +
+
+
+
+ 最近更新日期 +
+ +
+
+
+
+
+ 最近转存文件 +
+ +
+
+
+
+
+ 当日更新标识 +
+ +
+
+
@@ -690,6 +819,21 @@
# + + · {{ getTaskLatestRecordDisplay(task.taskname) }} + + + · {{ taskLatestFiles[task.taskname] }} + + + +
@@ -946,17 +1090,27 @@
-
{{ record.renamed_to }} + + +
{{ record.renamed_to }} + + +
@@ -1181,7 +1335,7 @@
- +
- +
{{ file.file_name }} + + +
- {{ file.file_name }} + {{ file.file_name }} + + +
@@ -1413,13 +1577,13 @@ style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 25px;" :title="file.file_name" v-check-modal-overflow="key + '|file_name'"> - {{file.file_name}} + {{file.file_name}}
- {{file.file_name}} + {{file.file_name}}
@@ -1522,7 +1686,10 @@ run_task: "always", delete_task: "always", refresh_plex: "always", - refresh_alist: "always" + refresh_alist: "always", + latest_update_date: "always", + latest_transfer_file: "always", + today_update_indicator: "always" }, file_performance: { api_page_size: 200, @@ -1551,6 +1718,8 @@ taskDirs: [""], taskDirSelected: "", taskNameFilter: "", + taskLatestRecords: {}, // 存储每个任务的最新转存记录日期 + taskLatestFiles: {}, // 存储每个任务的最近转存文件 modalLoading: false, smart_param: { index: null, @@ -1975,6 +2144,10 @@ document.removeEventListener('click', this.handleOutsideClick); }, methods: { + // 获取文件图标类名 + getFileIconClass(fileName, isDir = false) { + return getFileIconClass(fileName, isDir); + }, // 拼音排序辅助函数 sortTaskNamesByPinyin(taskNames) { return taskNames.sort((a, b) => { @@ -2443,9 +2616,24 @@ run_task: "always", delete_task: "always", refresh_plex: "always", - refresh_alist: "always" + refresh_alist: "always", + latest_update_date: "always", + latest_transfer_file: "always", + today_update_indicator: "always" }; } + // 确保最近更新日期配置存在(向后兼容) + if (!config_data.button_display.latest_update_date) { + config_data.button_display.latest_update_date = "always"; + } + // 确保最近转存文件配置存在(向后兼容) + if (!config_data.button_display.latest_transfer_file) { + config_data.button_display.latest_transfer_file = "always"; + } + // 确保当日更新图标配置存在(向后兼容) + if (!config_data.button_display.today_update_indicator) { + config_data.button_display.today_update_indicator = "always"; + } // 确保文件整理性能配置存在 if (!config_data.file_performance) { config_data.file_performance = { @@ -2468,7 +2656,10 @@ setTimeout(() => { this.configModified = false; }, 100); - + + // 加载任务最新信息(包括记录和文件) + this.loadTaskLatestInfo(); + // 数据加载完成后检查分享链接状态 if (this.activeTab === 'tasklist') { setTimeout(() => { @@ -4043,6 +4234,94 @@ } }); }, + loadTaskLatestInfo() { + // 获取所有任务的最新转存信息(包括日期和文件) + axios.get('/task_latest_info') + .then(response => { + if (response.data.success) { + this.taskLatestRecords = response.data.data.latest_records; + this.taskLatestFiles = response.data.data.latest_files; + } else { + console.error('获取任务最新信息失败:', response.data.message); + } + }) + .catch(error => { + console.error('获取任务最新信息失败:', error); + }); + }, + getTaskLatestRecordDisplay(taskName) { + // 获取任务最新记录的显示文本 + const latestRecord = this.taskLatestRecords[taskName]; + return latestRecord ? latestRecord.display : ''; + }, + isTaskUpdatedToday(taskName) { + // 检查任务是否在今天更新 + const latestRecord = this.taskLatestRecords[taskName]; + if (!latestRecord || !latestRecord.full) { + return false; + } + + // 获取今天的完整日期,格式为 YYYY-MM-DD + const today = new Date(); + const todayFormatted = today.getFullYear() + '-' + + String(today.getMonth() + 1).padStart(2, '0') + '-' + + String(today.getDate()).padStart(2, '0'); + + return latestRecord.full === todayFormatted; + }, + isRecordUpdatedToday(record) { + // 检查转存记录是否在今天更新 + if (!record || !record.transfer_time_readable) { + return false; + } + + // 获取今天的日期,格式为 YYYY-MM-DD + const today = new Date(); + const todayFormatted = today.getFullYear() + '-' + + String(today.getMonth() + 1).padStart(2, '0') + '-' + + String(today.getDate()).padStart(2, '0'); + + // 从 transfer_time_readable 中提取日期部分(格式通常为 "YYYY-MM-DD HH:MM:SS") + const recordDate = record.transfer_time_readable.split(' ')[0]; + + return recordDate === todayFormatted; + }, + isFileUpdatedToday(file) { + // 检查文件是否在今天更新(基于修改日期) + if (!file || !file.updated_at) { + return false; + } + + // 获取今天的日期,格式为 YYYY-MM-DD + const today = new Date(); + const todayFormatted = today.getFullYear() + '-' + + String(today.getMonth() + 1).padStart(2, '0') + '-' + + String(today.getDate()).padStart(2, '0'); + + // 使用与 formatDate 方法相同的逻辑处理时间戳 + try { + const fileDate = new Date(file.updated_at); + const fileDateFormatted = fileDate.getFullYear() + '-' + + String(fileDate.getMonth() + 1).padStart(2, '0') + '-' + + String(fileDate.getDate()).padStart(2, '0'); + + return fileDateFormatted === todayFormatted; + } catch (error) { + console.error('处理文件时间戳时出错:', error, file.updated_at); + return false; + } + }, + shouldShowTodayIndicator() { + // 检查是否应该显示当日更新图标 + return this.formData.button_display.today_update_indicator !== 'disabled'; + }, + getTodayIndicatorClass() { + // 获取当日更新图标的CSS类 + if (this.formData.button_display.today_update_indicator === 'hover') { + return 'hover-only'; + } + return ''; + }, openDatePicker(index) { // 使用$refs访问对应的日期选择器并打开它 const dateRef = this.$refs[`enddate_${index}`];