This commit is contained in:
2025-10-29 22:20:21 +08:00
commit 32b3b7b29a
111 changed files with 344425 additions and 0 deletions

35
app/core/__init__.py Normal file
View File

@@ -0,0 +1,35 @@
from .api import miaoStarsBasicApi
from .utils.morelang import lang
from .utils.config import qconfig, cfg, userConfig, policyConfig
from .utils.signal_bus import signalBus
from .utils.format import getFileIcon, formatSize, formatDate
from .services.login_thread import CaptchaThread, LoginThread, RegisterThread
from .services.user_thread import (
UserNickNameUpdateThread,
UserAvatarUpdateThread,
GetUserAvatarThread,
GetPackThread,
GetPoliciesThread,
ChangePolicyThread,
DeleteTagThread,
AddTagThread,
)
from .services.file_thread import (
ListFileThread,
CreateFolderThread,
DeleteFileThread,
ListSearchThread,
ListShareThread,
UploadThread,
DownloadShareThread,
DownloadThread,
GetShareFileInfoThread,
UpdateFileContentThread,
)
from .services.preview_thread import TextLoaderThread, ImageLoaderThread

9
app/core/api/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
from .basicApi import MiaoStarsBasicApi
from ..utils.config import userConfig
miaoStarsBasicApi = MiaoStarsBasicApi()
# 从userConfig中恢复token如果有
token = userConfig.getToken()
if token:
miaoStarsBasicApi.setToken(token)

578
app/core/api/basicApi.py Normal file
View File

@@ -0,0 +1,578 @@
"""这里存放基本API包括:
登录、注册、图形验证码获取
用户配置获取、用户头像获取
用户存储策略获取、用户仓内文件获取
"""
import time
from typing import Literal, Optional
from urllib.parse import quote_plus
import requests
from loguru import logger
from PyQt6.QtCore import QBuffer, QByteArray, QIODevice
from PyQt6.QtGui import QPixmap
from ..utils import getCode
from ..utils.config import policyConfig, userConfig
class MiaoStarsBasicApi:
_publicHeader = {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
}
def __init__(self, token: Optional[str] = None):
self.basicApi = "http://leonmmcoset.jjxmm.win:5212/api/v4"
self.session = requests.Session()
self.session.verify = True
self.session.headers.update(MiaoStarsBasicApi._publicHeader)
# Cloudreve V4 使用 JWT 认证
# 优先使用传入的token如果没有则尝试从userConfig获取
self.token = token or userConfig.getToken()
if self.token:
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
self.userId = None
def returnSession(self) -> requests.Session:
return self.session
def setToken(self, token: str):
"""设置 JWT token 并同步到全局配置"""
# 确保token是字符串类型
if isinstance(token, str):
self.token = token
self.session.headers.update({"Authorization": f"Bearer {token}"})
# 同步更新到全局userConfig确保认证信息持久化
userConfig.setToken(token)
elif isinstance(token, dict) and token.get("access_token"):
# 兼容处理如果传入的是token对象提取access_token
access_token = token["access_token"]
self.token = access_token
self.session.headers.update({"Authorization": f"Bearer {access_token}"})
userConfig.setToken(access_token)
def request(
self, method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"], url, **kwargs
) -> dict:
maxRetries = 3
timeout = 20
for attempt in range(maxRetries):
try:
# 发起请求
response = self.session.request(
method=method, url=self.basicApi + url, timeout=timeout, **kwargs
)
# 检查HTTP状态码
response.raise_for_status()
# 解析JSON响应
try:
r = response.json()
# 保留原始响应格式不要过早转换Cloudreve V4的响应
# Cloudreve V4的错误响应通常包含{code, msg},而不是{error}
# 让具体的API方法来处理响应格式转换
return r
except:
return response.content
except requests.exceptions.RequestException as e:
# 网络相关错误,进行重试
if attempt < maxRetries - 1:
time.sleep(1) # 简单固定延迟
continue
else:
logger.error(f"请求失败: {str(e)}")
return {"code": -1, "msg": str(e)}
except Exception as e:
logger.error(f"处理响应时出错: {str(e)}")
return {"code": -1, "msg": str(e)}
def getCaptcha(self):
"""获取图形验证码 (Cloudreve V4 API)"""
import logging
logger = logging.getLogger(__name__)
logger.debug(f"请求验证码API: {self.basicApi}/site/captcha")
r = self.request(method="GET", url="/site/captcha")
logger.debug(f"验证码API响应: {r}")
# 根据Cloudreve V4 API文档响应格式为
# {"code": 0, "data": {"image": "data:image/png;base64,...", "ticket": "..."}, "msg": ""}
# 转换响应格式以保持与前端的兼容性
if isinstance(r, dict):
# 检查是否是标准的Cloudreve V4成功响应
if r.get("code") == 0 and isinstance(r.get("data"), dict):
data = r["data"]
# 确保image字段存在且是有效的base64格式
if "image" in data:
# 保存ticket到实例变量供后续登录/注册使用
self.captcha_ticket = data.get("ticket", "")
logger.debug(f"成功获取验证码已保存ticket: {self.captcha_ticket}")
# 前端代码在CaptchaThread.run()中使用response["data"].split(",")[1]
# 所以需要确保返回的data格式符合前端期望
captcha_image = data["image"]
# 检查是否已经包含data:image/png;base64,前缀
if captcha_image.startswith("data:image/png;base64,"):
# 保持原样让前端去split
result = {"code": 0, "data": captcha_image}
else:
# 如果没有前缀加上前缀以确保前端能正确split
result = {"code": 0, "data": f"data:image/png;base64,{captcha_image}"}
logger.debug(f"返回前端验证码数据格式: {result}")
return result
# 处理可能的错误响应
elif "msg" in r:
logger.error(f"验证码API错误: {r['msg']}")
return {"code": -1, "msg": r["msg"]}
elif "error" in r:
logger.error(f"验证码API错误: {r['error']}")
return {"code": -1, "msg": r["error"]}
else:
logger.error(f"验证码API返回非字典格式: {type(r)}")
# 默认返回失败格式
logger.error("获取验证码失败,返回默认错误格式")
return {"code": -1, "msg": "获取验证码失败"}
def login(self, username, password, captcha):
"""登录 (Cloudreve V4 JWT认证)"""
url = "/session/token"
# Cloudreve V4 API参数规范使用email, password, captcha和ticket
payload = {
"email": username, # 更正参数名username -> email
"password": password,
"captcha": captcha # 更正参数名captcha_code -> captcha
}
# 如果有保存的ticket则添加到请求中使用正确的参数名ticket
if hasattr(self, 'captcha_ticket') and self.captcha_ticket:
payload['ticket'] = self.captcha_ticket # 更正参数名captcha_ticket -> ticket
r = self.request(
"POST",
url,
json=payload,
)
# 输出服务器返回的原始信息到控制台
print(f"登录API服务器返回原始信息: {r}")
# 处理登录响应
# Cloudreve V4的响应格式{code: 0, data: {user: {...}, token: {...}}, msg: ""}
if isinstance(r, dict):
# 检查是否是成功响应
if r.get("code") == 0 and r.get("data"):
data = r["data"]
# 设置 JWT token
if data.get("token") and isinstance(data["token"], dict):
# 提取access_token字段作为Bearer token值
access_token = data["token"].get("access_token")
if access_token:
# 设置到当前API实例
self.setToken(access_token)
# 同时保存到全局userConfig中确保认证信息持久化
userConfig.setToken(access_token)
# 存储用户信息
user_info = {
"code": 0,
"data": {
"id": data.get("user", {}).get("id"),
"nick": data.get("user", {}).get("nickname", data.get("user", {}).get("nick")),
"email": username
}
}
self.userId = data.get("user", {}).get("id")
return user_info
# 处理错误响应
# 优先检查msg字段因为Cloudreve V4返回的错误格式是{code, msg}而不是{error}
error_msg = r.get("msg", r.get("error", "登录失败"))
return {"code": -1, "msg": error_msg}
return {"code": -1, "msg": "登录失败:响应格式错误"}
def register(self, username, password, captcha):
"""注册"""
url = "/user"
# Cloudreve V4 API参数规范使用email, password, captcha和ticket
payload = {
"email": username,
"password": password,
"captcha": captcha # 更正参数名captcha_code -> captcha
}
# 如果有保存的ticket则添加到请求中使用正确的参数名ticket
if hasattr(self, 'captcha_ticket') and self.captcha_ticket:
payload['ticket'] = self.captcha_ticket # 更正参数名captcha_ticket -> ticket
r = self.request(
"POST",
url,
json=payload,
)
# 输出服务器返回的原始信息到控制台
print(f"注册API服务器返回原始信息: {r}")
# 处理注册响应
# Cloudreve V4的响应格式{code: 0, data: {...}, msg: ""} 或 {code: 错误码, msg: "错误信息"}
if isinstance(r, dict):
# 检查是否是成功响应
if r.get("code") == 0 or r.get("id"):
return {"code": 203, "msg": "注册成功"}
# 处理错误响应
# 优先检查msg字段因为Cloudreve V4返回的错误格式是{code, msg}而不是{error}
error_msg = r.get("msg", r.get("error", "注册失败"))
return {"code": -1, "msg": error_msg}
return {"code": -1, "msg": "注册失败:响应格式错误"}
def updateUserNickname(self, nickname):
"""更新用户昵称"""
url = "/user/profile"
r = self.request(
"PUT",
url,
json={"nick": nickname},
)
# 转换响应格式
if r.get("id"):
return {"code": 0, "msg": "更新成功"}
else:
return {"code": -1, "msg": r.get("error", "更新失败")}
def updateUserAvatar(self, qimage):
"""更新用户头像 - 专门处理 QImage 对象"""
url = "/user/avatar"
# 检查 QImage 是否有效
if qimage.isNull():
return {"code": -1, "msg": "QImage 对象为空"}
# 将 QImage 转换为 JPEG 格式字节数据
byte_array = QByteArray()
buffer = QBuffer(byte_array)
buffer.open(QIODevice.WriteOnly)
# 转换为 JPEG 格式,质量设为 90
success = qimage.save(buffer, "JPEG", 90)
buffer.close()
if not success:
return {"code": -1, "msg": "图像转换失败"}
# 获取字节数据
file_data = byte_array.data()
# 构建 multipart 请求
files = {
'avatar': ('avatar.jpg', file_data, 'image/jpeg')
}
# 移除 Content-Type 以便 requests 自动设置
headers = self.session.headers.copy()
if 'Content-Type' in headers:
del headers['Content-Type']
r = self.request("POST", url, files=files, headers=headers)
# 转换响应格式
if r.get("url"):
return {"code": 0, "msg": "头像更新成功"}
else:
return {"code": -1, "msg": r.get("error", "头像更新失败")}
def getUserAvatar(self, size: Literal["s", "m", "l"]):
"""获取用户头像"""
# Cloudreve V4 获取用户信息以获取头像URL
user_info = self.getUserInfo()
if user_info.get("code") != 0:
return QPixmap(":app/images/logo.png")
avatar_url = user_info.get("data", {}).get("avatar")
if not avatar_url:
return QPixmap(":app/images/logo.png")
try:
# 直接下载头像图片
response = requests.get(avatar_url, timeout=10)
response.raise_for_status()
pixmap = QPixmap()
if pixmap.loadFromData(response.content):
userConfig.setUserAvatarPixmap(pixmap)
return pixmap
else:
return QPixmap(":app/images/logo.png")
except Exception as e:
logger.error(f"获取头像失败: {e}")
return QPixmap(":app/images/logo.png")
def getUserInfo(self):
"""获取用户信息 (Cloudreve V4 API)"""
# 使用正确的API端点
if self.userId:
url = f"/user/info/{self.userId}"
else:
# 如果没有userId尝试获取当前用户信息
url = "/user/profile" # 这可能需要根据实际情况调整
r = self.request("GET", url)
# 转换响应格式
if isinstance(r, dict):
return {"code": 0, "data": r}
else:
return {"code": -1, "msg": "获取用户信息失败"}
def getUserPack(self):
"""获取用户存储详细 (Cloudreve V4 API)"""
# 使用正确的API端点
url = "/user/capacity"
r = self.request("GET", url)
# 转换响应格式以保持向后兼容
if isinstance(r, dict) and "used" in r:
return {
"code": 0,
"data": {
"base": r.get("total", 0) - r.get("extra", 0),
"pack": r.get("extra", 0),
"used": r.get("used", 0),
"total": r.get("total", 0),
"packs": []
}
}
else:
return {
"code": 0,
"data": {
"base": 0,
"pack": 0,
"used": 0,
"total": 0,
"packs": []
}
}
def list(self, path="/"):
"""列出用户仓内文件 (Cloudreve V4 API)"""
# 使用正确的API端点和必需参数
url = "/file"
# 将path转换为Cloudreve V4要求的URI格式
# 根目录映射到 cloudreve://my/
if path == "/" or path == "":
uri = "cloudreve://my/"
else:
# 使用quote_plus正确编码路径确保特殊字符被正确处理
# 首先规范化路径分隔符
normalized_path = path.strip("/").replace("\\", "/")
# 对路径部分进行URL编码
encoded_path = quote_plus(normalized_path)
# 由于quote_plus会将斜杠也编码我们需要恢复它们
encoded_path = encoded_path.replace("%2F", "/")
uri = f"cloudreve://my/{encoded_path}"
# 添加必需的分页参数
params = {
"uri": uri,
"page": 0, # 从第一页开始
"page_size": 100 # 使用合理的默认值
}
logger.debug(f"发送文件列表请求: URI={uri}")
r = self.request("GET", url, params=params)
# 转换响应格式以保持向后兼容
# 根据API规范文件列表在data.files中
if isinstance(r, dict) and "data" in r and "files" in r["data"]:
return {
"code": 0,
"data": r["data"]["files"]
}
else:
logger.warning(f"无效的响应格式: {r}")
return {"code": 0, "data": []}
def getPolicy(self):
"""获取用户存储策略"""
url = "/user/policies"
r = self.request("GET", url)
# 转换响应格式
if isinstance(r, list):
return {"code": 0, "data": r}
else:
return {"code": 0, "data": []}
def changePolicy(self, path, policy):
"""修改用户存储策略"""
url = "/file/move"
r = self.request("POST", url, json={"items": [path], "dst": path, "policy": policy})
# 转换响应格式
if r.get("count") == 1:
return {"code": 0, "msg": "存储策略修改成功"}
else:
return {"code": -1, "msg": r.get("error", "存储策略修改失败")}
def createFolder(self, name):
"""创建文件夹"""
url = "/file/create"
currentPath = policyConfig.returnCurrentPath()
# 根据Cloudreve V4 API规范构建uri参数确保不会有重复斜杠
if currentPath == "/":
uri = f"cloudreve://my/{name}"
else:
uri = f"cloudreve://my{currentPath}/{name}"
r = self.request("POST", url, json={"uri": uri, "type": "folder", "err_on_conflict": True})
# 转换响应格式
if r.get("data") and r.get("code") == 0:
return {"code": 0, "msg": "文件夹创建成功"}
else:
return {"code": -1, "msg": r.get("msg", "文件夹创建失败")}
def deleteFile(self, fileId, fileType: Literal["file", "dir"]):
"""删除文件"""
url = "/file/delete"
deleteData = {
"items": [fileId] if fileType == "file" else [],
"dirs": [fileId] if fileType == "dir" else []
}
r = self.request("POST", url, json=deleteData)
# 转换响应格式
if r.get("count") > 0:
return {"code": 0, "msg": "删除成功"}
else:
return {"code": -1, "msg": r.get("error", "删除失败")}
def wareSearch(
self,
searchContent,
searchType: Literal["keyword", "internalTag", "externalTag"],
):
"""搜索文件 (Cloudreve V4 API)"""
# 根据Cloudreve V4 API使用/file端点并添加搜索参数
url = "/file"
# 使用Cloudreve V4的URI格式
uri = "cloudreve://my/"
# 构建搜索参数
params = {
"uri": uri,
"keyword": searchContent,
"page": 0,
"page_size": 100
}
# 根据搜索类型调整参数
if searchType == "internalTag":
params["type"] = "internal"
elif searchType == "externalTag":
params["type"] = "tag"
logger.debug(f"发送文件搜索请求: 关键词={searchContent}, 类型={searchType}")
r = self.request("GET", url, params=params)
# 转换响应格式以保持向后兼容
if isinstance(r, dict):
if "data" in r:
if "files" in r["data"]:
# Cloudreve V4 API 格式
return {"code": 0, "data": {"objects": r["data"]["files"]}}
elif isinstance(r["data"], list):
# 如果data是列表直接使用
return {"code": 0, "data": {"objects": r["data"]}}
logger.warning(f"搜索响应格式不正确: {r}")
return {"code": 0, "data": {"objects": []}}
def shareSearch(self, keyword, orderBy, order, page):
"""搜索分享 (Cloudreve V4 API)"""
# 使用正确的API端点 - Cloudreve V4使用/share端点获取分享列表
url = "/share"
params = {
"page": page,
"page_size": 50, # 添加默认页面大小以避免"PageSize cannot be empty"错误
"order_by": orderBy,
"order": order,
"keyword": keyword, # 根据实际API支持情况调整
}
r = self.request("GET", url, params=params)
return r
def deleteTag(self, tagId):
"""删除标签"""
url = f"/tag/{tagId}"
r = self.request("DELETE", url)
# 转换响应格式
if r is None or (isinstance(r, dict) and not r.get("error")):
return {"code": 0, "msg": "标签删除成功"}
else:
return {"code": -1, "msg": r.get("error", "标签删除失败")}
def addTag(self, name, expression):
"""添加标签"""
url = "/tag/filter"
jsons = {
"expression": expression,
"name": name,
"color": "#ff9800",
"icon": "Circle",
}
r = self.request("POST", url, json=jsons)
# 转换响应格式
if r.get("id"):
return {"code": 0, "msg": "标签添加成功", "data": r}
else:
return {"code": -1, "msg": r.get("error", "标签添加失败")}
def getShareFileInfo(self, shareId):
"""获取分享文件信息"""
url = f"/share/{shareId}"
r = self.request("GET", url)
# 转换响应格式
if r.get("id"):
return {"code": 0, "data": r}
else:
return {"code": -1, "msg": r.get("error", "获取分享信息失败")}
def updateFileContent(self, fileId, content):
"""更新文件内容"""
url = f"/file/content/{fileId}"
headers = {
"Content-Type": "text/plain"
}
r = self.request("PUT", url, data=content.encode("utf-8"), headers=headers)
# 转换响应格式
if r.get("size") is not None:
return {"code": 0, "msg": "文件内容更新成功"}
else:
return {"code": -1, "msg": r.get("error", "文件内容更新失败")}
def updateUserNickname(self, nickName):
"""更新用户昵称 (Cloudreve V4 API)"""
url = "/user/profile"
data = {
"nick": nickName
}
r = self.request("PUT", url, json=data)
# 转换响应格式
if isinstance(r, dict) and not r.get("error"):
return {"code": 0, "msg": "昵称更新成功"}
else:
return {"code": -1, "msg": r.get("error", "昵称更新失败")}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
import base64
from loguru import logger
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import (QColor, QPainter, QPainterPath, QPen, QPixmap)
from ..api import miaoStarsBasicApi
from ...core import cfg, qconfig, userConfig
class CaptchaThread(QThread):
captchaReady = pyqtSignal(QPixmap)
captchaFailed = pyqtSignal(str)
def __init__(self):
super().__init__()
@staticmethod
def _createRoundedPixmap(pixmap, radius=10):
"""创建圆角图片"""
try:
# 获取原始图片尺寸
if pixmap.isNull():
logger.error("原始图片为空,无法创建圆角图片")
return pixmap
size = pixmap.size()
# 创建透明背景的图片
rounded_pixmap = QPixmap(size)
rounded_pixmap.fill(Qt.GlobalColor.transparent)
# 创建对象
painter = QPainter(rounded_pixmap)
painter.setRenderHints(
QPainter.RenderHint.Antialiasing
| QPainter.RenderHint.SmoothPixmapTransform
)
# 创建圆角矩形路径
path = QPainterPath()
path.addRoundedRect(0, 0, size.width(), size.height(), radius, radius)
# 设置裁剪区域
painter.setClipPath(path)
# 绘制原始图片
painter.drawPixmap(0, 0, pixmap)
# 绘制边框
pen = QPen(QColor(200, 200, 200)) # 浅灰色边框
pen.setWidth(1)
painter.setPen(pen)
painter.drawRoundedRect(
0, 0, size.width() - 1, size.height() - 1, radius, radius
)
painter.end()
return rounded_pixmap
except Exception as e:
logger.error(f"创建圆角图片失败:{e}")
return pixmap # 如果出错,返回原始图片
def run(self):
try:
logger.debug("开始获取验证码")
response = miaoStarsBasicApi.getCaptcha()
logger.debug(f"验证码API返回响应: {response}")
if response["code"] == 0:
# 确保data字段存在且为字符串
if "data" in response and isinstance(response["data"], str):
# 分割base64前缀和实际数据
try:
captchaImageData = response["data"].split(",")[1]
logger.debug(f"成功提取base64数据长度: {len(captchaImageData)}")
# 解码base64数据
captchaImage = base64.b64decode(captchaImageData)
logger.debug(f"成功解码base64数据长度: {len(captchaImage)} bytes")
# 加载图片
pixmap = QPixmap()
load_success = pixmap.loadFromData(captchaImage)
if load_success:
logger.debug(f"成功加载图片,尺寸: {pixmap.width()}x{pixmap.height()}")
# 创建圆角图片
pixmap = self._createRoundedPixmap(pixmap, radius=10)
self.captchaReady.emit(pixmap)
else:
logger.error("图片加载失败")
self.captchaFailed.emit("验证码图片加载失败")
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
except (IndexError, ValueError, TypeError) as e:
logger.error(f"验证码数据格式错误: {e}")
self.captchaFailed.emit(f"验证码数据格式错误: {str(e)}")
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
else:
logger.error("验证码响应中缺少有效的data字段")
self.captchaFailed.emit("验证码数据无效")
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
else:
error_msg = response.get("msg", "获取验证码失败")
logger.error(f"获取验证码失败: {error_msg}")
self.captchaFailed.emit(error_msg)
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
except Exception as e:
logger.exception(f"获取验证码过程中发生异常: {e}")
self.captchaFailed.emit(str(e))
self.captchaReady.emit(QPixmap(":app/images/loadFailure.png"))
class LoginThread(QThread):
successLogin = pyqtSignal()
errorLogin = pyqtSignal(str)
def __init__(self, email: str, password: str, captchaCode: str):
super().__init__()
logger.debug(f"初始化许可证服务线程 - 邮箱: {email}")
self.email = email
self.password = password
self.captchaCode = captchaCode
def run(self):
logger.info(f"开始验证用户登录 - 邮箱: {self.email}")
try:
loginResponse = miaoStarsBasicApi.login(
self.email, self.password, self.captchaCode
)
if loginResponse["code"] == 0:
self.successLogin.emit()
qconfig.set(cfg.email, self.email)
qconfig.set(cfg.activationCode, self.password)
userConfig.userData = loginResponse
# 从登录响应中提取token并保存
# 在basicApi的login方法中已经处理了token的设置这里保存到userConfig中以便程序启动时恢复
token = miaoStarsBasicApi.token
if token:
userConfig.setToken(token)
else:
self.errorLogin.emit(loginResponse["msg"])
except Exception as e:
logger.error(f"登录验证过程中发生异常: {e}")
self.errorLogin.emit("系统错误,请稍后重试")
class RegisterThread(QThread):
successRegister = pyqtSignal()
errorRegister = pyqtSignal(str)
def __init__(self, email: str, password: str, captchaCode: str):
super().__init__()
logger.debug(f"初始化许可证服务线程 - 邮箱: {email}")
self.email = email
self.password = password
self.captchaCode = captchaCode
def run(self):
logger.info(f"开始验证用户注册 - 邮箱: {self.email}")
try:
registerRespond = miaoStarsBasicApi.register(
self.email, self.password, self.captchaCode
)
if registerRespond["code"] == 203:
self.successRegister.emit()
else:
logger.error(f"注册失败: {registerRespond['msg']}")
self.errorRegister.emit(registerRespond["msg"])
except Exception as e:
logger.error(f"登录验证过程中发生异常: {e}")
self.errorRegister.emit("系统错误,请稍后重试")

View File

@@ -0,0 +1,246 @@
import os
from urllib.parse import urlparse
import requests
from PyQt6.QtCore import QThread, pyqtSignal
from PyQt6.QtGui import QImage, QPixmap
from app.core import miaoStarsBasicApi
class TextLoaderThread(QThread):
"""文本文件加载线程"""
textLoaded = pyqtSignal(str)
errorOccurred = pyqtSignal(str)
progressUpdated = pyqtSignal(int) # 进度更新信号
def __init__(self, url):
super().__init__()
self.url = url
def run(self):
"""线程执行函数"""
try:
# 1. 设置网络请求参数 - 优化连接参数
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=20,
pool_maxsize=20,
max_retries=5, # 增加重试次数
pool_block=False,
)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 2. 增加超时时间并添加重试机制
response = miaoStarsBasicApi.returnSession().get(
self.url,
stream=True,
timeout=(15, 30), # 增加超时时间连接15秒读取30秒
)
response.raise_for_status()
# 3. 获取文件大小(如果服务器支持)
total_size = int(response.headers.get("content-length", 0))
downloaded_size = 0
# 4. 分块读取并处理 - 使用二进制读取提高速度
content_chunks = []
for chunk in response.iter_content(chunk_size=16384): # 增大块大小
if chunk:
content_chunks.append(chunk)
downloaded_size += len(chunk)
# 更新进度(如果知道总大小)
if total_size > 0:
progress = int((downloaded_size / total_size) * 100)
self.progressUpdated.emit(progress)
# 5. 合并内容并解码
binary_content = b"".join(content_chunks)
if not binary_content:
self.errorOccurred.emit("下载内容为空")
return
# 6. 智能编码检测和解码
text_content = self._decode_content(binary_content)
# 7. 发射加载完成的信号
self.textLoaded.emit(text_content)
except requests.exceptions.Timeout:
self.errorOccurred.emit("请求超时,请检查网络连接或尝试重新加载")
except requests.exceptions.ConnectionError:
self.errorOccurred.emit("网络连接错误,请检查网络设置")
except requests.exceptions.RequestException as e:
self.errorOccurred.emit(f"网络请求错误: {str(e)}")
except Exception as e:
self.errorOccurred.emit(f"文本处理错误: {str(e)}")
def _decode_content(self, binary_content):
"""智能解码二进制内容"""
# 优先尝试UTF-8
encodings = ["utf-8", "gbk", "gb2312", "latin-1", "iso-8859-1", "cp1252"]
for encoding in encodings:
try:
return binary_content.decode(encoding)
except UnicodeDecodeError:
continue
# 如果所有编码都失败,使用替换错误处理
try:
return binary_content.decode("utf-8", errors="replace")
except:
# 最后尝试忽略错误
return binary_content.decode("utf-8", errors="ignore")
def cancel(self):
"""取消下载"""
if self.isRunning():
self.terminate()
self.wait(1000) # 等待线程结束
class ImageLoaderThread(QThread):
"""优化的图片加载线程"""
imageLoaded = pyqtSignal(QPixmap)
errorOccurred = pyqtSignal(str)
progressUpdated = pyqtSignal(int) # 进度更新信号
def __init__(
self, url, cache_dir="image_cache", max_cache_size=50 * 1024 * 1024
): # 50MB缓存
super().__init__()
self.url = url
self.cache_dir = cache_dir
self.max_cache_size = max_cache_size
self._setup_cache()
def _setup_cache(self):
"""设置图片缓存目录"""
if not os.path.exists(self.cache_dir):
os.makedirs(self.cache_dir)
def _get_cache_filename(self):
"""生成缓存文件名"""
parsed_url = urlparse(self.url)
filename = os.path.basename(parsed_url.path) or "image"
# 添加URL哈希避免重名
import hashlib
url_hash = hashlib.md5(self.url.encode()).hexdigest()[:8]
return f"{url_hash}_{filename}"
def _get_cached_image(self):
"""获取缓存图片"""
cache_file = os.path.join(self.cache_dir, self._get_cache_filename())
if os.path.exists(cache_file):
try:
pixmap = QPixmap(cache_file)
if not pixmap.isNull():
return pixmap
except Exception:
pass
return None
def _save_to_cache(self, pixmap):
"""保存图片到缓存"""
try:
cache_file = os.path.join(self.cache_dir, self._get_cache_filename())
pixmap.save(cache_file, "JPG", 80) # 压缩质量80%
self._cleanup_cache() # 清理过期缓存
except Exception:
pass
def _cleanup_cache(self):
"""清理过期的缓存文件"""
# noinspection PyBroadException
try:
files = []
for f in os.listdir(self.cache_dir):
filepath = os.path.join(self.cache_dir, f)
if os.path.isfile(filepath):
files.append((filepath, os.path.getmtime(filepath)))
# 按修改时间排序
files.sort(key=lambda x: x[1])
# 计算总大小
total_size = sum(os.path.getsize(f[0]) for f in files)
# 如果超过最大缓存大小,删除最旧的文件
while total_size > self.max_cache_size and files:
oldest_file = files.pop(0)
total_size -= os.path.getsize(oldest_file[0])
os.remove(oldest_file[0])
except Exception:
pass
def run(self):
"""线程执行函数"""
try:
# 1. 首先检查缓存
cached_pixmap = self._get_cached_image()
if cached_pixmap:
self.imageLoaded.emit(cached_pixmap)
return
# 2. 设置更短的超时时间
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=10, pool_maxsize=10, max_retries=5 # 重试2次
)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 3. 流式下载,支持进度显示
response = miaoStarsBasicApi.returnSession().get(
self.url, stream=True, timeout=(20, 30) # 连接超时5秒读取超时10秒
)
response.raise_for_status()
# 获取文件大小(如果服务器支持)
total_size = int(response.headers.get("content-length", 0))
downloaded_size = 0
# 4. 分块读取并处理
image_data = b""
for chunk in response.iter_content(chunk_size=8192):
if chunk:
image_data += chunk
downloaded_size += len(chunk)
# 更新进度(如果知道总大小)
if total_size > 0:
progress = int((downloaded_size / total_size) * 100)
self.progressUpdated.emit(progress)
# 5. 从数据创建QImage比QPixmap更快
image = QImage()
image.loadFromData(image_data)
if image.isNull():
raise Exception("无法加载图片数据")
# 6. 转换为QPixmap
pixmap = QPixmap.fromImage(image)
# 7. 保存到缓存
self._save_to_cache(pixmap)
# 发射加载完成的信号
self.imageLoaded.emit(pixmap)
except requests.exceptions.Timeout:
self.errorOccurred.emit("请求超时,请检查网络连接")
except requests.exceptions.ConnectionError:
self.errorOccurred.emit("网络连接错误")
except requests.exceptions.RequestException as e:
self.errorOccurred.emit(f"网络请求错误: {str(e)}")
except Exception as e:
self.errorOccurred.emit(f"图片处理错误: {str(e)}")

View File

