diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 444e98d..dffdf62 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -9,10 +9,10 @@ Copyright (c) Microsoft Corporation. Licensed under the MIT License. Source: -https://github.com/microsoft/fluentui-emoji + Modifications: None The full license text is included in: -licenses/Microsoft-Fluent-Emoji.txt +[licenses/Microsoft-Fluent-Emoji.txt](./licenses/Microsoft-Fluent-Emoji.txt) diff --git a/assets/README.md b/assets/README.md index fa0af04..63bec28 100644 --- a/assets/README.md +++ b/assets/README.md @@ -9,8 +9,8 @@ | `logo.icns` | macOS `.app` 应用包图标 | `tchMaterial-parser.spec` 中的 `BUNDLE(icon=...)` | | `icon.ico` | Windows 可执行文件图标 | `tchMaterial-parser.spec` 中的 `EXE(icon=...)` | | `window_icon.png` | 程序运行时的窗口图标(窗口左上角、Alt+Tab,以及主界面标题旁) | `src/tchmaterial_parser/app.py` 直接读取,`tchMaterial-parser.spec` 负责打包 | -| `sun_3d.png`、`crescent_moon_3d.png`、`last_quarter_moon_3d.png`(来自 [microsoft/fluentui-emoji](https://github.com/microsoft/fluentui-emoji)) | 程序右上角选择主题图标 | `src/tchmaterial_parser/images.py` 直接读取,`tchMaterial-parser.spec` 负责打包 | -| `sun_flat.svg`、`crescent_moon_flat.svg`、`last_quarter_moon_flat.svg`(来自 [microsoft/fluentui-emoji](https://github.com/microsoft/fluentui-emoji)) | 矢量图 | 仅供设计使用,不参与构建 | +| `last_quarter_moon_3d.png`、`sun_3d.png`、`crescent_moon_3d.png`、`information_3d.png`(来自 [microsoft/fluentui-emoji](https://github.com/microsoft/fluentui-emoji)) | 程序右上角选择主题、关于图标 | `src/tchmaterial_parser/images.py` 直接读取,`tchMaterial-parser.spec` 负责打包 | +| `last_quarter_moon_flat.svg`、`sun_flat.svg`、`crescent_moon_flat.svg`、`information_flat.svg`(来自 [microsoft/fluentui-emoji](https://github.com/microsoft/fluentui-emoji)) | 矢量图 | 仅供设计使用,不参与构建 | ## 更换图标时的操作 diff --git a/assets/information_3d.png b/assets/information_3d.png new file mode 100644 index 0000000..02fbb08 Binary files /dev/null and b/assets/information_3d.png differ diff --git a/assets/information_flat.svg b/assets/information_flat.svg new file mode 100644 index 0000000..ca168bd --- /dev/null +++ b/assets/information_flat.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/tchmaterial_parser/app.py b/src/tchmaterial_parser/app.py index ddf8011..b152e9d 100644 --- a/src/tchmaterial_parser/app.py +++ b/src/tchmaterial_parser/app.py @@ -10,9 +10,10 @@ from PIL import Image, ImageTk from . import __version__ from .catalog import ResourceHelper from .config import load_access_token, load_config, save_config -from .images import make_theme_icon_image, render_system_emoji +from .images import make_icon_image, render_system_emoji from .platform_utils import ctypes, os_name, print_error, resource_path, win32api, win32con, win32gui, win32print from .ui import download_panel, runtime, theme +from .ui.about_window import show_about_window from .ui.resource_tree import build_resource_tree from .ui.runtime import scaled from .ui.token_window import show_access_token_window @@ -130,7 +131,7 @@ def main() -> None: # 程序入口:初始化界面并进入主循环 container_frame = ttk.Frame(root, padding=(scaled(24), scaled(18))) # 通过内边距在窗口四周留白 container_frame.pack(expand=True, fill="both") - # 顶部:左侧为图标与标题,右侧为主题切换按钮 + # 顶部:左侧为图标与标题,右侧为关于、主题切换按钮 header_frame = ttk.Frame(container_frame) header_frame.pack(fill="x") @@ -154,7 +155,7 @@ def main() -> None: # 程序入口:初始化界面并进入主循环 def update_theme_button() -> None: # 更新按钮文字与图标,使其表示点击后将切换到的主题 if theme.switched_theme not in theme_icons: icon_size = max(scaled(16), 16) - theme_icons[theme.switched_theme] = ImageTk.PhotoImage(make_theme_icon_image(theme.switched_theme, icon_size)) + theme_icons[theme.switched_theme] = ImageTk.PhotoImage(make_icon_image(theme.switched_theme, icon_size)) theme_btn.config( text=" 浅色" if theme.switched_theme == "light" else " 深色" if theme.switched_theme == "dark" else " 跟随系统", image=theme_icons[theme.switched_theme], @@ -167,8 +168,17 @@ def main() -> None: # 程序入口:初始化界面并进入主循环 theme.apply_theme(target_theme) update_theme_button() - theme_btn = ttk.Button(header_frame, style="Toolbutton", command=switch_theme) - theme_btn.pack(side="right", anchor="n") + header_actions = ttk.Frame(header_frame) + header_actions.pack(side="right", anchor="n") + + # 关于按钮 + about_image = ImageTk.PhotoImage(make_icon_image("about", max(scaled(16), 16))) + about_btn = ttk.Button(header_actions, text=" 关于", image=about_image, compound="left", style="Toolbutton", command=show_about_window) + about_btn.pack(side="left", padx=(0, scaled(8))) + + # 切换主题按钮 + theme_btn = ttk.Button(header_actions, style="Toolbutton", command=switch_theme) + theme_btn.pack(side="left") update_theme_button() # 功能说明 diff --git a/src/tchmaterial_parser/images.py b/src/tchmaterial_parser/images.py index ee7d03f..e175ed6 100644 --- a/src/tchmaterial_parser/images.py +++ b/src/tchmaterial_parser/images.py @@ -93,14 +93,13 @@ def render_system_emoji(symbol: str, icon_size: int) -> Image.Image | None: # continue return None -def make_theme_icon_image(target_theme: Literal["system", "light", "dark"], icon_size: int) -> Image.Image: # 优先使用系统 Emoji 的原始字形,无法渲染时使用图片 - symbol = "☀️" if target_theme == "light" else "🌙" if target_theme == "dark" else "🌗" - emoji_icon = render_system_emoji(symbol, icon_size) +def make_icon_image(icon_name: Literal["system", "light", "dark", "about"], icon_size: int) -> Image.Image: # 优先使用系统 Emoji 的原始字形,无法渲染时使用图片 + icon_mapping = { "system": ("🌗", "last_quarter_moon_3d.png"), "light": ("☀️", "sun_3d.png"), "dark": ("🌙", "crescent_moon_3d.png"), "about": ("ℹ️", "information_3d.png") } + emoji_icon = render_system_emoji(icon_mapping[icon_name][0], icon_size) if emoji_icon is not None: return emoji_icon - icon_mapping = { "system": "last_quarter_moon_3d.png", "light": "sun_3d.png", "dark": "crescent_moon_3d.png" } - with Image.open(resource_path("assets", icon_mapping[target_theme])) as icon: + with Image.open(resource_path("assets", icon_mapping[icon_name][1])) as icon: icon_image = icon.copy() icon_image.thumbnail((icon_size, icon_size), Image.Resampling.LANCZOS) return icon_image diff --git a/src/tchmaterial_parser/ui/about_window.py b/src/tchmaterial_parser/ui/about_window.py new file mode 100644 index 0000000..acc04b6 --- /dev/null +++ b/src/tchmaterial_parser/ui/about_window.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# 关于窗口:展示程序、作者、项目地址与第三方许可证信息 + +import tkinter as tk +from tkinter import messagebox, ttk +import webbrowser + +from PIL import Image, ImageTk + +from .. import __version__ +from ..platform_utils import resource_path +from . import runtime, theme +from .runtime import scaled +from .widgets import center_window, make_card + +PROJECT_URL = "https://github.com/happycola233/tchMaterial-parser" +LICENSE_URL = "https://github.com/happycola233/tchMaterial-parser/blob/main/LICENSE" +FLUENT_EMOJI_LICENSE_URL = "https://github.com/microsoft/fluentui-emoji/blob/main/LICENSE" + +def open_url(url: str, parent: tk.Toplevel) -> None: # 使用系统默认浏览器打开链接,并在失败时向用户给出明确提示 + try: + if not webbrowser.open_new_tab(url): + messagebox.showerror("无法打开链接", f"请复制链接后在浏览器中打开:\n{url}", parent=parent) + except Exception as e: + messagebox.showerror("无法打开链接", f"请复制链接后在浏览器中打开:\n{url}\n\n{e}", parent=parent) + +def make_link(parent: tk.Widget, text: str, url: str, window: tk.Toplevel) -> ttk.Label: # 创建可通过鼠标或键盘打开的链接标签 + link = ttk.Label(parent, text=text, style="AboutLink.TLabel", cursor="hand2", takefocus=True) + link.bind("", lambda _event: open_url(url, window)) # 鼠标左键 + link.bind("", lambda _event: open_url(url, window)) # 回车键 + link.bind("", lambda _event: open_url(url, window)) # 空格键 + return link + +def show_about_window() -> None: # 打开关于窗口 + about_window = tk.Toplevel(runtime.root) + about_window.title("关于") + about_window.resizable(False, False) + about_window.focus() + about_window.transient(runtime.root) + about_window.bind("", lambda _event: about_window.destroy()) + + # 卡片内的标签需要使用与卡片一致的底色;链接通过下划线和强调色表明可点击 + style = ttk.Style(about_window) + style.configure( + "AboutCard.TLabel", + background=theme.current_colors["surface"], + foreground=theme.current_colors["fg"], + ) + style.configure( + "AboutCardMuted.TLabel", + font="AppCaptionFont", + background=theme.current_colors["surface"], + foreground=theme.current_colors["muted"], + ) + style.configure( + "AboutCardStrong.TLabel", + font="AppStrongFont", + background=theme.current_colors["surface"], + foreground=theme.current_colors["fg"], + ) + style.configure( + "AboutLink.TLabel", + font="AppLinkFont", + background=theme.current_colors["surface"], + foreground="#3973e6" if theme.current_theme == "light" else "#75a7ff", + ) + + frame = ttk.Frame(about_window, padding=scaled(24)) + frame.pack(fill="both", expand=True) + + # 程序图标与名称 + header = ttk.Frame(frame) + header.pack(fill="x") + + with Image.open(resource_path("assets", "window_icon.png")) as icon: + icon_image = icon.copy() + icon_image.thumbnail((scaled(64), scaled(64)), Image.Resampling.LANCZOS) + icon_photo = ImageTk.PhotoImage(icon_image) + icon_label = ttk.Label(header, image=icon_photo) + icon_label.pack(side="left", padx=(0, scaled(16))) + setattr(icon_label, "_image_ref", icon_photo) + + title_frame = ttk.Frame(header) + title_frame.pack(side="left", fill="x", expand=True) + ttk.Label(title_frame, text= "国家中小学智慧教育平台 资源下载工具", style="Title.TLabel").pack(anchor="w") + ttk.Label(title_frame, text=f"版本 {__version__}", style="Caption.TLabel").pack(anchor="w", pady=(scaled(3), 0)) + + # 作者与项目地址 + info_card = make_card(frame, padding=(scaled(16), scaled(12))) + info_card.pack(fill="x", pady=(scaled(18), 0)) + info_card.columnconfigure(1, weight=1) + + ttk.Label(info_card, text="作者", style="AboutCardMuted.TLabel").grid( + row=0, column=0, sticky="nw", padx=(0, scaled(16)) + ) + ttk.Label(info_card, text="肥宅水水呀、晨叶梦春 及其他贡献者", style="AboutCard.TLabel").grid( + row=0, column=1, sticky="nw" + ) + + ttk.Label(info_card, text="仓库", style="AboutCardMuted.TLabel").grid( + row=1, column=0, sticky="nw", padx=(0, scaled(16)), pady=(scaled(8), 0) + ) + project_link = make_link(info_card, "happycola233/tchMaterial-parser", PROJECT_URL, about_window) + project_link.grid(row=1, column=1, sticky="nw", pady=(scaled(8), 0)) + + ttk.Label(info_card, text="许可证", style="AboutCardMuted.TLabel").grid( + row=2, column=0, sticky="nw", padx=(0, scaled(16)), pady=(scaled(8), 0) + ) + license_link = make_link(info_card, "MIT License", LICENSE_URL, about_window) + license_link.grid(row=2, column=1, sticky="nw", pady=(scaled(8), 0)) + + # 第三方资源与许可证说明 + license_card = make_card(frame, padding=(scaled(16), scaled(12))) + license_card.pack(fill="x", pady=(scaled(12), 0)) + ttk.Label(license_card, text="第三方许可证", style="AboutCardStrong.TLabel").pack(anchor="w") + ttk.Label( + license_card, + text="本软件使用了 Microsoft Fluent Emoji 的部分图像资源,依据 MIT 许可证授权使用。\n相关图像资源及其版权归 Microsoft 所有。", + style="AboutCard.TLabel", + justify="left", + wraplength=scaled(600), + ).pack(anchor="w", pady=(scaled(6), 0)) + license_link = make_link(license_card, "查看 Microsoft Fluent Emoji 许可证 ↗", FLUENT_EMOJI_LICENSE_URL, about_window) + license_link.pack(anchor="w", pady=(scaled(8), 0)) + + # 声明 + notice_card = make_card(frame, padding=(scaled(16), scaled(12))) + notice_card.pack(fill="x", pady=(scaled(12), 0)) + ttk.Label(notice_card, text="声明", style="AboutCardStrong.TLabel").pack(anchor="w") + ttk.Label( + notice_card, + text="本软件采用 MIT 许可证开源,官方版本永久免费。\n谨防第三方付费倒卖、捆绑软件或冒充官方等行为;任何再分发行为均须保留原有版权及许可证声明。", + style="AboutCard.TLabel", + justify="left", + wraplength=scaled(600), + ).pack(anchor="w", pady=(scaled(6), 0)) + + button_frame = ttk.Frame(frame) + button_frame.pack(fill="x", pady=(scaled(16), 0)) + ttk.Button( + button_frame, text="关闭", style=theme.ACCENT_BUTTON_STYLE, command=about_window.destroy + ).pack(side="right") + + center_window(about_window, runtime.root) + theme.apply_titlebar_theme(about_window) + about_window.lift() diff --git a/src/tchmaterial_parser/ui/download_panel.py b/src/tchmaterial_parser/ui/download_panel.py index 0e63a82..962123d 100644 --- a/src/tchmaterial_parser/ui/download_panel.py +++ b/src/tchmaterial_parser/ui/download_panel.py @@ -121,7 +121,7 @@ def download_file(url: str, save_path: str, chapters: list[dict] | None = None) ui_call(progress_label.config, text=f"{format_bytes(all_downloaded_size)}/{format_bytes(all_total_size)} ({download_progress:.2f}%) 已下载 {downloaded_number}/{total_number}") # 更新标签以显示当前下载进度 if current_state["total_size"] > 0 and current_state["downloaded_size"] != current_state["total_size"]: # 文件下载不完整 - current_state["failed_reason"] = f"文件下载不完整,需下载 {current_state["total_size"]} 字节,实际下载 {current_state["downloaded_size"]} 字节" + current_state["failed_reason"] = f"文件下载不完整,需下载 {current_state['total_size']} 字节,实际下载 {current_state['downloaded_size']} 字节" current_state["downloaded_size"], current_state["total_size"] = 0, 0 current_state["finished"] = True try: diff --git a/src/tchmaterial_parser/ui/theme.py b/src/tchmaterial_parser/ui/theme.py index c75a757..8155fe6 100644 --- a/src/tchmaterial_parser/ui/theme.py +++ b/src/tchmaterial_parser/ui/theme.py @@ -27,12 +27,17 @@ THEME_COLORS = { ACCENT_BUTTON_STYLE = "Accent.TButton" SWITCH_STYLE = "Switch.TCheckbutton" -# 本程序使用的命名字体,格式为 字体名称: (基准字号(像素), 是否加粗) -APP_FONTS = { "AppCaptionFont": (12, False), "AppBodyFont": (14, False), "AppStrongFont": (14, True), "AppTitleFont": (20, True) } +# 本程序使用的命名字体,格式为 字体名称: (基准字号(像素), 是否加粗, 是否添加下划线) +APP_FONTS = { + "AppCaptionFont": (12, False, False), "AppBodyFont": (14, False, False), "AppStrongFont": (14, True, False), + "AppTitleFont": (20, True, False), "AppLinkFont": (12, False, True) +} # sv-ttk 内置的命名字体,需要一并改为中文字体(其默认字体不含中文字形),基准字号与 sv-ttk 原始取值保持一致 SV_FONTS = { - "SunValleyCaptionFont": (12, False), "SunValleyBodyFont": (14, False), "SunValleyBodyStrongFont": (14, True), "SunValleyBodyLargeFont": (18, False), - "SunValleySubtitleFont": (20, True), "SunValleyTitleFont": (28, True), "SunValleyTitleLargeFont": (40, True), "SunValleyDisplayFont": (68, True), + "SunValleyCaptionFont": (12, False, False), "SunValleyBodyFont": (14, False, False), + "SunValleyBodyStrongFont": (14, True, False), "SunValleyBodyLargeFont": (18, False, False), + "SunValleySubtitleFont": (20, True, False), "SunValleyTitleFont": (28, True, False), + "SunValleyTitleLargeFont": (40, True, False), "SunValleyDisplayFont": (68, True, False), } def bind_font_family(family: str) -> None: # 由 app.py 在选定界面字体后写入,供 setup_fonts() 使用 @@ -57,9 +62,9 @@ def pick_ui_font_family() -> str: # 选择一个合适的字体 def setup_fonts() -> None: # 创建(或更新)所有命名字体,使其使用中文字体并跟随缩放因子 # 此处直接调用 Tcl 命令而不使用 tkinter.font.Font,因为后者创建的字体会随 Python 对象被垃圾回收而一并删除 existing_fonts: tuple[str, ...] = runtime.root.tk.splitlist(runtime.root.tk.call("font", "names")) - for name, (size, bold) in { **APP_FONTS, **SV_FONTS }.items(): + for name, (size, bold, underline) in { **APP_FONTS, **SV_FONTS }.items(): # 字号取负值表示以像素为单位,从而避开 tk scaling 的二次缩放,与 sv-ttk 的取值方式保持一致 - options = ("-family", ui_font_family, "-size", -scaled(size), "-weight", "bold" if bold else "normal") + options = ("-family", ui_font_family, "-size", -scaled(size), "-weight", "bold" if bold else "normal", "-underline", 1 if underline else 0) runtime.root.tk.call("font", "configure" if name in existing_fonts else "create", name, *options) def detect_system_theme() -> Literal["light", "dark"]: # 获取系统当前使用的是浅色还是深色模式 diff --git a/src/tchmaterial_parser/ui/token_window.py b/src/tchmaterial_parser/ui/token_window.py index 5f787ab..2baf642 100644 --- a/src/tchmaterial_parser/ui/token_window.py +++ b/src/tchmaterial_parser/ui/token_window.py @@ -17,6 +17,7 @@ def show_access_token_window() -> None: # 打开输入 Access Token 的窗口 # 让窗口自动根据控件自适应尺寸;如需最小尺寸可用 token_window.minsize(...) token_window.focus() # 自动获得焦点 token_window.grab_set() # 阻止主窗口操作 + token_window.transient(runtime.root) # 使窗口依赖于主窗口 token_window.bind("", lambda event: token_window.destroy()) # 绑定 Esc 键关闭窗口 # 设置一个 Frame 用于留白,使布局更美观 @@ -64,9 +65,8 @@ def show_access_token_window() -> None: # 打开输入 Access Token 的窗口 help_win.title("获取 Access Token 方法") help_win.resizable(False, False) # 禁止调整窗口大小 help_win.focus() # 自动获得焦点 - help_win.grab_set() # 阻止主窗口操作 + help_win.transient(token_window) # 使窗口依赖于主窗口 help_win.bind("", lambda event: help_win.destroy()) # 绑定 Esc 键关闭窗口 - help_win.bind("", lambda event: token_window.grab_set()) help_frame = ttk.Frame(help_win, padding=scaled(20)) help_frame.pack(fill="both", expand=True) diff --git a/tchMaterial-parser.spec b/tchMaterial-parser.spec index 2a22f7a..52dc681 100644 --- a/tchMaterial-parser.spec +++ b/tchMaterial-parser.spec @@ -9,9 +9,10 @@ is_mac = sys.platform.startswith('darwin') # 下面 4 个图标文件是程序运行时读取的自有资源,目标目录保持为 assets/,与源码中的相对路径一致 data_files = collect_data_files('sv_ttk') + [ ('assets/window_icon.png', 'assets'), + ('assets/last_quarter_moon_3d.png', 'assets'), ('assets/sun_3d.png', 'assets'), ('assets/crescent_moon_3d.png', 'assets'), - ('assets/last_quarter_moon_3d.png', 'assets'), + ('assets/information_3d.png', 'assets'), ] a = Analysis(