惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tor Project blog
V2EX - 技术
V2EX - 技术
S
Security Affairs
Help Net Security
Help Net Security
Webroot Blog
Webroot Blog
N
News and Events Feed by Topic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Blog — PlanetScale
Blog — PlanetScale
S
SegmentFault 最新的问题
T
Threat Research - Cisco Blogs
Scott Helme
Scott Helme
IT之家
IT之家
W
WeLiveSecurity
U
Unit 42
博客园 - 聂微东
Vercel News
Vercel News
爱范儿
爱范儿
GbyAI
GbyAI
H
Hacker News: Front Page
Y
Y Combinator Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
PCI Perspectives
PCI Perspectives
博客园 - 三生石上(FineUI控件)
博客园_首页
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Microsoft Security Blog
Microsoft Security Blog
宝玉的分享
宝玉的分享
MyScale Blog
MyScale Blog
A
About on SuperTechFans
Cloudbric
Cloudbric
博客园 - 叶小钗
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
Troy Hunt's Blog
The GitHub Blog
The GitHub Blog
A
Arctic Wolf
Latest news
Latest news
AWS News Blog
AWS News Blog
MongoDB | Blog
MongoDB | Blog
量子位
Spread Privacy
Spread Privacy
D
DataBreaches.Net
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Schneier on Security
Recorded Future
Recorded Future
T
Threatpost
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

素颜-温馨的技术博客