@@ -0,0 +1,287 @@
# coding: utf-8
import os
import subprocess
import tempfile
from loguru import logger
from PyQt6.QtCore import QEventLoop, QObject, QThread, pyqtSignal
from PyQt6.QtCore import QUrl
from PyQt6.QtMultimedia import QAudioOutput, QMediaPlayer
class LocalTextToSpeechThread(QThread):
"""本地文本转语音播放线程 - Windows优化版"""
# 信号定义
playback_started = pyqtSignal() # 播放开始
playback_finished = pyqtSignal() # 播放完成
playback_error = pyqtSignal(str) # 播放错误
progress_updated = pyqtSignal(int) # 播放进度更新
synthesis_completed = pyqtSignal(str) # 语音合成完成(返回文件路径)
def __init__(self, text, parent=None):
super().__init__(parent)
self.text = text
self.audio_file_path = None
self.media_player = None
self.audio_output = None
self._stop_requested = False
def run(self):
"""线程执行函数"""
try:
# 1. 将文本转换为语音文件
self.audio_file_path = self._text_to_speech(self.text)
if not self.audio_file_path or self._stop_requested:
return
# 发射合成完成信号
self.synthesis_completed.emit(self.audio_file_path)
# 2. 播放语音
self._play_audio(self.audio_file_path)
except Exception as e:
self.playback_error.emit(f"语音播放错误: {str(e)}")
def _text_to_speech(self, text):
"""使用本地TTS引擎将文本转换为语音文件"""
try:
# 检查文本长度
if not text or len(text.strip()) == 0:
self.playback_error.emit("文本内容为空")
return None
# 限制文本长度,避免合成时间过长
max_length = 1000
if len(text) > max_length:
text = text[:max_length] + "。文本过长,已截断。"
self.playback_error.emit(f"文本过长,已截断前{max_length}个字符")
# 优先使用pyttsx3效率最高
try:
import pyttsx3
return self._pyttsx3_tts(text)
except ImportError:
# 备用方案使用Windows内置TTS
return self._windows_tts(text)
except Exception as e:
self.playback_error.emit(f"语音合成失败: {str(e)}")
return None
def _pyttsx3_tts(self, text):
"""使用pyttsx3合成语音 - 优化版"""
try:
import pyttsx3
# 初始化TTS引擎
engine = pyttsx3.init()
# 设置语音属性 - 提高效率
engine.setProperty('rate', 200) # 提高语速
engine.setProperty('volume', 0.9) # 提高音量
# 创建临时文件保存音频
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_file:
temp_path = temp_file.name
# 保存语音到文件
engine.save_to_file(text, temp_path)
engine.runAndWait()
# 检查文件是否成功创建
if os.path.exists(temp_path) and os.path.getsize(temp_path) > 0:
return temp_path
else:
logger.error("语音文件生成失败")
except Exception as e:
# 如果pyttsx3失败尝试Windows TTS
return self._windows_tts(text)
def _windows_tts(self, text):
"""Windows系统TTS - 优化版"""
try:
# 方法1: 使用PowerShell命令 - 最可靠
return self._powershell_tts(text)
except Exception as e:
logger.error(f"Windows TTS失败: {str(e)}")
def _powershell_tts(self, text):
"""使用PowerShell合成语音 - 优化版"""
try:
# 创建临时文件
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_file:
temp_path = temp_file.name
# 转义文本中的特殊字符
escaped_text = text.replace('"', '`"').replace("'", "`'")
# 使用PowerShell的SpeechSynthesizer - 简化命令
ps_script = f"""
Add-Type -AssemblyName System.Speech
$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer
$speak.SetOutputToWaveFile("{temp_path}")
$speak.Speak("{escaped_text}")
$speak.Dispose()
"""
# 使用更高效的方式执行PowerShell
process = subprocess.Popen(
["powershell", "-Command", ps_script],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True
)
# 等待进程完成,设置超时
try:
stdout, stderr = process.communicate(timeout=30)
if process.returncode != 0:
logger.error(f"PowerShell执行失败: {stderr.decode('gbk', errors='ignore')}")
except subprocess.TimeoutExpired:
process.kill()
logger.error(f"PowerShell超时")
if os.path.exists(temp_path) and os.path.getsize(temp_path) > 0:
return temp_path
else:
logger.error("语音文件生成失败")
except Exception as e:
# raise Exception(f"PowerShell TTS失败: {str(e)}")
logger.error(f"PowerShell TTS失败{e}")
def _play_audio(self, file_path):
"""播放音频文件 - 优化版"""
if self._stop_requested:
return
try:
# 创建媒体播放器和音频输出
self.media_player = QMediaPlayer()
self.audio_output = QAudioOutput()
self.media_player.setAudioOutput(self.audio_output)
# 设置音量
self.audio_output.setVolume(1.0)
# 连接信号
self.media_player.playbackStateChanged.connect(self._on_playback_state_changed)
self.media_player.positionChanged.connect(self._on_position_changed)
self.media_player.durationChanged.connect(self._on_duration_changed)
self.media_player.errorOccurred.connect(self._on_player_error)
# 设置媒体源并开始播放
self.media_player.setSource(QUrl.fromLocalFile(file_path))
self.media_player.play()
# 使用事件循环等待播放完成
loop = QEventLoop()
self.media_player.playbackStateChanged.connect(
lambda state: loop.quit() if state == QMediaPlayer.PlaybackState.StoppedState else None
)
loop.exec()
except Exception as e:
raise Exception(f"音频播放失败: {str(e)}")
finally:
# 清理临时文件
if file_path and os.path.exists(file_path):
try:
os.unlink(file_path)
except:
pass
def _on_playback_state_changed(self, state):
"""处理播放状态变化"""
from PyQt6.QtMultimedia import QMediaPlayer
if state == QMediaPlayer.PlaybackState.StoppedState:
self.playback_finished.emit()
def _on_position_changed(self, position):
"""处理播放位置变化"""
if (self.media_player and
self.media_player.duration() > 0):
progress = int((position / self.media_player.duration()) * 100)
self.progress_updated.emit(progress)
def _on_duration_changed(self, duration):
"""处理时长变化"""
if duration > 0:
self.playback_started.emit()
def _on_player_error(self, error, error_string):
"""处理播放器错误"""
self.playback_error.emit(f"播放器错误: {error_string}")
def stop_playback(self):
"""停止播放"""
self._stop_requested = True
if self.media_player and self.media_player.playbackState() != QMediaPlayer.PlaybackState.StoppedState:
self.media_player.stop()
# 清理临时文件
if self.audio_file_path and os.path.exists(self.audio_file_path):
try:
os.unlink(self.audio_file_path)
except:
pass
class LocalSpeechController(QObject):
"""本地语音播放控制器"""
def __init__(self, parent=None):
super().__init__(parent)
self.speech_thread = None
def play_text(self, text):
"""播放文本语音"""
# 停止当前播放
self.stop_playback()
# 创建新的语音线程
self.speech_thread = LocalTextToSpeechThread(text)
# 连接信号
self.speech_thread.playback_started.connect(self._on_playback_started)
self.speech_thread.playback_finished.connect(self._on_playback_finished)
self.speech_thread.playback_error.connect(self._on_playback_error)
self.speech_thread.progress_updated.connect(self._on_progress_updated)
self.speech_thread.synthesis_completed.connect(self._on_synthesis_completed)
# 开始播放
self.speech_thread.start()
def stop_playback(self):
"""停止播放"""
if self.speech_thread and self.speech_thread.isRunning():
self.speech_thread.stop_playback()
self.speech_thread.wait(1000) # 等待线程结束最多1秒
def is_playing(self):
"""检查是否正在播放"""
return self.speech_thread and self.speech_thread.isRunning()
def _on_playback_started(self):
"""处理播放开始"""
logger.info("语音播放开始")
def _on_playback_finished(self):
"""处理播放完成"""
logger.success("语音播放完成")
def _on_playback_error(self, error_msg):
"""处理播放错误"""
logger.warning(f"语音播放错误: {error_msg}")
def _on_progress_updated(self, progress):
...
def _on_synthesis_completed(self, file_path):
"""处理语音合成完成"""
logger.info(f"语音合成完成,文件路径: {file_path}")

View File

@@ -0,0 +1,199 @@
from loguru import logger
from PyQt6.QtCore import QThread, pyqtSignal
from PyQt6.QtGui import QPixmap
from ..api import miaoStarsBasicApi
class UserNickNameUpdateThread(QThread):
successUpdate = pyqtSignal()
errorUpdate = pyqtSignal(str)
def __init__(self, nickName: str):
super().__init__()
logger.debug(f"初始化用户昵称服务线程 - 昵称: {nickName}")
self.nickName = nickName
def run(self):
logger.info(f"开始更新用户昵称 - 昵称: {self.nickName}")
try:
response = miaoStarsBasicApi.updateUserNickname(self.nickName)
print(response)
if response["code"] == 0:
self.successUpdate.emit()
else:
logger.error("更新失败:", response["msg"])
self.errorUpdate.emit(response["msg"])
except Exception as e:
logger.error(f"更新用户昵称过程中发生异常: {e}")
self.errorUpdate.emit("系统错误,请稍后重试")
class UserAvatarUpdateThread(QThread):
successUpdate = pyqtSignal()
errorUpdate = pyqtSignal(str)
def __init__(self, avatarPath: str):
super().__init__()
logger.debug(f"初始化用户头像服务线程 - 头像路径: {avatarPath}")
self.avatarPath = avatarPath
def run(self):
logger.info(f"开始更新用户头像 - 头像路径: {self.avatarPath}")
try:
response = miaoStarsBasicApi.updateUserAvatar(self.avatarPath)
if response["code"] == 0:
logger.info("头像更新成功")
self.successUpdate.emit()
else:
logger.error(f"更新失败,错误信息: {response['msg']}")
self.errorUpdate.emit(f"更新失败: {response['msg']}")
except Exception as e:
logger.error(f"更新用户头像过程中发生异常: {e}")
self.errorUpdate.emit("系统错误,请稍后重试")
class GetUserAvatarThread(QThread):
avatarPixmap = pyqtSignal(QPixmap)
def __init__(self, size: str):
super().__init__()
logger.debug(f"初始化获取用户头像服务线程 - 头像尺寸: {size}")
self.size = size
def run(self):
logger.info(f"开始获取用户头像 - 头像尺寸: {self.size}")
try:
response = miaoStarsBasicApi.getUserAvatar(self.size)
self.avatarPixmap.emit(response)
except Exception as e:
logger.error(f"获取用户头像过程中发生异常: {e}")
self.avatarPixmap.emit(QPixmap(":app/images/logo.png"))
class GetPackThread(QThread):
storageDictSignal = pyqtSignal(dict)
def __init__(self):
super().__init__()
def run(self):
logger.info("开始请求用户配额包")
try:
response = miaoStarsBasicApi.getUserPack()
self.storageDictSignal.emit(response)
except Exception as e:
logger.error(f"获取用户配额包过程中发生异常: {e}")
self.storageDictSignal.emit(
{
"code": 0,
"data": {
"base": 0,
"pack": 0,
"used": 0,
"total": 0,
"packs": [],
},
"msg": "",
}
)
class GetPoliciesThread(QThread):
"""获取策略列表线程"""
successGetSignal = pyqtSignal(list)
errorSignal = pyqtSignal(str)
def __init__(self):
super().__init__()
def run(self):
try:
response = miaoStarsBasicApi.getPolicy()
if response["code"] == 0:
self.successGetSignal.emit(response["data"])
else:
self.errorSignal.emit(f"API返回错误: {response.get('msg')}")
except Exception as e:
self.errorSignal.emit(f"获取策略列表失败: {str(e)}")
class ChangePolicyThread(QThread):
"""更改策略线程"""
successChangedSignal = pyqtSignal()
errorSignal = pyqtSignal(str)
def __init__(self, path, policy_id):
super().__init__()
self.path = path
self.policy_id = policy_id
def run(self):
try:
response = miaoStarsBasicApi.changePolicy(self.path, self.policy_id)
if response["code"] == 0:
self.successChangedSignal.emit()
else:
self.errorSignal.emit(
f"更改策略失败: {response.get('msg', '未知错误')}"
)
except Exception as e:
self.errorSignal.emit(f"更改策略请求失败: {str(e)}")
class DeleteTagThread(QThread):
"""删除标签线程"""
successDeleteSignal = pyqtSignal()
errorSignal = pyqtSignal(str)
def __init__(self, tagId):
super().__init__()
self.tagId = tagId
def run(self):
try:
response = miaoStarsBasicApi.deleteTag(self.tagId)
if response["code"] == 0:
self.successDeleteSignal.emit()
logger.info(f"删除标签成功: {self.tagId}")
else:
logger.error(f"删除标签失败: {response.get('msg')}")
self.errorSignal.emit(f"删除标签失败: {response.get('msg')}")
except Exception as e:
self.errorSignal.emit(f"{str(e)}")
logger.error(f"删除标签请求失败: {str(e)}")
class AddTagThread(QThread):
"""添加标签的线程类"""
successSignal = pyqtSignal(str, dict) # 标签名称, 响应数据
errorSignal = pyqtSignal(str, str) # 标签名称, 错误信息
def __init__(self, name, expression, parent=None):
super().__init__(parent)
self.name = name
self.expression = expression
def run(self):
"""线程执行的主方法"""
try:
response = miaoStarsBasicApi.addTag(self.name, self.expression)
if response["code"] == 0:
logger.info(f"添加标签成功: {self.name}")
self.successSignal.emit(self.name, response)
else:
logger.error(f"添加标签失败: {self.name} - {response.get('msg')}")
self.errorSignal.emit(self.name, response.get("msg"))
except Exception as e:
logger.error(f"添加标签异常: {self.name} - {str(e)}")
self.errorSignal.emit(self.name, str(e))

View File

@@ -0,0 +1 @@
from .exceptions import getCode

202
app/core/utils/config.py Normal file
View File

@@ -0,0 +1,202 @@
# coding:utf-8
import sys
from datetime import datetime
from PyQt6.QtGui import QPixmap
from qfluentwidgets import (
BoolValidator,
ConfigItem,
ConfigSerializer,
OptionsConfigItem,
OptionsValidator,
qconfig,
QConfig,
FolderValidator,
Theme,
setThemeColor,
)
from .encryption import encrypt
from .setting import CONFIG_FILE, DOWNLOAD_FOLDER
def isWin11():
return sys.platform == "win32" and sys.getwindowsversion().build >= 22000
class EncrpytionSerializer(ConfigSerializer):
"""QColor serializer"""
def serialize(self, value):
return encrypt.encrypt(value)
def deserialize(self, value):
return encrypt.decrypt(value)
class Config(QConfig):
"""Config of application"""
# TODO: ADD YOUR CONFIG GROUP HERE
# register
rememberMe = ConfigItem(
"UmVnaXN0ZXI=", "UmVtZW1iZXJNZQ==", True, serializer=EncrpytionSerializer()
)
email = ConfigItem(
"UmVnaXN0ZXI=", "RW1haWw=", "", serializer=EncrpytionSerializer()
)
activationCode = ConfigItem(
"UmVnaXN0ZXI=", "QWN0aXZhdGlvbkNvZGU=", "", serializer=EncrpytionSerializer()
)
# main window
micaEnabled = ConfigItem("MainWindow", "MicaEnabled", isWin11(), BoolValidator())
dpiScale = OptionsConfigItem(
"MainWindow",
"DpiScale",
"Auto",
OptionsValidator([1, 1.25, 1.5, 1.75, 2, "Auto"]),
restart=True,
)
# software update
checkUpdateAtStartUp = ConfigItem(
"Update", "CheckUpdateAtStartUp", True, BoolValidator()
)
# bg
customBackground = ConfigItem(
"Background",
"CustomBackground",
"app\\resource\\images\\bg0.png",
)
customOpactity = ConfigItem("Background", "Opactity", 0.2)
downloadSavePath = ConfigItem(
"Download", "SavePath", DOWNLOAD_FOLDER, validator=FolderValidator()
)
# language
language = OptionsConfigItem(
"General", "Language", "zh", OptionsValidator(["zh", "en"]), restart=False
)
class UserConfig:
def __init__(self, userData):
self.userData = userData
self.token = None
self.avaterPixmap = None
@property
def userId(self):
if self.userData:
return self.userData["data"].get("id", "")
else:
return None
@property
def userAvatarURL(self):
if self.userData and "avatar" in self.userData["data"]:
return self.userData["data"].get("avatar", "")
else:
return ""
@property
def userName(self):
if self.userData:
return self.userData["data"].get("nickname", "")
else:
return ""
@property
def userEmail(self):
if self.userData:
return self.userData["data"].get("user_name", "")
else:
return ""
@property
def userGroup(self):
if self.userData:
return self.userData.get("data", {}).get("group", {}).get("name", "")
else:
return ""
@property
def userScore(self):
if self.userData:
return str(self.userData["data"].get("score", 0))
@property
def userCreatedTime(self):
if self.userData:
return self.format_date(self.userData["data"].get("created_at", ""))
def setUserAvatarPixmap(self, avaterPixmap):
self.avaterPixmap: QPixmap = avaterPixmap
def returnAvatarPixmap(self):
return self.avaterPixmap
def setToken(self, token):
"""设置JWT token"""
self.token = token
def getToken(self):
"""获取JWT token"""
return self.token
def format_date(self, date_str):
"""格式化日期时间"""
try:
# 处理带小数秒的情况
if "." in date_str:
# 分割日期和小数秒部分
date_part, fractional_part = date_str.split(".", 1)
# 去除末尾的"Z"并截取前6位小数
fractional_sec = fractional_part.rstrip("Z")[:6]
# 重新组合日期字符串
normalized_date_str = f"{date_part}.{fractional_sec}Z"
date_time = datetime.strptime(
normalized_date_str, "%Y-%m-%dT%H:%M:%S.%fZ"
)
else:
# 处理没有小数秒的情况
date_time = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ")
except ValueError:
# 如果所有格式都失败,返回原始字符串
return date_str
return date_time.strftime("%Y-%m-%d %H:%M:%S")
class PolicyConfig:
def __init__(self):
self.currentPolicy = {}
self.currentPath = "/"
def returnPolicy(self):
return self.currentPolicy
def setPolicy(self, policy):
self.currentPolicy = policy
def setCurrentPath(self, path):
self.currentPath = path
def returnCurrentPath(self):
return self.currentPath
cfg = Config()
cfg.themeMode.value = Theme.AUTO
# 设置默认主题色为蓝色 (使用RGB值)
setThemeColor('#2F80ED') # 这是一个标准的蓝色RGB值
qconfig.load(str(CONFIG_FILE.absolute()), cfg)
userConfig = UserConfig(None)
policyConfig = PolicyConfig()

View File

@@ -0,0 +1,77 @@
import json
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
# from .setting import ENCRYPTKEY
ENCRYPTKEY = b"lunminalinguaai_"
class AESCipher:
def __init__(self, key):
"""
初始化AES加密器
:param key: 加密密钥16/24/32字节可以是字符串或字节
"""
if isinstance(key, str):
key = key.encode("utf-8")
if len(key) not in [16, 24, 32]:
raise ValueError("密钥长度必须为16、24或32字节")
self.key = key
def encrypt(self, data):
"""
加密数据(支持字符串/列表/字典等JSON可序列化类型
:param data: 要加密的数据
:return: 返回Base64编码的加密字符串
"""
# 生成随机初始化向量
iv = get_random_bytes(AES.block_size)
# 创建AES加密器
cipher = AES.new(self.key, AES.MODE_CBC, iv)
# 序列化数据为JSON字符串并编码为字节
json_data = json.dumps(data)
plain_bytes = json_data.encode("utf-8")
# 填充并加密数据
padded_bytes = pad(plain_bytes, AES.block_size)
cipher_bytes = cipher.encrypt(padded_bytes)
# 组合IV和密文并进行Base64编码
encrypted_data = iv + cipher_bytes
return base64.b64encode(encrypted_data).decode("utf-8")
def decrypt(self, enc_data):
"""
解密数据并恢复原始格式
:param enc_data: Base64编码的加密字符串
:return: 原始数据(保持原始格式)
"""
# Base64解码
encrypted_data = base64.b64decode(enc_data)
# 提取初始化向量
iv = encrypted_data[: AES.block_size]
cipher_bytes = encrypted_data[AES.block_size :]
# 创建AES解密器
cipher = AES.new(self.key, AES.MODE_CBC, iv)
# 解密并去除填充
decrypted_bytes = cipher.decrypt(cipher_bytes)
unpadded_bytes = unpad(decrypted_bytes, AES.block_size)
# 解码JSON并恢复原始数据结构
json_data = unpadded_bytes.decode("utf-8")
return json.loads(json_data)
encrypt = AESCipher(ENCRYPTKEY)
if __name__ == '__main__':
data = "sk-3e47a49bf60e49e8ab08bb1f1550aa86"
enc_data = encrypt.encrypt(data)
print(enc_data)

View File

@@ -0,0 +1,14 @@
# API请求code
stateCodeList = {
"0": "成功",
"40026": "验证码错误",
"40001": "读取用户头像数据失败",
"40020": "邮箱或密码不正确",
"40018": "该账号未激活",
"40033": "用户未激活,已重新发送激活邮件",
}
def getCode(code):
return stateCodeList.get(str(code), "未知错误,请联系技术支持")

67
app/core/utils/format.py Normal file
View File

