mirror of
https://github.com/ihmily/StreamCap.git
synced 2026-06-05 05:19:41 +08:00
fix: adapt to v0.85.0 changes
This commit is contained in:
@@ -105,7 +105,7 @@ async def handle_app_close(page: ft.Page, app, save_progress_overlay) -> None:
|
||||
color=ft.Colors.GREY_500,
|
||||
text_align=ft.TextAlign.CENTER,
|
||||
),
|
||||
padding=ft.padding.all(8),
|
||||
padding=ft.Padding.all(8),
|
||||
border_radius=5,
|
||||
bgcolor=ft.Colors.with_opacity(0.1, ft.Colors.BLUE_GREY),
|
||||
)
|
||||
@@ -154,7 +154,7 @@ async def handle_app_close(page: ft.Page, app, save_progress_overlay) -> None:
|
||||
tight=True,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
),
|
||||
padding=ft.padding.symmetric(horizontal=20, vertical=10),
|
||||
padding=ft.Padding.symmetric(horizontal=20, vertical=10),
|
||||
width=400 if page.platform.value != "macos" else None,
|
||||
),
|
||||
actions=close_confirm_actions,
|
||||
|
||||
@@ -151,7 +151,7 @@ class RecordingCardManager:
|
||||
on_click=lambda e, rec=recording: self.app.page.run_task(self.recording_card_on_click, e, rec),
|
||||
bgcolor=self.get_card_background_color(recording),
|
||||
border_radius=5,
|
||||
border=ft.border.all(2, self.get_card_border_color(recording)),
|
||||
border=ft.Border.all(2, self.get_card_border_color(recording)),
|
||||
)
|
||||
card = ft.Card(key=str(recording.rec_id), content=card_container)
|
||||
|
||||
@@ -239,7 +239,7 @@ class RecordingCardManager:
|
||||
|
||||
if recording_card["card"] and recording_card["card"].content:
|
||||
recording_card["card"].content.bgcolor = self.get_card_background_color(recording)
|
||||
recording_card["card"].content.border = ft.border.all(2, self.get_card_border_color(recording))
|
||||
recording_card["card"].content.border = ft.Border.all(2, self.get_card_border_color(recording))
|
||||
try:
|
||||
self.app.page.update()
|
||||
except (ft.FletPageDisconnectedException, AssertionError) as e:
|
||||
@@ -401,6 +401,11 @@ class RecordingCardManager:
|
||||
if not recording or recording.rec_id not in self.cards_obj: # Stop task if card is removed
|
||||
break
|
||||
|
||||
# Skip update when not on recordings page (cards are detached from page tree)
|
||||
current_page = getattr(self.app, "current_page", None)
|
||||
if not current_page or getattr(current_page, "page_name", None) != "recordings":
|
||||
continue
|
||||
|
||||
if recording.is_recording:
|
||||
try:
|
||||
duration_label = self.cards_obj[recording.rec_id]["duration_label"]
|
||||
|
||||
@@ -329,7 +329,7 @@ class RecordingDialog:
|
||||
ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Container(margin=ft.margin.only(top=10)),
|
||||
ft.Container(margin=ft.Margin.only(top=10)),
|
||||
url_field,
|
||||
streamer_name_field,
|
||||
format_row,
|
||||
@@ -347,7 +347,7 @@ class RecordingDialog:
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
)
|
||||
),
|
||||
ft.Container(content=batch_input, margin=ft.margin.only(top=15)),
|
||||
ft.Container(content=batch_input, margin=ft.Margin.only(top=15)),
|
||||
],
|
||||
expand=True,
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
@@ -55,8 +56,13 @@ class VideoPlayer:
|
||||
async def open_in_browser(_):
|
||||
self.app.page.launch_url(room_url)
|
||||
|
||||
async def take_screenshot(_):
|
||||
await self._take_screenshot(video, video_source, is_file_path)
|
||||
|
||||
actions = [ft.TextButton(self._["close"], on_click=close_dialog)]
|
||||
|
||||
actions.insert(0, ft.TextButton(self._["screenshot"], on_click=take_screenshot))
|
||||
|
||||
if room_url:
|
||||
actions.insert(0, ft.TextButton(self._["open_live_room_page"], on_click=open_in_browser))
|
||||
if not is_file_path:
|
||||
@@ -91,8 +97,8 @@ class VideoPlayer:
|
||||
tight=True,
|
||||
),
|
||||
actions=[],
|
||||
inset_padding=ft.padding.only(left=10, right=10, top=5, bottom=5),
|
||||
content_padding=ft.padding.only(left=5, right=5, top=5, bottom=0),
|
||||
inset_padding=ft.Padding.only(left=10, right=10, top=5, bottom=5),
|
||||
content_padding=ft.Padding.only(left=5, right=5, top=5, bottom=0),
|
||||
)
|
||||
else:
|
||||
dialog = ft.AlertDialog(
|
||||
@@ -106,6 +112,41 @@ class VideoPlayer:
|
||||
self.app.dialog_area.content = dialog
|
||||
self.app.dialog_area.update()
|
||||
|
||||
async def _take_screenshot(self, video: ftv.Video, video_source: str, is_file_path: bool):
|
||||
try:
|
||||
image_bytes = await video.take_screenshot(format="image/png")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to take screenshot: {e}")
|
||||
await self.app.snack_bar.show_snack_bar(self._["screenshot_failed"])
|
||||
return
|
||||
|
||||
if not image_bytes:
|
||||
await self.app.snack_bar.show_snack_bar(self._["screenshot_failed"])
|
||||
return
|
||||
|
||||
try:
|
||||
if is_file_path and video_source and os.path.isfile(video_source):
|
||||
screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(video_source)), "screenshots")
|
||||
base_name = Path(video_source).stem
|
||||
else:
|
||||
save_root = self.app.settings.get_video_save_path()
|
||||
screenshot_dir = os.path.join(save_root, "screenshots")
|
||||
base_name = "screenshot"
|
||||
os.makedirs(screenshot_dir, exist_ok=True)
|
||||
filename = f"{base_name}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.png"
|
||||
file_path = os.path.join(screenshot_dir, filename)
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(image_bytes)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save screenshot: {e}")
|
||||
await self.app.snack_bar.show_snack_bar(self._["screenshot_failed"])
|
||||
return
|
||||
|
||||
logger.info(f"Screenshot saved: {file_path}")
|
||||
await self.app.snack_bar.show_snack_bar(
|
||||
f"{self._['screenshot_success']}", bgcolor=ft.Colors.PRIMARY, duration=3000
|
||||
)
|
||||
|
||||
async def preview_video(self, source: str, is_file_path: bool = True, room_url: str | None = None):
|
||||
"""
|
||||
Preview video
|
||||
|
||||
@@ -42,7 +42,7 @@ class SaveProgressOverlay:
|
||||
bgcolor="#FF5252",
|
||||
shape=ft.RoundedRectangleBorder(radius=8),
|
||||
elevation=0,
|
||||
padding=ft.padding.symmetric(horizontal=20, vertical=10),
|
||||
padding=ft.Padding.symmetric(horizontal=20, vertical=10),
|
||||
),
|
||||
tooltip=self._["force_close_tooltip"],
|
||||
visible=False,
|
||||
@@ -63,7 +63,7 @@ class SaveProgressOverlay:
|
||||
self.content_container = ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Container(content=self.progress_ring, margin=ft.margin.only(bottom=20)),
|
||||
ft.Container(content=self.progress_ring, margin=ft.Margin.only(bottom=20)),
|
||||
self.message_text,
|
||||
ft.Container(height=25),
|
||||
self.cancel_button,
|
||||
@@ -76,7 +76,7 @@ class SaveProgressOverlay:
|
||||
),
|
||||
width=400,
|
||||
height=280,
|
||||
padding=ft.padding.all(30),
|
||||
padding=ft.Padding.all(30),
|
||||
alignment=ft.alignment.Alignment.CENTER,
|
||||
bgcolor=ft.Colors.with_opacity(0.95, "#212121"),
|
||||
border_radius=16,
|
||||
@@ -91,7 +91,7 @@ class SaveProgressOverlay:
|
||||
self.simple_container = ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Container(content=self.simple_progress_ring, margin=ft.margin.only(bottom=15)),
|
||||
ft.Container(content=self.simple_progress_ring, margin=ft.Margin.only(bottom=15)),
|
||||
self.message_text,
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
@@ -100,7 +100,7 @@ class SaveProgressOverlay:
|
||||
),
|
||||
width=300,
|
||||
height=180,
|
||||
padding=ft.padding.all(25),
|
||||
padding=ft.Padding.all(25),
|
||||
alignment=ft.alignment.Alignment.CENTER,
|
||||
bgcolor=ft.Colors.with_opacity(0.95, "#212121"),
|
||||
border_radius=16,
|
||||
|
||||
@@ -46,7 +46,7 @@ class ShowSnackBar:
|
||||
|
||||
if not self.app.is_mobile:
|
||||
snack_bar_width = 350
|
||||
snack_bar.margin = ft.margin.only(left=self.app.page.width - snack_bar_width, top=0, right=10, bottom=10)
|
||||
snack_bar.margin = ft.Margin.only(left=self.app.page.width - snack_bar_width, top=0, right=10, bottom=10)
|
||||
|
||||
snack_bar.open = True
|
||||
self.app.snack_bar_area.content = snack_bar
|
||||
|
||||
@@ -24,7 +24,7 @@ class SearchDialog(ft.AlertDialog):
|
||||
|
||||
super().__init__(
|
||||
title=ft.Text(search_title, size=20, weight=ft.FontWeight.BOLD),
|
||||
content_padding=ft.padding.only(left=20, top=15, right=20, bottom=20),
|
||||
content_padding=ft.Padding.only(left=20, top=15, right=20, bottom=20),
|
||||
)
|
||||
self.query = ft.TextField(
|
||||
hint_text=self._["search_keyword"],
|
||||
|
||||
@@ -270,7 +270,7 @@ class AboutPage(PageBase):
|
||||
expand=True,
|
||||
),
|
||||
padding=20,
|
||||
margin=ft.margin.symmetric(0, 10),
|
||||
margin=ft.Margin.symmetric(vertical=0, horizontal=10),
|
||||
bgcolor=card_bg_color,
|
||||
border_radius=15,
|
||||
shadow=ft.BoxShadow(
|
||||
|
||||
@@ -64,7 +64,7 @@ class HomePage(PageBase):
|
||||
ft.Container(
|
||||
content=logo,
|
||||
alignment=ft.alignment.Alignment.CENTER,
|
||||
margin=ft.margin.only(top=30, bottom=10),
|
||||
margin=ft.Margin.only(top=30, bottom=10),
|
||||
),
|
||||
ft.Text(
|
||||
f"{greeting},{self._['welcome']}",
|
||||
@@ -94,7 +94,7 @@ class HomePage(PageBase):
|
||||
spacing=10,
|
||||
),
|
||||
alignment=ft.alignment.Alignment.CENTER,
|
||||
padding=ft.padding.only(bottom=20),
|
||||
padding=ft.Padding.only(bottom=20),
|
||||
)
|
||||
|
||||
def create_quick_action_area(self):
|
||||
@@ -170,7 +170,7 @@ class HomePage(PageBase):
|
||||
spacing=10,
|
||||
),
|
||||
alignment=ft.alignment.Alignment.CENTER,
|
||||
padding=ft.padding.only(bottom=20),
|
||||
padding=ft.Padding.only(bottom=20),
|
||||
)
|
||||
else:
|
||||
return ft.Container(
|
||||
@@ -199,7 +199,7 @@ class HomePage(PageBase):
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
spacing=button_spacing,
|
||||
),
|
||||
margin=ft.margin.only(bottom=10),
|
||||
margin=ft.Margin.only(bottom=10),
|
||||
),
|
||||
ft.Container(
|
||||
content=ft.Row(
|
||||
@@ -238,7 +238,7 @@ class HomePage(PageBase):
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
),
|
||||
alignment=ft.alignment.Alignment.CENTER,
|
||||
padding=ft.padding.only(bottom=20),
|
||||
padding=ft.Padding.only(bottom=20),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -259,7 +259,7 @@ class HomePage(PageBase):
|
||||
),
|
||||
style=ft.ButtonStyle(
|
||||
shape=ft.RoundedRectangleBorder(radius=10),
|
||||
padding=ft.padding.all(15),
|
||||
padding=ft.Padding.all(15),
|
||||
bgcolor=color,
|
||||
elevation=5,
|
||||
),
|
||||
@@ -295,15 +295,15 @@ class HomePage(PageBase):
|
||||
else ft.Colors.WHITE70
|
||||
),
|
||||
),
|
||||
margin=ft.margin.only(left=34),
|
||||
margin=ft.Margin.only(left=34),
|
||||
),
|
||||
],
|
||||
spacing=5,
|
||||
),
|
||||
padding=ft.padding.all(15),
|
||||
padding=ft.Padding.all(15),
|
||||
),
|
||||
elevation=2,
|
||||
margin=ft.margin.only(bottom=5),
|
||||
margin=ft.Margin.only(bottom=5),
|
||||
)
|
||||
|
||||
announcement_list = self.app.about.about_config["version_updates"][0]["announcement"][self.app.language_code]
|
||||
@@ -341,12 +341,12 @@ class HomePage(PageBase):
|
||||
controls=announcements,
|
||||
spacing=5,
|
||||
),
|
||||
padding=ft.padding.only(top=5),
|
||||
padding=ft.Padding.only(top=5),
|
||||
),
|
||||
],
|
||||
spacing=5,
|
||||
),
|
||||
padding=ft.padding.only(left=20, right=20),
|
||||
padding=ft.Padding.only(left=20, right=20),
|
||||
)
|
||||
|
||||
def create_stats_area(self):
|
||||
@@ -366,7 +366,7 @@ class HomePage(PageBase):
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
spacing=5,
|
||||
),
|
||||
padding=ft.padding.all(15),
|
||||
padding=ft.Padding.all(15),
|
||||
border_radius=10,
|
||||
bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
|
||||
width=150,
|
||||
@@ -435,14 +435,14 @@ class HomePage(PageBase):
|
||||
controls=recent_recordings,
|
||||
spacing=8,
|
||||
),
|
||||
padding=ft.padding.only(top=5),
|
||||
padding=ft.Padding.only(top=5),
|
||||
),
|
||||
],
|
||||
spacing=5,
|
||||
alignment=ft.MainAxisAlignment.START,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.START,
|
||||
),
|
||||
padding=ft.padding.all(15),
|
||||
padding=ft.Padding.all(15),
|
||||
border_radius=10,
|
||||
bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
|
||||
expand=True,
|
||||
@@ -529,12 +529,12 @@ class HomePage(PageBase):
|
||||
),
|
||||
ft.Container(
|
||||
content=stats_content,
|
||||
padding=ft.padding.only(top=10),
|
||||
padding=ft.Padding.only(top=10),
|
||||
),
|
||||
],
|
||||
spacing=5,
|
||||
),
|
||||
padding=ft.padding.only(left=20, right=20),
|
||||
padding=ft.Padding.only(left=20, right=20),
|
||||
)
|
||||
|
||||
def create_features_area(self):
|
||||
@@ -571,7 +571,7 @@ class HomePage(PageBase):
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
),
|
||||
padding=ft.padding.all(15),
|
||||
padding=ft.Padding.all(15),
|
||||
alignment=ft.alignment.Alignment.CENTER,
|
||||
width=None if is_mobile else 220,
|
||||
expand=is_mobile,
|
||||
@@ -653,13 +653,13 @@ class HomePage(PageBase):
|
||||
),
|
||||
ft.Container(
|
||||
content=feature_cards,
|
||||
padding=ft.padding.only(top=10),
|
||||
padding=ft.Padding.only(top=10),
|
||||
),
|
||||
],
|
||||
horizontal_alignment=ft.CrossAxisAlignment.START,
|
||||
),
|
||||
alignment=ft.alignment.Alignment.BOTTOM_LEFT,
|
||||
padding=ft.padding.only(left=20, right=20, bottom=30),
|
||||
padding=ft.Padding.only(left=20, right=20, bottom=30),
|
||||
)
|
||||
|
||||
async def on_start_recording_click(self, _):
|
||||
|
||||
@@ -56,7 +56,7 @@ class LoginPage:
|
||||
color="#ffffff",
|
||||
bgcolor="#0078d4",
|
||||
elevation=0,
|
||||
padding=ft.padding.symmetric(horizontal=10, vertical=4),
|
||||
padding=ft.Padding.symmetric(horizontal=10, vertical=4),
|
||||
animation_duration=300,
|
||||
),
|
||||
)
|
||||
@@ -79,7 +79,7 @@ class LoginPage:
|
||||
ft.Container(
|
||||
content=self.logo,
|
||||
alignment=ft.alignment.Alignment.CENTER,
|
||||
margin=ft.margin.only(bottom=10),
|
||||
margin=ft.Margin.only(bottom=10),
|
||||
),
|
||||
ft.Text(
|
||||
"StreamCap",
|
||||
@@ -100,7 +100,7 @@ class LoginPage:
|
||||
self.password_field,
|
||||
ft.Container(
|
||||
content=self.error_text,
|
||||
margin=ft.margin.only(top=10),
|
||||
margin=ft.Margin.only(top=10),
|
||||
alignment=ft.alignment.Alignment.CENTER,
|
||||
),
|
||||
ft.Container(height=20),
|
||||
|
||||
@@ -194,7 +194,7 @@ class RecordingsPage(PageBase):
|
||||
color=ft.Colors.WHITE if self.current_filter == "all" else None,
|
||||
style=ft.ButtonStyle(
|
||||
shape=ft.RoundedRectangleBorder(radius=5),
|
||||
padding=ft.padding.symmetric(horizontal=10, vertical=4),
|
||||
padding=ft.Padding.symmetric(horizontal=10, vertical=4),
|
||||
),
|
||||
),
|
||||
ft.Button(
|
||||
@@ -204,7 +204,7 @@ class RecordingsPage(PageBase):
|
||||
color=ft.Colors.WHITE if self.current_filter == "recording" else None,
|
||||
style=ft.ButtonStyle(
|
||||
shape=ft.RoundedRectangleBorder(radius=5),
|
||||
padding=ft.padding.symmetric(horizontal=10, vertical=4),
|
||||
padding=ft.Padding.symmetric(horizontal=10, vertical=4),
|
||||
),
|
||||
),
|
||||
ft.Button(
|
||||
@@ -214,7 +214,7 @@ class RecordingsPage(PageBase):
|
||||
color=ft.Colors.WHITE if self.current_filter == "living" else None,
|
||||
style=ft.ButtonStyle(
|
||||
shape=ft.RoundedRectangleBorder(radius=5),
|
||||
padding=ft.padding.symmetric(horizontal=10, vertical=4),
|
||||
padding=ft.Padding.symmetric(horizontal=10, vertical=4),
|
||||
),
|
||||
),
|
||||
ft.Button(
|
||||
@@ -224,7 +224,7 @@ class RecordingsPage(PageBase):
|
||||
color=ft.Colors.WHITE if self.current_filter == "offline" else None,
|
||||
style=ft.ButtonStyle(
|
||||
shape=ft.RoundedRectangleBorder(radius=5),
|
||||
padding=ft.padding.symmetric(horizontal=10, vertical=4),
|
||||
padding=ft.Padding.symmetric(horizontal=10, vertical=4),
|
||||
),
|
||||
),
|
||||
ft.Button(
|
||||
@@ -234,7 +234,7 @@ class RecordingsPage(PageBase):
|
||||
color=ft.Colors.WHITE if self.current_filter == "error" else None,
|
||||
style=ft.ButtonStyle(
|
||||
shape=ft.RoundedRectangleBorder(radius=5),
|
||||
padding=ft.padding.symmetric(horizontal=10, vertical=4),
|
||||
padding=ft.Padding.symmetric(horizontal=10, vertical=4),
|
||||
),
|
||||
),
|
||||
ft.Button(
|
||||
@@ -244,7 +244,7 @@ class RecordingsPage(PageBase):
|
||||
color=ft.Colors.WHITE if self.current_filter == "stopped" else None,
|
||||
style=ft.ButtonStyle(
|
||||
shape=ft.RoundedRectangleBorder(radius=5),
|
||||
padding=ft.padding.symmetric(horizontal=10, vertical=4),
|
||||
padding=ft.Padding.symmetric(horizontal=10, vertical=4),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -270,7 +270,7 @@ class RecordingsPage(PageBase):
|
||||
hover_color=ft.Colors.PRIMARY,
|
||||
width=120,
|
||||
text_size=14,
|
||||
content_padding=ft.padding.only(top=8, bottom=8, left=10, right=10),
|
||||
content_padding=ft.Padding.only(top=8, bottom=8, left=10, right=10),
|
||||
border_radius=5,
|
||||
border_color=ft.Colors.OUTLINE,
|
||||
focused_border_color=ft.Colors.PRIMARY,
|
||||
|
||||
@@ -1113,7 +1113,7 @@ class SettingsPage(PageBase):
|
||||
ft.Text(label, text_align=ft.TextAlign.LEFT, weight=ft.FontWeight.BOLD),
|
||||
ft.Container(
|
||||
content=checkbox_grid,
|
||||
margin=ft.margin.only(top=5, bottom=10),
|
||||
margin=ft.Margin.only(top=5, bottom=10),
|
||||
),
|
||||
],
|
||||
spacing=5,
|
||||
@@ -1215,7 +1215,7 @@ class SettingsPage(PageBase):
|
||||
ft.Text(label, text_align=ft.TextAlign.LEFT),
|
||||
ft.Container(
|
||||
content=control,
|
||||
margin=ft.margin.only(top=5, bottom=10),
|
||||
margin=ft.Margin.only(top=5, bottom=10),
|
||||
expand=True,
|
||||
width=float("inf"),
|
||||
),
|
||||
|
||||
@@ -456,7 +456,10 @@
|
||||
"stream_source": "Stream Source",
|
||||
"previewing": "Previewing",
|
||||
"view_stream_source_now": "Accessing live stream source",
|
||||
"unsupported_play_on_web": "Only MP4 files can be previewed on the web ⚠️ "
|
||||
"unsupported_play_on_web": "Only MP4 files can be previewed on the web ⚠️ ",
|
||||
"screenshot": "Screenshot",
|
||||
"screenshot_success": "Screenshot saved",
|
||||
"screenshot_failed": "Screenshot failed ⚠️"
|
||||
},
|
||||
"update": {
|
||||
"new_version": "New Version {version} Available",
|
||||
|
||||
@@ -456,7 +456,10 @@
|
||||
"stream_source": "直播源",
|
||||
"previewing":"正在预览",
|
||||
"view_stream_source_now": "正在访问直播源",
|
||||
"unsupported_play_on_web": "Web端只支持预览MP4文件 ⚠️"
|
||||
"unsupported_play_on_web": "Web端只支持预览MP4文件 ⚠️",
|
||||
"screenshot": "截图",
|
||||
"screenshot_success": "截图已保存",
|
||||
"screenshot_failed": "截图失败 ⚠️"
|
||||
},
|
||||
"update": {
|
||||
"new_version": "发现新版本 {version}",
|
||||
|
||||
Reference in New Issue
Block a user