OneDown一款面向个人站长的资源下载、技术教程、内容资讯类站点的 WordPress 主题 - 素颜 飞鹰控安卓远控源码仅供学习!已移除核心代码 - 素颜 ai生图创作系统 - 素颜 AI漫剧批量生成系统✨开局VIP 开启创作自由 - 素颜 Windows系统精简纯净系统镜像W11 win7 win1 0极致精减 - 素颜 Super File Manager Explorer 1.5.6.2 专业版 黄金积存金实时监测工具 - 素颜 文件文件夹批量重命名工具 - 素颜 普通人的赚钱机会正在悄悄转向小众市场 - 素颜 最新更新API管理系统源码 API计费 全开源 - 素颜 邮箱表白纪念日源码 - 素颜 网盘直链解析网页源码 - 素颜 【WEB】2026 AI前端面试题讲解视频 - 素颜 [玫瑰助手]LOL英雄联盟6.25免费换肤插件 Python版 - 素颜 7-zip-crypto 压缩软件 v26.02 加密算法增强版 - 素颜 【完结】JAVA+AI大模型智能应用开发实战 - 素颜 记录消费折旧:Coday 1.2.15 - 素颜 网页转APP生成器 - 素颜 王者荣耀登录游戏领7天绿钻 - 素颜 移动云盘每月领1~2亓立减金 - 素颜 一款摸鱼时开发的逗逼贪吃蛇游戏 - 素颜 【开源】AI浏览器 MCP v2.6 — 一句话采集·逆向·断点·自动化·工作流 全新小白网络验证3.0.1 永久免费版 支持X32,X64,X86任意exe一键加密 - 素颜 字幕翻译器 Subtitle.Translator 3.0.0 - 素颜 雷电模拟器14安装面具magisk - 素颜 免费分享鲸发卡企业级发卡系统修复版源码v13.01 - 素颜 免费了最新微信朋友圈访客记录系统修复版源码 - 素颜 字幕翻译 妙幕SmartSub v2.16.0 - 素颜 远程桌面云控台(最大同时操作25台服务器) - 素颜 定时关机和定时任务的软件TimerTask.exe - 素颜 安卓逆向神器NP管理器v3.1.3 - 素颜 好看的导航页源码 无复杂东西 - 素颜 注册表清理Wise Registry Cleaner v10.4.1 - 素颜 QQ音乐收听歌曲抽1~30天绿钻 - 素颜 移动灵犀口令抽取话费加赠券 - 素颜 傲梅备份AOMEI Backupper v8.4.0 - 素颜 腾讯网盘即将上线! 有微云为何还出新网盘? - 素颜 豆包去水印,浏览器插件,支持图片与视频 - 素颜 素颜个人主页引导页 - 素颜 DCSHOP自动发卡商城用户可开通分店分销,支持实物发货,自带博客功能 - 素颜 纯离线html:积分小管家:用积分带娃 - 素颜 雷电模拟器 v9.5.17绿色版 - 素颜 AIGC全方位实战课 主流AIGC工具,多场景落地(视频文档) - 素颜 纪念币预约助手v2网页插件版 - 素颜 TG 发卡机器人(支持双语 + 用户充值) - 素颜 Codex AI编程教程安装Obisidian配置网站开发项目实战课程 - 素颜 生成器自定义生成恶搞必备 - 素颜 封面制作工具v1.2 - 素颜 2026最新去水印小程序,带涂抹去水印,源码全开源 - 素颜 CMS 自动发文助手 Pro 内测版发布,支持多站点多 CMS 全能可修改水印相机打卡留痕神器自定义时间地点 - 素颜 祈福导航系统V1.3 新增广告AD模式自动下架 优化后端UI和PHP版本等 - 素颜 QQ直接领7天QQ超级会员 - 素颜 电脑酷我音乐v8.7 旧版豪华VIP版 永久可用 - 素颜 123云盘客户端v3.1.7绿色版 - 素颜 朔风下载25110109 -磁力下载神器-去VIP限制版本 - 素颜 Windows 11 26H1 Build 28000.2269 RTM 腾讯版龙虾 Marvis V1.60.13.18 - 素颜 聚合解析工具箱 V1.7.5 - 素颜 php多用户打卡记录无图片版源码 - 素颜 雷电模拟器 v14.0.8.2绿色版 - 素颜 短视频无水印下载器V1.0支持抖音小红书 - 素颜 B站知乎视频下载,油猴脚本 - 素颜 软件包管理器 UniGetUI v2026.2.0 - 素颜 花坊鲜花售卖商店微信小程序源码 - 素颜 课堂单词背诵记忆小游戏网页源码 快乐背单词游戏 - 素颜 全网音乐搜索器最新修复版 - 素颜 网页抢车位游戏源码 全开源 - 素颜 上百套求职简历模板word电子版可编辑打印 - 素颜 Adobe Acrobat Pro DC 2026安装版 广告跳过 GKD v1.12.1 - 素颜 远程桌面控制工具 AnyDesk v9.7.5 - 素颜 海康NVR录像下载以及转码 - 素颜 去水印小程序视频提取小程序源码支持二次开发流量主源码小程序 - 素颜 移动app灵犀口令送话费 - 素颜 VMware Workstation Pro 26H1精简版 - 素颜 硬盘备份Drive SnapShot v1.51.0.1791 - 素颜 子比文章同步公众号插件优化版本 - 素颜 Flux2 Klein 9B 一键整合包 AI绘画工具 文字 图片 重绘 软件模型 文本转语音工具 Balabolka v2.15.0.917 - 素颜 摇一摇禁用工具 安装即用 - 素颜 改良了一下QQ号码价值评估仅供娱乐 - 素颜 多语言自定义产品系统/企鹅养殖投资返利/一键安装 - 素颜 禁止程序联网工具Netcontrol - 素颜 RustDesk跨平台远程控制v1.4.7 - 素颜 祈福导航系统V1.1更新 优化后端控制逻辑和前台UI - 素颜 PanTools v1.1.18 全功能型的网盘批量管理&操作工具 - 素颜 安卓设备信息(Deviceinfo)纯净版 - 素颜 安卓第二空间v10.6.2高级版 - 素颜 祈福导航系统V1.0 毛玻璃效果支持ping延迟带后端管理 - 素颜 白嫖QQ紫钻,时长半年起步 - 素颜 岛链 IslandChain 正式开源 —— 博客互联程序源码 子比主题-头像修改次数插件 - 素颜 一站式AI图片视频创作系统 - 素颜 小粉圈壁纸内置多种动漫风景等4k高清壁纸 - 素颜 园园复制粘贴小工具,解决日常工作中频繁复制粘贴的效率问题 - 素颜 WiFi安全测试仪查看连接人数检测WiFi安全 - 素颜 霄屿实用工具箱(解锁VIP功能) - 素颜 车辆上牌登记系统开源版源码/车辆登记管理系统源码 - 素颜
自用 python写的 Rclone WebDav 挂载工具 挂载本地磁盘
suyan · 2026-06-29 · via 素颜-温馨的技术博客

共计 18682 个字符,预计需要花费 47 分钟才能阅读完成。

Rclone  单 WebDav  图形化挂载工具

功能简单,托盘图标,自定义图标(同目录 app.ico)
本地磁盘和网络磁盘位置可选
自定义显示容量大小(只是显示大小,和实际容量无关)
需要安装 winfsp,同目录需要 rclone.exe。
欢迎修改打包分享!