@@ -0,0 +1,67 @@
from datetime import datetime
def formatSize(size):
"""格式化文件大小"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} PB"
def formatDate(date_str):
"""格式化日期时间"""
try:
# 处理带小数秒的情况
if "." in date_str:
# 分割日期和小数秒部分
date_part, fractional_part = date_str.split(".", 1)
# 去除末尾的'Z'并截取前6位小数
fractional_sec = fractional_part.rstrip("Z")[:6]
# 重新组合日期字符串
normalized_date_str = f"{date_part}.{fractional_sec}Z"
date_time = datetime.strptime(normalized_date_str, "%Y-%m-%dT%H:%M:%S.%fZ")
else:
# 处理没有小数秒的情况
date_time = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ")
except ValueError:
# 如果所有格式都失败,返回原始字符串
return date_str
return date_time.strftime("%Y-%m-%d %H:%M:%S")
def getFileIcon(fileType, fileName):
if fileType == "file":
suffix = fileName.split(".")[-1].lower()
icon_map = {
"txt": "Txt.svg",
"png": "Image.svg",
"jpg": "Image.svg",
"svg": "Image.svg",
"jpeg": "Image.svg",
"bmp": "Image.svg",
"gif": "Gif.svg",
"xls": "Excel.svg",
"xlsx": "Excel.svg",
"doc": "Word.svg",
"docx": "Word.svg",
"pdf": "Pdf.svg",
"ppt": "PPT.svg",
"mp4": "Video.svg",
"mkv": "Video.svg",
"mp3": "music.svg",
"wav": "music.svg",
"zip": "Zip.svg",
"rar": "Zip.svg",
"csv": "Excel.svg",
"db": "Database.svg",
"py": "Programme.svg",
"c": "Programme.svg",
"cpp": "Programme.svg",
"go": "Programme.svg",
}
return icon_map.get(suffix, "None.svg") # 默认图标
else:
return "Folder.svg"

View File

@@ -0,0 +1,90 @@
# coding: utf-8
import json
import os
from loguru import logger
from pathlib import Path
# 导入配置
from .config import cfg, qconfig
# 当前语言设置,默认为中文
_current_language = "zh"
# 翻译词典
_translations = {}
# 语言文件目录
_LANG_DIR = Path("app/resource/lang").absolute()
# print(Path("app/resource/lang").absolute())
def load_language(lang_code="zh"):
"""
加载指定语言的翻译文件
Args:
lang_code: 语言代码,如 "zh""en"
"""
global _current_language, _translations
try:
# 构建语言文件路径
lang_file = os.path.join(_LANG_DIR, f"{lang_code}.json")
# 检查文件是否存在
if not os.path.exists(lang_file):
logger.warning(f"语言文件不存在: {lang_file},使用默认语言")
lang_code = "zh"
lang_file = os.path.join(_LANG_DIR, "zh.json")
# 读取语言文件
with open(lang_file, "r", encoding="utf-8") as f:
_translations = json.load(f)
_current_language = lang_code
logger.info(f"已加载语言: {lang_code}")
except Exception as e:
logger.error(f"加载语言文件失败: {e}")
# 如果加载失败,使用空字典
_translations = {}
def lang(text):
"""
翻译文本
Args:
text: 要翻译的原文
Returns:
翻译后的文本,如果没有找到翻译,则返回原文
"""
# 如果翻译词典为空,尝试加载默认语言
if not _translations:
load_language(_current_language)
# 返回翻译后的文本,找不到则返回原文
return _translations.get(text, text)
def get_current_language():
"""
获取当前语言代码
Returns:
当前语言代码
"""
return _current_language
# 初始化时从配置加载语言
try:
if hasattr(cfg, "language"):
initial_lang = qconfig.get(cfg.language)
load_language(initial_lang)
else:
# 如果配置中没有语言设置,使用默认值
load_language()
except Exception as e:
logger.error(f"加载语言配置时出错: {e}")
# 出错时使用默认语言
load_language("zh")

16
app/core/utils/setting.py Normal file
View File

@@ -0,0 +1,16 @@
# coding: utf-8
from pathlib import Path
# change DEBUG to False if you want to compile the code to exe
DEBUG = "__compiled__" not in globals()
YEAR = 2025
AUTHOR = "Miao"
VERSION = "v0.0.1"
APP_NAME = "miaostarspan"
CONFIG_FOLDER = Path("config").absolute()
CONFIG_FILE = CONFIG_FOLDER / "config.json"
DOWNLOAD_FOLDER = Path("download").absolute()
# 23

View File

@@ -0,0 +1,34 @@
# coding: utf-8
from PyQt6.QtCore import QObject, pyqtSignal
from PyQt6.QtGui import QPixmap
class SignalBus(QObject):
"""Signal bus"""
checkUpdateSig = pyqtSignal()
micaEnableChanged = pyqtSignal(bool)
dirOpenSignal = pyqtSignal(str)
shareDirOpenSignal = pyqtSignal(str)
avatarUpdated = pyqtSignal(QPixmap) # 头像更新信号
imagePreviewSignal = pyqtSignal(str)
txtPreviewSignal = pyqtSignal(str)
opacityChanged = pyqtSignal() # 透明度变化信号
backgroundChanged = pyqtSignal() # 背景颜色变化信号
refreshFolderListSignal = pyqtSignal()
addUploadFileTask = pyqtSignal(str) # 添加upload任务信号
addDownloadFileTask = pyqtSignal(str, str, str) # 添加download任务信号
shareFolderViewSignal = pyqtSignal(str) # 分享文件夹
shareFileDownloadSignal = pyqtSignal() # 分享文件下载
languageChanged = pyqtSignal() # 语言变更信号
loginSuccessSignal = pyqtSignal() # 登录成功信号
signalBus = SignalBus()

View File

@@ -0,0 +1,6 @@
# coding: utf-8
"""
Version information for the application
"""
version = "0.0.2"

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.19H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.69c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a119.958 119.958 0 0 0-84.85-35.15z" fill="#8B72F7" /><path d="M862.07 216.83l-117.5-117.5a120.029 120.029 0 0 0-61.74-32.89v131.88c0 41.87 33.94 75.81 75.81 75.81H894a120.006 120.006 0 0 0-31.93-57.3zM470.34 532.68l-90.17-48.97c-14.7-6.9-26.82-0.52-26.95 14.18l-0.9 106.41c-0.12 14.7 11.8 32.38 26.5 39.28l90.17 48.97c14.7 6.9 26.82 0.52 26.95-14.18l0.9-106.41c0.12-14.7-11.8-32.37-26.5-39.28zM653.45 453.92c16.61-11.17 17.55-28.04 2.08-37.48L539.56 360.1c-16.34-8.45-30.61-9.12-50.33 1.13l-116.51 55.9c-16.61 11.17-17.55 28.04-2.08 37.48l111.97 60.34c15.47 9.45 38.27 9.2 58.33-1.13l112.51-59.9zM646.12 481.71l-97.17 51.97c-14.7 6.9-26.62 24.58-26.5 39.28l0.9 108.41c0.12 14.7 12.25 21.08 26.95 14.18l97.17-51.97c14.7-6.9 26.62-24.58 26.5-39.28l-0.9-108.41c-0.12-14.7-12.25-21.08-26.95-14.18z" fill="#7463EA" /><path d="M470.34 521.68l-90.17-48.97c-14.7-6.9-26.82-0.52-26.95 14.18l-0.9 106.41c-0.12 14.7 11.8 32.38 26.5 39.28l90.17 48.97c14.7 6.9 26.82 0.52 26.95-14.18l0.9-106.41c0.12-14.7-11.8-32.37-26.5-39.28zM653.45 442.92c16.61-11.17 17.55-28.04 2.08-37.48L539.56 349.1c-16.34-8.45-30.61-9.12-50.33 1.13l-116.51 55.9c-16.61 11.17-17.55 28.04-2.08 37.48l111.97 60.34c15.47 9.45 38.27 9.2 58.33-1.13l112.51-59.9zM673.07 484.89c-0.12-14.7-12.25-21.08-26.95-14.18l-97.17 51.97c-14.7 6.9-26.62 24.58-26.5 39.28l0.9 108.41c0.12 14.7 12.25 21.08 26.95 14.18l97.17-51.97c14.7-6.9 26.62-24.58 26.5-39.28l-0.9-108.41z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg t="1759502775435" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4881" width="200" height="200"><path d="M924.16 274.048v475.904L512 987.904l-412.16-237.952V274.048L512 36.032l412.16 238.016zM512 460.736L372.16 367.488l-47.36 71.04 144.512 96.32v147.84h85.376v-147.84l144.512-96.32-47.36-71.04L512 460.8z" p-id="4882" fill="#2F80ED"></path></svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1758946089830" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4887"
width="200" height="200"><path d="M128.299 128C92.788 128 64 156.788 64 192.299v639.4C64 867.212 92.788 896 128.299 896H895.7c35.512 0 64.3-28.788 64.3-64.299V192.299C960 156.788 931.212 128 895.701 128H128.299zM128 588.313l178.162-178.162c24.795-24.795 64.996-24.795 89.792 0L817.803 832H128V588.313z m768 231.375L666.385 590.073l64.718-64.718c25.153-25.152 65.933-25.152 91.085 0L896 599.167v220.521zM694.65 471.299l-73.519 73.519L432.653 356.34c-45.064-45.064-118.127-45.064-163.19 0L128 497.803V192h768v316.657l-37.358-37.358c-45.286-45.285-118.707-45.285-163.992 0z" p-id="4888" fill="#2F80ED"></path></svg>

After

Width:  |  Height:  |  Size: 874 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.8 64.2H270.4c-78.1 0-141.3 63.3-141.3 141.3v612.9c0 78.1 63.3 141.3 141.3 141.3h485.5c78.1 0 141.3-63.3 141.3-141.3V301.7c0-31.8-12.6-62.3-35.1-84.9L744.6 99.3c-22.5-22.5-53-35.1-84.8-35.1z" fill="#8B72F7" /><path d="M861.7 216.8L744.2 99.3c-17-17-38.5-28.3-61.7-32.9v131.9c0 41.9 33.9 75.8 75.8 75.8h135.4c-5.2-21.6-16.1-41.5-32-57.3zM585.6 350.4H440.8c-17.2 0-33.1 9.2-41.7 24.1l-72.4 125.4c-8.6 14.9-8.6 33.3 0 48.2l72.4 125.4c8.6 14.9 24.5 24.1 41.7 24.1h144.8c17.2 0 33.1-9.2 41.7-24.1l72.4-125.4c8.6-14.9 8.6-33.3 0-48.2l-72.4-125.4c-8.6-14.9-24.5-24.1-41.7-24.1z m-71 247.7c-42 0.8-76.2-33.4-75.5-75.5 0.7-39.6 33.1-71.9 72.7-72.7 42-0.8 76.2 33.4 75.5 75.5-0.8 39.6-33.1 71.9-72.7 72.7z" fill="#7463EA" /><path d="M585.6 338.4H440.8c-17.2 0-33.1 9.2-41.7 24.1l-72.4 125.4c-8.6 14.9-8.6 33.3 0 48.2l72.4 125.4c8.6 14.9 24.5 24.1 41.7 24.1h144.8c17.2 0 33.1-9.2 41.7-24.1l72.4-125.4c8.6-14.9 8.6-33.3 0-48.2l-72.4-125.4c-8.6-14.9-24.5-24.1-41.7-24.1z m-71 247.7c-42 0.8-76.2-33.4-75.5-75.5 0.7-39.6 33.1-71.9 72.7-72.7 42-0.8 76.2 33.4 75.5 75.5-0.8 39.6-33.1 71.9-72.7 72.7z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.7 64.2H270.4c-78.1 0-141.3 63.3-141.3 141.3v612.9c0 78.1 63.3 141.3 141.3 141.3h485.5c78.1 0 141.3-63.3 141.3-141.3V301.7c0-31.8-12.6-62.3-35.1-84.9L744.6 99.3c-22.5-22.5-53.1-35.1-84.9-35.1z" fill="#F98950" /><path d="M862.1 216.8L744.6 99.3c-17-17-38.5-28.3-61.7-32.9v131.9c0 41.9 33.9 75.8 75.8 75.8H894c-5.1-21.5-16-41.4-31.9-57.3z" fill="#F26C38" /><path d="M369.4 399.3a143.7 42.6 0 1 0 287.4 0 143.7 42.6 0 1 0-287.4 0Z" fill="#F26C38" /><path d="M513.1 532.1c79.3 0 143.7-19.1 143.7-42.6v-58.3c0-0.8-0.1-1.7-0.2-2.5-4.3 22.4-66.9 40.2-143.4 40.2S374 451 369.7 428.6c-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.6 64.3 42.7 143.6 42.7z" fill="#F26C38" /><path d="M513.1 619.9c79.3 0 143.7-19.1 143.7-42.6V519c0-0.8-0.1-1.7-0.2-2.5-4.3 22.4-66.9 40.2-143.4 40.2S374 538.9 369.7 516.5c-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.5 64.3 42.6 143.6 42.6z" fill="#F26C38" /><path d="M513.2 644.1c-76.5 0-139.1-17.8-143.4-40.2-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.5 64.3 42.6 143.7 42.6S657 688.2 657 664.7v-58.3c0-0.8-0.1-1.7-0.2-2.5-4.5 22.4-67.1 40.2-143.6 40.2z" fill="#F26C38" /><path d="M369.4 387.3a143.7 42.6 0 1 0 287.4 0 143.7 42.6 0 1 0-287.4 0Z" fill="#FFFFFF" /><path d="M513.1 520.1c79.3 0 143.7-19.1 143.7-42.6v-58.3c0-0.8-0.1-1.7-0.2-2.5-4.3 22.4-66.9 40.2-143.4 40.2S374 439 369.7 416.6c-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.6 64.3 42.7 143.6 42.7z" fill="#FFFFFF" /><path d="M513.1 607.9c79.3 0 143.7-19.1 143.7-42.6V507c0-0.8-0.1-1.7-0.2-2.5-4.3 22.4-66.9 40.2-143.4 40.2S374 526.9 369.7 504.5c-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.5 64.3 42.6 143.6 42.6z" fill="#FFFFFF" /><path d="M656.6 591.9c-4.3 22.4-66.9 40.2-143.4 40.2s-139.1-17.8-143.4-40.2c-0.2 0.8-0.2 1.6-0.2 2.5v58.3c0 23.5 64.3 42.6 143.7 42.6 79.3 0 143.7-19.1 143.7-42.6v-58.3c-0.2-0.8-0.3-1.7-0.4-2.5z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1758303622544" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="11979"
width="200" height="200"><path d="M833 128c0-53-43-96-96-96h-16c-53 0-96 43-96 96H400c0-53-43-96-96-96h-16c-53 0-96 43-96 96H64c-35.3 0-64 28.7-64 64v736c0 35.3 28.7 64 64 64h896c35.3 0 64-28.7 64-64V192c0-35.3-28.7-64-64-64H833zM729 96c22.1 0 40 17.9 40 40v80c0 22.1-17.9 40-40 40s-40-17.9-40-40v-80c0-22.1 17.9-40 40-40z m-433 0c22.1 0 40 17.9 40 40v80c0 22.1-17.9 40-40 40s-40-17.9-40-40v-80c0-22.1 17.9-40 40-40z m632 832H96c-17.7 0-32-14.3-32-32V448h896v448c0 17.7-14.3 32-32 32z m32-544H64V224c0-17.7 14.3-32 32-32h96v32c0 53 43 96 96 96h16c53 0 96-43 96-96v-32h225v24c0 57.4 46.6 104 104 104s104-46.6 104-104v-24h95c17.7 0 32 14.3 32 32v160z" p-id="11980" fill="#2F80ED"></path></svg>

After

Width:  |  Height:  |  Size: 959 B

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1758303572643" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8837"
width="200" height="200"><path d="M597.172 531.547c-9.668-9.857-9.514-25.686 0.344-35.353 9.857-9.668 25.686-9.514 35.353 0.343L941.85 811.58c9.667 9.858 9.514 25.686-0.344 35.354-9.857 9.668-25.686 9.514-35.354-0.343L597.172 531.547zM117.85 846.59c-9.668 9.857-25.497 10.01-35.354 0.343-9.858-9.668-10.011-25.496-0.344-35.354l308.98-315.042c9.667-9.857 25.496-10.011 35.353-0.343 9.858 9.667 10.012 25.496 0.344 35.353L117.848 846.59z" fill="#2F80ED" p-id="8838"></path><path d="M82.151 216.505c-9.667-9.857-9.514-25.686 0.344-35.354 9.857-9.667 25.686-9.514 35.354 0.344l340.605 347.29c29.004 29.572 76.489 30.033 106.061 1.03 0.347-0.34 0.69-0.684 1.03-1.03l340.606-347.29c9.668-9.858 25.497-10.011 35.354-0.344 9.858 9.668 10.011 25.497 0.344 35.354l-340.606 347.29a125 125 0 0 1-1.718 1.717c-49.287 48.339-128.429 47.57-176.768-1.718L82.151 216.505z" fill="#2F80ED" p-id="8839"></path><path d="M95 191v643h835V191H95z m0-60h835c33.137 0 60 26.863 60 60v643c0 33.137-26.863 60-60 60H95c-33.137 0-60-26.863-60-60V191c0-33.137 26.863-60 60-60z" fill="#2F80ED" p-id="8840"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M658.68 63.95H269.36c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.44c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.008 120.008 0 0 0-84.85-35.14z" fill="#53D39C" /><path d="M655.49 388.82c-9.37-9.37-24.57-9.37-33.94 0L510.8 499.57 400.05 388.82c-9.37-9.37-24.57-9.37-33.94 0-9.37 9.37-9.37 24.57 0 33.94l110.75 110.75L366.1 644.26c-9.37 9.37-9.37 24.57 0 33.94 4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03L510.8 567.45 621.54 678.2c4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03c9.37-9.37 9.37-24.57 0-33.94L544.74 533.51l110.75-110.75c9.37-9.37 9.37-24.57 0-33.94zM861.03 216.59l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81h135.37a120.058 120.058 0 0 0-31.93-57.29z" fill="#25BF79" /><path d="M544.74 521.51l110.75-110.75c9.37-9.37 9.37-24.57 0-33.94-9.37-9.37-24.57-9.37-33.94 0L510.8 487.57 400.05 376.82c-9.37-9.37-24.57-9.37-33.94 0-9.37 9.37-9.37 24.57 0 33.94l110.75 110.75L366.1 632.26c-9.37 9.37-9.37 24.57 0 33.94 4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03L510.8 555.45 621.54 666.2c4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03c9.37-9.37 9.37-24.57 0-33.94L544.74 521.51z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.19H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.69c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.017 120.017 0 0 0-84.85-35.15z" fill="#8B72F7" /><path d="M862.07 216.84l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81H894a119.975 119.975 0 0 0-31.93-57.29zM627.66 386.46c13.25 0 24-10.75 24-24s-10.75-24-24-24H398.62c-13.25 0-24 10.75-24 24v323.08c0 13.25 10.75 24 24 24h229.05c13.25 0 24-10.75 24-24s-10.75-24-24-24H422.62V548h178.85c13.25 0 24-10.75 24-24s-10.75-24-24-24H422.62V386.46h205.04z" fill="#7463EA" /><path d="M627.66 649.54H422.62V536h178.85c13.25 0 24-10.75 24-24s-10.75-24-24-24H422.62V374.46h205.05c13.25 0 24-10.75 24-24s-10.75-24-24-24H398.62c-13.25 0-24 10.75-24 24v323.08c0 13.25 10.75 24 24 24h229.05c13.25 0 24-10.75 24-24s-10.75-24-24.01-24z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M739.82 290.16H64.21V234.1c0-58.59 47.5-106.09 106.09-106.09h393.16c43.85 0 84.74 22.11 108.75 58.81l67.61 103.34z" fill="#FFBA53" /><path d="M960.19 396.82v357.49c0 78.06-63.28 141.33-141.33 141.33H205.54c-78.06 0-141.33-63.28-141.33-141.33V255.49h754.64c78.06 0 141.34 63.28 141.34 141.33z" fill="#FFDC53" /></svg>

After

Width:  |  Height:  |  Size: 583 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M788.14 927h-550c-77.32 0-140-62.68-140-140V237c0-77.32 62.68-140 140-140h550c77.32 0 140 62.68 140 140v550c0 77.32-62.68 140-140 140z" fill="#FF7878" /><path d="M671.15 510.18h-135.9c-13.25 0-24 10.75-24 24s10.75 24 24 24h111.9v91.1c-1.21 0.66-2.37 1.42-3.49 2.31-29.1 23.19-64.2 35.44-101.5 35.44-89.89 0-163.03-73.13-163.03-163.03 0-89.89 73.13-163.03 163.03-163.03 37.61 0 72.94 12.44 102.17 35.98 10.32 8.31 25.43 6.69 33.75-3.64 8.31-10.32 6.68-25.43-3.64-33.75-37.31-30.05-84.29-46.6-132.28-46.6-116.36 0-211.03 94.67-211.03 211.03s94.67 211.03 211.03 211.03c36.98 0 73.4-9.85 105.23-28.12 1.64 11.66 11.65 20.63 23.76 20.63 13.25 0 24-10.75 24-24V534.18c0-13.26-10.75-24-24-24z" fill="#EF5252" /><path d="M671.15 498.18h-135.9c-13.25 0-24 10.75-24 24s10.75 24 24 24h111.9v91.1c-1.21 0.66-2.37 1.42-3.49 2.31-29.1 23.19-64.2 35.44-101.5 35.44-89.89 0-163.03-73.13-163.03-163.03 0-89.89 73.13-163.03 163.03-163.03 37.61 0 72.94 12.44 102.17 35.98 10.32 8.31 25.43 6.69 33.75-3.64 8.31-10.32 6.68-25.43-3.64-33.75-37.31-30.05-84.29-46.6-132.28-46.6-116.36 0-211.03 94.67-211.03 211.03s94.67 211.03 211.03 211.03c36.98 0 73.4-9.85 105.23-28.12 1.64 11.66 11.65 20.63 23.76 20.63 13.25 0 24-10.75 24-24V522.18c0-13.26-10.75-24-24-24z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M788.14 928.23h-550c-77.32 0-140-62.68-140-140v-550c0-77.32 62.68-140 140-140h550c77.32 0 140 62.68 140 140v550c0 77.32-62.68 140-140 140z" fill="#FFC757" /><path d="M770.19 644.92l-67.56-117.01c-15.4-26.68-53.91-26.68-69.31 0l-64.01 110.88L450.3 432.65c-15.4-26.67-53.89-26.67-69.29 0L258.45 644.94c-15.4 26.67 3.85 60.01 34.65 60.01h442.44c30.8-0.01 50.05-33.35 34.65-60.03z" fill="#EF9F2B" /><path d="M595.103921 388.209544a55.27 55.27 0 1 0 100.661393-45.676862 55.27 55.27 0 1 0-100.661393 45.676862Z" fill="#EF9F2B" /><path d="M770.19 632.92l-67.56-117.01c-15.4-26.68-53.91-26.68-69.31 0l-64.01 110.88L450.3 420.65c-15.4-26.67-53.89-26.67-69.29 0L258.45 632.94c-15.4 26.67 3.85 60.01 34.65 60.01h442.44c30.8-0.01 50.05-33.35 34.65-60.03z" fill="#FFFFFF" /><path d="M595.104335 376.209454a55.27 55.27 0 1 0 100.661392-45.676862 55.27 55.27 0 1 0-100.661392 45.676862Z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M788.14 928.23h-550c-77.32 0-140-62.68-140-140v-550c0-77.32 62.68-140 140-140h550c77.32 0 140 62.68 140 140v550c0 77.32-62.68 140-140 140z" fill="#FF7878" /><path d="M682.2 326.8a27.8 27.8 0 0 0-10.04-21.39 27.807 27.807 0 0 0-22.87-5.93l-261.58 49.04c-13.15 2.46-22.67 13.94-22.67 27.32l0.18 250.12a64.668 64.668 0 0 0-22.39-3.98c-35.76 0-64.74 28.99-64.74 64.74 0 35.76 28.99 64.74 64.74 64.74s64.74-28.99 64.74-64.74V450.77l232.29-43.55v169.7a64.668 64.668 0 0 0-22.39-3.98c-35.76 0-64.74 28.99-64.74 64.74s28.99 64.74 64.74 64.74 64.74-28.99 64.74-64.74c-0.01-1.19-0.01-310.88-0.01-310.88z" fill="#EF5252" /><path d="M682.2 314.8a27.8 27.8 0 0 0-10.04-21.39 27.807 27.807 0 0 0-22.87-5.93l-261.58 49.04c-13.15 2.46-22.67 13.94-22.67 27.32l0.18 250.12a64.668 64.668 0 0 0-22.39-3.98c-35.76 0-64.74 28.99-64.74 64.74 0 35.76 28.99 64.74 64.74 64.74s64.74-28.99 64.74-64.74V438.77l232.29-43.55v169.7a64.668 64.668 0 0 0-22.39-3.98c-35.76 0-64.74 28.99-64.74 64.74s28.99 64.74 64.74 64.74 64.74-28.99 64.74-64.74c-0.01-1.19-0.01-310.88-0.01-310.88z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1759421169656" class="icon" viewBox="0 0 1102 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6039" xmlns:xlink="http://www.w3.org/1999/xlink" width="215.234375" height="200"><path d="M1026.914462 133.041231H516.883692v-63.015385C516.962462 28.041846 482.540308 0 441.107692 0H75.854769C34.500923 0 0 34.973538 0 70.025846v210.077539h1102.769231v-70.104616a77.115077 77.115077 0 0 0-75.854769-76.957538zM82.707692 329.097846H0v574.148923c0 41.984 34.422154 76.957538 82.707692 76.957539h937.353846c48.285538 0 82.707692-34.973538 82.707693-76.957539v-574.227692H82.707692z" fill="#2F80ED" p-id="6040"></path></svg>

After

Width:  |  Height:  |  Size: 770 B

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1758303555137" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7732"
width="200" height="200"><path d="M512 981.333333C251.733333 981.333333 42.666667 772.266667 42.666667 512S251.733333 42.666667 512 42.666667s469.333333 209.066667 469.333333 469.333333-209.066667 469.333333-469.333333 469.333333z m0-874.666666C288 106.666667 106.666667 288 106.666667 512s181.333333 405.333333 405.333333 405.333333 405.333333-181.333333 405.333333-405.333333S736 106.666667 512 106.666667z" fill="#2F80ED" p-id="7733"></path><path d="M768 800c-17.066667 0-32-14.933333-32-32 0-123.733333-100.266667-224-224-224S290.133333 644.266667 290.133333 768c0 17.066667-14.933333 32-32 32s-32-14.933333-32-32c0-157.866667 128-288 288-288s288 128 288 288c-2.133333 17.066667-17.066667 32-34.133333 32z" fill="#2F80ED" p-id="7734"></path><path d="M512 544c-87.466667 0-160-70.4-160-160 0-87.466667 70.4-160 160-160 87.466667 0 160 70.4 160 160 0 87.466667-72.533333 160-160 160z m0-256c-53.333333 0-96 42.666667-96 96s42.666667 96 96 96 96-42.666667 96-96-42.666667-96-96-96z" fill="#2F80ED" p-id="7735"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.19H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.69c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.017 120.017 0 0 0-84.85-35.15z" fill="#C7DADD" /><path d="M862.07 216.84l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81H894a119.975 119.975 0 0 0-31.93-57.29zM512.53 295.92c-82.1 0-148.9 66.8-148.9 148.9 0 12.53 10.15 22.68 22.68 22.68s22.68-10.15 22.68-22.68c0-57.09 46.45-103.54 103.54-103.54 27.63 0 53.62 10.78 73.19 30.35s30.35 45.57 30.35 73.19c0 21.11-6.31 41.41-18.24 58.71-11.68 16.92-27.89 29.89-46.9 37.48-37.11 14.83-61.08 49.9-61.08 89.33V632c0 12.53 10.15 22.68 22.68 22.68s22.68-10.15 22.68-22.68v-1.66c0-20.78 12.78-39.31 32.56-47.21 27.33-10.92 50.63-29.54 67.4-53.84 17.18-24.9 26.26-54.11 26.26-84.47 0-39.74-15.5-77.13-43.64-105.27-28.13-28.13-65.52-43.63-105.26-43.63z" fill="#9DC0C9" /><path d="M500.973351 750.25961a30.24 30.24 0 1 0 23.144694-55.876234 30.24 30.24 0 1 0-23.144694 55.876234Z" fill="#9DC0C9" /><path d="M618.4 327.31c-28.14-28.14-65.52-43.64-105.26-43.64-82.1 0-148.9 66.8-148.9 148.9 0 12.53 10.15 22.68 22.68 22.68s22.68-10.15 22.68-22.68c0-57.09 46.45-103.54 103.54-103.54 27.63 0 53.62 10.78 73.19 30.35s30.35 45.57 30.35 73.19c0 21.11-6.31 41.41-18.24 58.71-11.68 16.92-27.89 29.89-46.9 37.48-37.11 14.83-61.08 49.9-61.08 89.33v1.66c0 12.53 10.15 22.68 22.68 22.68s22.68-10.15 22.68-22.68v-1.66c0-20.78 12.78-39.31 32.56-47.21 27.33-10.92 50.63-29.54 67.4-53.84 17.18-24.9 26.26-54.11 26.26-84.47 0-39.74-15.5-77.12-43.64-105.26z" fill="#FFFFFF" /><path d="M513.14 710.09m-30.24 0a30.24 30.24 0 1 0 60.48 0 30.24 30.24 0 1 0-60.48 0Z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1758946130633" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5976"
width="200" height="200"><path d="M512 0a512 512 0 0 1 17.554286 1023.707429L512 1024A512 512 0 0 1 512 0z m0 73.142857a438.857143 438.857143 0 1 0 0 877.714286V73.142857z" fill="#2F80ED" p-id="5977"></path></svg>

After

Width:  |  Height:  |  Size: 475 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M658.58 63.94H269.26c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.44c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.017 120.017 0 0 0-84.85-35.15z" fill="#F98950" /><path d="M860.93 216.59l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81h135.37a119.93 119.93 0 0 0-31.93-57.29zM556.02 348.38H380.37c-13.25 0-24 10.75-24 24v302.74c0 13.25 10.75 24 24 24s24-10.75 24-24V595.6h151.65c68.16 0 123.61-55.45 123.61-123.61s-55.45-123.61-123.61-123.61z m0 199.22H404.37V396.38h151.65c41.69 0 75.61 33.92 75.61 75.61s-33.92 75.61-75.61 75.61z" fill="#F26C38" /><path d="M556.02 336.38H380.37c-13.25 0-24 10.75-24 24v302.74c0 13.25 10.75 24 24 24s24-10.75 24-24V583.6h151.65c68.16 0 123.61-55.45 123.61-123.61s-55.45-123.61-123.61-123.61z m0 199.22H404.37V384.38h151.65c41.69 0 75.61 33.92 75.61 75.61s-33.92 75.61-75.61 75.61z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M658.68 63.95H269.36c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.44c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.008 120.008 0 0 0-84.85-35.14z" fill="#FF7878" /><path d="M861.03 216.59l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81h135.37a120.058 120.058 0 0 0-31.93-57.29zM649.24 554.59c-22.79 0-45.38 3.31-68.16 7.6-26.87-24.76-49.66-54-66.99-86.56 18.5-60.63 19.47-101.76 5.45-121.26-6.43-8.58-17.14-14.04-28.04-14.04-14.02-0.97-27.07 5.46-33.5 17.35-19.47 32.56 8.76 96.5 21.81 122.43-15 46.59-33.5 91.04-57.25 134.32-102.82 44.45-104.97 71.54-104.97 81.29 0 11.89 6.43 23.78 18.31 29.24 4.28 3.12 10.71 4.29 16.16 4.29 27.07 0 58.42-30.41 91.92-90.06 42.26-17.35 84.32-31.39 128.73-41.13 22.79 19.49 50.83 30.41 80.04 32.56 18.5 0 54.14 0 53.75-37.04 1.17-14.04-6.43-37.82-57.26-38.99zM353.53 696.45l-3.26 1.17c9.6-14.06 22.26-25 38.38-31.64-8.44 14.26-20.15 25-35.12 30.47z m132.59-322.36c1.02-1.16 3.17-2.35 4.4-2.35l3.57 1.19c5.52 18.41 5.52 37.8-1.23 56.02-7.96-17.26-11.23-36.64-6.74-54.86zM542.26 584c-23.61 5.5-49.55 13.17-73.16 21.81l-2.45 0.96 0.32-1.94c11.81-23.97 22.64-49.13 32.32-74.28l1.16-2.45 1.17 1.47c12 18.47 27.2 37.51 43.46 53.82l-2.82 0.61z m111.17 13.58c-10.87-0.95-21.54-3.04-32.4-8.35 9.7-2.09 18.24-2.09 27.94-2.09 21.54 0 25.81 5.32 26 8.35-6.41 2.09-13.98 3.23-21.54 2.09z" fill="#F25555" /><path d="M648.96 575.14c-9.7 0-18.24 0-27.94 2.09 10.87 5.32 21.54 7.4 32.4 8.35 7.57 1.14 15.14 0 21.54-2.09-0.19-3.03-4.46-8.35-26-8.35z m-147.34-57.57l-1.17-1.47-1.16 2.45c-9.68 25.15-20.52 50.31-32.32 74.28l-0.32 1.94 2.45-0.96c23.61-8.65 49.55-16.31 73.16-21.81l2.81-0.61c-16.25-16.32-31.45-35.35-43.45-53.82z m-7.52-156.65l-3.57-1.19c-1.23 0-3.38 1.19-4.4 2.35-4.5 18.22-1.23 37.6 6.75 54.85 6.74-18.21 6.74-37.59 1.22-56.01z m-143.83 324.7l3.26-1.17c14.97-5.47 26.68-16.21 35.12-30.47-16.12 6.64-28.78 17.58-38.38 31.64z m302.48-67c-29.21-2.14-57.25-13.06-80.04-32.56-44.4 9.75-86.47 23.78-128.73 41.13-33.5 59.65-64.85 90.06-91.92 90.06-5.45 0-11.88-1.17-16.16-4.29-11.88-5.46-18.31-17.35-18.31-29.24 0-9.75 2.14-36.84 104.97-81.29 23.76-43.28 42.26-87.73 57.25-134.32-13.05-25.93-41.29-89.87-21.81-122.43 6.43-11.89 19.47-18.32 33.5-17.35 10.91 0 21.62 5.46 28.04 14.04 14.02 19.49 13.05 60.63-5.45 121.26 17.33 32.56 40.12 61.8 66.99 86.56 22.79-4.29 45.38-7.6 68.16-7.6 50.83 1.17 58.42 24.95 57.25 38.99 0.4 37.04-35.24 37.04-53.74 37.04z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.19H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.69c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.017 120.017 0 0 0-84.85-35.15z" fill="#53B7F4" /><path d="M862.07 216.84l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81H894a119.975 119.975 0 0 0-31.93-57.29zM410.97 407.96c-9.37-9.37-24.57-9.37-33.94 0L274.97 510.01c-9.37 9.37-9.37 24.57 0 33.94L377.02 646a23.919 23.919 0 0 0 16.97 7.03c6.14 0 12.29-2.34 16.97-7.03 9.37-9.37 9.37-24.57 0-33.94l-85.08-85.08 85.08-85.08c9.38-9.37 9.38-24.57 0.01-33.94zM649.25 407.96c-9.37-9.37-24.57-9.37-33.94 0-9.37 9.37-9.37 24.57 0 33.94l85.08 85.08-85.08 85.08c-9.37 9.37-9.37 24.57 0 33.94 4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03L751.3 543.95c9.37-9.37 9.37-24.57 0-33.94L649.25 407.96zM566.34 360.5c-12.64-3.99-26.12 3.02-30.11 15.66l-91.94 291.23c-3.99 12.64 3.02 26.12 15.66 30.11 2.4 0.76 4.84 1.12 7.23 1.12 10.19 0 19.65-6.55 22.88-16.78L582 390.61c3.99-12.64-3.02-26.12-15.66-30.11z" fill="#29A3D3" /><path d="M410.97 395.96c-9.37-9.37-24.57-9.37-33.94 0L274.97 498.01c-9.37 9.37-9.37 24.57 0 33.94L377.02 634a23.919 23.919 0 0 0 16.97 7.03c6.14 0 12.29-2.34 16.97-7.03 9.37-9.37 9.37-24.57 0-33.94l-85.08-85.08 85.08-85.08c9.38-9.37 9.38-24.57 0.01-33.94zM751.31 498.01L649.25 395.96c-9.37-9.37-24.57-9.37-33.94 0-9.37 9.37-9.37 24.57 0 33.94l85.08 85.08-85.08 85.08c-9.37 9.37-9.37 24.57 0 33.94 4.69 4.69 10.83 7.03 16.97 7.03s12.28-2.34 16.97-7.03L751.3 531.95c9.38-9.37 9.38-24.56 0.01-33.94zM566.34 348.5c-12.64-3.99-26.12 3.02-30.11 15.66l-91.94 291.23c-3.99 12.64 3.02 26.12 15.66 30.11 2.4 0.76 4.84 1.12 7.23 1.12 10.19 0 19.65-6.55 22.88-16.78L582 378.61c3.99-12.64-3.02-26.12-15.66-30.11z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1759421129815" class="icon" viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4996" xmlns:xlink="http://www.w3.org/1999/xlink" width="228.515625" height="200"><path d="M447.783497 0c12.068571 0 23.552 5.485714 31.305143 14.921143L602.553783 166.034286h469.357714c52.955429 0.585143 95.451429 43.885714 95.085714 96.914285v664.137143a96.109714 96.109714 0 0 1-95.085714 96.914286H95.08864A96.109714 96.109714 0 0 1 0.002926 927.158857V96.841143C0.002926 43.446857 42.572069 0.073143 95.015497 0h352.768z m-26.038857 95.085714H95.08864v831.926857l0.146286 1.755429 976.457143 0.146286 0.146285-1.755429-0.219428-665.819428 0.292571-0.219429H557.42464L421.74464 95.085714z m323.730286 448.512c22.454857 0 40.667429 21.211429 40.667428 47.542857 0 26.258286-18.212571 47.542857-40.667428 47.542858H419.769783c-22.454857 0-40.667429-21.284571-40.667429-47.542858 0-26.331429 18.285714-47.542857 40.667429-47.542857h325.705143z" fill="#2F80ED" p-id="4997"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1758303610044" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10913" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M844.8 182.4c-34.4-16.8-80.8-30.4-140-40C645.6 132.8 581.6 128 512 128c-69.6 0-133.6 4.8-192.8 14.4-59.2 9.6-105.6 23.2-140 40-34.4 16.8-51.2 35.2-51.2 55.2v103.2c0 20 16.8 38.4 51.2 55.2 34.4 16.8 80.8 30.4 140 40 59.2 9.6 123.2 14.4 192.8 14.4 69.6 0 133.6-4.8 192.8-14.4 59.2-9.6 105.6-23.2 140-40 34.4-16.8 51.2-35.2 51.2-55.2V237.6c0-20-16.8-38.4-51.2-55.2zM832 328.8c-3.2 2.4-8 5.6-16 8.8-28.8 14.4-69.6 25.6-122.4 34.4-55.2 9.6-116.8 13.6-181.6 13.6-65.6 0-126.4-4.8-181.6-13.6-52.8-8.8-94.4-20-122.4-34.4-8-4-12.8-7.2-16-8.8v-80c3.2-2.4 8-5.6 16-8.8 28.8-14.4 69.6-25.6 122.4-34.4C385.6 196 447.2 192 512 192c65.6 0 126.4 4.8 181.6 13.6 52.8 8.8 93.6 20.8 122.4 34.4 8 4 12.8 7.2 16 8.8v80zM128 760v26.4c0 20 16.8 38.4 51.2 55.2s80.8 30.4 140 40c59.2 9.6 123.2 14.4 192.8 14.4s133.6-4.8 192.8-14.4c59.2-9.6 105.6-23.2 140-40 34.4-16.8 51.2-35.2 51.2-55.2v-32.8c-40 24-93.6 42.4-162.4 54.4-68.8 12-142.4 18.4-221.6 18.4s-152.8-6.4-221.6-18.4C221.6 795.2 168 777.6 128 753.6M128 608v29.6c0 20 16.8 38.4 51.2 55.2 34.4 16.8 80.8 30.4 140 40 59.2 9.6 123.2 14.4 192.8 14.4s133.6-4.8 192.8-14.4c59.2-9.6 105.6-23.2 140-40 34.4-16.8 51.2-35.2 51.2-55.2v-32.8c-40 24-93.6 42.4-162.4 54.4-68.8 12-142.4 18.4-221.6 18.4s-152.8-6.4-221.6-18.4C221.6 646.4 168 628.8 128 604.8M128 456v29.6c0 20 16.8 38.4 51.2 55.2 34.4 16.8 80.8 30.4 140 40 59.2 9.6 123.2 14.4 192.8 14.4s133.6-4.8 192.8-14.4c59.2-9.6 105.6-23.2 140-40 34.4-16.8 51.2-35.2 51.2-55.2v-32.8c-40 24-93.6 42.4-162.4 54.4-68.8 12-142.4 18.4-221.6 18.4s-152.8-6.4-221.6-18.4C221.6 494.4 168 476.8 128 452.8" p-id="10914" fill="#2F80ED"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1759421203348" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9072" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M128 0h64v128a64 64 0 0 0 64 64h512a64 64 0 0 0 64-64V0h64a128 128 0 0 1 128 128v768a128 128 0 0 1-128 128H128a128 128 0 0 1-128-128V128a128 128 0 0 1 128-128z m641.152 355.776l-303.936 303.936-175.872-205.184-72.896 62.464 243.328 283.904 377.216-377.216-67.84-67.904zM768 0v64a64 64 0 0 1-64 64H320a64 64 0 0 1-64-64V0h512z" fill="#2F80ED" p-id="9073"></path></svg>

After

Width:  |  Height:  |  Size: 700 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.19H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.69c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.017 120.017 0 0 0-84.85-35.15z" fill="#4FD397" /><path d="M862.07 216.84l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81H894a119.975 119.975 0 0 0-31.93-57.29zM670.25 381.88H517.97a24.052 24.052 0 0 0-9.66 0H356.03c-13.25 0-24 10.75-24 24s10.75 24 24 24h133.11v258.73c0 13.25 10.75 24 24 24s24-10.75 24-24V429.88h133.11c13.25 0 24-10.75 24-24s-10.75-24-24-24z" fill="#19BC6E" /><path d="M670.25 369.88H517.97a24.052 24.052 0 0 0-9.66 0H356.03c-13.25 0-24 10.75-24 24s10.75 24 24 24h133.11v258.73c0 13.25 10.75 24 24 24s24-10.75 24-24V417.88h133.11c13.25 0 24-10.75 24-24s-10.75-24-24-24z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M65.3 755.4V269.9c0-78.1 63.3-141.3 141.3-141.3h612.9c78.1 0 141.3 63.3 141.3 141.3v485.5c0 78.1-63.3 141.3-141.3 141.3H206.7c-78.1 0-141.4-63.3-141.4-141.3z" fill="#8B72F7" /><path d="M557.3 406.6H308c-22.1 0-40 17.9-40 40v155.9c0 22.1 17.9 40 40 40h249.3c22.1 0 40-17.9 40-40V446.6c0-22-17.9-40-40-40zM738.8 412.7L655 491c-19.5 18.2-19.5 49 0 67.2l83.8 78.3c14.2 13.3 37.5 3.2 37.5-16.2V428.9c0-19.4-23.3-29.5-37.5-16.2z" fill="#7463EA" /><path d="M557.3 394.6H308c-22.1 0-40 17.9-40 40v155.9c0 22.1 17.9 40 40 40h249.3c22.1 0 40-17.9 40-40V434.6c0-22-17.9-40-40-40zM738.8 400.7L655 479c-19.5 18.2-19.5 49 0 67.2l83.8 78.3c14.2 13.3 37.5 3.2 37.5-16.2V416.9c0-19.4-23.3-29.5-37.5-16.2z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 979 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.7 64.2H270.4c-78.1 0-141.3 63.3-141.3 141.3v612.9c0 78.1 63.3 141.3 141.3 141.3h485.5c78.1 0 141.3-63.3 141.3-141.3V301.7c0-31.8-12.6-62.3-35.1-84.9L744.6 99.3c-22.5-22.5-53.1-35.1-84.9-35.1z" fill="#53B7F4" /><path d="M758.6 274.1H894c-5.1-21.5-16-41.4-31.9-57.3L744.6 99.3c-17-17-38.5-28.3-61.7-32.9v131.9c-0.1 41.9 33.9 75.8 75.7 75.8zM741.4 401.2H591.2c-8.3 0-16.1 4.3-20.4 11.4L465 584.4l-1.2 2-26.2 42.5-109.9-179.7h96.8l54.7 81.4 27.6-45.1-49.5-73.8c-4.5-6.6-11.9-10.6-19.9-10.6H284.9c-8.7 0-16.7 4.7-20.9 12.3-4.2 7.6-4.1 16.9 0.5 24.3l152.7 249.6c4.3 7.1 12.1 11.5 20.4 11.5 8.3 0 16.1-4.3 20.4-11.4l146.5-238.2h96.4l-98.1 179.4-46.8-69.8-27.7 44.9 56.7 84.5c4.5 6.7 11.9 10.6 19.9 10.6h1.1c8.4-0.4 15.9-5.1 20-12.5l136.4-249.6c4.1-7.4 3.9-16.5-0.4-23.8-4.3-7.2-12.1-11.7-20.6-11.7z" fill="#29A3D3" /><path d="M762.4 424.7L626 674.3c-4 7.4-11.6 12.1-20 12.5h-1.1c-8 0-15.4-4-19.9-10.6l-56.7-84.5 27.7-44.9 46.9 69.8L701 437.2h-96.4L458 675.4c-4.4 7.1-12.1 11.4-20.4 11.4-8.4 0-16.1-4.4-20.4-11.5L264.4 425.7c-4.5-7.4-4.7-16.7-0.5-24.3 4.2-7.6 12.2-12.3 20.9-12.3h152.4c8 0 15.5 4 19.9 10.6l49.5 73.8-27.6 45.1-54.7-81.4h-96.8l109.9 179.7 26.2-42.5 1.2-2 105.7-171.8c4.4-7.1 12.1-11.4 20.4-11.4h150.2c8.5 0 16.3 4.5 20.6 11.8 4.6 7.2 4.8 16.3 0.7 23.7z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M658.78 64.39H269.46c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V301.88c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.008 120.008 0 0 0-84.85-35.14z" fill="#53B7F4" /><path d="M647.85 360.65c-13.25 0-24 10.75-24 24v226.26l-94.35-93.29c-9.33-9.22-24.34-9.25-33.69-0.05l-95.26 93.6V383.99c0-13.25-10.75-24-24-24s-24 10.75-24 24V668.4c0 9.67 5.8 18.39 14.72 22.13a24.002 24.002 0 0 0 26.1-5.01L512.58 568.4l118.4 117.07a23.993 23.993 0 0 0 16.88 6.93c3.11 0 6.25-0.61 9.24-1.85a24 24 0 0 0 14.75-22.15V384.65c0-13.25-10.74-24-24-24zM861.13 217.03l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81h135.37a119.975 119.975 0 0 0-31.93-57.29z" fill="#29A3D3" /><path d="M647.86 680.4c-6.2 0-12.3-2.4-16.88-6.93L512.58 556.4 393.37 673.52a23.99 23.99 0 0 1-26.1 5.01 23.992 23.992 0 0 1-14.72-22.13V371.99c0-13.25 10.75-24 24-24s24 10.75 24 24v227.19l95.26-93.6c9.36-9.19 24.37-9.17 33.69 0.05l94.35 93.29V372.65c0-13.25 10.75-24 24-24s24 10.75 24 24V656.4c0 9.68-5.82 18.42-14.75 22.15a24.112 24.112 0 0 1-9.24 1.85z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M659.72 64.81H270.4c-78.06 0-141.33 63.28-141.33 141.33v612.95c0 78.06 63.28 141.33 141.33 141.33h485.49c78.06 0 141.33-63.28 141.33-141.33V302.3c0-31.83-12.64-62.35-35.15-84.85l-117.5-117.5a120.008 120.008 0 0 0-84.85-35.14z" fill="#FFC757" /><path d="M304.85 196.81h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.87-6.38-14.24-14.24-14.24h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.86 6.38 14.24 14.24 14.24zM476.37 196.81h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.87 6.38 14.24 14.24 14.24h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.86-6.38-14.24-14.24-14.24zM304.85 396.81h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.87-6.38-14.24-14.24-14.24h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.86 6.38 14.24 14.24 14.24zM476.37 396.81h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.87 6.38 14.24 14.24 14.24h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.86-6.38-14.24-14.24-14.24zM455.65 552.02H321.52c-17.07 0-30.91 13.84-30.91 30.91v155.48c0 17.07 13.84 30.91 30.91 30.91h134.13c17.07 0 30.91-13.84 30.91-30.91V582.93c0-17.07-13.84-30.91-30.91-30.91z m-4.93 164.59c0 13.72-11.12 24.84-24.84 24.84h-74.6c-13.72 0-24.84-11.12-24.84-24.84v-27.36c0-13.72 11.12-24.84 24.84-24.84h74.6c13.72 0 24.84 11.12 24.84 24.84v27.36zM862.07 217.45l-117.5-117.5a120.001 120.001 0 0 0-61.75-32.9v131.88c0 41.87 33.94 75.81 75.81 75.81H894a120.021 120.021 0 0 0-31.93-57.29z" fill="#F79F2B" /><path d="M304.85 184.81h71.52c7.86 0 14.24-6.38 14.24-14.24V99.05c0-7.87-6.38-14.24-14.24-14.24h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.86 6.38 14.24 14.24 14.24zM476.37 184.81h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.87 6.38 14.24 14.24 14.24h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.86-6.38-14.24-14.24-14.24zM304.85 384.81h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.87-6.38-14.24-14.24-14.24h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.86 6.38 14.24 14.24 14.24zM476.37 384.81h-71.52c-7.86 0-14.24 6.38-14.24 14.24v71.52c0 7.87 6.38 14.24 14.24 14.24h71.52c7.86 0 14.24-6.38 14.24-14.24v-71.52c0-7.86-6.38-14.24-14.24-14.24zM455.65 540.02H321.52c-17.07 0-30.91 13.84-30.91 30.91v155.48c0 17.07 13.84 30.91 30.91 30.91h134.13c17.07 0 30.91-13.84 30.91-30.91V570.93c0-17.07-13.84-30.91-30.91-30.91z m-4.93 164.59c0 13.72-11.12 24.84-24.84 24.84h-74.6c-13.72 0-24.84-11.12-24.84-24.84v-27.36c0-13.72 11.12-24.84 24.84-24.84h74.6c13.72 0 24.84 11.12 24.84 24.84v27.36z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#2F80ED" d="M819.292 623.785c-40.844-40.844-88.387-72.547-140.151-94.102 69.587-51.392 114.809-133.97 114.809-226.921 0-155.467-126.483-281.951-281.951-281.951s-281.951 126.483-281.951 281.951c0 92.95 45.221 175.529 114.809 226.921-51.762 21.555-99.308 53.26-140.151 94.102-82.080 82.080-127.284 191.213-127.284 307.292 0 32.174 26.082 58.254 58.254 58.254s58.254-26.080 58.254-58.254c0-175.385 142.685-318.068 318.068-318.068 175.385 0 318.068 142.685 318.068 318.068 0 32.174 26.080 58.254 58.254 58.254s58.254-26.080 58.254-58.254c0-116.081-45.204-225.211-127.284-307.292zM512 137.32c91.225 0 165.442 74.218 165.442 165.442s-74.218 165.442-165.442 165.442-165.442-74.218-165.442-165.442 74.218-165.442 165.442-165.442z" /></svg>

After

Width:  |  Height:  |  Size: 1001 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#2F80ED" d="M722.6 645.295c17.497-26.999 9.793-63.072-17.206-80.567-21.997-14.255-45.104-26.406-69.045-36.377 69.66-51.387 114.937-134.006 114.937-227.013 0-155.467-126.483-281.951-281.951-281.951s-281.951 126.483-281.951 281.951c0 92.95 45.223 175.527 114.811 226.919-51.763 21.557-99.309 53.261-140.153 94.103-82.080 82.080-127.284 191.213-127.284 307.292 0 32.174 26.082 58.254 58.254 58.254s58.254-26.080 58.254-58.254c0-175.385 142.685-318.068 318.068-318.068 61.579 0 121.298 17.606 172.699 50.915 27.002 17.498 63.074 9.793 80.567-17.206zM469.334 135.897c91.225 0 165.442 74.218 165.442 165.442s-74.218 165.442-165.442 165.442c-91.225 0-165.442-74.217-165.442-165.442s74.218-165.442 165.442-165.442zM926.63 743.197h-66.992v-66.992c0-32.174-26.080-58.254-58.254-58.254s-58.254 26.080-58.254 58.254v66.992h-66.992c-32.174 0-58.254 26.080-58.254 58.254s26.080 58.254 58.254 58.254h66.992v66.992c0 32.174 26.080 58.254 58.254 58.254s58.254-26.080 58.254-58.254v-66.992h66.992c32.174 0 58.254-26.080 58.254-58.254s-26.080-58.254-58.254-58.254z" /></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
app/resource/images/bg0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
app/resource/images/bg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

BIN
app/resource/images/bg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

BIN
app/resource/images/bg3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

BIN
app/resource/images/bg4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

BIN
app/resource/images/bg5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

103
app/resource/lang/en.json Normal file
View File

@@ -0,0 +1,103 @@
{
"你好": "Hello",
"我的文件": "My Files",
"存储配额": "Storage Quota",
"任务管理": "Task Management",
"应用信息": "Application Information",
"此页面正在建设中...": "This page is under construction...",
"语言设置:": "Language Settings:",
"中文": "Chinese",
"English": "English",
"上传": "Upload",
"设置": "Settings",
"确定": "OK",
"取消": "Cancel",
"关闭": "Close",
"刷新": "Refresh",
"删除": "Delete",
"重命名": "Rename",
"移动": "Move",
"复制": "Copy",
"分享": "Share",
"新建文件夹": "New Folder",
"文件信息": "File Info",
"大小": "Size",
"类型": "Type",
"修改时间": "Modified Time",
"创建时间": "Created Time",
"状态": "Status",
"进度": "Progress",
"速度": "Speed",
"剩余时间": "Remaining Time",
"暂停": "Pause",
"继续": "Resume",
"重试": "Retry",
"完成": "Completed",
"失败": "Failed",
"等待中": "Waiting",
"进行中": "In Progress",
"仓内搜索": "Search Within Folder",
"站内搜索": "Search Within Site",
"搜索文件": "Search Files",
"添加标签": "Add Tag",
"标签名称": "Tag Name",
"标签通配符": "Tag Expression",
"文件上传": "File Upload",
"文件下载": "File Download",
"用户组基础容量": "User Group Basic Capacity",
"有效容量包附加附加容量": "Valid Capacity Package Additional Capacity",
"已使用容量": "Used Capacity",
"总容量": "Total Capacity",
"LeonPan": "LeonPan",
"修改昵称": "Modify Nickname",
"用户信息": "User Information",
"修改头像": "Modify Avatar",
"用户头像": "User Avatar",
"点击修改头像": "Click to Modify Avatar",
"电子邮箱": "Email Address",
"当前用户组": "Current User Group",
"用户注册时间": "User Registration Time",
"修改成功": "Modification Successful",
"昵称修改成功": "Nickname Modification Successful",
"选择图片": "Select Image",
"头像修改成功": "Avatar Modification Successful",
"选择下载保存路径": "Select Download Save Path",
"选择文件夹": "Select Folder",
"下载保存路径修改成功": "Download Save Path Modification Successful",
"背景图片设置": "Background Image Settings",
"官方背景图": "Official Background Image",
"选择自定义背景": "Select Custom Background",
"图片背景透明度": "Image Background Opacity",
"设置图片背景透明度": "Set Image Background Opacity",
"透明度": "Opacity",
"透明度范围": "Opacity Range",
"选择自定义图片,选择后请不要更改图片位置": "Select Custom Image, Do Not Change Image Location After Selection",
"隐私协议": "Privacy Policy",
"用户协议": "User Agreement",
"官方预设背景图片": "Official Preset Background Images",
"选择背景图片": "Select Background Image",
"自定义背景图片": "Custom Background Image",
"选择保存路径": "Select Save Path",
"用户昵称": "User Nickname",
"更新设置": "Update Settings",
"检查更新": "Check for Updates",
"检查是否有新版本可用": "Check if new version is available",
"当前版本": "Current Version",
"发现新版本": "New Version Found",
"最新版本": "Latest Version",
"更新内容": "Update Content",
"立即更新": "Update Now",
"稍后更新": "Update Later",
"检查更新失败": "Failed to Check Updates",
"无法连接到更新服务器,请稍后再试。": "Failed to connect to update server, please try again later.",
"开启自动更新": "Enable Auto Update",
"在应用启动时自动检查更新": "Automatically check for updates when the application starts",
"已是最新版本": "Already Latest Version",
"语言设置": "Language Settings",
"下载": "download",
"预览": "preview",
"进入": "enter",
"刷新当前": "refresh current",
"上传文件": "upload file",
"设置存储策略": "Set Storage Strategy"
}

103
app/resource/lang/zh.json Normal file
View File

@@ -0,0 +1,103 @@
{
"你好": "你好",
"我的文件": "我的文件",
"存储配额": "存储配额",
"任务管理": "任务管理",
"应用信息": "应用信息",
"此页面正在建设中...": "此页面正在建设中...",
"语言设置:": "语言设置:",
"中文": "中文",
"English": "English",
"上传": "上传",
"设置": "设置",
"确定": "确定",
"取消": "取消",
"关闭": "关闭",
"刷新": "刷新",
"删除": "删除",
"重命名": "重命名",
"移动": "移动",
"复制": "复制",
"分享": "分享",
"新建文件夹": "新建文件夹",
"文件信息": "文件信息",
"大小": "大小",
"类型": "类型",
"修改时间": "修改时间",
"创建时间": "创建时间",
"状态": "状态",
"进度": "进度",
"速度": "速度",
"剩余时间": "剩余时间",
"暂停": "暂停",
"继续": "继续",
"重试": "重试",
"完成": "完成",
"失败": "失败",
"等待中": "等待中",
"进行中": "进行中",
"仓内搜索": "仓内搜索",
"站内搜索": "站内搜索",
"搜索文件": "搜索文件",
"添加标签": "添加标签",
"标签名称": "标签名称",
"标签通配符": "标签通配符",
"文件上传": "文件上传",
"文件下载": "文件下载",
"用户组基础容量": "用户组基础容量",
"有效容量包附加附加容量": "有效容量包附加附加容量",
"已使用容量": "已使用容量",
"总容量": "总容量",
"LeonPan": "LeonPan",
"修改昵称": "修改昵称",
"用户信息": "用户信息",
"修改头像": "修改头像",
"用户头像": "用户头像",
"点击修改头像": "点击修改头像",
"电子邮箱": "电子邮箱",
"当前用户组": "当前用户组",
"用户注册时间": "用户注册时间",
"修改成功": "修改成功",
"昵称修改成功": "昵称修改成功",
"选择图片": "选择图片",
"头像修改成功": "头像修改成功",
"选择下载保存路径": "选择下载保存路径",
"选择文件夹": "选择文件夹",
"下载保存路径修改成功": "下载保存路径修改成功",
"背景图片设置": "背景图片设置",
"官方背景图": "官方背景图",
"选择自定义背景": "选择自定义背景",
"图片背景透明度": "图片背景透明度",
"设置图片背景透明度": "设置图片背景透明度",
"透明度": "透明度",
"透明度范围": "透明度范围",
"选择自定义图片,选择后请不要更改图片位置": "选择自定义图片,选择后请不要更改图片位置",
"隐私协议": "隐私协议",
"用户协议": "用户协议",
"官方预设背景图片": "官方预设背景图片",
"选择背景图片": "选择背景图片",
"自定义背景图片": "自定义背景图片",
"选择保存路径": "选择保存路径",
"用户昵称": "用户昵称",
"更新设置": "更新设置",
"检查更新": "检查更新",
"检查是否有新版本可用": "检查是否有新版本可用",
"当前版本": "当前版本",
"发现新版本": "发现新版本",
"最新版本": "最新版本",
"更新内容": "更新内容",
"立即更新": "立即更新",
"稍后更新": "稍后更新",
"检查更新失败": "检查更新失败",
"无法连接到更新服务器,请稍后再试。": "无法连接到更新服务器,请稍后再试。",
"开启自动更新": "开启自动更新",
"在应用启动时自动检查更新": "在应用启动时自动检查更新",
"已是最新版本": "已是最新版本",
"语言设置": "语言设置",
"下载": "下载",
"预览": "预览",
"进入": "进入",
"刷新当前": "刷新当前",
"上传文件": "上传文件",
"设置存储策略": "设置存储策略"
}

334284
app/resource/resource.py Normal file

File diff suppressed because it is too large Load Diff

47
app/resource/resource.qrc Normal file
View File

@@ -0,0 +1,47 @@
<RCC>
<qresource prefix="/app">
<file>images/logo.png</file>
<file>images/background.png</file>
<file>images/load.png</file>
<file>images/error.png</file>
<file>images/empty.png</file>
<file>images/title.jpg</file>
<file>images/loadFailure.png</file>
<file>icons/login.svg</file>
<file>icons/register.svg</file>
<file>icons/3D.svg</file>
<file>icons/Config.svg</file>
<file>icons/Database.svg</file>
<file>icons/Excel.svg</file>
<file>icons/Exe.svg</file>
<file>icons/Folder.svg</file>
<file>icons/Gif.svg</file>
<file>icons/Image.svg</file>
<file>icons/music.svg</file>
<file>icons/None.svg</file>
<file>icons/Pdf.svg</file>
<file>icons/PPT.svg</file>
<file>icons/Programme.svg</file>
<file>icons/Txt.svg</file>
<file>icons/Video.svg</file>
<file>icons/Word.svg</file>
<file>icons/WPS.svg</file>
<file>icons/Zip.svg</file>
<file>icons/Score.svg</file>
<file>icons/Date.svg</file>
<file>icons/Email.svg</file>
<file>icons/Group.svg</file>
<file>icons/Nickname.svg</file>
<file>icons/BgImage.svg</file>
<file>icons/Opacity.svg</file>
<file>icons/SavePath.svg</file>
<file>icons/Myfile.svg</file>
<file>icons/Task.svg</file>
<file>icons/Storage.svg</file>
<file>icons/Info.svg</file>
<file>icons/Application.svg</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,300 @@
# coding: utf-8
import os
import sys
import requests
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtWidgets import (
QLabel,
QMessageBox,
QSizePolicy,
QSpacerItem,
QVBoxLayout,
QWidget,
)
from qfluentwidgets import (ComboBoxSettingCard, FluentIcon, ImageLabel, MessageBox, PrimaryPushSettingCard,
SettingCardGroup, SwitchSettingCard)
from app.core import cfg, lang, qconfig, signalBus
from app.core.utils.version import version
class AppInfoInterface(QWidget):
"""
APP信息页面
包含语言切换功能
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("appInfoInterface")
self.initUI()
self.connectSignalToSlot()
# 移除初始化时的自动检查,改为由登录成功信号触发
def initUI(self):
# 创建主布局
mainLayout = QVBoxLayout(self)
mainLayout.setContentsMargins(30, 30, 30, 30)
mainLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.titleImageLabel = ImageLabel(":app/images/title.jpg", self)
self.titleImageLabel.scaledToHeight(130)
mainLayout.addWidget(self.titleImageLabel, 0, Qt.AlignmentFlag.AlignHCenter)
# 添加标题
self.titleLabel = QLabel(lang("应用信息"))
self.titleLabel.setStyleSheet("QLabel { font-size: 24px; font-weight: bold; }")
mainLayout.addWidget(self.titleLabel)
# 创建设置卡组
self.languageGroup = SettingCardGroup(lang("语言设置"), self)
# 语言选择设置卡
self.languageCard = ComboBoxSettingCard(
title=lang("语言设置"),
icon=FluentIcon.LANGUAGE,
texts=["中文", "English"],
configItem=cfg.language,
parent=self.languageGroup,
)
# 将设置卡添加到组
self.languageGroup.addSettingCard(self.languageCard)
# 将设置卡组添加到主布局
mainLayout.addWidget(self.languageGroup)
# 创建更新设置卡组
self.updateGroup = SettingCardGroup(lang("更新设置"), self)
# 自动更新设置开关
self.autoUpdateSwitch = SwitchSettingCard(
title=lang("开启自动更新"),
icon=FluentIcon.UPDATE,
configItem=cfg.checkUpdateAtStartUp,
parent=self.updateGroup,
)
# 手动检查更新设置卡
self.checkUpdateCard = PrimaryPushSettingCard(
title=lang("检查更新"),
text=lang("检查是否有新版本可用"),
icon=FluentIcon.UPDATE,
parent=self.updateGroup,
)
# 当前版本信息
self.versionLabel = QLabel(f"{lang('当前版本')}: {version}")
self.versionLabel.setStyleSheet(
"QLabel { font-size: 14px; color: #666; margin-top: 10px; margin-left: 10px; }"
)
# 将设置卡添加到组
self.updateGroup.addSettingCard(self.autoUpdateSwitch)
self.updateGroup.addSettingCard(self.checkUpdateCard)
self.updateGroup.layout().addWidget(self.versionLabel)
# 将更新设置卡组添加到主布局
mainLayout.addWidget(self.updateGroup)
# 添加空白占位符
spacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
mainLayout.addItem(spacer)
# 底部空间
bottomSpacer = QSpacerItem(20, 100, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
mainLayout.addItem(bottomSpacer)
def connectSignalToSlot(self):
# 连接语言变更信号
signalBus.languageChanged.connect(self.updateUI)
# 连接ComboBox的当前文本变更信号
self.languageCard.comboBox.currentTextChanged.connect(self.onLanguageChanged)
# 连接检查更新按钮信号
self.checkUpdateCard.clicked.connect(self.manualCheckUpdate)
# 自动更新开关的信号已通过configItem自动连接无需额外处理
# 连接登录成功信号,在用户登录后执行自动检查更新
signalBus.loginSuccessSignal.connect(self.onLoginSuccess)
def checkUpdate(self):
"""检查应用更新"""
try:
# 发送请求获取应用信息
url = "https://leon.miaostars.com/api.php?t=getappinfo&id=23"
response = requests.get(url, timeout=10)
response.raise_for_status()
# 解析JSON响应
data = response.json()
if data.get("status") == "success":
app_data = data.get("data", {})
versions = app_data.get("versions", [])
if versions:
# 获取最新版本
latest_version = versions[0].get("version", "")
# 比较版本号 - 只要版本不同就提示更新
if latest_version and latest_version != version:
# 有新版本
changelog = versions[0].get("changelog", "")
api_file_path = versions[0].get("file_path", "")
# 确保使用完整的URL如果路径不包含协议则添加域名前缀
if api_file_path:
# 检查是否是完整的URL
if not (
api_file_path.startswith("http://")
or api_file_path.startswith("https://")
):
# 添加域名前缀,确保链接完整
file_path = (
f"https://leon.miaostars.com/{api_file_path}"
)
else:
file_path = api_file_path
else:
# 使用默认下载链接
file_path = "https://leon.miaostars.com/app.php?id=23"
# 使用QFluentWidgets的MessageBox提示用户更新
msg_box = MessageBox(
lang("发现新版本"),
f"{lang('当前版本')}: {version}\n{lang('最新版本')}: {latest_version}\n\n{lang('更新内容')}:\n{changelog}",
self,
)
msg_box.yesButton.setText(lang("立即更新"))
msg_box.cancelButton.setText(lang("稍后更新"))
# QFluentWidgets的MessageBox.exec()返回True表示用户点击了确认按钮
if msg_box.exec():
# 添加下载更新的逻辑
# 例如:打开浏览器访问下载链接
if file_path:
import webbrowser
webbrowser.open(file_path)
return True
# 没有新版本或请求失败
return False
except Exception as e:
print(f"检查更新失败: {e}")
# 如果是手动检查更新,则显示错误提示
if hasattr(self, "is_manual_check") and self.is_manual_check:
error_box = MessageBox(
lang("检查更新失败"),
f"{lang('无法连接到更新服务器,请稍后再试。')}\n{str(e)}",
self,
)
error_box.cancelButton.setVisible(False)
error_box.exec()
return False
def manualCheckUpdate(self):
"""手动检查更新"""
self.is_manual_check = True
has_update = self.checkUpdate()
if not has_update and self.is_manual_check:
# 如果是手动检查且没有更新
no_update_box = MessageBox(
lang("已是最新版本"),
f"{lang('当前版本')} {version} {lang('已是最新版本。')}",
self,
)
no_update_box.cancelButton.setVisible(False)
no_update_box.exec()
self.is_manual_check = False
def autoCheckUpdate(self):
"""自动检查更新"""
print(f"自动检查更新已触发,配置状态: {cfg.checkUpdateAtStartUp.value}")
# 在单独的线程中执行避免阻塞UI
class UpdateCheckThread(QThread):
update_available = pyqtSignal(bool)
def run(self):
try:
print("开始检查更新...")
url = "https://leon.miaostars.com/api.php?t=getappinfo&id=23"
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
if data.get("status") == "success":
app_data = data.get("data", {})
versions = app_data.get("versions", [])
if versions:
latest_version = versions[0].get("version", "")
print(f"当前版本: {version}, 最新版本: {latest_version}")
if latest_version and latest_version != version:
print("发现新版本,准备显示更新提示")
self.update_available.emit(True)
return
except Exception as e:
print(f"自动检查更新出错: {e}")
self.update_available.emit(False)
# 创建并启动线程
self.update_thread = UpdateCheckThread()
self.update_thread.update_available.connect(self.onAutoUpdateAvailable)
self.update_thread.start()
def onAutoUpdateAvailable(self, available):
"""自动检查更新结果处理"""
print(f"自动检查更新结果: {'有更新' if available else '无更新'}")
if available:
# 自动检查到更新时再次调用checkUpdate显示提示
self.is_manual_check = False
self.checkUpdate()
def onLoginSuccess(self):
"""用户登录成功后的处理"""
print(f"用户登录成功,检查是否开启自动更新: {cfg.checkUpdateAtStartUp.value}")
# 在用户登录成功后,根据配置决定是否自动检查更新
if cfg.checkUpdateAtStartUp.value:
self.autoCheckUpdate()
def onLanguageChanged(self, text):
# 语言变更处理
# 从选项映射获取对应的语言代码
lang_map = {"中文": "zh", "English": "en"}
lang_code = lang_map.get(text, "zh")
# 保存到配置
qconfig.set(cfg.language, lang_code)
# 显示重启提示
reply = QMessageBox.question(
self,
lang("语言变更"),
lang("语言已变更,是否立即重启应用以应用新语言?"),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
)
if reply == QMessageBox.Yes:
# 重启应用
self.restartApplication()
def updateUI(self):
# 更新UI文本
self.titleLabel.setText(lang("应用信息"))
self.hintLabel.setText(lang("此页面正在建设中..."))
# 注意SettingCardGroup可能没有setTitle方法需要根据实际API调整
def restartApplication(self):
"""重启应用程序"""
# 保存配置
qconfig.save()
# 获取当前Python解释器路径和脚本路径
python = sys.executable
script = os.path.abspath(sys.argv[0])
# 退出当前进程
sys.exit()
# 注意在实际应用中这里应该使用subprocess重新启动应用但为了安全考虑
# 这里仅退出当前进程,让用户手动重启

View File

@@ -0,0 +1,49 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout
from qfluentwidgets import CardWidget, ImageLabel, SubtitleLabel
class EmptyCard(CardWidget):
def __init__(self, parent=None, text=None):
super().__init__(parent=parent)
self.setMinimumWidth(200)
self.setBorderRadius(10)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.iconLabel = ImageLabel(self)
self.iconLabel.setImage(":app/images/empty.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel = SubtitleLabel(self)
self.titleLabel.setText(text)
self.titleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.vBoxLayout.addWidget(self.iconLabel, 0, Qt.AlignmentFlag.AlignHCenter)
self.vBoxLayout.addWidget(self.titleLabel)
def setText(self, text):
self.titleLabel.setText(text)
self.update()
def load(self):
self.iconLabel.setImage(":app/images/load.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel.setText("加载中...")
def error(self):
self.iconLabel.setImage(":app/images/error.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel.setText("加载失败,请重试")
def empty(self):
self.iconLabel.setImage(":app/images/empty.png")
self.iconLabel.scaledToHeight(130)
self.iconLabel.scaledToWidth(130)
self.titleLabel.setText("这里空空如也")

View File

@@ -0,0 +1,362 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QHBoxLayout
from qfluentwidgets import Action, BodyLabel, CardWidget, ImageLabel, InfoBar, InfoBarPosition, MenuAnimationType, \
MessageBox, PushButton, RoundMenu, StrongBodyLabel
from qfluentwidgets import FluentIcon as FIF
from app.core import (DeleteFileThread, formatDate, formatSize, getFileIcon, lang, signalBus)
from app.view.widgets.share_file_messageBox import ShareFileMessageBox
class FileCard(CardWidget):
def __init__(self, _id, fileName, fileType, path, date, size, parent=None):
super().__init__(parent)
self._id = _id
self.fileName = fileName
self.fileSize = size
self.changeTime = date
self.fileType = fileType
self.filePath = path
self.setFixedHeight(50)
self.iconLabel = ImageLabel(self)
self.fileNameLabel = StrongBodyLabel(self.fileName, self)
self.fileSizeLabel = BodyLabel(formatSize(self.fileSize), self)
self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.iconLabel)
self.hBoxLayout.addWidget(self.fileNameLabel)
self.hBoxLayout.addWidget(self.fileSizeLabel)
self.hBoxLayout.addWidget(self.changeTimeLabel)
self.hBoxLayout.setStretch(0, 1)
self.hBoxLayout.setStretch(1, 2)
self.hBoxLayout.setStretch(2, 1)
self.hBoxLayout.setContentsMargins(10, 10, 10, 10)
self.hBoxLayout.setSpacing(10)
self.loadIcon()
if self.fileType == "dir":
self.clicked.connect(self.dirClicked)
self.suffix = fileName.split(".")[-1].lower()
def mousePressEvent(self, event):
"""重写鼠标点击事件,区分左右键"""
if event.button() == Qt.MouseButton.RightButton:
# 使用globalPosition()获取全局位置并转换为适合菜单显示的坐标
global_pos = event.globalPosition().toPoint()
if self.fileType == "file":
self.showFileContextMenu(global_pos)
else:
self.showFolderContextMenu(global_pos)
else:
super().mousePressEvent(event)
def loadIcon(self):
icon_name = getFileIcon(self.fileType, self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(25)
self.iconLabel.scaledToWidth(25)
def dirClicked(self):
if self.filePath == "/":
paths = "/" + self.fileName
else:
paths = f"{self.filePath}/{self.fileName}"
signalBus.dirOpenSignal.emit(paths)
def selfPreview(self):
if self.fileType == "file" and self.suffix in [
"jpg",
"png",
"jpeg",
"bmp",
"gif",
]:
signalBus.imagePreviewSignal.emit(self._id)
if self.fileType == "file" and self.suffix in ["txt", "py", "md"]:
signalBus.txtPreviewSignal.emit(self._id)
def downloadFile(self):
if self.fileType == "file":
# 构建Cloudreve V4 API所需的正确路径格式
# 确保不会出现重复的前缀和文件名
if self.filePath == "/":
# 根目录情况
full_path = f"cloudreve://my/{self.fileName}"
else:
# 子目录情况,确保正确拼接路径
# 清理路径,避免重复的斜杠
clean_path = self.filePath.lstrip("/")
full_path = f"cloudreve://my/{clean_path}/{self.fileName}"
# 确保路径格式正确,没有重复的部分
full_path = full_path.replace("cloudreve://my/cloudreve://my", "cloudreve://my")
# 确保没有重复的文件名
if f"/{self.fileName}/{self.fileName}" in full_path:
full_path = full_path.replace(f"/{self.fileName}/{self.fileName}", f"/{self.fileName}")
signalBus.addDownloadFileTask.emit(
f"own.{self.suffix}", self.fileName, full_path
)
def showFileContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(
FIF.DOWNLOAD, lang("下载"), triggered=lambda: self.downloadFile()
),
Action(
FIF.PROJECTOR, lang("预览"), triggered=lambda: self.selfPreview()
),
]
)
menu.addSeparator()
menu.addActions(
[
Action(FIF.DELETE, lang("删除"), triggered=lambda: self.deleteSelf()),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)
def showFolderContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(FIF.DOWNLOAD, lang("进入"), triggered=lambda: self.dirClicked()),
]
)
menu.addSeparator()
menu.addActions(
[
Action(FIF.DELETE, lang("删除"), triggered=lambda: self.deleteSelf()),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)
def deleteSelf(self):
w = MessageBox(
"确认删除",
f"你确定要删除{self.fileName}吗?\n删除后不可恢复噢!",
parent=self.window(),
)
if w.exec():
self.deleteThread = DeleteFileThread(self._id, self.fileType)
self.deleteThread.successDelete.connect(self.deleteSuccess)
self.deleteThread.errorDelete.connect(self.deleteError)
self.deleteThread.start()
else:
InfoBar.info(
"提示",
"删除已取消",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def deleteSuccess(self):
InfoBar.success(
"成功",
"成功删除",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
signalBus.refreshFolderListSignal.emit()
def deleteError(self, error_msg):
InfoBar.error(
"失败",
"删除失败",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
logger.error(f"删除文件失败:{error_msg}")
def contextMenuEvent(self, e):
"""重写上下文菜单事件,确保只有右键点击才会触发"""
pass
class ShareFileCard(CardWidget):
def __init__(self, data, parent=None):
super().__init__(parent)
self._id = data["key"]
self.fileName = data["source"]["name"]
self.fileSize = data["source"]["size"]
self.changeTime = data["create_date"]
self.fileType = "dir" if data["is_dir"] else "file"
self.preview = data["preview"]
self.passWord = data["password"]
self.remainDownloads = data["remain_downloads"]
self.downloads = data["downloads"]
self.score = data["score"]
self.views = data["views"]
self.expireTime = data["expire"]
self.setFixedHeight(50)
self.iconLabel = ImageLabel(self)
self.fileNameLabel = StrongBodyLabel(self.fileName, self)
self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self)
self.viewButton = PushButton("查看", self)
self.viewButton.clicked.connect(self.viewFile)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.iconLabel)
self.hBoxLayout.addWidget(self.fileNameLabel)
self.hBoxLayout.addWidget(self.changeTimeLabel)
self.hBoxLayout.addWidget(self.viewButton)
self.hBoxLayout.setStretch(0, 1)
self.hBoxLayout.setStretch(1, 2)
self.hBoxLayout.setStretch(2, 1)
self.hBoxLayout.setContentsMargins(10, 10, 10, 10)
self.hBoxLayout.setSpacing(10)
self.loadIcon()
self.suffix = self.fileName.split(".")[-1].lower()
def loadIcon(self):
icon_name = getFileIcon(self.fileType, self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(25)
self.iconLabel.scaledToWidth(25)
def viewFile(self):
if self.fileType == "file":
w = ShareFileMessageBox(
self._id, self.iconLabel.pixmap(), self.suffix, self.window()
)
if w.exec():
...
else:
signalBus.shareFolderViewSignal.emit(self._id)
class SharedFolderFileCard(CardWidget):
shareFileDownloadSignal = pyqtSignal() # 共享文件下载信号
def __init__(self, key, _id, fileName, fileType, path, date, size, parent=None):
super().__init__(parent)
self._id = _id
self.key = key
self.fileName = fileName
self.fileSize = size
self.changeTime = date
self.fileType = fileType
self.filePath = path
self.setFixedHeight(50)
self.iconLabel = ImageLabel(self)
self.fileNameLabel = StrongBodyLabel(self.fileName, self)
self.fileSizeLabel = BodyLabel(formatSize(self.fileSize), self)
self.changeTimeLabel = BodyLabel(formatDate(self.changeTime), self)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.iconLabel)
self.hBoxLayout.addWidget(self.fileNameLabel)
self.hBoxLayout.addWidget(self.fileSizeLabel)
self.hBoxLayout.addWidget(self.changeTimeLabel)
self.hBoxLayout.setStretch(0, 1)
self.hBoxLayout.setStretch(1, 2)
self.hBoxLayout.setStretch(2, 1)
self.hBoxLayout.setContentsMargins(10, 10, 10, 10)
self.hBoxLayout.setSpacing(10)
self.loadIcon()
self.suffix = self.fileName.split(".")[-1].lower()
if self.fileType == "dir":
# 连接左键点击信号
self.clicked.connect(self.dirClicked)
def mousePressEvent(self, event):
"""重写鼠标点击事件,区分左右键"""
if event.button() == Qt.MouseButton.RightButton:
# 右键点击,显示上下文菜单
if self.fileType == "file":
self.showFileContextMenu(event.globalPos())
else:
self.showFolderContextMenu(event.globalPos())
else:
# 左键或其他按钮点击,调用父类处理
super().mousePressEvent(event)
def loadIcon(self):
icon_name = getFileIcon(self.fileType, self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(25)
self.iconLabel.scaledToWidth(25)
def dirClicked(self):
if self.filePath == "/":
paths = "/" + self.fileName
else:
paths = f"{self.filePath}/{self.fileName}"
signalBus.shareDirOpenSignal.emit(paths)
def downloadFile(self):
if self.fileType == "file":
signalBus.addDownloadFileTask.emit(
f"share.{self.suffix}",
self.fileName,
f"{self.filePath}/{self.fileName}.{self.key}",
)
signalBus.shareFileDownloadSignal.emit()
def showFileContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(
FIF.DOWNLOAD, lang("下载"), triggered=lambda: self.downloadFile()
),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)
def showFolderContextMenu(self, pos):
"""显示上下文菜单"""
menu = RoundMenu(parent=self)
menu.addActions(
[
Action(FIF.DOWNLOAD, lang("进入"), triggered=lambda: self.dirClicked()),
]
)
menu.exec(pos, aniType=MenuAnimationType.DROP_DOWN)

View File

@@ -0,0 +1,263 @@
# coding: utf-8
import os
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout
from qfluentwidgets import (BodyLabel, CardWidget, FluentIcon, ImageLabel, InfoBar, InfoBarPosition, PrimaryToolButton,
ProgressBar, SubtitleLabel)
from app.core import (DownloadShareThread, DownloadThread, formatSize, getFileIcon, signalBus, UploadThread)
class UploadCard(CardWidget):
def __init__(self, fileType, filePath, parent=None):
super().__init__(parent=parent)
self.fileType = fileType
self.filePath = filePath
self.fileName = os.path.basename(filePath)
self.setFixedHeight(75)
self.hBoxLayout = QHBoxLayout(self)
self.infomationLayout = QVBoxLayout()
self.iconLabel = ImageLabel(self)
self.fileNameLabel = SubtitleLabel(self.fileName, self)
self.currentStatusLabel = BodyLabel("等待中...", self)
self.progressBar = ProgressBar(self)
self.progressBar.setValue(0)
self.cancelButton = PrimaryToolButton(FluentIcon.DELETE, self)
self.retryButton = PrimaryToolButton(FluentIcon.RETURN, self)
self.retryButton.clicked.connect(self.retryUpload)
self.cancelButton.clicked.connect(self.cancelUpload)
self.hBoxLayout.addWidget(
self.iconLabel, 0, Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignLeft
)
self.hBoxLayout.addSpacing(5)
self.infomationLayout.addWidget(
self.fileNameLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(
self.currentStatusLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(self.progressBar)
self.hBoxLayout.addLayout(self.infomationLayout)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(
self.retryButton,
0,
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
)
self.hBoxLayout.addWidget(
self.cancelButton,
0,
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
)
self.setIcon()
self.uploadThread = None
self.startUpload()
def startUpload(self):
self.retryButton.setEnabled(False)
self.uploadThread = UploadThread(self.filePath)
self.uploadThread.uploadApplicationApprovedSignal.connect(
self.uploadApplication
)
self.uploadThread.uploadFinished.connect(self.uploadFinished)
self.uploadThread.uploadFailed.connect(self.uploadFailed)
self.uploadThread.uploadProgress.connect(self.uploadProgress)
self.uploadThread.start()
def retryUpload(self):
self.currentStatusLabel.setText("重试...")
self.startUpload()
def cancelUpload(self):
if self.uploadThread:
self.progressBar.setValue(0)
self.selfDelete()
else:
self.selfDelete()
def selfDelete(self):
self.currentStatusLabel.setText("取消中...")
self.cancelButton.setEnabled(False)
self.retryButton.setEnabled(False)
if self.uploadThread:
self.uploadThread.cancelUpload()
self.uploadThread.terminate()
self.uploadThread = None
QTimer.singleShot(1000, self.deleteLater)
def uploadApplication(self):
self.currentStatusLabel.setText("已向服务器提交任务,读取文件中...")
def uploadFinished(self):
self.currentStatusLabel.setText("上传成功")
InfoBar.success(
"成功",
f"{self.fileName}上传成功,请注意查看~",
Qt.Orientation.Horizontal,
True,
5000,
InfoBarPosition.BOTTOM_RIGHT,
InfoBar.desktopView(),
)
signalBus.refreshFolderListSignal.emit()
self.retryButton.setEnabled(False)
self.progressBar.setValue(100)
def uploadProgress(self, progress, uploaded_size, total_size):
self.progressBar.setValue(int(progress))
self.currentStatusLabel.setText(
f"上传中...({formatSize(uploaded_size)}/{formatSize(total_size)})"
)
def uploadFailed(self, error_message):
self.currentStatusLabel.setText(f"上传失败:{error_message}")
self.progressBar.setValue(0)
self.retryButton.setEnabled(True)
self.uploadThread.terminate()
self.uploadThread = None
def setIcon(self):
icon_name = getFileIcon("file", self.fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(50)
self.iconLabel.scaledToWidth(50)
class DownloadCard(CardWidget):
def __init__(self, suffix, fileName, _id, parent=None):
super().__init__(parent=parent)
self._id = _id
self.suffix = suffix
self.setFixedHeight(75)
self.hBoxLayout = QHBoxLayout(self)
self.infomationLayout = QVBoxLayout()
self.iconLabel = ImageLabel(self)
self.fileNameLabel = SubtitleLabel(fileName, self)
self.currentStatusLabel = BodyLabel("请求中...", self)
self.progressBar = ProgressBar(self)
self.progressBar.setValue(0)
self.cancelButton = PrimaryToolButton(FluentIcon.DELETE, self)
self.cancelButton.clicked.connect(self.cancelDownload)
self.hBoxLayout.addWidget(
self.iconLabel, 0, Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignLeft
)
self.hBoxLayout.addSpacing(5)
self.infomationLayout.addWidget(
self.fileNameLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(
self.currentStatusLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
self.infomationLayout.addWidget(self.progressBar)
self.hBoxLayout.addLayout(self.infomationLayout)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(
self.cancelButton,
0,
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
)
suffix = self.suffix.split(".")[1].lower()
self._type = self.suffix.split(".")[0].lower()
self.setIcon(suffix)
self.downloadThread = None
self.startUpload()
def startUpload(self):
if self._type == "own":
# 同时传递file_id和file_path确保file_path不为空
# 对于own类型使用self._id作为file_path因为它已经包含了完整的cloudreve://格式路径
self.downloadThread = DownloadThread(self._id, self._id)
elif self._type == "share":
self.downloadThread = DownloadShareThread(self._id)
self.downloadThread.downloadUrlAcquired.connect(self.downloadUrlAcquired)
self.downloadThread.downloadFinished.connect(self.downloadFinished)
self.downloadThread.downloadFailed.connect(self.downloadFailed)
self.downloadThread.downloadProgress.connect(self.downloadProgress)
self.downloadThread.start()
def cancelDownload(self):
if self.downloadThread:
self.progressBar.setValue(0)
self.selfDelete()
else:
self.selfDelete()
def selfDelete(self):
self.currentStatusLabel.setText("取消中...")
self.cancelButton.setEnabled(False)
if self.downloadThread:
self.downloadThread.cancelDownload()
self.downloadThread.terminate()
self.downloadThread = None
QTimer.singleShot(1000, self.deleteLater)
def downloadUrlAcquired(self):
self.currentStatusLabel.setText("成功获取下载链接,准备下载...")
def downloadFinished(self):
self.currentStatusLabel.setText("下载成功")
InfoBar.success(
"成功",
f"{self._id}下载成功,请注意查看~",
Qt.Orientation.Horizontal,
True,
5000,
InfoBarPosition.BOTTOM_RIGHT,
InfoBar.desktopView(),
)
self.progressBar.setValue(100)
def downloadProgress(self, progress, uploaded_size, total_size):
self.progressBar.setValue(int(progress))
self.currentStatusLabel.setText(
f"下载中...({formatSize(uploaded_size)}/{formatSize(total_size)})"
)
def downloadFailed(self, error_message):
self.currentStatusLabel.setText(f"下载失败:{error_message}")
self.progressBar.setValue(0)
self.downloadThread.terminate()
self.downloadThread = None
def setIcon(self, fileName):
icon_name = getFileIcon("file", fileName)
self.iconLabel.setImage(QPixmap(f":app/icons/{icon_name}"))
self.iconLabel.scaledToHeight(50)
self.iconLabel.scaledToWidth(50)

View File

@@ -0,0 +1,37 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout
from qfluentwidgets import BodyLabel, ElevatedCardWidget, SubtitleLabel
class GbInformationCard(ElevatedCardWidget):
def __init__(self, amount,station,parent=None):
super().__init__(parent=parent)
self.currentAmountLabel = SubtitleLabel(self)
self.currentAmountLabel.setText(self.formatSize(amount))
self.currentAmountLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.stationLabel = BodyLabel(station,self)
self.stationLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setContentsMargins(2,5,2,5)
self.vBoxLayout.addWidget(self.currentAmountLabel,0,Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addSpacing(0)
self.vBoxLayout.addWidget(self.stationLabel,0,Qt.AlignmentFlag.AlignTop)
@staticmethod
def formatSize(size):
"""格式化文件大小"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} PB"
def updateValue(self,value):
self.currentAmountLabel.setText(self.formatSize(value))

View File

@@ -0,0 +1,362 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import QFileDialog, QVBoxLayout, QWidget
from qfluentwidgets import (
Action,
InfoBar,
InfoBarPosition,
MenuAnimationType,
RoundMenu,
ScrollArea,
)
from qfluentwidgets import FluentIcon as FIF
from app.core import (lang, ListFileThread, ListSearchThread, ListShareThread, policyConfig, signalBus)
from app.view.components.file_card import FileCard, ShareFileCard
from app.view.widgets.new_folder_messageBox import NewFolderMessageBox
from app.view.widgets.policy_messageBox import PolicyChooseMessageBox
class LinkageSwitchingBase(ScrollArea):
"""文件卡片滚动区域组件基"""
def __init__(self, parent=None):
super().__init__(parent)
self.fileCardsDict = {} # 存储所有文件卡片
self.widgets = QWidget()
self.layouts = QVBoxLayout(self.widgets)
self.layouts.setAlignment(Qt.AlignmentFlag.AlignTop)
self.setWidget(self.widgets)
self.setWidgetResizable(True)
self.layouts.setContentsMargins(5, 5, 5, 0)
self.layouts.setSpacing(5)
self.widgets.setStyleSheet("background-color: transparent; border: none;")
self.setStyleSheet("background-color: transparent; border: none;")
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
def addFileCard(self, fileId, data):
"""
添加文件卡片
Args:
fileId: 文件的唯一标识符
data: 文件数据对象
Returns:
创建的文件卡片对象
"""
if fileId in self.fileCardsDict:
logger.warning(f"文件卡片已存在: {fileId}")
return self.fileCardsDict[fileId]
# 安全地获取对象属性提供默认值以避免KeyError
fileId = data.get("id", "")
fileName = data.get("name", "未知文件")
# Cloudreve V4 API使用数字1表示文件夹0表示文件
fileType_num = data.get("type", 0)
# 将数字类型转换为字符串表示
fileType = "folder" if fileType_num == 1 else "file"
filePath = data.get("path", "")
# 使用created_at或updated_at作为日期
fileDate = data.get("created_at", data.get("date", ""))
fileSize = data.get("size", 0)
fileCard = FileCard(
fileId,
fileName,
fileType,
filePath,
fileDate,
fileSize,
self,
)
fileCard.setObjectName(f"fileCard_{fileId}")
self.fileCardsDict[fileId] = fileCard
self.layouts.addWidget(fileCard)
return fileCard
def removeFileCard(self, fileId):
"""移除文件卡片"""
if fileId in self.fileCardsDict:
fileCard = self.fileCardsDict[fileId]
self.layouts.removeWidget(fileCard)
fileCard.deleteLater()
del self.fileCardsDict[fileId]
else:
logger.warning(f"尝试移除不存在的文件卡片: {fileId}")
def clearFileCards(self):
"""清除所有文件卡片"""
logger.debug("清除所有文件卡片")
fileIds = list(self.fileCardsDict.keys())
for fileId in fileIds:
self.removeFileCard(fileId)
def refreshFolderList(self):
logger.debug("刷新文件夹列表")
InfoBar.success(
"成功",
"刷新成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
signalBus.refreshFolderListSignal.emit()
# 个人文件浏览区域
class OwnFileLinkageSwitching(LinkageSwitchingBase):
"""个人件卡片滚动区域组件"""
def __init__(self, paths, parent=None):
super(OwnFileLinkageSwitching, self).__init__(parent)
self.currentPath = paths
self.fileCardsDict = {} # 存储所有文件卡片
self.loadDict("/")
def contextMenuEvent(self, e):
"""菜单事件"""
logger.debug("触发上下文菜单事件")
menu = RoundMenu(parent=self)
menu.addAction(
Action(FIF.SYNC, lang("刷新当前"), triggered=self.refreshFolderList)
)
menu.addSeparator()
menu.addAction(
Action(FIF.ADD, lang("新建文件夹"), triggered=self._createFolder)
)
menu.addSeparator()
menu.addAction(Action(FIF.UP, lang("上传文件"), triggered=self._uploadFile))
menu.addSeparator()
menu.addAction(
Action(FIF.CLOUD, lang("设置存储策略"), triggered=self._choosePolicy)
)
menu.exec(e.globalPos(), aniType=MenuAnimationType.DROP_DOWN)
def _choosePolicy(self):
w = PolicyChooseMessageBox(self.window())
if w.exec():
...
def _createFolder(self):
w = NewFolderMessageBox(self.window())
if w.exec():
...
def _uploadFile(self):
file_name, _ = QFileDialog.getOpenFileName(
self.window(), "选择文件", "", "所有文件 (*)"
)
if file_name:
signalBus.addUploadFileTask.emit(file_name)
def loadDict(self, paths):
"""加载目录数据"""
logger.info(f"加载目录数据: {paths}")
policyConfig.setCurrentPath(paths)
self.currentPath = paths
self.loadDataThread = ListFileThread(paths)
self.loadDataThread.listDictSignal.connect(self.dealData)
self.loadDataThread.errorSignal.connect(self._errorLoadDict)
self.loadDataThread.start()
def dealData(self, data):
"""处理目录数据"""
self.clearFileCards()
logger.info("设置当前页策略")
# 安全地访问策略信息考虑data["data"]可能是列表的情况
if isinstance(data, dict) and "data" in data:
data_content = data["data"]
if isinstance(data_content, dict):
# Cloudreve V4 API格式处理
if "storage_policy" in data_content:
policyConfig.setPolicy(data_content["storage_policy"])
elif "policy" in data_content:
policyConfig.setPolicy(data_content["policy"])
elif isinstance(data_content, list) and data_content:
# 如果data_content是列表尝试从第一个元素获取策略
logger.warning("data['data']是列表而不是字典可能需要调整API响应处理")
# 处理data["data"]可能是列表或字典的情况
data_content = data.get("data", {})
if isinstance(data_content, list):
# 如果是列表直接使用列表作为objects
logger.info(f"成功加载目录数据,对象数量: {len(data_content)}")
self.objects = data_content
elif isinstance(data_content, dict):
# Cloudreve V4 API格式处理先检查files字段
if "files" in data_content:
# Cloudreve V4 API 使用files字段存储文件列表
logger.info(f"成功加载目录数据,对象数量: {len(data_content['files'])}")
self.objects = data_content["files"]
elif "objects" in data_content:
# 向后兼容旧版API
logger.info(f"成功加载目录数据,对象数量: {len(data_content['objects'])}")
self.objects = data_content["objects"]
else:
logger.error("目录数据格式错误字典中没有files或objects字段")
return
else:
logger.error("目录数据格式错误")
return
for obj in self.objects:
self.addFileCard(obj["id"], obj)
def _errorLoadDict(self, error_msg):
"""处理加载目录数据失败"""
logger.error(f"加载目录数据失败: {error_msg}")
InfoBar.error(
"错误",
f"加载目录数据失败: {error_msg}",
Qt.Orientation.Horizontal,
True,
-1,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.loadDict("/")
# 搜索文件浏览区域
class SearchLinkageSwitching(LinkageSwitchingBase):
"""文件卡片滚动区域组件"""
def __init__(self, parent=None):
super(SearchLinkageSwitching, self).__init__(parent)
self.fileCardsDict = {} # 存储所有文件卡片
def search(self, searchType, searchContent):
"""加载数据"""
self.loadDataThread = ListSearchThread(searchContent, searchType)
self.loadDataThread.listDictSignal.connect(self._dealData)
self.loadDataThread.errorSignal.connect(self._error)
self.loadDataThread.start()
def _dealData(self, data):
"""处理数据"""
if not data or "data" not in data or "objects" not in data["data"]:
logger.error("数据格式错误")
return
logger.info(f"成功加载数据,对象数量: {len(data['data']['objects'])}")
self.objects = data["data"]["objects"]
self.clearFileCards()
for obj in self.objects:
self.addFileCard(obj["id"], obj)
def _error(self, msg):
"""处理错误"""
logger.error(f"加载数据失败: {msg}")
InfoBar.error(
"错误",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 分享文件浏览区域
class ShareLinkageSwitching(LinkageSwitchingBase):
"""文件卡片滚动区域组件"""
totalItemsSignal = pyqtSignal(int) # 信号:传递总数量
def __init__(self, parent=None):
super().__init__(parent)
self.fileCardsDict = {} # 存储所有文件卡片
logger.debug(f"初始化搜索卡片滚动区域")
def addFileCard(self, fileId, obj):
if fileId in self.fileCardsDict:
logger.warning(f"文件卡片已存在: {fileId}")
return self.fileCardsDict[fileId]
fileCard = ShareFileCard(
obj,
self,
)
fileCard.setObjectName(f"fileCard_{fileId}")
self.fileCardsDict[fileId] = fileCard
self.layouts.addWidget(fileCard)
return fileCard
def search(self, keyword, orderBy, order, page):
"""加载数据"""
self.loadDataThread = ListShareThread(keyword, orderBy, order, page)
self.loadDataThread.listDictSignal.connect(self._dealData)
self.loadDataThread.errorSignal.connect(self._error)
self.loadDataThread.start()
def _dealData(self, data):
"""处理数据"""
if not data or "data" not in data:
logger.error("数据格式错误缺少data字段")
return
# 处理data["data"]可能是列表或字典的情况
data_content = data["data"]
if isinstance(data_content, list):
logger.warning("data['data']是列表而不是字典,将直接使用列表数据")
self.objects = data_content
elif isinstance(data_content, dict):
# 尝试从字典中获取对象列表按照Cloudreve V4 API格式处理
if "files" in data_content:
# Cloudreve V4 API 使用files字段存储文件列表
self.objects = data_content["files"]
elif "items" in data_content:
self.objects = data_content["items"]
elif "objects" in data_content:
self.objects = data_content["objects"]
else:
logger.error("数据格式错误字典中没有files、items或objects字段")
return
else:
logger.error(f"数据格式错误data['data']类型为{type(data_content).__name__},应为列表或字典")
return
logger.info(f"成功加载数据,对象数量: {len(self.objects)}")
# 尝试获取总数,如果不存在则不发送信号
if isinstance(data_content, dict) and "total" in data_content:
self.totalItemsSignal.emit(data_content["total"])
self.clearFileCards()
for obj in self.objects:
# 使用obj中可能存在的不同键名
file_id = obj.get("key", obj.get("id", None))
file_path = obj.get("path", None)
if file_id:
self.addFileCard(file_id, obj)
def _error(self, error):
"""处理错误"""
logger.error(f"加载数据失败: {error}")
InfoBar.error(
"错误",
f"加载数据失败: {error}",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)

256
app/view/login_window.py Normal file
View File

@@ -0,0 +1,256 @@
# coding:utf-8
from loguru import logger
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtGui import (
QColor,
QIcon,
)
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import (
ImageLabel,
InfoBar,
InfoBarPosition,
isDarkTheme,
MSFluentTitleBar,
Pivot,
PopUpAniStackedWidget,
setThemeColor,
VerticalSeparator
)
from qframelesswindow import FramelessWindow as Window
from app.core import LoginThread, RegisterThread
from app.view.widgets.login_widget import LoginWidget
from app.view.widgets.register_widget import RegisterWidget
class RegisterWindow(Window):
"""登录注册页面"""
loginSignal = pyqtSignal()
def __init__(self, parent=None):
logger.info("初始化注册窗口")
super().__init__(parent=parent)
setThemeColor("#2F80ED")
self.setTitleBar(MSFluentTitleBar(self))
self.verificationCode = ""
self.hBoxLayout = QHBoxLayout(self)
self.loginLayout = QVBoxLayout()
self.promotionalImageLabel = ImageLabel(self)
self.pivot = Pivot(self)
self.stackedWidget = PopUpAniStackedWidget(self)
self.loginWidget = LoginWidget(self)
self.registerWidget = RegisterWidget(self)
self.__initWidgets()
logger.info("注册窗口初始化完成")
def __initWidgets(self):
logger.debug("初始化注册窗口组件")
self.titleBar.maxBtn.hide()
self.titleBar.setDoubleClickEnabled(False)
self.__initLayout()
color = QColor(25, 33, 42) if isDarkTheme() else QColor(240, 244, 249)
self.setStyleSheet(f"RegisterWindow{{background: {color.name()}}}")
self.setWindowIcon(QIcon(":app/images/logo.png"))
self.setFixedSize(690, 470)
self.promotionalImageLabel.setImage(":app/images/background.png")
self.promotionalImageLabel.scaledToWidth(300)
self.pivot.addItem("LoginWidget", "登录", icon=":app/icons/login.svg")
# TODO: 内测版本隐藏注册页面
self.pivot.addItem("RegisterWidget", "注册", icon=":app/icons/register.svg")
self.pivot.setCurrentItem("LoginWidget")
self.pivot.currentItemChanged.connect(
lambda routeKey: self.stackedWidget.setCurrentWidget(
self.findChild(QWidget, routeKey)
)
)
self.loginWidget.loginButton.clicked.connect(self.login)
self.registerWidget.registerButton.clicked.connect(self.register)
self.stackedWidget.setMaximumWidth(300)
self.stackedWidget.addWidget(self.loginWidget)
self.stackedWidget.addWidget(self.registerWidget)
self.titleBar.titleLabel.setStyleSheet(
"""
QLabel{
background: transparent;
font: 14px 'Segoe UI', 'Microsoft YaHei', 'PingFang SC';
padding: 0 4px;
color: black
}
"""
)
desktop = QApplication.screens()[0].availableGeometry()
w, h = desktop.width(), desktop.height()
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
self.titleBar.raise_()
logger.debug("注册窗口组件初始化完成")
def __initLayout(self):
logger.debug("初始化注册窗口布局")
self.loginLayout.setContentsMargins(10, 40, 10, 40)
self.hBoxLayout.setContentsMargins(25, 30, 15, 30)
self.loginLayout.addWidget(
self.pivot, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.loginLayout.addSpacing(25)
self.loginLayout.addWidget(self.stackedWidget)
self.hBoxLayout.addWidget(
self.promotionalImageLabel, 0, Qt.AlignmentFlag.AlignBottom
)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(VerticalSeparator(self))
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addLayout(self.loginLayout)
logger.debug("注册窗口布局初始化完成")
def keyPressEvent(self, event):
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
if self.stackedWidget.currentWidget() == self.loginWidget:
self.login()
elif self.stackedWidget.currentWidget() == self.registerWidget:
self.register()
else:
super().keyPressEvent(event)
def login(self):
"""登录"""
self.loginWidget.loginButton.setEnabled(False)
userName = self.loginWidget.emailLineEdit.text()
password = self.loginWidget.passwordLineEdit.text()
captcha = self.loginWidget.verificationCodeLineEdit.text()
if not userName or not password or not captcha:
InfoBar.warning(
"提示",
"你需要填写所有项",
Qt.Orientation.Horizontal,
True,
2000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
return
self.loginThread = LoginThread(userName, password, captcha)
self.loginThread.successLogin.connect(self._loginSuccess)
self.loginThread.errorLogin.connect(self._loginFailed)
self.loginThread.start()
def _loginSuccess(self):
InfoBar.success(
"成功",
"登录成功,正在跳转",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
QTimer.singleShot(500, self.loginSignal.emit)
def _loginFailed(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.loginWidget.refreshVerificationCode()
self.loginWidget.verificationCodeLineEdit.clear()
self.loginWidget.loginButton.setEnabled(True)
def register(self):
"""注册"""
self.registerWidget.registerButton.setEnabled(False)
userName = self.registerWidget.emailLineEdit.text()
password = self.registerWidget.passwordLineEdit.text()
confirmPassword = self.registerWidget.confirmPasswordLineEdit.text()
captchaCode = self.registerWidget.verificationCodeLineEdit.text()
if not userName or not password or not confirmPassword:
InfoBar.warning(
"提示",
"你需要填写所有项",
Qt.Orientation.Horizontal,
True,
2000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.registerWidget.registerButton.setEnabled(True)
return
if password != confirmPassword:
InfoBar.warning(
"提示",
"两次输入的密码不一致",
Qt.Orientation.Horizontal,
True,
2000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.registerWidget.confirmPasswordLineEdit.clear()
self.registerWidget.registerButton.setEnabled(True)
return
self.registerThread = RegisterThread(userName, password, captchaCode)
self.registerThread.successRegister.connect(self._registerSuccess)
self.registerThread.errorRegister.connect(self._registerFailed)
self.registerThread.start()
def _registerSuccess(self):
InfoBar.info(
"成功",
"注册成功,请前往邮箱激活账号",
Qt.Orientation.Horizontal,
True,
-1,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.stackedWidget.setCurrentWidget(self.loginWidget)
self.loginWidget.emailLineEdit.setText(self.registerWidget.emailLineEdit.text())
self.registerWidget.emailLineEdit.clear()
self.registerWidget.passwordLineEdit.clear()
self.registerWidget.confirmPasswordLineEdit.clear()
self.registerWidget.registerButton.setEnabled(True)
def _registerFailed(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.registerWidget.refreshVerificationCode()
self.registerWidget.verificationCodeLineEdit.clear()
self.registerWidget.passwordLineEdit.clear()
self.registerWidget.confirmPasswordLineEdit.clear()
self.registerWidget.emailLineEdit.clear()
self.registerWidget.registerButton.setEnabled(True)

201
app/view/main_window.py Normal file
View File

@@ -0,0 +1,201 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import QSize
from PyQt6.QtGui import QColor, QIcon
from PyQt6.QtWidgets import QApplication, QWidget
from qfluentwidgets import NavigationAvatarWidget, NavigationItemPosition, SplashScreen
from app.core import cfg, qconfig, userConfig, GetUserAvatarThread,lang,signalBus
from app.view.app_info_interface import AppInfoInterface
from app.view.ownFiled_interface import OwnFiledInterface
from app.view.setting_interface import SettingInterface
from app.view.storagespace_interface import StoragespaceInterface
from app.view.task_interface import TaskInterface
from app.view.widgets.custom_fluent_window import CustomFluentWindow
from app.view.widgets.preview_box import OptimizedPreviewBox, PreviewTextBox
from app.view.widgets.share_folder_messageBox import ShareFolderMessageBox
class MainWindow(CustomFluentWindow):
def __init__(self):
logger.info("开始初始化主窗口")
super().__init__()
self.initWindow()
self.ownFiledInterface = OwnFiledInterface(self)
self.storagespaceInterface = StoragespaceInterface(self)
self.taskInterface = TaskInterface(self)
self.appInfoInterface = AppInfoInterface(self)
self.connectSignalToSlot()
self.initNavigation()
logger.info("主窗口初始化完成")
def connectSignalToSlot(self):
logger.debug("连接信号和槽")
signalBus.micaEnableChanged.connect(self.setMicaEffectEnabled)
# 预览信号连接
signalBus.imagePreviewSignal.connect(self.imagePreview)
signalBus.txtPreviewSignal.connect(self.txtPreview)
# 背景信号连接
signalBus.backgroundChanged.connect(self.updateBackground)
signalBus.opacityChanged.connect(self.updateBackground)
# 下载上传任务信号连接
signalBus.addUploadFileTask.connect(self.addUploadFileTask)
signalBus.addDownloadFileTask.connect(self.addDownloadFileTask)
signalBus.shareFolderViewSignal.connect(self.shareFolderView)
# 语言变更信号连接
signalBus.languageChanged.connect(self.updateNavigation)
def updateNavigation(self):
# 更新导航项文本
self.navigationInterface.setItemText(self.ownFiledInterface, lang("我的文件"))
self.navigationInterface.setItemText(
self.storagespaceInterface, lang("存储配额")
)
self.navigationInterface.setItemText(self.taskInterface, lang("任务管理"))
self.navigationInterface.setItemText(self.appInfoInterface, lang("应用信息"))
...
def initNavigation(self):
self.navigationInterface.setAcrylicEnabled(True)
self.navigationInterface.setExpandWidth(200)
logger.info("开始初始化导航界面")
self.addSubInterface(
self.ownFiledInterface,
QIcon(":app/icons/Myfile.svg"),
lang("我的文件"),
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.storagespaceInterface,
QIcon(":app/icons/Storage.svg"),
lang("存储配额"),
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.taskInterface,
QIcon(":app/icons/Task.svg"),
lang("任务管理"),
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.appInfoInterface,
QIcon(":app/icons/Application.svg"),
lang("应用信息"),
NavigationItemPosition.BOTTOM,
)
# 创建默认头像widget先使用本地默认头像
self.avatarWidget = NavigationAvatarWidget(
userConfig.userName, ":app/images/logo.png"
)
self.navigationInterface.addWidget(
routeKey="settingInterface",
widget=self.avatarWidget,
position=NavigationItemPosition.BOTTOM,
onClick=self.setPersonalInfoWidget,
)
self.settingInterface = SettingInterface(self)
self.stackedWidget.addWidget(self.settingInterface)
self.splashScreen.finish()
logger.info("导航界面初始化完成")
self.avatarThread = GetUserAvatarThread("l")
self.avatarThread.avatarPixmap.connect(self.onAvatarDownloaded)
self.avatarThread.start()
def shareFolderView(self, _id):
w = ShareFolderMessageBox(_id, self)
if w.exec():
...
def addUploadFileTask(self, filePath):
logger.info(f"添加上传文件任务: {filePath}")
self.taskInterface.uploadScrollWidget.addUploadTask(filePath)
self.stackedWidget.setCurrentWidget(self.taskInterface)
self.navigationInterface.setCurrentItem("taskInterface")
self.taskInterface._changePivot("Upload")
def addDownloadFileTask(self, suffix, fileName, _id):
logger.info(f"添加下载文件任务: {fileName}")
self.taskInterface.downloadScrollWidget.addDownloadTask(suffix, fileName, _id)
self.stackedWidget.setCurrentWidget(self.taskInterface)
self.navigationInterface.setCurrentItem("taskInterface")
self.taskInterface._changePivot("Download")
def setPersonalInfoWidget(self):
self.stackedWidget.setCurrentWidget(
self.stackedWidget.findChild(QWidget, "settingInterface")
)
self.navigationInterface.setCurrentItem("settingInterface")
def onAvatarDownloaded(self, pixmap):
userConfig.setUserAvatarPixmap(pixmap)
self.avatarWidget.setAvatar(pixmap)
self.settingInterface.updateAvatar(pixmap)
def initWindow(self):
logger.info("开始初始化窗口设置")
self.resize(960, 780)
self.setMinimumWidth(760)
self.setWindowIcon(QIcon(":app/images/logo.png"))
self.setWindowTitle(lang("LeonPan"))
logger.debug("已设置窗口基本属性")
self.setCustomBackgroundColor(QColor(240, 244, 249), QColor(32, 32, 32))
self.setMicaEffectEnabled(cfg.get(cfg.micaEnabled))
logger.debug("已设置窗口背景和Mica效果")
self.setBackgroundImage(
qconfig.get(cfg.customBackground), qconfig.get(cfg.customOpactity)
) # create splash screen
# 使用自定义的背景设置方法
# create splash screen
self.splashScreen = SplashScreen(self.windowIcon(), self)
self.splashScreen.setIconSize(QSize(106, 106))
self.splashScreen.raise_()
logger.debug("已创建并设置启动屏幕")
desktop = QApplication.primaryScreen().availableGeometry()
w, h = desktop.width(), desktop.height()
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
logger.debug("已移动窗口到屏幕中心")
self.show()
QApplication.processEvents()
logger.info("窗口初始化完成并显示")
def resizeEvent(self, e):
super().resizeEvent(e)
if hasattr(self, "splashScreen"):
self.splashScreen.resize(self.size())
# 窗口大小改变时更新背景
def imagePreview(self, _id):
# 使用V4 API进行预览
url = f"/file/preview/{_id}"
self.previewBox = OptimizedPreviewBox(self, url)
if self.previewBox.exec():
pass
def txtPreview(self, _id):
# 使用V4 API获取内容
url = f"/file/content/{_id}"
self.previewBox = PreviewTextBox(self, url, _id)
if self.previewBox.exec():
pass
def updateBackground(self):
"""更新窗口背景"""
self.setBackgroundImage(
qconfig.get(cfg.customBackground), qconfig.get(cfg.customOpactity)
)

View File

@@ -0,0 +1,138 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import (
InfoBar,
InfoBarPosition,
VerticalSeparator,
)
from app.core import signalBus
from app.view.widgets.ownfile_scroll_widget import OwnFileScrollWidget
from app.view.widgets.ownFiled_widgets import SearchWidget, TagWidget
from app.view.widgets.share_search_widgets import ShareSearchScrollWidget
from app.view.widgets.ware_search_widgets import WareSearchScrollWidget
class OwnFiledInterface(QWidget):
"""主文件管理界面"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("OwnFiledInterface")
self.currentPath = "/"
logger.debug("初始化主文件管理界面")
# 初始化组件
self.searchWidget = SearchWidget(self)
self.tagWidget = TagWidget(self)
self.ownFileScrollWidget = OwnFileScrollWidget(self)
self.wareSearchScrollWidget = WareSearchScrollWidget(self)
self.wareSearchScrollWidget.hide()
self.shareSearchScrollWidget = ShareSearchScrollWidget(self)
self.shareSearchScrollWidget.hide()
self.setupUi()
self.connectSignals()
def setupUi(self):
"""初始化UI"""
logger.debug("设置主文件管理界面UI")
# 设置主布局
self.initLayout()
def initLayout(self):
"""初始化布局"""
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setContentsMargins(10, 0, 10, 0)
# 创建顶部布局
self.topLayout = QHBoxLayout()
self.topLayout.setContentsMargins(0, 0, 0, 0)
self.topLayout.setAlignment(
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignVCenter
)
self.topLayout.addWidget(
self.tagWidget, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.topLayout.addSpacing(20)
self.verticalSeparator = VerticalSeparator(self)
self.verticalSeparator.setFixedHeight(25)
self.topLayout.addWidget(self.verticalSeparator)
self.topLayout.addSpacing(20)
self.topLayout.addWidget(self.searchWidget, 0, Qt.AlignmentFlag.AlignTop)
# 添加所有组件到主布局
self.vBoxLayout.addLayout(self.topLayout)
self.vBoxLayout.addWidget(self.ownFileScrollWidget)
self.vBoxLayout.addWidget(self.wareSearchScrollWidget)
self.vBoxLayout.addWidget(self.shareSearchScrollWidget)
def tagSearch(self, types, keyword):
self.wareSearchScrollWidget.show()
self.ownFileScrollWidget.hide()
self.wareSearchScrollWidget.wareSearch(types, keyword)
def search(self):
keyword = self.searchWidget.searchLineEdit.text()
searchType = self.searchWidget.searchButton.text()
if keyword == "" or keyword == ".":
InfoBar.warning(
"注意",
"搜索内容为空",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
return
if searchType == "仓内搜索":
self.wareSearchScrollWidget.show()
self.ownFileScrollWidget.hide()
self.shareSearchScrollWidget.hide()
self.wareSearchScrollWidget.wareSearch("keyword", keyword)
self.tagWidget.tagScrollArea.clearChecked()
elif searchType == "站内搜索":
self.wareSearchScrollWidget.hide()
self.ownFileScrollWidget.hide()
self.shareSearchScrollWidget.show()
self.shareSearchScrollWidget.shareSearch(keyword, 1)
self.tagWidget.tagScrollArea.clearChecked()
def returnLinkageSwitchingPage(self):
self.wareSearchScrollWidget.hide()
self.shareSearchScrollWidget.hide()
self.ownFileScrollWidget.show()
self.tagWidget.tagScrollArea.clearChecked()
self.searchWidget.searchLineEdit.clear()
def connectSignals(self):
"""连接信号与槽"""
logger.debug("连接主文件管理界面信号")
# 连接搜索信号
signalBus.dirOpenSignal.connect(
lambda x: self.ownFileScrollWidget.onChangeDir(x)
)
signalBus.refreshFolderListSignal.connect(
self.ownFileScrollWidget.refreshCurrentDirectory
)
self.wareSearchScrollWidget.returnSignal.connect(
self.returnLinkageSwitchingPage
)
self.shareSearchScrollWidget.returnSignal.connect(
self.returnLinkageSwitchingPage
)
self.searchWidget.searchButton.clicked.connect(self.search)
self.tagWidget.tagScrollArea.tagClicked.connect(self.tagSearch)

View File

@@ -0,0 +1,363 @@
# coding:utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QFileDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets import (
AvatarWidget,
BodyLabel,
GroupHeaderCardWidget,
HyperlinkLabel,
InfoBarPosition,
LineEdit,
PushButton,
ScrollArea,
Slider,
SubtitleLabel,
VerticalSeparator,
)
from qfluentwidgets import InfoBar, MessageBoxBase
from app.core import cfg, lang, qconfig, signalBus, UserAvatarUpdateThread, userConfig, UserNickNameUpdateThread
from app.view.widgets.custom_background_messageBox import CustomBgMessageBox
class NickNameEdit(MessageBoxBase):
def __init__(self, parent=None):
super().__init__(parent)
self.titleLabel = SubtitleLabel(lang("修改昵称"), self)
self.lineEdit = LineEdit(self)
self.widget.setMinimumWidth(200)
self.viewLayout.addWidget(
self.titleLabel, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.viewLayout.addWidget(self.lineEdit)
self.yesButton.setText("确定")
self.cancelButton.setText("取消")
class BasicInformationSettingCard(GroupHeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("basicInformationSettingCard")
self.setTitle(lang("用户信息"))
self.nickNameEdit = PushButton(lang("修改昵称"), self)
self.nickNameEdit.clicked.connect(self._changeNickName)
self.addGroup(":app/icons/Nickname.svg", userConfig.userId, "UID", QLabel(self))
self.addGroup(
":app/icons/Nickname.svg",
userConfig.userName,
lang("用户昵称"),
self.nickNameEdit,
)
self.addGroup(
":app/icons/Email.svg",
userConfig.userEmail,
lang("电子邮箱"),
QLabel(self),
)
self.addGroup(
":app/icons/Group.svg",
userConfig.userGroup,
lang("当前用户组"),
QLabel(self),
)
self.addGroup(
":app/icons/Score.svg", userConfig.userScore, "积分", QLabel(self)
)
self.addGroup(
":app/icons/Date.svg",
userConfig.userCreatedTime,
lang("用户注册时间"),
QLabel(self),
)
def _changeNickName(self):
w = NickNameEdit(self.window())
def _onNickNameSuccess():
self.groupWidgets[2].setTitle(newNickName)
InfoBar.success(
lang("修改成功"),
lang("昵称修改成功"),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def _onNickNameError(error):
InfoBar.error(
lang("修改失败"),
error,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
if w.exec():
newNickName = w.lineEdit.text()
self.nickNameServiceThread = UserNickNameUpdateThread(newNickName)
self.nickNameServiceThread.successUpdate.connect(_onNickNameSuccess)
self.nickNameServiceThread.errorUpdate.connect(_onNickNameError)
self.nickNameServiceThread.start()
class SoftWardSettingWidget(GroupHeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("软件设置")
self.downloadSavePathButton = PushButton(lang("选择保存路径"), self)
self.downloadSavePathButton.clicked.connect(self._chooseDownloadSavePath)
self.addGroup(
":app/icons/SavePath.svg",
qconfig.get(cfg.downloadSavePath),
lang("选择下载保存路径"),
self.downloadSavePathButton,
)
def _chooseDownloadSavePath(self):
folder_path = QFileDialog.getExistingDirectory(self, lang("选择文件夹"))
if folder_path:
print(f"选择的文件夹路径是: {folder_path}")
qconfig.set(cfg.downloadSavePath, folder_path)
self.groupWidgets[0].setTitle(folder_path)
InfoBar.success(
lang("修改成功"),
lang("下载保存路径修改成功"),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
class ThemeSettingWidget(GroupHeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle(lang("背景图片设置"))
self.officialBackgroundButton = PushButton(lang("官方背景图"), self)
self.officialBackgroundButton.clicked.connect(self.officialBackground)
self.customBackgroundButton = PushButton(lang("选择自定义背景"), self)
self.customBackgroundButton.clicked.connect(self.customBackground)
self.opacitySlider = Slider(Qt.Orientation.Horizontal, self)
self.opacitySlider.setRange(0, 10)
self.opacitySlider.setFixedWidth(100)
self.opacitySlider.setValue(int(qconfig.get(cfg.customOpactity) * 10))
self.opacitySlider.valueChanged.connect(self.setOpacity)
self.addGroup(
":app/icons/BgImage.svg",
lang("官方预设背景图片"),
lang("选择背景图片"),
self.officialBackgroundButton,
)
self.addGroup(
":app/icons/BgImage.svg",
lang("自定义背景图片"),
lang("选择自定义图片,选择后请不要更改图片位置"),
self.customBackgroundButton,
)
self.addGroup(
":app/icons/Opacity.svg",
lang("图片背景透明度"),
lang("设置图片背景透明度"),
self.opacitySlider,
)
def officialBackground(self):
w = CustomBgMessageBox(self.window())
if w.exec():
index = w.returnImage()
qconfig.set(cfg.customBackground, f"app\\resource\\images\\bg{index}.png")
signalBus.backgroundChanged.emit()
def customBackground(self):
file_name, _ = QFileDialog.getOpenFileName(
self,
"选择背景",
"",
"Image Files (*.png *.jpg *.jpeg *.bmp);;All Files (*)",
)
qconfig.set(cfg.customBackground, file_name)
signalBus.backgroundChanged.emit()
def setOpacity(self, opacity):
qconfig.set(cfg.customOpactity, opacity / 10)
signalBus.opacityChanged.emit()
class AgreementLabelWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.privacyPolicy = HyperlinkLabel(
lang("隐私协议"),
self,
)
self.userAgreement = HyperlinkLabel(
lang("用户协议"),
self,
)
self.verticalSeparator = VerticalSeparator(self)
self.verticalSeparator.setFixedHeight(15)
self.privacyPolicy.setUrl("https://mp.miaostars.com/ysxy")
self.userAgreement.setUrl("https://mp.miaostars.com/xy")
self.hBoxLayout.addWidget(self.privacyPolicy, 0, Qt.AlignmentFlag.AlignCenter)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(self.verticalSeparator)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(
self.userAgreement,
)
class SettingInterface(ScrollArea):
"""Setting interface"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self.scrollWidget = QWidget()
self.expandLayout = QVBoxLayout(self.scrollWidget)
self.avatarWidget = AvatarWidget(self.scrollWidget)
self.basicInformationSettingCard = BasicInformationSettingCard(
self.scrollWidget
)
self.softWardSettingWidget = SoftWardSettingWidget(self.scrollWidget)
self.themeSettingWidget = ThemeSettingWidget(self.scrollWidget)
self.agreementLabelWidget = AgreementLabelWidget(self.scrollWidget)
self.infoLabel = BodyLabel(
"增值电信业务经营许可证B1-20191399鄂ICP备2025132158号 \n ©2025 LeonPan \n 武汉喵星创想互联网科技有限公司",
self.scrollWidget,
)
self.__initWidget()
def __initWidget(self):
self.resize(1000, 800)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# self.setViewportMargins(0, 100, 0, 20)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setObjectName("settingInterface")
self.scrollWidget.setObjectName("scrollWidget")
self.scrollWidget.setStyleSheet("background:transparent;border:none;")
self.setStyleSheet("background:transparent;border:none;")
self.avatarWidget.setImage(QPixmap(":app/images/logo.png"))
# Connect click event to open file dialog for avatar selection
self.avatarWidget.clicked.connect(self._openAvatarDialog)
self.infoLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.infoLabel.setStyleSheet("color:gray;font-size:12px;")
# initialize layout
self.__initLayout()
self._connectSignalToSlot()
def __initLayout(self):
# add setting card group to layout
# self.expandLayout.setSpacing(28)
self.expandLayout.addWidget(
self.avatarWidget,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.expandLayout.addWidget(
self.basicInformationSettingCard, 0, Qt.AlignmentFlag.AlignTop
)
self.expandLayout.addWidget(
self.softWardSettingWidget, 0, Qt.AlignmentFlag.AlignTop
)
self.expandLayout.addWidget(
self.themeSettingWidget, 0, Qt.AlignmentFlag.AlignTop
)
self.expandLayout.addWidget(
self.agreementLabelWidget, 1, Qt.AlignmentFlag.AlignBottom
)
self.expandLayout.addSpacing(5)
self.expandLayout.addWidget(self.infoLabel, 0, Qt.AlignmentFlag.AlignBottom)
def _connectSignalToSlot(self): ...
def _openAvatarDialog(self):
"""Open file dialog to select avatar image"""
fileDialog = QFileDialog(self)
fileDialog.setWindowTitle(lang("选择图片"))
fileDialog.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen)
fileDialog.setFileMode(QFileDialog.FileMode.ExistingFile)
fileDialog.setNameFilter("图片文件 (*.png *.jpg *.jpeg *.bmp)")
if fileDialog.exec():
file_path = fileDialog.selectedFiles()[0]
try:
image = QImage(file_path)
if not image.isNull():
self.changeAvatar(image)
# Update the avatar widget with the new image
self.avatarWidget.setImage(QPixmap.fromImage(image))
except Exception as e:
InfoBar.error(
lang("选择失败"),
str(e),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def updateAvatar(self, avatarPixmap):
self.avatarWidget.setImage(avatarPixmap)
def changeAvatar(self, image):
def avatarUpdateError(error):
InfoBar.error(
lang("修改失败"),
error,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.avatarWidget.setEnabled(True)
def avatarUpdateSuccess():
InfoBar.success(
lang("修改成功"),
lang("头像修改成功"),
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.avatarWidget.setEnabled(True)
self.avatarUpdateThread = UserAvatarUpdateThread(image)
self.avatarUpdateThread.successUpdate.connect(avatarUpdateSuccess)
self.avatarUpdateThread.errorUpdate.connect(avatarUpdateError)
self.avatarUpdateThread.start()
self.avatarWidget.setEnabled(False)

View File

@@ -0,0 +1,76 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import ScrollArea, TitleLabel
from app.core import GetPackThread, lang
from app.view.components.gb_information_card import GbInformationCard
class NumInformationWidget(QWidget):
def __init__(self, parent=None):
super(NumInformationWidget, self).__init__(parent)
self.hBoxLayout = QHBoxLayout(self)
self.basicSizeCard = GbInformationCard(0, lang("用户组基础容量"), self)
self.packSizeCard = GbInformationCard(0, lang("有效容量包附加附加容量"), self)
self.usedSizeCard = GbInformationCard(0, lang("已使用容量"), self)
self.totalSizeCard = GbInformationCard(0, lang("总容量"), self)
self.hBoxLayout.setSpacing(10)
self.hBoxLayout.addWidget(self.basicSizeCard)
self.hBoxLayout.addWidget(self.packSizeCard)
self.hBoxLayout.addWidget(self.usedSizeCard)
self.hBoxLayout.addWidget(self.totalSizeCard)
class StoragespaceInterface(ScrollArea):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.widgets = QWidget()
self.vBoxLayout = QVBoxLayout(self.widgets)
self.firstLoad = True
self.titleLabel = TitleLabel(lang("存储配额"), self)
self.numInformationWidget = NumInformationWidget(self)
self.__initWidget()
self._loadUserCustomStorage()
def __initWidget(self):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.widgets)
self.setWidgetResizable(True)
self.setObjectName("storageInterface")
self.widgets.setObjectName("scrollWidgets")
self.widgets.setStyleSheet("background:transparent;border:none;")
self.setStyleSheet("background:transparent;border:none;")
self.titleLabel.setContentsMargins(10, 5, 5, 5)
self.__initLayout()
def __initLayout(self):
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addWidget(self.titleLabel)
self.vBoxLayout.addWidget(self.numInformationWidget)
def _loadUserCustomStorage(self):
if not self.firstLoad:
return
self.packThread = GetPackThread()
self.packThread.storageDictSignal.connect(self._successGetPack)
self.packThread.start()
def _successGetPack(self, datas):
self.packData = datas["data"]
self.firstLoad = False
self.numInformationWidget.packSizeCard.updateValue(self.packData["pack"])
self.numInformationWidget.basicSizeCard.updateValue(self.packData["base"])
self.numInformationWidget.usedSizeCard.updateValue(self.packData["used"])
self.numInformationWidget.totalSizeCard.updateValue(self.packData["total"])
logger.success("用户配额加载,已刷新")

View File

@@ -0,0 +1,54 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from qfluentwidgets import PopUpAniStackedWidget, SegmentedWidget, TitleLabel
from app.core import lang
from app.view.widgets.download_widget import DownloadScrollWidget
from app.view.widgets.upload_widget import UploadScrollWidget
class TaskInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.vBoxLayout = QVBoxLayout(self)
self.titleLabel = TitleLabel(lang("任务管理"), self)
self.pivot = SegmentedWidget(self)
self.stackedWidget = PopUpAniStackedWidget(self)
self.uploadScrollWidget = UploadScrollWidget(self)
self.downloadScrollWidget = DownloadScrollWidget(self)
self.__initWidget()
def __initWidget(self):
self.setObjectName("taskInterface")
self.titleLabel.setContentsMargins(10, 5, 5, 5)
self.pivot.setMinimumWidth(200)
self.pivot.addItem("Upload", lang("文件上传"))
self.pivot.addItem("Download", lang("文件下载"))
self.pivot.setCurrentItem("Upload")
self.pivot.currentItemChanged.connect(self._changePivot)
self.stackedWidget.addWidget(self.uploadScrollWidget)
self.stackedWidget.addWidget(self.downloadScrollWidget)
self.__initLayout()
def _changePivot(self, routeKey):
self.stackedWidget.setCurrentWidget(
self.stackedWidget.findChild(QWidget, routeKey + "ScrollWidget")
)
self.pivot.setCurrentItem(routeKey)
def __initLayout(self):
self.vBoxLayout.addWidget(
self.titleLabel, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.vBoxLayout.addSpacing(5)
self.vBoxLayout.addWidget(
self.pivot, 0, Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
)
self.vBoxLayout.addSpacing(5)
self.vBoxLayout.addWidget(self.stackedWidget)

View File

@@ -0,0 +1,45 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import pyqtSignal
from qfluentwidgets import LineEdit, MessageBoxBase, SubtitleLabel
from app.core import AddTagThread,lang
class AddTagMessageBox(MessageBoxBase):
successAddTagSignal = pyqtSignal(str, dict) # 标签名称, 响应数据
def __init__(self, parent=None):
super().__init__(parent=parent)
self.widget.setMinimumWidth(250)
self.titleLabel = SubtitleLabel(lang("添加标签"), self)
self.nameLineEdit = LineEdit(self)
self.nameLineEdit.setPlaceholderText(lang("标签名称"))
self.expressionLineEdit = LineEdit(self)
self.expressionLineEdit.setPlaceholderText(lang("标签通配符"))
self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.addWidget(self.nameLineEdit)
self.viewLayout.addWidget(self.expressionLineEdit)
self.yesButton.setText(lang("添加"))
self.yesButton.clicked.connect(self.addTag)
self.cancelButton.setText(lang("取消"))
def addTag(self):
name = self.nameLineEdit.text()
expression = self.expressionLineEdit.text()
if not name or not expression:
return
self.addTagThread = AddTagThread(name, expression)
self.addTagThread.successSignal.connect(self.onAddTagSuccess)
self.addTagThread.errorSignal.connect(self.onAddTagError)
self.addTagThread.start()
def onAddTagSuccess(self, name, result):
self.accept()
self.successAddTagSignal.emit(name, result)
def onAddTagError(self, name, error_msg):
logger.error(f"添加标签失败: {name} - {error_msg}")

View File

@@ -0,0 +1,23 @@
# coding: utf-8
from pathlib import Path
from qfluentwidgets import HorizontalFlipView, MessageBoxBase, SubtitleLabel
class CustomBgMessageBox(MessageBoxBase):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.titleLabel = SubtitleLabel(parent=self)
self.titleLabel.setText("内设壁纸")
self.viewLayout.addWidget(self.titleLabel)
self.imageChoice = HorizontalFlipView(parent=self)
self.imageChoice.setBorderRadius(8)
for i in range(0, 6):
self.imageChoice.addImage(f"app\\resource\\images\\bg{i}.png")
self.viewLayout.addWidget(self.imageChoice)
self.yesButton.setText("确定")
self.cancelButton.hide()
def returnImage(self):
return self.imageChoice.currentIndex()

View File

@@ -0,0 +1,363 @@
# coding:utf-8
import sys
from typing import Union
from PyQt6.QtCore import QRect, QSize, Qt
from PyQt6.QtGui import QColor, QIcon, QPainter, QPixmap
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets.common.animation import BackgroundAnimationWidget
from qfluentwidgets.common.config import qconfig
from qfluentwidgets.common.icon import FluentIconBase
from qfluentwidgets.common.router import qrouter
from qfluentwidgets.common.style_sheet import (
FluentStyleSheet,
isDarkTheme,
)
from qfluentwidgets.components.navigation import (
NavigationInterface,
NavigationItemPosition,
NavigationTreeWidget,
)
from qfluentwidgets.components.widgets.frameless_window import FramelessWindow
from qframelesswindow import TitleBar, TitleBarBase
from app.view.widgets.stacked_widget import StackedWidget
class CustomFluentWindowBase(BackgroundAnimationWidget, FramelessWindow):
"""Fluent window base class"""
def __init__(self, parent=None):
self._isMicaEnabled = False
self._lightBackgroundColor = QColor(240, 244, 249)
self._darkBackgroundColor = QColor(32, 32, 32)
self._backgroundPixmap = None # 存储背景图片
self._backgroundOpacity = 1.0 # 背景图片不透明度
super().__init__(parent=parent)
self.hBoxLayout = QHBoxLayout(self)
self.stackedWidget = StackedWidget(self)
self.navigationInterface = None
# initialize layout
self.hBoxLayout.setSpacing(0)
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
FluentStyleSheet.FLUENT_WINDOW.apply(self.stackedWidget)
# enable mica effect on win11
self.setMicaEffectEnabled(True)
# show system title bar buttons on macOS
if sys.platform == "darwin":
self.setSystemTitleBarButtonVisible(True)
qconfig.themeChangedFinished.connect(self._onThemeChangedFinished)
def addSubInterface(
self,
interface: QWidget,
icon: Union[FluentIconBase, QIcon, str],
text: str,
position=NavigationItemPosition.TOP,
):
"""add sub interface"""
raise NotImplementedError
def removeInterface(self, interface: QWidget, isDelete=False):
"""remove sub interface
Parameters
----------
interface: QWidget
sub interface to be removed
isDelete: bool
whether to delete the sub interface
"""
raise NotImplementedError
def switchTo(self, interface: QWidget):
self.stackedWidget.setCurrentWidget(interface, popOut=False)
def _onCurrentInterfaceChanged(self, index: int):
widget = self.stackedWidget.widget(index)
self.navigationInterface.setCurrentItem(widget.objectName())
qrouter.push(self.stackedWidget, widget.objectName())
self._updateStackedBackground()
def _updateStackedBackground(self):
isTransparent = self.stackedWidget.currentWidget().property(
"isStackedTransparent"
)
if bool(self.stackedWidget.property("isTransparent")) == isTransparent:
return
self.stackedWidget.setProperty("isTransparent", isTransparent)
self.stackedWidget.setStyle(QApplication.style())
def setCustomBackgroundColor(self, light, dark):
"""set custom background color
Parameters
----------
light, dark: QColor | Qt.GlobalColor | str
background color in light/dark theme mode
"""
self._lightBackgroundColor = QColor(light)
self._darkBackgroundColor = QColor(dark)
self._updateBackgroundColor()
def setBackgroundImage(self, imagePath: str, opacity: float = 1.0):
"""设置背景图片
Parameters
----------
imagePath: str
背景图片路径
opacity: float
背景图片不透明度范围0.0-1.0
"""
self._backgroundPixmap = QPixmap(imagePath)
if self._backgroundPixmap.isNull():
print(f"无法加载背景图片: {imagePath}")
return
self._backgroundOpacity = max(0.0, min(1.0, opacity)) # 确保在0-1范围内
# 设置StackedWidget为透明以便显示背景图片
self.stackedWidget.setProperty("isTransparent", True)
self.stackedWidget.setStyle(QApplication.style())
self.update() # 触发重绘
def removeBackgroundImage(self):
"""移除背景图片"""
self._backgroundPixmap = None
# 恢复StackedWidget的默认背景
self.stackedWidget.setProperty("isTransparent", False)
self.stackedWidget.setStyle(QApplication.style())
self.update()
def _normalBackgroundColor(self):
if not self.isMicaEffectEnabled():
return (
self._darkBackgroundColor
if isDarkTheme()
else self._lightBackgroundColor
)
return QColor(0, 0, 0, 0)
def _onThemeChangedFinished(self):
if self.isMicaEffectEnabled():
self.windowEffect.setMicaEffect(self.winId(), isDarkTheme())
def paintEvent(self, e):
# 创建绘制器
painter = QPainter(self)
# 如果有背景图片,先绘制背景图片
if self._backgroundPixmap and not self._backgroundPixmap.isNull():
# 设置不透明度
painter.setOpacity(self._backgroundOpacity)
# 缩放图片以适应窗口大小
scaled_pixmap = self._backgroundPixmap.scaled(
self.size(), Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation
)
painter.drawPixmap(0, 0, scaled_pixmap)
# 然后调用父类的绘制方法
super().paintEvent(e)
def setMicaEffectEnabled(self, isEnabled: bool):
"""set whether the mica effect is enabled, only available on Win11"""
if sys.platform != "win32" or sys.getwindowsversion().build < 22000:
return
self._isMicaEnabled = isEnabled
if isEnabled:
self.windowEffect.setMicaEffect(self.winId(), isDarkTheme())
# 启用Mica效果时移除背景图片
self.removeBackgroundImage()
else:
self.windowEffect.removeBackgroundEffect(self.winId())
self.setBackgroundColor(self._normalBackgroundColor())
def isMicaEffectEnabled(self):
return self._isMicaEnabled
def systemTitleBarRect(self, size: QSize) -> QRect:
"""Returns the system title bar rect, only works for macOS
Parameters
----------
size: QSize
original system title bar rect
"""
return QRect(
size.width() - 75, 0 if self.isFullScreen() else 9, 75, size.height()
)
def setTitleBar(self, titleBar):
super().setTitleBar(titleBar)
# hide title bar buttons on macOS
if (
sys.platform == "darwin"
and self.isSystemButtonVisible()
and isinstance(titleBar, TitleBarBase)
):
titleBar.minBtn.hide()
titleBar.maxBtn.hide()
titleBar.closeBtn.hide()
class FluentTitleBar(TitleBar):
"""Fluent title bar"""
def __init__(self, parent):
super().__init__(parent)
self.setFixedHeight(48)
self.hBoxLayout.removeWidget(self.minBtn)
self.hBoxLayout.removeWidget(self.maxBtn)
self.hBoxLayout.removeWidget(self.closeBtn)
# add window icon
self.iconLabel = QLabel(self)
self.iconLabel.setFixedSize(18, 18)
self.hBoxLayout.insertWidget(
0, self.iconLabel, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
)
self.window().windowIconChanged.connect(self.setIcon)
# add title label
self.titleLabel = QLabel(self)
self.hBoxLayout.insertWidget(
1, self.titleLabel, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
)
self.titleLabel.setObjectName("titleLabel")
self.window().windowTitleChanged.connect(self.setTitle)
self.vBoxLayout = QVBoxLayout()
self.buttonLayout = QHBoxLayout()
self.buttonLayout.setSpacing(0)
self.buttonLayout.setContentsMargins(0, 0, 0, 0)
self.buttonLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.buttonLayout.addWidget(self.minBtn)
self.buttonLayout.addWidget(self.maxBtn)
self.buttonLayout.addWidget(self.closeBtn)
self.vBoxLayout.addLayout(self.buttonLayout)
self.vBoxLayout.addStretch(1)
self.hBoxLayout.addLayout(self.vBoxLayout, 0)
FluentStyleSheet.FLUENT_WINDOW.apply(self)
def setTitle(self, title):
self.titleLabel.setText(title)
self.titleLabel.adjustSize()
def setIcon(self, icon):
self.iconLabel.setPixmap(QIcon(icon).pixmap(18, 18))
class CustomFluentWindow(CustomFluentWindowBase):
"""Fluent window"""
def __init__(self, parent=None):
super().__init__(parent)
self.setTitleBar(FluentTitleBar(self))
self.navigationInterface = NavigationInterface(self, showReturnButton=True)
self.widgetLayout = QHBoxLayout()
# initialize layout
self.hBoxLayout.addWidget(self.navigationInterface)
self.hBoxLayout.addLayout(self.widgetLayout)
self.hBoxLayout.setStretchFactor(self.widgetLayout, 1)
self.widgetLayout.addWidget(self.stackedWidget)
self.widgetLayout.setContentsMargins(0, 48, 0, 0)
self.navigationInterface.displayModeChanged.connect(self.titleBar.raise_)
self.titleBar.raise_()
def addSubInterface(
self,
interface: QWidget,
icon: Union[FluentIconBase, QIcon, str],
text: str,
position=NavigationItemPosition.TOP,
parent=None,
isTransparent=False,
) -> NavigationTreeWidget:
"""add sub interface, the object name of `interface` should be set already
before calling this method
Parameters
----------
interface: QWidget
the subinterface to be added
icon: FluentIconBase | QIcon | str
the icon of navigation item
text: str
the text of navigation item
position: NavigationItemPosition
the position of navigation item
parent: QWidget
the parent of navigation item
isTransparent: bool
whether to use transparent background
"""
if not interface.objectName():
raise ValueError("The object name of `interface` can't be empty string.")
if parent and not parent.objectName():
raise ValueError("The object name of `parent` can't be empty string.")
interface.setProperty("isStackedTransparent", isTransparent)
self.stackedWidget.addWidget(interface)
# add navigation item
routeKey = interface.objectName()
item = self.navigationInterface.addItem(
routeKey=routeKey,
icon=icon,
text=text,
onClick=lambda: self.switchTo(interface),
position=position,
tooltip=text,
parentRouteKey=parent.objectName() if parent else None,
)
# initialize selected item
if self.stackedWidget.count() == 1:
self.stackedWidget.currentChanged.connect(self._onCurrentInterfaceChanged)
self.navigationInterface.setCurrentItem(routeKey)
qrouter.setDefaultRouteKey(self.stackedWidget, routeKey)
self._updateStackedBackground()
return item
def removeInterface(self, interface, isDelete=False):
self.navigationInterface.removeWidget(interface.objectName())
self.stackedWidget.removeWidget(interface)
interface.hide()
if isDelete:
interface.deleteLater()
def resizeEvent(self, e):
self.titleBar.move(46, 0)
self.titleBar.resize(self.width() - 46, self.titleBar.height())

View File

@@ -0,0 +1,42 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from qfluentwidgets import ScrollArea
from app.view.components.file_deal_cards import DownloadCard
class DownloadScrollWidget(ScrollArea):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.scrollWidget = QWidget()
self.vBoxLayout = QVBoxLayout(self.scrollWidget)
self.__initWidget()
def __initWidget(self):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setObjectName("DownloadScrollWidget")
self.scrollWidget.setObjectName("scrollWidget")
self.scrollWidget.setStyleSheet("background:transparent;border:none;")
self.setStyleSheet("background:transparent;border:none;")
self.__initLayout()
def __initLayout(self):
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
def addDownloadTask(self, suffix, fileName, _id):
self.vBoxLayout.addWidget(
DownloadCard(
suffix,
fileName,
_id,
self.scrollWidget,
)
)

View File

@@ -0,0 +1,117 @@
from loguru import logger
from PyQt6.QtCore import QRegularExpression, Qt, pyqtSignal
from PyQt6.QtGui import (
QRegularExpressionValidator,
)
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets import (
CheckBox,
LineEdit,
PasswordLineEdit,
PushButton,
)
from app.core import CaptchaThread,cfg, qconfig
class LoginWidget(QWidget):
loginSignal = pyqtSignal(dict)
def __init__(self, parent=None):
logger.debug("初始化登录组件")
super().__init__(parent)
self.setObjectName("LoginWidget")
self.setWindowTitle("LeonPan")
self.emailLineEdit = LineEdit(self)
self.emailLineEdit.setPlaceholderText("请输入邮箱")
self.passwordLineEdit = PasswordLineEdit(self)
self.passwordLineEdit.setPlaceholderText("请输入密码")
self.rememberMeCheckBox = CheckBox("记住我", self)
self.rememberMeCheckBox.checkStateChanged.connect(
lambda: qconfig.set(cfg.rememberMe, self.rememberMeCheckBox.isChecked())
)
self.loginButton = PushButton("登录", self)
self.loginButton.setDisabled(False)
self.verificationCodeLabel = QLabel(self)
self.verificationCodeLabel.setFixedSize(120, 35)
self.verificationCodeLabel.setScaledContents(True) # 设置图片自适应
self.verificationCodeLabel.mousePressEvent = (
self.refreshVerificationCode
) # 绑定点击事件
self.verificationCodeLineEdit = LineEdit(self)
self.verificationCodeLineEdit.setPlaceholderText("请输入验证码")
self.verificationLayout = QHBoxLayout()
self.verificationLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
self.verificationLayout.addWidget(self.verificationCodeLineEdit)
self.verificationLayout.addWidget(self.verificationCodeLabel)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addSpacing(10)
self.vBoxLayout.addWidget(self.emailLineEdit)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(self.passwordLineEdit)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addLayout(self.verificationLayout)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(
self.rememberMeCheckBox, 0, Qt.AlignmentFlag.AlignLeft
)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(self.loginButton)
email_regex = QRegularExpression(
r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
)
# TODO: 内测时用的邮箱匹配
# email_regex = QRegularExpression(r"^[a-zA-Z0-9_.+-]+@miaostars\.cn$")
validator = QRegularExpressionValidator(email_regex, self)
self.emailLineEdit.setValidator(validator)
self.emailLineEdit.textChanged.connect(self.checkEmail)
self.refreshVerificationCode()
self.rememberMe()
logger.debug("登录组件初始化完成")
def rememberMe(self):
logger.debug("检查记住我选项")
if qconfig.get(cfg.rememberMe):
logger.debug("已启用记住我功能,填充保存的邮箱和密码")
self.emailLineEdit.setText(qconfig.get(cfg.email))
self.passwordLineEdit.setText(qconfig.get(cfg.activationCode))
self.rememberMeCheckBox.setChecked(True)
else:
logger.debug("已禁用记住我功能,清空保存的邮箱和密码")
self.emailLineEdit.clear()
self.passwordLineEdit.clear()
def checkEmail(self, text):
# 检查当前输入是否通过验证器
state, _, _ = self.emailLineEdit.validator().validate(text, 0)
if state == QRegularExpressionValidator.State.Acceptable:
logger.debug("邮箱格式验证通过")
self.loginButton.setDisabled(False)
else:
self.loginButton.setDisabled(True)
def refreshVerificationCode(self, event=None):
logger.debug("刷新验证码")
self.verificationCodeLabel.setEnabled(False)
self.captchaThread = CaptchaThread()
self.captchaThread.captchaReady.connect(self._showVerificationCode)
self.captchaThread.captchaFailed.connect(self._showCaptchaFailed)
self.captchaThread.start()
def _showVerificationCode(self, pixmap):
logger.debug("显示验证码")
self.verificationCodeLabel.setEnabled(True)
self.verificationCodeLabel.setPixmap(pixmap)
def _showCaptchaFailed(self, message):
logger.debug(f"验证码刷新失败:{message}")
self.verificationCodeLabel.setEnabled(True)
self.verificationCodeLineEdit.clear()

View File

@@ -0,0 +1,142 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from qfluentwidgets import (
InfoBar,
InfoBarPosition,
LineEdit,
MessageBoxBase,
SubtitleLabel,
)
from app.core import CreateFolderThread, signalBus
class NewFolderMessageBox(MessageBoxBase):
"""新建文件夹对话框"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self._setupUi()
self._connectSignals()
# 线程引用,防止被垃圾回收
self.createFolderThread = None
def _setupUi(self):
"""设置UI界面"""
self.titleLabel = SubtitleLabel("新建文件夹", self)
self.nameLineEdit = LineEdit(self)
self.nameLineEdit.setPlaceholderText("请输入文件夹名称")
self.nameLineEdit.setClearButtonEnabled(True)
# 设置对话框属性
self.widget.setMinimumWidth(400)
self.yesButton.setText("新建")
self.cancelButton.setText("取消")
# 添加组件到布局
self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.addWidget(self.nameLineEdit)
# 初始时禁用确认按钮
self.yesButton.setEnabled(False)
def _connectSignals(self):
"""连接信号槽"""
self.yesButton.clicked.connect(self._onCreateClicked)
self.nameLineEdit.textChanged.connect(self._onTextChanged)
self.nameLineEdit.returnPressed.connect(self._onReturnPressed)
def _onTextChanged(self, text):
"""文本框内容变化时的处理"""
# 检查文件夹名称是否有效
is_valid = bool(text.strip()) and not any(char in text for char in '/\\:*?"<>|')
self.yesButton.setEnabled(is_valid)
if not is_valid and text.strip():
self.nameLineEdit.setToolTip('文件夹名称不能包含 /\\:*?"<>| 等特殊字符')
else:
self.nameLineEdit.setToolTip("")
def _onReturnPressed(self):
"""回车键处理"""
if self.yesButton.isEnabled():
self._onCreateClicked()
def _onCreateClicked(self):
"""创建文件夹按钮点击处理"""
folder_name = self.nameLineEdit.text().strip()
if not folder_name:
return
# 禁用按钮防止重复点击
self._setUiEnabled(False)
self.yesButton.setText("创建中...")
# 创建并启动线程
self.createFolderThread = CreateFolderThread(folder_name)
self.createFolderThread.successSignal.connect(self._onCreateSuccess)
self.createFolderThread.errorSignal.connect(self._onCreateError)
self.createFolderThread.start()
def _setUiEnabled(self, enabled):
"""设置UI启用状态"""
self.yesButton.setEnabled(enabled)
self.cancelButton.setEnabled(enabled)
self.nameLineEdit.setEnabled(enabled)
def _onCreateSuccess(self):
"""创建成功处理"""
self._showInfoBar("success", "操作成功", "新建文件夹成功")
signalBus.refreshFolderListSignal.emit()
"""线程完成时的清理工作"""
self.yesButton.setText("新建")
self._setUiEnabled(True)
self.accept()
def _onCreateError(self, error_msg):
"""创建失败处理"""
self._showInfoBar("error", "操作失败", error_msg)
# 不关闭对话框,让用户有机会修改后重试
self.nameLineEdit.setFocus()
self.nameLineEdit.selectAll()
"""线程完成时的清理工作"""
self.yesButton.setText("新建")
self._setUiEnabled(True)
def _showInfoBar(self, type_, title, content):
"""显示信息栏"""
if type_ == "success":
InfoBar.success(
title=title,
content=content,
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=2000,
parent=self.window(),
)
else:
InfoBar.error(
title=title,
content=content,
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=3000, # 错误信息显示稍长时间
parent=self.window(),
)
def showEvent(self, event):
"""显示事件处理"""
super().showEvent(event)
self.nameLineEdit.setFocus()
def closeEvent(self, event):
"""关闭事件处理"""
# 确保线程安全退出
if self.createFolderThread and self.createFolderThread.isRunning():
self.createFolderThread.quit()
self.createFolderThread.wait(1000) # 等待1秒
super().closeEvent(event)

View File

@@ -0,0 +1,297 @@
from loguru import logger
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import (
QHBoxLayout,
QWidget,
)
from qfluentwidgets import (Action, InfoBar, InfoBarPosition, LineEdit, MenuAnimationType, PillPushButton,
PrimarySplitPushButton, PushButton, RoundMenu, ScrollArea)
from app.core import DeleteTagThread, userConfig, lang
from app.view.widgets.add_tag_messageBox import AddTagMessageBox
class TagsScrollArea(ScrollArea):
"""标签滚动区域组件,支持动态添加和移除标签,支持单选模式"""
# 信号:标签被点击时发出,传递标签文本
tagClicked = pyqtSignal(str, str)
TAG_TYPES = {"video": "视频", "doc": "文档", "image": "图片", "audio": "音乐"}
def __init__(self, parent=None):
super().__init__(parent)
self.tagsDict = {} # 存储所有标签按钮
self.currentCheckedTag = None # 当前选中的标签ID
self.setupUi()
logger.debug("初始化标签滚动区域组件")
def setupUi(self):
"""初始化UI"""
self.widgets = QWidget()
self.layouts = QHBoxLayout(self.widgets)
self.setWidget(self.widgets)
self.setWidgetResizable(True)
self.setMaximumWidth(400)
# 设置布局属性
self.layouts.setContentsMargins(0, 0, 0, 0)
self.layouts.setSpacing(10)
# 初始化默认标签
self.initDefaultTags()
# 设置样式
self.widgets.setStyleSheet("background-color: transparent; border: none;")
self.setStyleSheet("background-color: transparent; border: none;")
# 设置滚动策略
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# 安全地获取 tags 字段,如果不存在则使用空列表
self.tags = userConfig.userData.get("data", {}).get("tags", [])
for tag in self.tags:
self.addTag(tag["id"], tag["name"])
self.tagsDict[tag["id"]] = tag["name"]
def initDefaultTags(self):
"""初始化默认标签"""
logger.debug("初始化默认标签")
for tagId, tagText in self.TAG_TYPES.items():
self.addTag(tagId, tagText)
def addTag(self, tagId, text):
"""添加一个新标签
Args:
tagId: 标签的唯一标识符
text: 标签显示的文本
Returns:
创建的标签按钮对象
"""
self.tagsDict[tagId] = text
logger.debug(f"添加新标签: {tagId} - {text}")
tagBtn = PillPushButton(text, self.widgets)
tagBtn.setObjectName(f"tag_{tagId}")
tagBtn.setCheckable(True) # 设置为可选中状态
tagBtn.setContextMenuPolicy(
Qt.ContextMenuPolicy.CustomContextMenu
) # 启用自定义上下文菜单
# 连接点击信号,实现单选逻辑
tagBtn.clicked.connect(
lambda checked, tid=tagId: self.onTagClicked(tid, checked)
)
# 连接右键菜单信号
tagBtn.customContextMenuRequested.connect(
lambda pos, tid=tagId: self.onTagRightClicked(tid, pos)
)
self.layouts.addWidget(tagBtn)
return tagBtn
def onTagClicked(self, tagId, checked):
"""处理标签点击事件,实现单选逻辑"""
if checked:
# 如果当前点击的标签被选中,取消之前选中的标签
if self.currentCheckedTag and self.currentCheckedTag != tagId:
# 找到之前选中的标签并取消选中
previousTagBtn = self.findChild(
QWidget, f"tag_{self.currentCheckedTag}"
)
if previousTagBtn:
previousTagBtn.setChecked(False)
# 更新当前选中的标签
self.currentCheckedTag = tagId
if tagId in ["video", "doc", "image", "audio"]:
self.tagClicked.emit("internalTag", tagId)
else:
self.tagClicked.emit("externalTag", tagId)
logger.debug(f"选中标签: {tagId}")
else:
# 如果取消选中当前标签,清空当前选中的标签
if self.currentCheckedTag == tagId:
self.currentCheckedTag = None
logger.debug(f"取消选中标签: {tagId}")
# 发出标签点击信号
def onTagRightClicked(self, tagId, pos):
"""处理标签右键点击事件"""
logger.debug(f"标签被右键点击: {tagId}")
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
if tagBtn:
global_pos = tagBtn.mapToGlobal(pos)
if tagBtn.text() in ["视频", "文档", "图片", "音乐"]:
return
menu = RoundMenu(parent=self)
menu.addAction(Action(lang("删除"), triggered=lambda: self.deleteTag(tagId)))
menu.exec(global_pos, aniType=MenuAnimationType.DROP_DOWN)
def deleteTag(self, tagId):
self.deleteTagThread = DeleteTagThread(tagId)
self.deleteTagThread.successDeleteSignal.connect(
lambda: self._onTagDeleteError(tagId)
)
self.deleteTagThread.errorSignal.connect(self._onTagDeleteError)
self.deleteTagThread.start()
def _onTagDeleteError(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def _onTagDeleteError(self, tagId):
self.removeTag(tagId)
InfoBar.success(
"成功",
"标签删除成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
def removeTag(self, tagId):
"""移除指定标签"""
if tagId in self.tagsDict:
logger.debug(f"移除标签: {tagId}")
# 如果移除的是当前选中的标签,清空选中状态
if self.currentCheckedTag == tagId:
self.currentCheckedTag = None
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
if tagBtn:
self.layouts.removeWidget(tagBtn)
tagBtn.deleteLater()
del self.tagsDict[tagId]
else:
logger.warning(f"尝试移除不存在的标签: {tagId}")
def getCheckedTag(self):
"""获取当前选中的标签ID"""
return self.currentCheckedTag
def setCheckedTag(self, tagId):
"""设置指定标签为选中状态"""
if tagId in self.tagsDict:
tagBtn = self.findChild(QWidget, f"tag_{tagId}")
if tagBtn:
tagBtn.setChecked(True)
# onTagClicked 方法会自动处理单选逻辑
else:
logger.warning(f"尝试选中不存在的标签: {tagId}")
def clearChecked(self):
"""清除所有选中状态"""
if self.currentCheckedTag:
tagBtn = self.findChild(QWidget, f"tag_{self.currentCheckedTag}")
if tagBtn:
tagBtn.setChecked(False)
self.currentCheckedTag = None
class TagWidget(QWidget):
"""标签管理组件"""
def __init__(self, parent=None):
super().__init__(parent)
self.tagScrollArea = TagsScrollArea(self)
self.addPushButton = PushButton(lang("添加标签"), self)
logger.debug("初始化标签管理组件")
self.setupUi()
self.connectSignals()
def setupUi(self):
"""初始化UI"""
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.addWidget(self.tagScrollArea)
self.hBoxLayout.addSpacing(10)
self.hBoxLayout.addWidget(self.addPushButton, Qt.AlignmentFlag.AlignRight)
def connectSignals(self):
"""连接信号与槽"""
logger.debug("连接标签管理组件信号")
self.addPushButton.clicked.connect(self.addTag)
def addTag(self):
w = AddTagMessageBox(self.window())
w.successAddTagSignal.connect(self.onAddTagSuccess)
if w.exec():
...
def onAddTagSuccess(self, name, result):
"""处理标签添加成功事件"""
InfoBar.success(
"成功",
"标签添加成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
self.tagScrollArea.addTag(result["data"], name)
class SearchWidget(QWidget):
"""搜索组件"""
# 信号:搜索请求时发出,传递搜索关键词
searchRequested = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.searchLineEdit = LineEdit(self)
self.searchButton = PrimarySplitPushButton(lang("仓内搜索"), self)
self.menu = RoundMenu(parent=self)
self.menu.addAction(
Action(
lang("仓内搜索"),
triggered=lambda: self.changeButtonText(lang("仓内搜索")),
)
)
self.menu.addAction(
Action(
lang("站内搜索"),
triggered=lambda: self.changeButtonText(lang("站内搜索")),
)
)
self.searchButton.setFlyout(self.menu)
logger.debug("初始化搜索组件")
self.setupUi()
def changeButtonText(self, text):
self.searchButton.setText(text)
def setupUi(self):
"""初始化UI"""
self.searchLineEdit.setPlaceholderText(lang("搜索文件"))
self.hBoxLayout = QHBoxLayout(self)
self.hBoxLayout.setContentsMargins(0, 24, 0, 0)
self.hBoxLayout.addWidget(self.searchLineEdit)
self.hBoxLayout.addWidget(self.searchButton)

View File

@@ -0,0 +1,91 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from qfluentwidgets import (
BreadcrumbBar,
setFont,
)
from app.view.components.linkage_switching import OwnFileLinkageSwitching
class OwnFileScrollWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.currentPath = "/"
self.breadcrumbBar = BreadcrumbBar(self)
self.ownFileLinkageSwitching = OwnFileLinkageSwitching("/", self)
self.__initWidget()
def __initWidget(self):
self.setObjectName("OwnFileScrollWidget")
self.breadcrumbBar.addItem("/", "/")
setFont(self.breadcrumbBar, 18)
self.breadcrumbBar.currentItemChanged.connect(self.clickChangeDir)
self.breadcrumbBar.setSpacing(15)
self.__initLayout()
def __initLayout(self):
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.setSpacing(5)
self.vBoxLayout.addWidget(self.breadcrumbBar, 0, Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.addWidget(self.ownFileLinkageSwitching)
def loadDict(self, paths):
self.ownFileLinkageSwitching.loadDict(paths)
def onChangeDir(self, path):
"""处理目录变更"""
logger.info(f"变更目录: {path}")
if path == "":
self.currentPath = "/"
else:
self.currentPath = path
self.ownFileLinkageSwitching.loadDict(path)
# 更新面包屑导航
displayName = path.split("/")[-1] if path != "/" else "/"
self.breadcrumbBar.addItem(displayName, displayName)
def clickChangeDir(self, name):
"""处理面包屑导航项点击事件"""
logger.info(f"面包屑导航项点击: {name}")
# 获取点击的路径
if name == "":
name = "/"
pathList = []
for item in self.breadcrumbBar.items:
if item.text == name:
pathList.append(item.text)
break
else:
pathList.append(item.text)
for i in pathList:
if i == "":
pathList.remove(i)
path = "/".join(pathList) if pathList else "/"
path = path[1:]
if path == "":
path = "/"
if path == self.currentPath:
logger.debug("路径未变化,跳过导航")
return
self.onChangeDir(path)
def refreshCurrentDirectory(self):
"""刷新当前目录的文件卡片"""
logger.info(f"刷新当前目录: {self.currentPath}")
self.loadDict(self.currentPath)

View File

@@ -0,0 +1,162 @@
# coding: utf-8
# flip bookmark
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QIntValidator
from PyQt6.QtWidgets import QHBoxLayout
from qfluentwidgets import (BodyLabel, CardWidget, FluentIcon, LineEdit, PushButton, ToolButton)
class PageFlipWidget(CardWidget):
# 定义页码变化信号
pageChangeSignal = pyqtSignal(int)
"""
page : 总页码,默认为10
currentPage : 当前页码
numberButtonList : 页码按钮列表
currentButtonNumbet : 当前显示的按钮数字列表
"""
def __init__(self, parent=None, page=10):
super().__init__(parent)
self.page = page
self.currentPage = 1
self.numberButtonList = []
self.currentButtonNumber = []
# 定义翻页按钮
self.leftPageButton = ToolButton(self)
self.rightPageButton = ToolButton(self)
# 定义跳转页面组件
self.pageLineEdit = LineEdit(self)
self.allPageLabel = BodyLabel(self)
self.turnPageButton = PushButton(self)
self.__initWidget()
# 动态设置按钮
self._addButton()
def __initWidget(self):
# 组件设置
self.leftPageButton.setIcon(FluentIcon.PAGE_LEFT)
self.leftPageButton.setFixedSize(40, 40)
self.leftPageButton.clicked.connect(self.backPage)
self.rightPageButton.setIcon(FluentIcon.PAGE_RIGHT)
self.rightPageButton.setFixedSize(40, 40)
self.rightPageButton.clicked.connect(self.forwardPage)
self.pageLineEdit.setText("1")
self.pageLineEdit.setValidator(QIntValidator())
self.pageLineEdit.editingFinished.connect(self._validator)
self.pageLineEdit.setFixedWidth(45)
self.allPageLabel.setText(f"/{self.page}")
self.turnPageButton.setText(self.tr("jump"))
self.turnPageButton.setFixedWidth(60)
self.turnPageButton.clicked.connect(self.turnPage)
self.__initLayout()
def __initLayout(self):
# 布局设置
self.layouts = QHBoxLayout(self)
self.layouts.addWidget(self.leftPageButton)
self.layouts.addWidget(self.rightPageButton)
self.layouts.addSpacing(30)
self.layouts.addWidget(self.pageLineEdit)
self.layouts.addWidget(self.allPageLabel)
self.layouts.addWidget(self.turnPageButton)
# 向后翻页
def forwardPage(self):
if self.page > 5:
if int(self.numberButtonList[-1].text()) < self.page:
for i in self.numberButtonList:
i.setText(str(int(i.text()) + 1))
for i in range(len(self.currentButtonNumber)):
self.currentButtonNumber[i] += 1
if self.currentPage < self.page:
self.currentPage += 1
self.pageLineEdit.setText(str(self.currentPage))
self.pageChangeSignal.emit(self.currentPage)
# 向前翻页
def backPage(self):
if int(self.numberButtonList[0].text()) > 1:
for i in self.numberButtonList:
i.setText(str(int(i.text()) - 1))
for i in range(len(self.currentButtonNumber)):
self.currentButtonNumber[i] -= 1
if self.currentPage > 1:
self.currentPage -= 1
self.pageLineEdit.setText(str(self.currentPage))
self.pageChangeSignal.emit(self.currentPage)
# 跳转页面
def turnPage(self, page: int):
"""
page : 目标跳转页
"""
page = int(self.pageLineEdit.text())
numberList = [1, 2, 3, 4, 5]
if page not in numberList:
while True:
numberList = [x + 1 for x in numberList]
if page in numberList and max(numberList) <= self.page:
break
self.currentButtonNumber = numberList
for i in self.numberButtonList:
i.setText(str(self.currentButtonNumber[self.numberButtonList.index(i)]))
self.pageChangeSignal.emit(page)
self.currentPage = page
# 输入框判断器,规定只可输入数字,并且数字不能超过规定范围
def _validator(self):
page = int(self.pageLineEdit.text())
if page <= 0:
page = 1
elif page > self.page:
page = self.page
self.pageLineEdit.setText(str(page))
# 动态添加按钮
def _addButton(self):
if self.page >= 5:
for i in range(1, 6):
numberButton = PushButton(str(i), self)
numberButton.setFixedSize(40, 40)
numberButton.clicked.connect(self._pageChanged)
self.layouts.insertWidget(i, numberButton)
self.numberButtonList.append(numberButton)
self.currentButtonNumber = [1, 2, 3, 4, 5]
else:
for i in range(1, self.page + 1):
numberButton = PushButton(str(i), self)
numberButton.setFixedSize(40, 40)
numberButton.clicked.connect(self._pageChanged)
self.layouts.insertWidget(i, numberButton)
self.numberButtonList.append(numberButton)
self.currentButtonNumber.append(i)
# 页面翻页时发出信号
def _pageChanged(self, checked):
sender = self.sender()
if sender:
button_text = sender.text()
self.pageChangeSignal.emit(int(button_text))
self.pageLineEdit.setText(button_text)
self.currentPage = int(button_text)

View File

@@ -0,0 +1,165 @@
# coding: utf-8
import logging
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtWidgets import QListWidgetItem
from qfluentwidgets import (
InfoBar,
InfoBarPosition,
ListWidget,
MessageBoxBase,
SubtitleLabel,
)
from app.core import ChangePolicyThread, GetPoliciesThread, policyConfig, signalBus
class PolicyChooseMessageBox(MessageBoxBase):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.currentPath = "/"
self.policyDict = {}
self.isLoading = True
self.originalTitle = "选择存储策略"
self.setClosableOnMaskClicked(True)
self.setupUI()
# 开始获取策略列表
self.getPoliciesThread = GetPoliciesThread()
self.getPoliciesThread.successGetSignal.connect(self.refreshPolicyList)
self.getPoliciesThread.errorSignal.connect(self._handleGetPoliciesError)
self.getPoliciesThread.start()
# 初始化更改策略线程(但不启动)
self.changePolicyThread = None
def setupUI(self):
"""设置UI界面"""
self.titleLabel = SubtitleLabel(self.originalTitle, self)
self.policyListWidget = ListWidget(self)
# 添加加载提示
self.loadingItem = QListWidgetItem("正在加载策略列表...")
self.policyListWidget.addItem(self.loadingItem)
self.viewLayout.addWidget(self.titleLabel)
self.viewLayout.addWidget(self.policyListWidget)
# 隐藏确定取消按钮组
self.buttonGroup.hide()
def connectSignals(self):
"""连接信号与槽"""
self.policyListWidget.currentTextChanged.connect(self.onPolicyChanged)
self.policyListWidget.itemClicked.connect(self.selfClicked)
def selfClicked(self, listWidget: QListWidgetItem):
if listWidget.text() == policyConfig.returnPolicy()["name"]:
QTimer.singleShot(100, self.accept)
def onPolicyChanged(self, text):
"""处理策略更改"""
if not text or self.isLoading or text == "正在加载策略列表...":
return
policy_id = self.policyDict.get(text)
if not policy_id:
return
# 如果已经有更改线程在运行,先停止它
if self.changePolicyThread and self.changePolicyThread.isRunning():
self.changePolicyThread.quit()
self.changePolicyThread.wait()
# 创建并启动新的更改线程
self.changePolicyThread = ChangePolicyThread(self.currentPath, policy_id)
self.changePolicyThread.successChangedSignal.connect(
self._handlePolicyChangeSuccess
)
self.changePolicyThread.errorSignal.connect(self._handlePolicyChangeError)
self.changePolicyThread.start()
# 更新UI状态 - 只更改标题
self._setLoadingState(True, f"正在切换到策略: {text}")
def refreshPolicyList(self, policiesList):
"""刷新策略列表"""
self.isLoading = False
self.policyListWidget.clear()
self.policyDict.clear()
currentPolicy = policyConfig.returnPolicy()
currentIndex = 0
for i, policy in enumerate(policiesList):
self.policyListWidget.addItem(QListWidgetItem(policy["name"]))
self.policyDict[policy["name"]] = policy["id"]
if policy["id"] == currentPolicy["id"]:
currentIndex = i
# 设置当前选中项
if self.policyListWidget.count() > 0:
self.policyListWidget.setCurrentRow(currentIndex)
self.currentPath = policyConfig.returnCurrentPath()
self.connectSignals()
# 恢复原始标题
self.titleLabel.setText(self.originalTitle)
def _handleGetPoliciesError(self, error_msg):
"""处理获取策略列表错误"""
self.policyListWidget.clear()
errorItem = QListWidgetItem(f"加载失败: {error_msg}")
self.policyListWidget.addItem(errorItem)
self.isLoading = False
# 恢复原始标题
self.titleLabel.setText(self.originalTitle)
def _handlePolicyChangeSuccess(self):
"""处理策略更改成功"""
self._setLoadingState(False)
# 显示成功提示
if self.parent():
InfoBar.success(
title="操作成功",
content="存储策略已成功更改",
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=2000,
parent=self.window(),
)
signalBus.refreshFolderListSignal.emit()
QTimer.singleShot(1000, self.accept)
def _handlePolicyChangeError(self, error_msg):
"""处理策略更改错误"""
self._setLoadingState(False)
# 显示错误提示
if self.parent():
InfoBar.error(
title="操作失败",
content=f"更改策略时出错: {error_msg}",
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=3000,
parent=self.window(),
)
QTimer.singleShot(1000, self.reject)
def _setLoadingState(self, loading, message=None):
"""设置加载状态"""
if loading:
self.policyListWidget.setEnabled(False)
if message:
logging.info(message)
else:
self.policyListWidget.setEnabled(True)
# 恢复原始标题
self.titleLabel.setText(self.originalTitle)

View File

@@ -0,0 +1,292 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QPixmap
from qfluentwidgets import (
ImageLabel,
InfoBar,
InfoBarPosition,
IndeterminateProgressBar,
MessageBoxBase,
PlainTextEdit,
PushButton,
)
from app.core import (ImageLoaderThread, TextLoaderThread, UpdateFileContentThread)
from app.core.services.text_speech import LocalSpeechController
from app.view.components.empty_card import EmptyCard
# 图片预览类
def createThumbnail(pixmap, max_size=200):
"""创建快速缩略图"""
if pixmap.isNull():
return pixmap
# 使用快速缩放算法
return pixmap.scaled(
max_size,
max_size,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.FastTransformation,
)
class OptimizedPreviewBox(MessageBoxBase):
def __init__(self, parent=None, url=None):
super().__init__(parent=parent)
self.widget.setMinimumSize(500, 500)
self.original_pixmap = None
self.current_scale = 1.0
# 加载状态显示
self.loadingCard = EmptyCard(self)
self.loadingCard.load()
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
# 图片显示标签
self.previewLabel = ImageLabel(self)
self.previewLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.previewLabel.setScaledContents(False) # 重要:禁用自动缩放
self.viewLayout.addWidget(self.previewLabel, 0, Qt.AlignmentFlag.AlignCenter)
# 使用优化的图片加载线程
self.imageLoaderThread = ImageLoaderThread(url)
self.imageLoaderThread.imageLoaded.connect(self.setPreviewImg)
self.imageLoaderThread.errorOccurred.connect(self.handleError)
self.imageLoaderThread.progressUpdated.connect(self.updateProgress)
# 延迟启动加载避免阻塞UI初始化
from PyQt6.QtCore import QTimer
QTimer.singleShot(100, self.startLoading)
def startLoading(self):
"""开始加载图片"""
self.imageLoaderThread.start()
def updateProgress(self, progress):
"""更新加载进度"""
self.loadingCard.setText(f"正在加载图片... {progress}%")
def setPreviewImg(self, img: QPixmap):
"""设置预览图片"""
self.loadingCard.hide()
self.original_pixmap = img
# 立即显示缩略图
thumbnail = createThumbnail(img)
self.previewLabel.setPixmap(thumbnail)
# 然后异步加载高质量版本
self.adjustImageSize()
def resizeEvent(self, event):
"""重写窗口大小改变事件"""
super().resizeEvent(event)
if self.original_pixmap and not self.original_pixmap.isNull():
# 使用定时器延迟调整,避免频繁调整
from PyQt6.QtCore import QTimer
QTimer.singleShot(50, self.adjustImageSize)
def adjustImageSize(self):
"""根据窗口大小动态调整图片尺寸"""
if not self.original_pixmap or self.original_pixmap.isNull():
return
# 获取可用显示区域大小
margin = 80
available_width = self.width() - margin * 2
available_height = self.height() - margin * 2
# 获取原始图片尺寸
original_width = self.original_pixmap.width()
original_height = self.original_pixmap.height()
# 计算缩放比例
width_ratio = available_width / original_width
height_ratio = available_height / original_height
scale_ratio = min(width_ratio, height_ratio, 1.0)
# 只在需要时重新缩放
if abs(scale_ratio - self.current_scale) > 0.05: # 变化超过5%才重新缩放
self.current_scale = scale_ratio
new_width = int(original_width * scale_ratio)
new_height = int(original_height * scale_ratio)
# 使用平滑缩放
scaled_pixmap = self.original_pixmap.scaled(
new_width,
new_height,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
self.previewLabel.setPixmap(scaled_pixmap)
def handleError(self, msg):
"""处理加载错误"""
self.loadingCard.error()
self.previewLabel.hide()
# 文本文档预览类
class PreviewTextBox(MessageBoxBase):
"""文本预览对话框"""
def __init__(self, parent=None, url=None, _id=None):
super().__init__(parent=parent)
self.updateTxtThread = None
self.widget.setMinimumSize(600, 400)
self._id = _id
self.isChanged = False
self.speech_controller = LocalSpeechController(self)
self.textSpeakButton = PushButton("朗读文本", self)
self.textSpeakButton.hide()
self.isSpeaking = False
self.textSpeakButton.clicked.connect(self.playTextSpeech)
self.viewLayout.addWidget(
self.textSpeakButton,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
# 创建文本编辑框
self.textEdit = PlainTextEdit(self)
self.textEdit.hide()
self.textEdit.setLineWrapMode(PlainTextEdit.LineWrapMode.NoWrap) # 不自动换行
# 设置等宽字体,便于阅读代码或日志
from PyQt6.QtGui import QFont
font = QFont("微软雅黑", 10) # 等宽字体
self.textEdit.setFont(font)
self.viewLayout.addWidget(self.textEdit)
# 加载状态显示
self.loadingCard = EmptyCard(self)
self.loadingCard.load()
self.viewLayout.addWidget(self.loadingCard, 0, Qt.AlignmentFlag.AlignCenter)
# 使用文本加载线程
self.textLoaderThread = TextLoaderThread(url)
self.textLoaderThread.textLoaded.connect(self.setTextContent)
self.textLoaderThread.errorOccurred.connect(self.handleError)
self.textLoaderThread.progressUpdated.connect(self.updateProgress)
self.yesButton.hide()
# 创建保存按钮
self.saveButton = PushButton("保存修改", self)
# 创建进度条
self.saveProgressBar = IndeterminateProgressBar(self)
self.saveProgressBar.setFixedHeight(4)
self.saveProgressBar.hide()
# 添加按钮和进度条到布局
self.buttonLayout.insertWidget(
0, self.saveButton, 1, Qt.AlignmentFlag.AlignVCenter
)
self.buttonLayout.insertWidget(
1, self.saveProgressBar, 1, Qt.AlignmentFlag.AlignVCenter
)
self.saveButton.setEnabled(False)
self.saveButton.clicked.connect(self.saveText)
self.cancelButton.setText("返回")
# 延迟启动加载避免阻塞UI初始化
QTimer.singleShot(100, self.startLoading)
def saveText(self):
logger.info("保存用户修改")
# 显示进度条并禁用按钮
self.saveProgressBar.show()
self.saveButton.setEnabled(False)
self.saveTextThread = UpdateFileContentThread(
self._id,
self.textEdit.toPlainText(),
)
self.saveTextThread.successUpdated.connect(self._successSave)
self.saveTextThread.errorUpdated.connect(self._errorSave)
self.saveTextThread.start()
def _successSave(self):
InfoBar.success(
"成功",
"修改保存成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 隐藏进度条
self.saveProgressBar.hide()
QTimer.singleShot(700, self.accept)
def _errorSave(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
# 隐藏进度条并重新启用按钮
self.saveProgressBar.hide()
self.saveButton.setEnabled(True)
def playTextSpeech(self):
"""播放文本语音"""
if not self.isSpeaking:
text = self.textEdit.toPlainText()
if text and len(text.strip()) > 0:
self.speech_controller.play_text(text)
self.isSpeaking = True
self.textSpeakButton.setText("暂停朗读")
else:
self.speech_controller.stop_playback()
self.isSpeaking = False
self.textSpeakButton.setText("朗读文本")
def startLoading(self):
"""开始加载文本"""
self.textLoaderThread.start()
def updateProgress(self, progress):
"""更新加载进度"""
self.loadingCard.setText(f"正在加载文本... {progress}%")
def setTextContent(self, content):
"""设置文本内容"""
self.loadingCard.hide()
self.textEdit.show()
self.textSpeakButton.show()
self.saveButton.setEnabled(True)
# 限制显示的内容长度,避免性能问题
max_display_length = 100000 # 最多显示10万个字符
if len(content) > max_display_length:
content = (
content[:max_display_length]
+ f"\n\n... (内容过长,已截断前{max_display_length}个字符,完整内容请下载文件查看)"
)
self.textEdit.setPlainText(content)
def handleError(self, error_msg):
"""处理加载错误"""
self.loadingCard.error()
def resizeEvent(self, event):
"""重写窗口大小改变事件"""
super().resizeEvent(event)
# 文本预览框会自动适应大小,无需特殊处理

View File

@@ -0,0 +1,86 @@
from loguru import logger
from PyQt6.QtCore import QRegularExpression, Qt
from PyQt6.QtGui import (
QRegularExpressionValidator,
)
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
from qfluentwidgets import (
LineEdit,
PasswordLineEdit,
PrimaryPushButton,
)
from app.core import CaptchaThread
class RegisterWidget(QWidget):
def __init__(self, parent=None):
logger.debug("初始化注册组件")
super().__init__(parent)
self.setObjectName("RegisterWidget")
self.emailLineEdit = LineEdit(self)
self.emailLineEdit.setPlaceholderText("请输入注册邮箱")
self.passwordLineEdit = PasswordLineEdit(self)
self.passwordLineEdit.setPlaceholderText("请输入密码")
self.confirmPasswordLineEdit = PasswordLineEdit(self)
self.confirmPasswordLineEdit.setPlaceholderText("请确认您的密码")
self.verificationCodeLabel = QLabel(self)
self.verificationCodeLabel.setFixedSize(120, 35)
self.verificationCodeLabel.setScaledContents(True) # 设置图片自适应
self.verificationCodeLabel.mousePressEvent = (
self.refreshVerificationCode
) # 绑定点击事件
self.verificationCodeLineEdit = LineEdit(self)
self.verificationCodeLineEdit.setPlaceholderText("请输入验证码")
self.verificationLayout = QHBoxLayout()
self.verificationLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
self.verificationLayout.addWidget(self.verificationCodeLineEdit)
self.verificationLayout.addWidget(self.verificationCodeLabel)
self.registerButton = PrimaryPushButton("注册", self)
self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.vBoxLayout.setSpacing(15)
self.vBoxLayout.addSpacing(15)
self.vBoxLayout.addWidget(self.emailLineEdit)
self.vBoxLayout.addWidget(self.passwordLineEdit)
self.vBoxLayout.addWidget(self.confirmPasswordLineEdit)
self.vBoxLayout.addLayout(self.verificationLayout)
self.vBoxLayout.addWidget(self.registerButton)
email_regex = QRegularExpression(
r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
)
validator = QRegularExpressionValidator(email_regex, self)
self.emailLineEdit.setValidator(validator)
self.emailLineEdit.textChanged.connect(self.checkeEmail)
logger.debug("注册组件初始化完成")
self.refreshVerificationCode()
def checkeEmail(self, text):
# 检查当前输入是否通过验证器
state, _, _ = self.emailLineEdit.validator().validate(text, 0)
if state == QRegularExpressionValidator.Acceptable:
logger.debug("注册邮箱格式验证通过")
self.registerButton.setDisabled(False)
else:
self.registerButton.setDisabled(True)
def refreshVerificationCode(self, event=None):
logger.debug("刷新验证码")
self.captchaThread = CaptchaThread()
self.captchaThread.captchaReady.connect(self._showVerificationCode)
self.captchaThread.captchaFailed.connect(self._showCaptchaFailed)
self.captchaThread.start()
def _showVerificationCode(self, pixmap):
logger.debug("显示验证码")
self.verificationCodeLabel.setPixmap(pixmap)
def _showCaptchaFailed(self, message):
logger.debug(f"验证码刷新失败:{message}")
self.verificationCodeLineEdit.clear()

View File

@@ -0,0 +1,147 @@
# coding: utf-8
from loguru import logger
from PyQt6.QtCore import Qt, QTimer, QUrl
from PyQt6.QtGui import QPixmap
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PyQt6.QtWidgets import QHBoxLayout
from qfluentwidgets import (BodyLabel, HorizontalSeparator, ImageLabel, InfoBar, InfoBarPosition, MessageBoxBase,
SubtitleLabel)
from app.core import formatDate, formatSize, GetShareFileInfoThread, signalBus
class ShareFileMessageBox(MessageBoxBase):
def __init__(self, _id, fileIcon=None, suffix="", parent=None):
super().__init__(parent=parent)
self.widget.setFixedWidth(350)
self.suffix = suffix
self._id = _id
self.fileTypeImageLabel = ImageLabel(parent=self)
self.fileTypeImageLabel.setImage(fileIcon)
self.fileTypeImageLabel.scaledToHeight(60)
self.fileTypeImageLabel.scaledToWidth(60)
self.fileNameLabel = SubtitleLabel(parent=self)
self.fileSizeLabel = BodyLabel(parent=self)
self.fileInformationLabel = BodyLabel(parent=self)
self.fileInformationLabel.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.userImageLabel = ImageLabel(":app/images/logo.png", parent=self)
self.userImageLabel.setBorderRadius(20, 20, 20, 20)
self.userImageLabel.setFixedSize(30, 30)
self.userImageLabel.scaledToHeight(30)
self.userImageLabel.scaledToWidth(30)
self.userNameLabel = SubtitleLabel(parent=self)
self.userLayout = QHBoxLayout()
self.userLayout.addWidget(self.userImageLabel)
self.userLayout.addWidget(self.userNameLabel)
self.viewLayout.addWidget(
self.fileTypeImageLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(
self.fileNameLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(
self.fileSizeLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(
self.fileInformationLabel,
0,
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter,
)
self.viewLayout.addWidget(HorizontalSeparator(parent=self))
self.viewLayout.addLayout(self.userLayout)
self.yesButton.setText("下载")
self.yesButton.clicked.connect(self.downloadFile)
self.cancelButton.setText("取消")
self.apiWorker = GetShareFileInfoThread(_id)
self.apiWorker.shareFileInfoSignal.connect(self.handleApiResponse)
self.apiWorker.errorSignal.connect(self.handleError)
self.apiWorker.start()
self.networkManager = QNetworkAccessManager(self)
self.networkManager.finished.connect(self.onAvatarDownloaded)
def downloadFile(self):
signalBus.addDownloadFileTask.emit(
f"share.{self.suffix}",
self.fileNameLabel.text(),
f"undefined/undefined.{self._id}",
)
self.accept()
def handleApiResponse(self, response_data):
response_data = response_data["data"]
self.fileNameLabel.setText(response_data["source"]["name"])
self.fileSizeLabel.setText(
f"大小: {formatSize(response_data['source']['size'])}"
)
infoLabel = f"创建时间: {formatDate(response_data['create_date'])}\n浏览次数: {response_data['views']}\n下载次数: {response_data['downloads']}"
self.fileInformationLabel.setText(infoLabel)
self.userNameLabel.setText(response_data["creator"]["nick"])
self.loadAvatarFromId(response_data["creator"]["key"])
def handleError(self, msg):
InfoBar.error(
"失败",
msg,
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.window(),
)
QTimer.singleShot(1000, self.accept)
def loadAvatarFromId(self, _id):
"""从网络URL加载头像"""
# 使用V4 API获取头像 - 假设格式变为/api/v4/user/avatar/{_id}/l
url = f"/user/avatar/{_id}/l"
logger.info(f"开始从网络加载头像")
request = QNetworkRequest(QUrl(url))
self.networkManager.get(request)
def onAvatarDownloaded(self, reply):
"""处理头像下载完成"""
if reply.error() == QNetworkReply.NetworkError.NoError:
# 读取下载的数据
data = reply.readAll()
# 创建QPixmap并加载数据
pixmap = QPixmap()
if pixmap.loadFromData(data):
# 更新头像
self.userImageLabel.setImage(pixmap)
logger.info("网络头像加载成功")
else:
logger.error("头像数据格式不支持")
else:
logger.error(f"头像下载失败: {reply.errorString()}")
pixmap = QPixmap(":app/images/logo.png")
self.userImageLabel.setImage(pixmap)
logger.info("使用默认头像")
self.userImageLabel.scaledToHeight(30)
self.userImageLabel.scaledToWidth(30)
reply.deleteLater()
def format_size(self, size):
"""格式化文件大小"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} PB"

View File

@@ -0,0 +1,463 @@
# coding: utf-8
# 导入loguru日志库
from datetime import datetime
from loguru import logger
from PyQt6.QtCore import Qt, QThread, QUrl, pyqtSignal
from PyQt6.QtGui import QPixmap
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PyQt6.QtWidgets import (
QHBoxLayout,
QVBoxLayout,
QWidget,
)
from qfluentwidgets import Action, BodyLabel, BreadcrumbBar, HorizontalSeparator, ImageLabel, InfoBar, InfoBarPosition, \
MenuAnimationType, MessageBoxBase, RoundMenu, ScrollArea, setFont, SubtitleLabel
from qfluentwidgets import FluentIcon as FIF
from app.core import lang
from app.core import miaoStarsBasicApi
from app.core import signalBus
from app.view.components.file_card import SharedFolderFileCard
# 使用miaoStarsBasicApi中已经配置好的V4 API
UserSession = miaoStarsBasicApi.returnSession()
class APIWorker(QThread):
"""API 工作线程"""
finished = pyqtSignal(object, str) # 信号:传递响应数据和错误信息
def __init__(self, url_path, method="GET", data=None):
super().__init__()
self.url_path = url_path # 相对路径,如 "/share/list/{id}/path"
self.method = method
self.data = data
logger.debug(f"初始化API工作线程路径: {url_path}, 方法: {method}")
def run(self):
"""执行API请求使用miaoStarsBasicApi中已配置好的Cloudreve V4 API"""
try:
logger.info(f"开始API请求")
# 使用miaoStarsBasicApi进行请求
if self.method == "GET":
response = miaoStarsBasicApi.request(method="GET", url=self.url_path)
else:
response = miaoStarsBasicApi.request(method=self.method, url=self.url_path, json=self.data)
# Cloudreve V4 API 返回的是处理后的结果,不需要再解析响应
logger.success(f"API请求成功")
# 由于miaoStarsBasicApi.request已经处理了错误直接返回结果
if isinstance(response, dict) and "code" in response and response["code"] == 0:
self.finished.emit(response, "")
else:
error_msg = response.get("msg", "请求失败") if isinstance(response, dict) else "请求失败"
logger.error(f"API请求失败, 错误: {error_msg}")
self.finished.emit(None, error_msg)
except Exception as e:
error_msg = f"请求异常: {str(e)}"
logger.exception(f"API请求异常, 异常信息: {error_msg}")
self.finished.emit(None, error_msg)
class LinkageSwitching(ScrollArea):
"""文件卡片滚动区域组件"""
# 信号:文件卡片相关操作时发出
fileActionRequested = pyqtSignal(str, str) # (actionName, fileId)
def __init__(self, _id, paths, breadcrumbBar, parent=None):
super().__init__(parent)
self.paths = paths
self._id = _id
self.currentPath = paths
self.breadcrumbBar = breadcrumbBar
self.fileCardsDict = {} # 存储所有文件卡片
logger.debug(f"初始化文件卡片滚动区域,路径: {paths}")
self.setupUi()
def setupUi(self):
"""初始化UI"""
self.widgets = QWidget()
self.layouts = QVBoxLayout(self.widgets)
self.layouts.setAlignment(Qt.AlignmentFlag.AlignTop)
self.setWidget(self.widgets)
self.setWidgetResizable(True)
# 设置布局属性
self.layouts.setContentsMargins(5, 5, 5, 0)
self.layouts.setSpacing(5)
# 设置样式
self.widgets.setStyleSheet("background-color: transparent; border: none;")
self.setStyleSheet("background-color: transparent; border: none;")
# 设置滚动策略
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self._onLoadDict("/")
def addFileCard(self, fileId, key, obj):
"""添加文件卡片
Args:
fileId: 文件的唯一标识符
obj: 文件数据对象
Returns:
创建的文件卡片对象
"""
if fileId in self.fileCardsDict:
logger.warning(f"文件卡片已存在: {fileId}")
return self.fileCardsDict[fileId]
# logger.debug(f"添加文件卡片: {fileId} - {obj.get('name', '未知')}")
fileCard = SharedFolderFileCard(
key,
obj["id"],
obj["name"],
obj["type"],
obj["path"],
obj["date"],
obj["size"],
self,
)
fileCard.setObjectName(f"fileCard_{fileId}")
self.fileCardsDict[fileId] = fileCard
self.layouts.addWidget(fileCard)
return fileCard
def removeFileCard(self, fileId):
"""移除文件卡片"""
if fileId in self.fileCardsDict:
# logger.debug(f"移除文件卡片: {fileId}")
fileCard = self.fileCardsDict[fileId]
self.layouts.removeWidget(fileCard)
fileCard.deleteLater()
del self.fileCardsDict[fileId]
else:
logger.warning(f"尝试移除不存在的文件卡片: {fileId}")
def clearFileCards(self):
"""清除所有文件卡片"""
logger.debug("清除所有文件卡片")
fileIds = list(self.fileCardsDict.keys())
for fileId in fileIds:
self.removeFileCard(fileId)
def contextMenuEvent(self, e):
"""重写上下文菜单事件"""
logger.debug("触发上下文菜单事件")
menu = RoundMenu(parent=self)
# 添加操作
menu.addAction(
Action(FIF.SYNC, lang("刷新当前"), triggered=self._refreshFolderList)
)
menu.addSeparator()
# 显示菜单
menu.exec(e.globalPos(), aniType=MenuAnimationType.DROP_DOWN)
def _refreshFolderList(self):
logger.debug("刷新文件夹列表")
InfoBar.success(
"成功",
"刷新成功",
Qt.Orientation.Horizontal,
True,
1000,
InfoBarPosition.TOP_RIGHT,
self.parent(),
)
def _onLoadDict(self, paths):
"""加载目录数据"""
logger.info(f"加载目录数据: {paths}")
self.currentPath = paths
# 使用Cloudreve V4 API的分享列表路径
# 注意V4 API中分享列表可能使用不同的路径和参数格式
# 这里假设路径为 /share/list/{share_id}?path={path}
url_path = f"/share/list/{self._id}?path={paths}"
self.loadDataThread = APIWorker(url_path)
self.loadDataThread.finished.connect(self._dealDatas)
self.loadDataThread.start()
self.breadcrumbBar.setEnabled(False)
def _dealDatas(self, data, msg):
"""处理目录数据"""
logger.info("设置当前页策略")
if msg:
logger.error(f"加载目录数据失败: {msg}")
return
if not data or "data" not in data or "objects" not in data["data"]:
logger.error("目录数据格式错误")
return
logger.info(f"成功加载目录数据,对象数量: {len(data['data']['objects'])}")
self.objects = data["data"]["objects"]
self.clearFileCards()
self.breadcrumbBar.setEnabled(True)
for obj in self.objects:
try:
self.addFileCard(obj["id"], obj["key"], obj)
except:
self.addFileCard(obj["id"], obj["id"], obj)
class BasicInfoThread(QThread):
"""API 工作线程"""
finished = pyqtSignal(object, str) # 信号:传递响应数据和错误信息
def __init__(self, _id):
super().__init__()
self._id = _id
logger.debug(f"初始化API工作线程")
def run(self):
"""执行API请求使用miaoStarsBasicApi中已配置好的Cloudreve V4 API"""
try:
# 使用Cloudreve V4 API的URL格式
url_path = f"/share/{self._id}"
logger.info(f"开始API请求")
# 使用miaoStarsBasicApi进行请求
response = miaoStarsBasicApi.request(method="GET", url=url_path)
# 由于miaoStarsBasicApi.request已经处理了错误直接返回结果
if isinstance(response, dict) and "code" in response and response["code"] == 0:
logger.success(f"API请求成功")
self.finished.emit(response, "")
else:
error_msg = response.get("msg", "请求失败") if isinstance(response, dict) else "请求失败"
logger.error(f"API请求失败, 错误: {error_msg}")
self.finished.emit(None, error_msg)
except Exception as e:
error_msg = f"请求异常: {str(e)}"
logger.exception(f"API请求异常, 异常信息: {error_msg}")
self.finished.emit(None, error_msg)
class ShareFolderMessageBox(MessageBoxBase):
"""主文件管理界面"""
# 信号:面包屑导航项点击时发出,传递路径
breadcrumbItemClicked = pyqtSignal(str)
def __init__(self, _id, parent=None):
super().__init__(parent=parent)
self.setObjectName("ShareFolderMessageBox")
self.widget.setMinimumWidth(900)
self.widget.setMinimumHeight(600)
self.currentPath = "/"
self._id = _id
logger.debug("初始化分析文件管理")
self.folderTitleLabel = SubtitleLabel(self)
self.infomationLabel = BodyLabel(self)
self.authorAvatar = ImageLabel(":app/images/logo.png", self)
self.authorAvatar.setBorderRadius(20, 20, 20, 20)
self.authorAvatar.scaledToHeight(20)
self.authorAvatar.scaledToWidth(20)
self.authorNameLabel = BodyLabel(self)
self.breadcrumbBar = BreadcrumbBar(self)
self.basicInfoThread = BasicInfoThread(self._id)
self.basicInfoThread.finished.connect(self.handleApiResponse)
self.basicInfoThread.start()
self.networkManager = QNetworkAccessManager(self)
self.networkManager.finished.connect(self.onAvatarDownloaded)
self.setupUi()
self.connectSignals()
def setupUi(self):
"""初始化UI"""
logger.debug("设置分析文件管理界面UI")
# 初始化面包屑导航
self.breadcrumbBar.addItem("/", "/")
setFont(self.breadcrumbBar, 18)
self.breadcrumbBar.setSpacing(15)
self.breadcrumbBar.currentItemChanged.connect(self.clickChangeDir)
# 初始化堆叠窗口
self.linkageSwitching = LinkageSwitching(
self._id, "/", self.breadcrumbBar, self
)
# 设置主布局
self.initLayout()
def initLayout(self):
"""初始化布局"""
self.viewLayout.setContentsMargins(10, 20, 10, 5)
self.viewLayout.addWidget(self.folderTitleLabel, 0, Qt.AlignmentFlag.AlignTop)
self.viewLayout.addWidget(self.infomationLabel, 0, Qt.AlignmentFlag.AlignTop)
self.viewLayout.addWidget(HorizontalSeparator(self))
self.authorLayout = QHBoxLayout()
self.authorLayout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.authorLayout.addWidget(self.authorAvatar, 0, Qt.AlignmentFlag.AlignLeft)
self.authorLayout.addWidget(self.authorNameLabel, 0, Qt.AlignmentFlag.AlignLeft)
self.viewLayout.addLayout(self.authorLayout)
self.viewLayout.addWidget(HorizontalSeparator(self))
# 添加所有组件到主布局
self.viewLayout.addWidget(self.breadcrumbBar, 0, Qt.AlignmentFlag.AlignTop)
self.viewLayout.addWidget(self.linkageSwitching)
def handleApiResponse(self, response_data):
response_data = response_data["data"]
self.folderTitleLabel.setText(response_data["source"]["name"])
infoLabel = f"创建时间: {self.format_date(response_data['create_date'])} | 浏览次数: {response_data['views']} | 下载次数: {response_data['downloads']}"
self.infomationLabel.setText(infoLabel)
self.authorAvatar.setText(response_data["creator"]["nick"])
self.authorNameLabel.setText(response_data["creator"]["nick"])
self.loadAvatarFromId(response_data["creator"]["key"])
def loadAvatarFromId(self, _id):
"""从网络URL加载头像"""
# 使用V4 API获取头像
url = f"/user/avatar/{_id}/l"
logger.info(f"开始从网络加载头像")
request = QNetworkRequest(QUrl(url))
self.networkManager.get(request)
def onAvatarDownloaded(self, reply):
"""处理头像下载完成"""
if reply.error() == QNetworkReply.NetworkError.NoError:
# 读取下载的数据
data = reply.readAll()
# 创建QPixmap并加载数据
pixmap = QPixmap()
if pixmap.loadFromData(data):
# 更新头像
self.authorAvatar.setImage(pixmap)
logger.info("网络头像加载成功")
else:
logger.error("头像数据格式不支持")
else:
logger.error(f"头像下载失败: {reply.errorString()}")
pixmap = QPixmap(":app/images/logo.png")
self.authorAvatar.setImage(pixmap)
logger.info("使用默认头像")
self.authorAvatar.scaledToHeight(20)
self.authorAvatar.scaledToWidth(20)
reply.deleteLater()
def format_size(self, size):
"""格式化文件大小"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024
return f"{size:.2f} PB"
def format_date(self, date_str):
"""格式化日期时间"""
try:
# 处理带小数秒的情况
if "." in date_str:
# 分割日期和小数秒部分
date_part, fractional_part = date_str.split(".", 1)
# 去除末尾的'Z'并截取前6位小数
fractional_sec = fractional_part.rstrip("Z")[:6]
# 重新组合日期字符串
normalized_date_str = f"{date_part}.{fractional_sec}Z"
date_time = datetime.strptime(
normalized_date_str, "%Y-%m-%dT%H:%M:%S.%fZ"
)
else:
# 处理没有小数秒的情况
date_time = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ")
except ValueError:
# 如果所有格式都失败,返回原始字符串
return date_str
return date_time.strftime("%Y-%m-%d %H:%M:%S")
def refreshCurrentDirectory(self):
"""刷新当前目录的文件卡片"""
logger.info(f"刷新当前目录: {self.currentPath}")
# 重新加载当前目录数据
self.linkageSwitching._onLoadDict(self.currentPath)
def clickChangeDir(self, name):
"""处理面包屑导航项点击事件"""
logger.info(f"面包屑导航项点击: {name}")
# 获取点击的路径
if name == "":
name = "/"
pathList = []
for item in self.breadcrumbBar.items:
if item.text == name:
pathList.append(item.text)
break
else:
pathList.append(item.text)
# 清理空路径
for i in pathList:
if i == "":
pathList.remove(i)
path = "/".join(pathList) if pathList else "/"
path = path[1:]
if path == "":
path = "/"
if path == self.currentPath:
logger.debug("路径未变化,跳过导航")
return
self.onChangeDir(path)
def connectSignals(self):
"""连接信号与槽"""
logger.debug("连接主文件管理界面信号")
# 连接搜索信号
signalBus.shareDirOpenSignal.connect(lambda x: self.onChangeDir(x))
# signalBus.refreshFolderListSignal.connect(self.refreshCurrentDirectory)
signalBus.shareFileDownloadSignal.connect(self.accept)
def onChangeDir(self, path):
"""处理目录变更"""
logger.info(f"变更目录: {path}")
self.linkageSwitching._onLoadDict(path)
if path == "":
self.currentPath = "/"
else:
self.currentPath = path
# 更新面包屑导航
display_name = path.split("/")[-1] if path != "/" else "/"
self.breadcrumbBar.addItem(display_name, display_name)

View File

@@ -0,0 +1,94 @@
# coding: utf-8
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import ComboBox, PushButton
from qfluentwidgets import FluentIcon as FIF
from app.view.components.linkage_switching import ShareLinkageSwitching
from app.view.widgets.page_flip_widget import PageFlipWidget
#
class ShareSearchScrollWidget(QWidget):
returnSignal = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.currentKeyword = ""
self.currentPage = 1
self.vBoxLayout = QVBoxLayout(self)
self.topLayout = QHBoxLayout()
self.returnButton = PushButton(
FIF.RETURN,
"返回",
self,
)
self.searchMethod = ComboBox(self)
self.searchMethod.addItems(["创建时间", "下载次数", "浏览次数"])
self.searchMethod.currentTextChanged.connect(
lambda: self.shareSearch(self.currentKeyword, self.currentPage)
)
self.sortMethod = ComboBox(self)
self.sortMethod.addItems(["从大到小", "从小到大"])
self.sortMethod.currentTextChanged.connect(
lambda: self.shareSearch(self.currentKeyword, self.currentPage)
)
self.returnButton.clicked.connect(self.clear)
self.searchScrolledArea = ShareLinkageSwitching(self)
self.searchScrolledArea.totalItemsSignal.connect(self.updatePageFlip)
self.pageFlipWidget = None
self.topLayout.addWidget(self.returnButton, 0, Qt.AlignmentFlag.AlignLeft)
self.topLayout.addWidget(self.searchMethod, 1, Qt.AlignmentFlag.AlignRight)
self.topLayout.addWidget(self.sortMethod, 0, Qt.AlignmentFlag.AlignRight)
self.vBoxLayout.addLayout(self.topLayout)
self.vBoxLayout.addWidget(self.searchScrolledArea)
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
QTimer.singleShot(1000, lambda: self.shareSearch("A", 1))
def updatePageFlip(self, total):
if not self.pageFlipWidget:
pages = total // 18 if total % 18 == 0 else total // 18 + 1
self.pageFlipWidget = PageFlipWidget(self, pages)
self.vBoxLayout.addWidget(
self.pageFlipWidget,
0,
Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
)
self.pageFlipWidget.pageChangeSignal.connect(
lambda page: self.shareSearch(self.currentKeyword, page)
)
def shareSearch(self, keyword, page):
if self.currentKeyword != keyword:
self.currentKeyword = keyword
if self.currentPage != page:
self.currentPage = page
orderByDict = {
"创建时间": "created_at",
"下载次数": "downloads",
"浏览次数": "views",
}
sortDict = {
"从大到小": "DESC",
"从小到大": "ASC",
}
self.searchScrolledArea.search(
keyword,
orderByDict[self.searchMethod.currentText()],
sortDict[self.sortMethod.currentText()],
page,
)
def clear(self):
self.searchScrolledArea.clearFileCards()
self.returnSignal.emit()

View File

@@ -0,0 +1,66 @@
# coding:utf-8
from PyQt6.QtCore import Qt, pyqtSignal, QEasingCurve
from PyQt6.QtWidgets import QFrame, QHBoxLayout, QAbstractScrollArea
from qfluentwidgets.components.widgets.stacked_widget import PopUpAniStackedWidget
class StackedWidget(QFrame):
""" Stacked widget """
currentChanged = pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent=parent)
self.hBoxLayout = QHBoxLayout(self)
self.view = PopUpAniStackedWidget(self)
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
self.hBoxLayout.addWidget(self.view)
self.view.currentChanged.connect(self.currentChanged)
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground)
def isAnimationEnabled(self) -> bool:
return self.view.isAnimationEnabled
def setAnimationEnabled(self, isEnabled: bool):
"""set whether the pop animation is enabled"""
self.view.setAnimationEnabled(isEnabled)
def addWidget(self, widget):
""" add widget to view """
self.view.addWidget(widget)
def removeWidget(self, widget):
""" remove widget from view """
self.view.removeWidget(widget)
def widget(self, index: int):
return self.view.widget(index)
def setCurrentWidget(self, widget, popOut=True):
if isinstance(widget, QAbstractScrollArea):
widget.verticalScrollBar().setValue(0)
if not popOut:
self.view.setCurrentWidget(widget, duration=300)
else:
self.view.setCurrentWidget(
widget, True, False, 200, QEasingCurve.Type.InQuad)
def setCurrentIndex(self, index, popOut=True):
self.setCurrentWidget(self.view.widget(index), popOut)
def currentIndex(self):
return self.view.currentIndex()
def currentWidget(self):
return self.view.currentWidget()
def indexOf(self, widget):
return self.view.indexOf(widget)
def count(self):
return self.view.count()

View File

@@ -0,0 +1,38 @@
# encoding:utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QSystemTrayIcon
from qfluentwidgets import Action, InfoBar, InfoBarPosition, MessageBox, SystemTrayMenu
class SystemTrayIcon(QSystemTrayIcon):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setIcon(parent.windowIcon())
self.setToolTip("-六棱光界-")
self.menu = SystemTrayMenu(parent=parent)
self.menu.addActions(
[
Action("🙂 显示界面", triggered=self.showSofware),
Action("🙃 退出软件", triggered=self.exitSoftware),
]
)
self.setContextMenu(self.menu)
def exitSoftware(self):
self.parent().window().showNormal()
InfoBar.info("提示","你正在进行退出操作,请返回软件",Qt.Orientation.Horizontal,True,5000,InfoBarPosition.BOTTOM_RIGHT,InfoBar.desktopView())
w = MessageBox(
title="提示",
content="确定退出软件吗?正在进行的任务将会取消",
parent=self.parent().window(),
)
w.yesButton.setText("确定退出")
w.cancelButton.setText("算了我想想")
if w.exec():
self.parent().window().close()
def showSofware(self):
self.parent().window().showNormal()

View File

@@ -0,0 +1,42 @@
# coding: utf-8
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from qfluentwidgets import ScrollArea
from app.view.components.file_deal_cards import UploadCard
class UploadScrollWidget(ScrollArea):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.scrollWidget = QWidget()
self.vBoxLayout = QVBoxLayout(self.scrollWidget)
self.__initWidget()
def __initWidget(self):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# self.setViewportMargins(0, 100, 0, 20)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setObjectName("UploadScrollWidget")
self.scrollWidget.setObjectName("scrollWidget")
self.scrollWidget.setStyleSheet("background:transparent;border:none;")
self.setStyleSheet("background:transparent;border:none;")
self.__initLayout()
def __initLayout(self):
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
def addUploadTask(self, filePath):
self.vBoxLayout.addWidget(
UploadCard(
"file",
filePath,
self.scrollWidget,
)
)

View File

@@ -0,0 +1,41 @@
# coding: utf-8
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import (
QVBoxLayout,
QWidget,
)
from qfluentwidgets import FluentIcon as FIF
from qfluentwidgets import (
PushButton,
)
from app.view.components.linkage_switching import SearchLinkageSwitching
class WareSearchScrollWidget(QWidget):
returnSignal = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.vBoxLayout = QVBoxLayout(self)
self.returnButton = PushButton(
FIF.RETURN,
"返回",
self,
)
self.returnButton.clicked.connect(self.clear)
self.searchScrolledArea = SearchLinkageSwitching(self)
self.vBoxLayout.addWidget(
self.returnButton, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
)
self.vBoxLayout.addWidget(self.searchScrolledArea)
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
def wareSearch(self, searchType, searchContent):
self.searchScrolledArea.search(searchType, searchContent)
def clear(self):
self.searchScrolledArea.clearFileCards()
self.returnSignal.emit()