mirror of
https://github.com/JoeanAmier/XHS-Downloader.git
synced 2026-03-22 06:57:16 +08:00
发布 1.9 版本
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
from typing import Callable
|
||||
|
||||
from rich.text import Text
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.screen import Screen
|
||||
@@ -7,10 +10,9 @@ from textual.widgets import Label
|
||||
|
||||
from source.module import (
|
||||
PROJECT,
|
||||
)
|
||||
from source.translator import (
|
||||
Chinese,
|
||||
English,
|
||||
PROMPT,
|
||||
MASTER,
|
||||
INFO,
|
||||
)
|
||||
|
||||
__all__ = ["About"]
|
||||
@@ -32,13 +34,19 @@ class About(Screen):
|
||||
description="返回首页/Back"),
|
||||
]
|
||||
|
||||
def __init__(self, language: Chinese | English):
|
||||
def __init__(self, message: Callable[[str], str]):
|
||||
super().__init__()
|
||||
self.prompt = language
|
||||
self.message = message
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Label()
|
||||
yield Label(Text(self.message("如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star,感谢您的支持!"), style=INFO),
|
||||
classes="prompt", )
|
||||
yield Label(Text(self.message("作者的其他开源项目"), style=PROMPT), classes="prompt", )
|
||||
yield Label(Text("TikTokDownloader (抖音 / TikTok)", style=MASTER), classes="prompt", )
|
||||
yield Label("https://github.com/JoeanAmier/TikTokDownloader")
|
||||
yield Label(Text("KS-Downloader (快手)", style=MASTER), classes="prompt", )
|
||||
yield Label("https://github.com/JoeanAmier/KS-Downloader")
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Type
|
||||
from typing import Callable
|
||||
|
||||
from textual.app import App
|
||||
from textual.widgets import RichLog
|
||||
@@ -8,12 +8,8 @@ from source.module import (
|
||||
ROOT,
|
||||
)
|
||||
from source.module import Settings
|
||||
from source.translator import (
|
||||
LANGUAGE,
|
||||
Chinese,
|
||||
English,
|
||||
)
|
||||
# from .about import About
|
||||
from source.module import Translate
|
||||
from .about import About
|
||||
from .index import Index
|
||||
from .loading import Loading
|
||||
from .monitor import Monitor
|
||||
@@ -31,7 +27,7 @@ class XHSDownloader(App):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.parameter: dict
|
||||
self.prompt: Type[Chinese | English]
|
||||
self.message: Callable[[str], str]
|
||||
self.APP: XHS
|
||||
self.__initialization()
|
||||
|
||||
@@ -44,19 +40,19 @@ class XHSDownloader(App):
|
||||
|
||||
def __initialization(self) -> None:
|
||||
self.parameter = self.SETTINGS.run()
|
||||
self.prompt = LANGUAGE.get(self.parameter["language"], Chinese)
|
||||
self.APP = XHS(**self.parameter, language_object=self.prompt)
|
||||
self.message = Translate(self.parameter["language"]).message()
|
||||
self.APP = XHS(**self.parameter, transition=self.message)
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
self.install_screen(
|
||||
Setting(
|
||||
self.parameter,
|
||||
self.prompt),
|
||||
self.message),
|
||||
name="setting")
|
||||
self.install_screen(Index(self.APP, self.prompt), name="index")
|
||||
self.install_screen(Loading(self.prompt), name="loading")
|
||||
# self.install_screen(About(self.prompt), name="about")
|
||||
self.install_screen(Record(self.APP, self.prompt), name="record")
|
||||
self.install_screen(Index(self.APP, self.message), name="index")
|
||||
self.install_screen(Loading(self.message), name="loading")
|
||||
self.install_screen(About(self.message), name="about")
|
||||
self.install_screen(Record(self.APP, self.message), name="record")
|
||||
await self.push_screen("index")
|
||||
|
||||
async def action_settings(self):
|
||||
@@ -77,24 +73,24 @@ class XHSDownloader(App):
|
||||
|
||||
async def refresh_screen(self):
|
||||
self.pop_screen()
|
||||
await self.APP.recorder.database.close()
|
||||
await self.close_database()
|
||||
await self.APP.close()
|
||||
self.__initialization()
|
||||
await self.__aenter__()
|
||||
self.uninstall_screen("index")
|
||||
self.uninstall_screen("setting")
|
||||
self.uninstall_screen("loading")
|
||||
# self.uninstall_screen("about")
|
||||
self.uninstall_screen("about")
|
||||
self.uninstall_screen("record")
|
||||
self.install_screen(Index(self.APP, self.prompt), name="index")
|
||||
self.install_screen(Index(self.APP, self.message), name="index")
|
||||
self.install_screen(
|
||||
Setting(
|
||||
self.parameter,
|
||||
self.prompt),
|
||||
self.message),
|
||||
name="setting")
|
||||
self.install_screen(Loading(self.prompt), name="loading")
|
||||
# self.install_screen(About(self.prompt), name="about")
|
||||
self.install_screen(Record(self.APP, self.prompt), name="record")
|
||||
self.install_screen(Loading(self.message), name="loading")
|
||||
self.install_screen(About(self.message), name="about")
|
||||
self.install_screen(Record(self.APP, self.message), name="record")
|
||||
await self.push_screen("index")
|
||||
|
||||
def update_result(self, tip: str) -> None:
|
||||
@@ -103,11 +99,17 @@ class XHSDownloader(App):
|
||||
log.write(">" * 50)
|
||||
|
||||
async def action_check_update(self):
|
||||
await self.push_screen(Update(self.APP, self.prompt), callback=self.update_result)
|
||||
await self.push_screen(Update(self.APP, self.message), callback=self.update_result)
|
||||
|
||||
async def action_check_update_about(self):
|
||||
await self.push_screen("index")
|
||||
await self.action_check_update()
|
||||
|
||||
async def action_monitor(self):
|
||||
await self.push_screen(Monitor(self.APP, self.prompt))
|
||||
await self.push_screen(Monitor(self.APP, self.message))
|
||||
|
||||
async def close_database(self):
|
||||
await self.APP.id_recorder.cursor.close()
|
||||
await self.APP.id_recorder.database.close()
|
||||
await self.APP.data_recorder.cursor.close()
|
||||
await self.APP.data_recorder.database.close()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Callable
|
||||
|
||||
from pyperclip import paste
|
||||
from rich.text import Text
|
||||
from textual import on
|
||||
@@ -25,10 +27,6 @@ from source.module import (
|
||||
REPOSITORY,
|
||||
GENERAL,
|
||||
)
|
||||
from source.translator import (
|
||||
English,
|
||||
Chinese,
|
||||
)
|
||||
|
||||
__all__ = ["Index"]
|
||||
|
||||
@@ -40,13 +38,13 @@ class Index(Screen):
|
||||
Binding(key="s", action="settings", description="程序设置/Settings"),
|
||||
Binding(key="r", action="record", description="下载记录/Record"),
|
||||
Binding(key="m", action="monitor", description="开启监听/Monitor"),
|
||||
# Binding(key="a", action="about", description="关于项目/About"),
|
||||
Binding(key="a", action="about", description="关于项目/About"),
|
||||
]
|
||||
|
||||
def __init__(self, app: XHS, language: Chinese | English):
|
||||
def __init__(self, app: XHS, message: Callable[[str], str]):
|
||||
super().__init__()
|
||||
self.xhs = app
|
||||
self.prompt = language
|
||||
self.message = message
|
||||
self.url = None
|
||||
self.tip = None
|
||||
|
||||
@@ -55,24 +53,24 @@ class Index(Screen):
|
||||
yield ScrollableContainer(
|
||||
Label(
|
||||
Text(
|
||||
f"{self.prompt.open_source_protocol}{LICENCE}",
|
||||
f"{self.message("开源协议")}: {LICENCE}",
|
||||
style=MASTER)
|
||||
),
|
||||
Label(
|
||||
Text(
|
||||
f"{self.prompt.project_address}{REPOSITORY}",
|
||||
f"{self.message("项目地址")}{REPOSITORY}",
|
||||
style=MASTER)
|
||||
),
|
||||
Label(
|
||||
Text(
|
||||
self.prompt.input_box_title,
|
||||
self.message("请输入小红书图文/视频作品链接"),
|
||||
style=PROMPT), classes="prompt",
|
||||
),
|
||||
Input(placeholder=self.prompt.input_prompt),
|
||||
Input(placeholder=self.message("多个链接之间使用空格分隔")),
|
||||
HorizontalScroll(
|
||||
Button(self.prompt.download_button, id="deal"),
|
||||
Button(self.prompt.paste_button, id="paste"),
|
||||
Button(self.prompt.reset_button, id="reset"),
|
||||
Button(self.message("下载无水印作品文件"), id="deal"),
|
||||
Button(self.message("读取剪贴板"), id="paste"),
|
||||
Button(self.message("清空输入框"), id="reset"),
|
||||
),
|
||||
)
|
||||
yield RichLog(markup=True, wrap=True)
|
||||
@@ -82,14 +80,20 @@ class Index(Screen):
|
||||
self.title = PROJECT
|
||||
self.url = self.query_one(Input)
|
||||
self.tip = self.query_one(RichLog)
|
||||
self.tip.write(Text("\n".join(self.prompt.disclaimer), style=MASTER))
|
||||
self.tip.write(
|
||||
Text(
|
||||
self.message("免责声明\n") +
|
||||
f"\n{
|
||||
">" *
|
||||
50}",
|
||||
style=MASTER), scroll_end=False)
|
||||
|
||||
@on(Button.Pressed, "#deal")
|
||||
async def deal_button(self):
|
||||
if self.url.value:
|
||||
self.deal()
|
||||
else:
|
||||
self.tip.write(Text(self.prompt.invalid_link, style=WARNING))
|
||||
self.tip.write(Text(self.message("未输入任何小红书作品链接"), style=WARNING))
|
||||
self.tip.write(Text(">" * 50, style=GENERAL))
|
||||
|
||||
@on(Button.Pressed, "#reset")
|
||||
@@ -106,6 +110,6 @@ class Index(Screen):
|
||||
if any(await self.xhs.extract(self.url.value, True, log=self.tip)):
|
||||
self.url.value = ""
|
||||
else:
|
||||
self.tip.write(Text(self.prompt.download_failure, style=ERROR))
|
||||
self.tip.write(Text(self.message("下载小红书作品文件失败"), style=ERROR))
|
||||
self.tip.write(Text(">" * 50, style=GENERAL))
|
||||
self.app.pop_screen()
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
from typing import Callable
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Grid
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Label
|
||||
from textual.widgets import LoadingIndicator
|
||||
|
||||
from source.translator import (
|
||||
English,
|
||||
Chinese,
|
||||
)
|
||||
|
||||
__all__ = ["Loading"]
|
||||
|
||||
|
||||
class Loading(ModalScreen):
|
||||
def __init__(self, language: Chinese | English):
|
||||
def __init__(self, message: Callable[[str], str]):
|
||||
super().__init__()
|
||||
self.prompt = language
|
||||
self.message = message
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
Label(self.prompt.processing),
|
||||
Label(self.message("程序处理中...")),
|
||||
LoadingIndicator(),
|
||||
classes="loading",
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Callable
|
||||
|
||||
from rich.text import Text
|
||||
from textual import on
|
||||
from textual import work
|
||||
@@ -16,10 +18,6 @@ from source.module import (
|
||||
MASTER,
|
||||
INFO,
|
||||
)
|
||||
from source.translator import (
|
||||
English,
|
||||
Chinese,
|
||||
)
|
||||
|
||||
__all__ = ["Monitor"]
|
||||
|
||||
@@ -30,16 +28,16 @@ class Monitor(Screen):
|
||||
Binding(key="c", action="close", description="关闭监听/Close"),
|
||||
]
|
||||
|
||||
def __init__(self, app: XHS, language: Chinese | English):
|
||||
def __init__(self, app: XHS, message: Callable[[str], str]):
|
||||
super().__init__()
|
||||
self.xhs = app
|
||||
self.prompt = language
|
||||
self.message = message
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Label(Text(self.prompt.monitor_mode, style=INFO), classes="prompt")
|
||||
yield Label(Text(self.message("已启动监听剪贴板模式"), style=INFO), classes="prompt")
|
||||
yield RichLog(markup=True, wrap=True)
|
||||
yield Button(self.prompt.close_monitor, id="close")
|
||||
yield Button(self.message("退出监听剪贴板模式"), id="close")
|
||||
yield Footer()
|
||||
|
||||
@on(Button.Pressed, "#close")
|
||||
@@ -54,7 +52,9 @@ class Monitor(Screen):
|
||||
def on_mount(self) -> None:
|
||||
self.title = PROJECT
|
||||
self.query_one(RichLog).write(
|
||||
Text(self.prompt.monitor_text, style=MASTER))
|
||||
Text(self.message(
|
||||
"程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"),
|
||||
style=MASTER))
|
||||
self.run_monitor()
|
||||
|
||||
def action_close(self):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Callable
|
||||
|
||||
from textual import on
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Grid
|
||||
@@ -8,32 +10,30 @@ from textual.widgets import Input
|
||||
from textual.widgets import Label
|
||||
|
||||
from source.application import XHS
|
||||
from source.translator import (
|
||||
Chinese,
|
||||
English,
|
||||
)
|
||||
|
||||
__all__ = ["Record"]
|
||||
|
||||
|
||||
class Record(ModalScreen):
|
||||
def __init__(self, app: XHS, language: Chinese | English):
|
||||
|
||||
def __init__(self, app: XHS, message: Callable[[str], str]):
|
||||
super().__init__()
|
||||
self.xhs = app
|
||||
self.prompt = language
|
||||
self.message = message
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
Label(self.prompt.record_title, classes="prompt"),
|
||||
Input(placeholder=self.prompt.record_placeholder, id="id", ),
|
||||
Label(self.message("请输入待删除的小红书作品链接或作品 ID"), classes="prompt"),
|
||||
Input(placeholder=self.message("支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"),
|
||||
id="id", ),
|
||||
HorizontalScroll(
|
||||
Button(self.prompt.record_enter_button, id="enter", ),
|
||||
Button(self.prompt.record_close_button, id="close"), ),
|
||||
Button(self.message("删除指定作品 ID"), id="enter", ),
|
||||
Button(self.message("返回首页"), id="close"), ),
|
||||
id="record",
|
||||
)
|
||||
|
||||
async def delete(self, text: str):
|
||||
await self.xhs.recorder.delete_many(text.split())
|
||||
await self.xhs.id_recorder.delete(text)
|
||||
|
||||
@on(Button.Pressed, "#enter")
|
||||
async def save_settings(self):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Callable
|
||||
|
||||
from textual import on
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
@@ -12,12 +14,6 @@ from textual.widgets import Input
|
||||
from textual.widgets import Label
|
||||
from textual.widgets import Select
|
||||
|
||||
from source.translator import (
|
||||
LANGUAGE,
|
||||
Chinese,
|
||||
English,
|
||||
)
|
||||
|
||||
__all__ = ["Setting"]
|
||||
|
||||
|
||||
@@ -27,66 +23,65 @@ class Setting(Screen):
|
||||
Binding(key="b", action="index", description="返回首页/Back"),
|
||||
]
|
||||
|
||||
def __init__(self, data: dict, language: Chinese | English):
|
||||
def __init__(self, data: dict, message: Callable[[str], str]):
|
||||
super().__init__()
|
||||
self.data = data
|
||||
self.prompt = language
|
||||
|
||||
self.message = message
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield ScrollableContainer(
|
||||
Label(self.prompt.work_path, classes="params", ),
|
||||
Input(self.data["work_path"], placeholder=self.prompt.work_path_placeholder, valid_empty=True,
|
||||
Label(self.message("作品数据 / 文件保存根路径"), classes="params", ),
|
||||
Input(self.data["work_path"], placeholder=self.message("程序根路径"), valid_empty=True,
|
||||
id="work_path", ),
|
||||
Label(self.prompt.folder_name, classes="params", ),
|
||||
Label(self.message("作品文件储存文件夹名称"), classes="params", ),
|
||||
Input(self.data["folder_name"], placeholder="Download", id="folder_name", ),
|
||||
Label(self.prompt.user_agent, classes="params", ),
|
||||
Input(self.data["user_agent"], placeholder=self.prompt.user_agent_placeholder, valid_empty=True,
|
||||
Label(self.message("User-Agent"), classes="params", ),
|
||||
Input(self.data["user_agent"], placeholder=self.message("默认 User-Agent"), valid_empty=True,
|
||||
id="user_agent", ),
|
||||
Label(self.prompt.cookie, classes="params", ),
|
||||
Label(self.message("小红书网页版 Cookie"), classes="params", ),
|
||||
Input(placeholder=self.__check_cookie(), valid_empty=True, id="cookie", ),
|
||||
Label(self.prompt.proxy, classes="params", ),
|
||||
Input(self.data["proxy"], placeholder=self.prompt.proxy_placeholder, valid_empty=True, id="proxy", ),
|
||||
Label(self.prompt.timeout, classes="params", ),
|
||||
Label(self.message("网络代理"), classes="params", ),
|
||||
Input(self.data["proxy"], placeholder=self.message("不使用代理"), valid_empty=True, id="proxy", ),
|
||||
Label(self.message("请求数据超时限制,单位:秒"), classes="params", ),
|
||||
Input(str(self.data["timeout"]), placeholder="10", type="integer", id="timeout", ),
|
||||
Label(self.prompt.chunk, classes="params", ),
|
||||
Label(self.message("下载文件时,每次从服务器获取的数据块大小,单位:字节"), classes="params", ),
|
||||
Input(str(self.data["chunk"]), placeholder="1048576", type="integer", id="chunk", ),
|
||||
Label(self.prompt.max_retry, classes="params", ),
|
||||
Label(self.message("请求数据失败时,重试的最大次数"), classes="params", ),
|
||||
Input(str(self.data["max_retry"]), placeholder="5", type="integer", id="max_retry", ),
|
||||
Container(
|
||||
Label("", classes="params", ),
|
||||
Label("", classes="params", ),
|
||||
Label(self.prompt.image_format, classes="params", ),
|
||||
Label(self.prompt.language, classes="params", ),
|
||||
Label(self.message("图片下载格式"), classes="params", ),
|
||||
Label(self.message("程序语言"), classes="params", ),
|
||||
classes="horizontal-layout",
|
||||
),
|
||||
Container(
|
||||
Checkbox(self.prompt.record_data, id="record_data", value=self.data["record_data"], ),
|
||||
Checkbox(self.prompt.folder_mode, id="folder_mode", value=self.data["folder_mode"], ),
|
||||
Checkbox(self.message("记录作品数据"), id="record_data", value=self.data["record_data"], ),
|
||||
Checkbox(self.message("作品文件夹归档模式"), id="folder_mode", value=self.data["folder_mode"], ),
|
||||
Select.from_values(
|
||||
("PNG", "WEBP"),
|
||||
value=self.data["image_format"],
|
||||
allow_blank=False,
|
||||
id="image_format"),
|
||||
Select.from_values(list(LANGUAGE.keys()),
|
||||
Select.from_values(["zh_CN", "en_GB"],
|
||||
value=self.data["language"],
|
||||
allow_blank=False,
|
||||
id="language", ),
|
||||
classes="horizontal-layout"),
|
||||
Container(
|
||||
Button(self.prompt.save_button, id="save", ),
|
||||
Button(self.prompt.abandon_button, id="abandon", ),
|
||||
Button(self.message("保存配置"), id="save", ),
|
||||
Button(self.message("放弃更改"), id="abandon", ),
|
||||
classes="settings_button", ),
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def __check_cookie(self) -> str:
|
||||
if self.data["cookie"]:
|
||||
return self.prompt.cookie_placeholder_true
|
||||
return self.prompt.cookie_placeholder_false
|
||||
return self.message("小红书网页版 Cookie,无需登录,参数已设置")
|
||||
return self.message("小红书网页版 Cookie,无需登录,参数未设置")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.title = self.prompt.settings_title
|
||||
self.title = self.message("程序设置")
|
||||
|
||||
@on(Button.Pressed, "#save")
|
||||
def save_settings(self):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Callable
|
||||
|
||||
from aiohttp import ClientTimeout
|
||||
from rich.text import Text
|
||||
from textual import work
|
||||
@@ -17,23 +19,19 @@ from source.module import (
|
||||
INFO,
|
||||
RELEASES,
|
||||
)
|
||||
from source.translator import (
|
||||
English,
|
||||
Chinese,
|
||||
)
|
||||
|
||||
__all__ = ["Update"]
|
||||
|
||||
|
||||
class Update(ModalScreen):
|
||||
def __init__(self, app: XHS, language: Chinese | English):
|
||||
def __init__(self, app: XHS, message: Callable[[str], str]):
|
||||
super().__init__()
|
||||
self.xhs = app
|
||||
self.prompt = language
|
||||
self.message = message
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
Label(self.prompt.check_update_notification),
|
||||
Label(self.message("正在检查新版本,请稍等...")),
|
||||
LoadingIndicator(),
|
||||
classes="loading",
|
||||
)
|
||||
@@ -45,25 +43,22 @@ class Update(ModalScreen):
|
||||
latest_major, latest_minor = map(
|
||||
int, url.split("/")[-1].split(".", 1))
|
||||
if latest_major > VERSION_MAJOR or latest_minor > VERSION_MINOR:
|
||||
tip = Text(
|
||||
f"{self.prompt.official_version_update(
|
||||
latest_major,
|
||||
latest_minor)}\n{RELEASES}",
|
||||
style=WARNING)
|
||||
tip = Text(f"{self.message("检测到新版本:{0}.{1}").format(
|
||||
VERSION_MAJOR, VERSION_MINOR)}\n{RELEASES}", style=WARNING)
|
||||
elif latest_minor == VERSION_MINOR and VERSION_BETA:
|
||||
tip = Text(
|
||||
f"{self.prompt.development_version_update}\n{RELEASES}",
|
||||
f"{self.message("当前版本为开发版, 可更新至正式版")}\n{RELEASES}",
|
||||
style=WARNING)
|
||||
elif VERSION_BETA:
|
||||
tip = Text(
|
||||
self.prompt.latest_development_version,
|
||||
self.message("当前已是最新开发版"),
|
||||
style=WARNING)
|
||||
else:
|
||||
tip = Text(
|
||||
self.prompt.latest_official_version,
|
||||
self.message("当前已是最新正式版"),
|
||||
style=INFO)
|
||||
except ValueError:
|
||||
tip = Text(self.prompt.check_update_failure, style=ERROR)
|
||||
tip = Text(self.message("检测新版本失败"), style=ERROR)
|
||||
self.dismiss(tip)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user