自用 python 写的 Rclone WebDav 挂载工具 挂载本地磁盘 自用 python 写的 Rclone WebDav 挂载工具 挂载本地磁盘

https://wwbhx.lanzout.com/b00mqfsi0b  密码:8ysl

import sys
import os
import json
import ctypes
import subprocess
import time
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
    QListWidget, QListWidgetItem, QMessageBox, QSystemTrayIcon,
    QMenu, QAction, QStyle, QDialog, QFormLayout, QLineEdit,
    QComboBox, QSpinBox, QCheckBox, QLabel, QDialogButtonBox,
)
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QFont, QIcon
 
DEBUG = True
def debug_print(*args, **kwargs):
    if DEBUG:
        print("[DEBUG]", *args, **kwargs)
 
RCLONE_EXE = "rclone.exe"
CONFIG_FILE = "mounts.json"
 
# ---------- 工具函数 ----------
def get_used_drives():
    drives = set()
    try:
        mask = ctypes.windll.kernel32.GetLogicalDrives()
        for i in range(26):
            if mask & (1 << i): drives.add(chr(ord('A') + i)) except: for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": if os.path.exists(f"{letter}:\"): drives.add(letter) return drives def rclone_reveal(obscured: str) -> str:
    cmd = [RCLONE_EXE, "reveal", obscured]
    try:
        proc = subprocess.run(cmd, capture_output=True, text=True,
                              encoding='utf-8', errors='replace',
                              creationflags=subprocess.CREATE_NO_WINDOW)
        if proc.returncode == 0:
            return proc.stdout.strip()
    except:
        pass
    return ""
 
def get_remote_password(remote_name: str) -> str:
    ret, out, err = rclone_cmd(["config", "dump"])
    if ret != 0:
        return ""
    try:
        config = json.loads(out)
        remote_config = config.get(remote_name, {})
        obscured = remote_config.get("pass", "")
        if obscured:
            return rclone_reveal(obscured)
    except:
        pass
    return ""
 
def rclone_cmd(args, cwd=None):
    cmd = [RCLONE_EXE] + args
    debug_print("执行:", " ".join(cmd))
    try:
        proc = subprocess.run(
            cmd, capture_output=True, text=True, encoding='utf-8',
            errors='replace', cwd=cwd,
            creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
        )
        stdout = proc.stdout.strip() if proc.stdout else ""stderr = proc.stderr.strip() if proc.stderr else""
        debug_print("返回码:", proc.returncode)
        if stdout:
            debug_print("stdout:", stdout[:200])
        if stderr:
            debug_print("stderr:", stderr[:200])
        return proc.returncode, stdout, stderr
    except Exception as e:
        debug_print("命令执行异常:", e)
        return -1, "", str(e)
 
# ---------- 编辑对话框 ----------
class MountEditDialog(QDialog):
    def __init__(self, parent=None, existing=None, existing_password=None):
        super().__init__(parent)
        self.existing = existing
        self.setWindowTitle("编辑挂载配置" if existing else "添加挂载配置")
        self.setFixedSize(420, 350)
        self.setStyleSheet(parent.styleSheet())
 
        layout = QFormLayout(self)
        layout.setSpacing(12)
 
        self.name_edit = QLineEdit()
        self.name_edit.setPlaceholderText("显示名称(也是 remote 名)")
        layout.addRow("配置名称:", self.name_edit)
 
        self.url_edit = QLineEdit()
        self.url_edit.setPlaceholderText("http://192.168.1.5:5244/alist/dav")
        layout.addRow("DAV 地址:", self.url_edit)
 
        self.user_edit = QLineEdit()
        self.user_edit.setPlaceholderText("用户名")
        layout.addRow("用户名:", self.user_edit)
 
        self.pass_edit = QLineEdit()
        self.pass_edit.setEchoMode(QLineEdit.Password)
        self.pass_edit.setPlaceholderText("密码(留空则不修改)" if existing else "密码")
        layout.addRow("密码:", self.pass_edit)
 
        drive_layout = QHBoxLayout()
        self.drive_combo = QComboBox()
        self.drive_combo.setFixedWidth(80)
        drive_layout.addWidget(self.drive_combo)
        drive_layout.addStretch()
        layout.addRow("盘符:", drive_layout)
 
        size_layout = QHBoxLayout()
        self.size_spin = QSpinBox()
        self.size_spin.setRange(1, 99999)
        self.size_spin.setValue(20)
        self.size_spin.setFixedWidth(100)
        size_layout.addWidget(self.size_spin)
        self.unit_combo = QComboBox()
        self.unit_combo.addItems(["M", "G", "T", "P", "E"])
        self.unit_combo.setCurrentText("G")
        self.unit_combo.setFixedWidth(80)
        size_layout.addWidget(self.unit_combo)
        size_layout.addStretch()
        layout.addRow("磁盘容量:", size_layout)
 
        self.network_check = QCheckBox("网络位置")
        self.network_check.setChecked(False)
        layout.addRow("", self.network_check)
 
        self.auto_mount_check = QCheckBox("启动时自动挂载")
        self.auto_mount_check.setChecked(False)
        layout.addRow("", self.auto_mount_check)
 
        # 自定义确认 / 取消按钮
        btn_confirm = QPushButton("确认")
        btn_confirm.setFixedSize(80, 30)
        btn_confirm.setStyleSheet("""
            QPushButton {background-color: #3498DB; color: white; border-radius: 4px; font-weight: bold;}
            QPushButton:hover {background-color: #2980B9;}
        """)
        btn_confirm.clicked.connect(self.accept)
 
        btn_cancel = QPushButton("取消")
        btn_cancel.setFixedSize(80, 30)
        btn_cancel.setStyleSheet("""
            QPushButton {background-color: #95A5A6; color: white; border-radius: 4px; font-weight: bold;}
            QPushButton:hover {background-color: #7F8C8D;}
        """)
        btn_cancel.clicked.connect(self.reject)
 
        btn_layout = QHBoxLayout()
        btn_layout.addStretch()
        btn_layout.addWidget(btn_confirm)
        btn_layout.addWidget(btn_cancel)
        layout.addRow(btn_layout)
 
        if existing:
            self.name_edit.setText(existing.get("name", ""))
            self.url_edit.setText(existing.get("url", ""))
            self.user_edit.setText(existing.get("user", ""))
            self.size_spin.setValue(existing.get("size_value", 20))
            old_unit = existing.get("size_unit", "G")
            if old_unit.endswith("B"): old_unit = old_unit[0]
            self.unit_combo.setCurrentText(old_unit)
            self.network_check.setChecked(existing.get("network_mode", True))
            self.auto_mount_check.setChecked(existing.get("auto_mount", False))
 
            if existing_password is not None:
                self.pass_edit.setText(existing_password)
 
    def populate_drives(self, used_letters, reserved_letters=set()):
        self.drive_combo.clear()
        all_letters = set(chr(i) for i in range(ord('D'), ord('Z')+1))
        excluded = used_letters - reserved_letters
        available = sorted(all_letters - excluded)
        self.drive_combo.addItems([f"{d}:" for d in available])
        if self.existing and self.existing.get("drive"):
            idx = self.drive_combo.findText(self.existing["drive"])
            if idx >= 0:
                self.drive_combo.setCurrentIndex(idx)
 
    def get_data(self):
        return {"name": self.name_edit.text().strip(),
            "url": self.url_edit.text().strip(),
            "user": self.user_edit.text().strip(),
            "password": self.pass_edit.text(),
            "drive": self.drive_combo.currentText(),
            "size_value": self.size_spin.value(),
            "size_unit": self.unit_combo.currentText(),
            "network_mode": self.network_check.isChecked(),
            "auto_mount": self.auto_mount_check.isChecked()}
 
# ---------- 列表项部件 ----------
class MountItemWidget(QWidget):
    BTN_OFFSET = "margin-top: -3px;"
 
    @staticmethod
    def _btn_style(bg_color, extra=""):
        return f"{MountItemWidget.BTN_OFFSET} background-color: {bg_color}; color: white; border-radius:4px; font-weight:bold; {extra}"
 
    def __init__(self, entry, parent_app):
        super().__init__()
        self.entry = entry
        self.app = parent_app
        self.is_mounted = False
 
        layout = QHBoxLayout(self)
        layout.setContentsMargins(8, 6, 8, 6)
        layout.setSpacing(6)
        layout.setAlignment(Qt.AlignVCenter)
 
        drive = entry.get("drive", "") or"?"
        self.drive_label = QLabel(drive)
        self.drive_label.setFixedWidth(30)
        self.drive_label.setStyleSheet("font-weight: bold; color: #2c3e50;")
 
        mode = "网络" if entry.get("network_mode", True) else "本地"
        self.mode_label = QLabel(mode)
        self.mode_label.setFixedWidth(40)
        color = "#2980b9" if mode == "网络" else "#27ae60"
        self.mode_label.setStyleSheet(f"color: {color}; font-weight: bold;")
 
        self.name_label = QLabel(entry["name"])
        self.name_label.setFixedWidth(140)
        self.name_label.setStyleSheet("font-weight: bold; color: #2c3e50;")
 
        cap = f"{entry.get('size_value',20)}{entry.get('size_unit','G')}"
        self.cap_label = QLabel(cap)
        self.cap_label.setFixedWidth(60)
        self.cap_label.setStyleSheet("color: #7f8c8d;")
 
        self.btn_auto = QPushButton("自动挂载")
        self.btn_auto.setFixedSize(64, 30)
        self.btn_auto.setCheckable(True)
        self.btn_auto.setChecked(entry.get("auto_mount", False))
        self.btn_auto.clicked.connect(self.on_auto_toggled)
        self.update_auto_style()
 
        self.btn_action = QPushButton("启动")
        self.btn_action.setFixedSize(64, 30)
        self.btn_action.clicked.connect(self.on_action_clicked)
        self.btn_action.setStyleSheet(self._btn_style("#3498DB"))
 
        self.btn_edit = QPushButton("编辑")
        self.btn_edit.setFixedSize(64, 30)
        self.btn_edit.clicked.connect(self.edit_entry)
        self.btn_edit.setStyleSheet(self._btn_style("#3498DB"))
 
        self.btn_delete = QPushButton("删除")
        self.btn_delete.setFixedSize(64, 30)
        self.btn_delete.clicked.connect(self.delete_entry)
        self.btn_delete.setStyleSheet(self._btn_style("#E74C3C"))
 
        layout.addWidget(self.drive_label)
        layout.addWidget(self.mode_label)
        layout.addWidget(self.name_label)
        layout.addWidget(self.cap_label)
        layout.addWidget(self.btn_auto)
        layout.addWidget(self.btn_action)
        layout.addWidget(self.btn_edit)
        layout.addWidget(self.btn_delete)
 
    def on_auto_toggled(self):
        checked = self.btn_auto.isChecked()
        self.entry["auto_mount"] = checked
        self.app.update_entry_metadata(self.entry)
        self.update_auto_style()
 
    def update_auto_style(self):
        if self.btn_auto.isChecked():
            self.btn_auto.setText("自动: 开")
            self.btn_auto.setStyleSheet(self._btn_style("#27AE60"))
        else:
            self.btn_auto.setText("自动: 关")
            self.btn_auto.setStyleSheet(self._btn_style("#95A5A6"))
 
    def on_action_clicked(self):
        if not self.entry.get("drive"):
            QMessageBox.warning(self, "错误", "请先设置盘符!")
            return
        if self.is_mounted:
            self.open_drive()
        else:
            self.app.mount_entry(self.entry)
 
    def open_drive(self):
        drive = self.entry.get("drive")
        if not drive:
            QMessageBox.warning(self, "错误", "盘符未设置!")
            return
        for _ in range(30):
            if os.path.exists(drive + "\"):
                os.startfile(drive + "\")
                return
            time.sleep(0.1)
        QMessageBox.warning(self, "错误", f"盘符 {drive} 不可用,请等待挂载完成。")
 
    def edit_entry(self):
        self.app.edit_entry_dialog(self.entry)
 
    def delete_entry(self):
        self.app.delete_entry(self.entry)
 
# ---------- 主窗口 ----------
class RcloneTrayApp(QWidget):
    def __init__(self):
        super().__init__()
        self.base_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
        self.metadata = {}
        self.active_mounts = {}
        self.load_metadata()
        self.init_ui()
        self.init_tray()
        self.sync_from_rclone()
        self.refresh_list()
        QTimer.singleShot(1000, self.auto_mount_selected)
        debug_print("启动完成,程序目录:", self.base_dir)
 
    def get_app_icon(self):
        """优先使用 app.ico,找不到则使用系统标准图标"""
        # 1. 程序所在目录
        ico_path = os.path.join(self.base_dir, "app.ico")
        if os.path.exists(ico_path):
            return QIcon(ico_path)
 
        # 2. PyInstaller 打包后的释放目录
        if getattr(sys, 'frozen', False):
            meipass = getattr(sys, '_MEIPASS', None)
            if meipass:
                ico_path = os.path.join(meipass, "app.ico")
                if os.path.exists(ico_path):
                    return QIcon(ico_path)
 
        # 3. 兜底:Qt 内置图标
        return self.style().standardIcon(QStyle.SP_DriveNetIcon)
 
    def load_metadata(self):
        try:
            with open(CONFIG_FILE, "r", encoding="utf-8") as f:
                self.metadata = json.load(f)
        except:
            self.metadata = {}
 
    def save_metadata(self):
        with open(CONFIG_FILE, "w", encoding="utf-8") as f:
            json.dump(self.metadata, f, indent=2, ensure_ascii=False)
 
    def update_entry_metadata(self, entry):
        self.metadata[entry["id"]] = {k: entry[k] for k in ("name", "url", "user", "drive",
                                 "size_value", "size_unit", "network_mode",
                                 "auto_mount")
        }
        self.save_metadata()
 
    def sync_from_rclone(self):
        ret, out, err = rclone_cmd(["listremotes"], cwd=self.base_dir)
        if ret != 0:
            debug_print("rclone listremotes 失败:", err)
            return
        remotes = [r.strip(":") for r in out.splitlines() if r.strip()]
        debug_print("发现远程配置:", remotes)
        for remote in remotes:
            if remote not in self.metadata:
                self.metadata[remote] = {
                    "name": remote, "url": "","user":"",
                    "drive": "","size_value": 20,"size_unit":"G","network_mode": True,"auto_mount": False
                }
        for remote in list(self.metadata.keys()):
            if remote not in remotes:
                del self.metadata[remote]
        self.save_metadata()
 
    def get_all_entries(self):
        entries = []
        for rid, meta in self.metadata.items():
            entry = {"id": rid}
            entry.update(meta)
            entries.append(entry)
        return entries
 
    def init_ui(self):
        self.setWindowTitle("Rclone WebDav 挂载管理")
        self.setFixedSize(620, 480)
        self.setWindowIcon(self.get_app_icon())
 
        # 全局样式
        self.setStyleSheet("""
            QWidget {
                background: #FFFFFF;
                font-family: "Microsoft YaHei", "微软雅黑";
                font-size: 13px;
                color: #2c3e50;
            }
            QLineEdit, QComboBox, QSpinBox {
                background: #FFFFFF;
                border: 1px solid #BDC3C7;
                border-radius: 4px;
                padding: 5px 8px;
                color: #2c3e50;
                selection-background-color: #3498DB;
            }
            QLineEdit:focus, QComboBox:focus, QSpinBox:focus {border-color: #3498DB;}
            QComboBox::drop-down {
                width: 0px;
                border: none;
            }
            QSpinBox::up-button, QSpinBox::down-button {
                width: 0px;
                border: none;
            }
            QCheckBox {
                spacing: 6px;
                color: #2c3e50;
            }
            QListWidget {
                background: transparent;
                border: none;
                outline: none;
            }
            QListWidget::item {
                background: #FFFFFF;
                border: none;
                border-bottom: 1px solid #E0E4E8;
                margin: 0px;
                padding: 0px;
            }
            QListWidget::item:selected {background: #EBF5FB;}
        """)
 
        main_layout = QVBoxLayout(self)
        main_layout.setContentsMargins(10, 10, 10, 10)
        main_layout.setSpacing(6)
 
        top_layout = QHBoxLayout()
        top_layout.setSpacing(10)
 
        self.btn_add = QPushButton("+ 添加配置")
        self.btn_add.setFixedSize(120, 30)
        self.btn_add.setStyleSheet("QPushButton { background-color: #3498DB; color: white; border-radius: 4px; font-weight: bold;}"
            "QPushButton:hover {background-color: #2980B9;}"
        )
        self.btn_add.clicked.connect(self.add_entry_dialog)
 
        self.btn_disconnect = QPushButton("断开挂载")
        self.btn_disconnect.setFixedSize(120, 30)
        self.btn_disconnect.setStyleSheet("QPushButton { background-color: #E74C3C; color: white; border-radius: 4px; font-weight: bold;}"
            "QPushButton:hover {background-color: #C0392B;}"
        )
        self.btn_disconnect.clicked.connect(self.disconnect_all_mounts)
 
        top_layout.addWidget(self.btn_add)
        top_layout.addWidget(self.btn_disconnect)
        top_layout.addStretch()
        main_layout.addLayout(top_layout)
 
        self.list_widget = QListWidget()
        self.list_widget.setSpacing(0)
        self.list_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        main_layout.addWidget(self.list_widget)
 
    def init_tray(self):
        tray_icon = self.get_app_icon()
        self.tray_icon = QSystemTrayIcon(self)
        self.tray_icon.setIcon(tray_icon)
        tray_menu = QMenu()
        show_act = QAction("显示窗口", self, triggered=self.show_window)
        exit_act = QAction("退出", self, triggered=self.quit_app)
        tray_menu.addAction(show_act)
        tray_menu.addAction(exit_act)
        self.tray_icon.setContextMenu(tray_menu)
        self.tray_icon.activated.connect(self.on_tray_activated)
        self.tray_icon.show()
 
    def show_window(self):
        self.showNormal()
        self.activateWindow()
        self.raise_()
 
    def close_to_tray(self):
        self.hide()
 
    def quit_app(self):
        subprocess.run("taskkill /IM rclone.exe /F", shell=True,
                       capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW)
        self.active_mounts.clear()
        self.tray_icon.hide()
        QApplication.quit()
 
    def on_tray_activated(self, reason):
        if reason == QSystemTrayIcon.DoubleClick:
            self.show_window()
 
    def closeEvent(self, event):
        event.ignore()
        self.hide()
 
    def disconnect_all_mounts(self):
        subprocess.run("taskkill /IM rclone.exe /F", shell=True,
                       capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW)
        self.active_mounts.clear()
        self.refresh_list()
        QMessageBox.information(self, "提示", "所有挂载已断开")
 
    def refresh_list(self):
        self.list_widget.clear()
        entries = self.get_all_entries()
        for entry in entries:
            item = QListWidgetItem()
            widget = MountItemWidget(entry, self)
            item.setSizeHint(widget.sizeHint())
            self.list_widget.addItem(item)
            self.list_widget.setItemWidget(item, widget)
 
            auto_on = entry.get("auto_mount", False)
            widget.btn_auto.setChecked(auto_on)
            widget.update_auto_style()
 
            is_mounted = (entry["drive"] in self.active_mounts)
            widget.is_mounted = is_mounted
            if is_mounted:
                widget.btn_action.setText("打开")
                widget.btn_action.setStyleSheet(MountItemWidget._btn_style("#27AE60"))
            else:
                widget.btn_action.setText("启动")
                widget.btn_action.setStyleSheet(MountItemWidget._btn_style("#3498DB"))
 
    def get_used_drive_letters(self):
        used = get_used_drives()
        for entry in self.get_all_entries():
            if entry.get("drive"):
                used.add(entry["drive"][0])
        return used
 
    def auto_mount_selected(self):
        auto_entries = [e for e in self.get_all_entries() if e.get("auto_mount") and e.get("drive")]
        debug_print(f"自动挂载条目数: {len(auto_entries)}")
        for entry in auto_entries:
            debug_print(f"自动挂载: {entry['name']} -> {entry['drive']}")
            self.mount_entry(entry)
 
    # ---------- 增删改 ----------
    def add_entry_dialog(self):
        used = self.get_used_drive_letters()
        dlg = MountEditDialog(self)
        dlg.populate_drives(used)
        if dlg.exec_() != QDialog.Accepted:
            return
        data = dlg.get_data()
        if not data["name"] or not data["url"] or not data["user"] or not data["password"]:
            QMessageBox.warning(self, "警告", "必填项不能为空")
            return
 
        remote_name = data["name"]
        if remote_name in self.metadata:
            QMessageBox.warning(self, "警告", "配置名称已存在")
            return
 
        ret, out, err = rclone_cmd([
            "config", "create", remote_name, "webdav",
            "vendor=other", f"url={data['url']}", f"user={data['user']}",
            f"pass={data['password']}", "--obscure"
        ], cwd=self.base_dir)
        if ret != 0:
            QMessageBox.warning(self, "创建失败", err)
            return
 
        self.metadata[remote_name] = {"name": data["name"], "url": data["url"], "user": data["user"],
            "drive": data["drive"], "size_value": data["size_value"],
            "size_unit": data["size_unit"], "network_mode": data["network_mode"],
            "auto_mount": data["auto_mount"]
        }
        self.save_metadata()
        self.refresh_list()
 
    def edit_entry_dialog(self, entry):
        used = self.get_used_drive_letters()
        reserved = set(entry["drive"][0]) if entry["drive"] else set()
        existing_password = get_remote_password(entry["id"])
 
        dlg = MountEditDialog(self, existing=entry, existing_password=existing_password)
        dlg.populate_drives(used, reserved)
        if dlg.exec_() != QDialog.Accepted:
            return
        data = dlg.get_data()
        if not data["name"] or not data["url"] or not data["user"]:
            QMessageBox.warning(self, "警告", "名称、地址、用户名不能为空")
            return
 
        old_remote = entry["id"]
        new_name = data["name"]
        name_changed = (new_name != old_remote)
        new_pass = data["password"]
 
        need_recreate = name_changed or bool(new_pass)
 
        if need_recreate:
            if name_changed and new_name in self.metadata:
                QMessageBox.warning(self, "警告", "新配置名称已存在")
                return
 
            actual_pass = new_pass if new_pass else existing_password
            if not actual_pass:
                QMessageBox.warning(self, "错误", "无法获取原始密码,请重新输入密码")
                return
 
            if name_changed:
                rclone_cmd(["config", "delete", old_remote], cwd=self.base_dir)
 
            ret, _, err = rclone_cmd([
                "config", "create", new_name, "webdav",
                "vendor=other", f"url={data['url']}", f"user={data['user']}",
                f"pass={actual_pass}", "--obscure"
            ], cwd=self.base_dir)
            if ret != 0:
                QMessageBox.warning(self, "更新失败", err)
                return
 
        if name_changed:
            del self.metadata[old_remote]
        self.metadata[new_name] = {"name": data["name"], "url": data["url"], "user": data["user"],
            "drive": data["drive"], "size_value": data["size_value"],
            "size_unit": data["size_unit"], "network_mode": data["network_mode"],
            "auto_mount": data["auto_mount"]
        }
        self.save_metadata()
 
        if entry["drive"] in self.active_mounts:
            self.unmount_entry(entry)
        self.refresh_list()
 
    def delete_entry(self, entry):
        if QMessageBox.question(self, "确认", f"删除'{entry['name']}'?") != QMessageBox.Yes:
            return
        if entry["drive"] in self.active_mounts:
            self.unmount_entry(entry)
        rclone_cmd(["config", "delete", entry["id"]], cwd=self.base_dir)
        del self.metadata[entry["id"]]
        self.save_metadata()
        self.refresh_list()
 
    # ---------- 挂载 / 卸载 ----------
    def mount_entry(self, entry):
        drive = entry.get("drive")
        if not drive:
            QMessageBox.warning(self, "挂载失败", "未设置盘符")
            return
 
        if drive in self.active_mounts:
            self.unmount_entry(entry)
 
        cache_subdir = os.path.join(self.base_dir, "Temp", entry["name"])
        os.makedirs(cache_subdir, exist_ok=True)
 
        total_size = f"{entry.get('size_value',20)}{entry.get('size_unit','G')}"
        args = ["mount", f"{entry['id']}:", drive,
            "--cache-dir", f"./Temp/{entry['name']}",
            "--vfs-cache-mode", "full",
            "--allow-non-empty",
            "--dir-cache-time", "5s",
            "--allow-other",
            "--vfs-disk-space-total-size", total_size
        ]
        if entry.get("network_mode", True):
            args.append("--network-mode")
 
        debug_print("挂载命令:", f'rclone {" ".join(args)}')
        try:
            proc = subprocess.Popen([RCLONE_EXE] + args,
                cwd=self.base_dir,
                creationflags=subprocess.CREATE_NO_WINDOW
            )
            self.active_mounts[drive] = proc
            self.refresh_list()
            debug_print(f"挂载成功: {drive}")
        except Exception as e:
            QMessageBox.warning(self, "挂载失败", str(e))
 
    def unmount_entry(self, entry):
        drive = entry["drive"]
        if drive not in self.active_mounts:
            return
        proc = self.active_mounts.pop(drive)
        rclone_cmd(["unmount", drive], cwd=self.base_dir)
        if proc.poll() is None:
            try:
                proc.terminate()
                proc.wait(timeout=5)
            except:
                proc.kill()
        self.refresh_list()
 
if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setQuitOnLastWindowClosed(False)
    app.setFont(QFont("Microsoft YaHei", 9))
    window = RcloneTrayApp()
    window.show()
    sys.exit(app.exec_())