mirror of
https://github.com/ihmily/StreamCap.git
synced 2026-05-06 21:51:36 +08:00
390 lines
17 KiB
Python
390 lines
17 KiB
Python
import asyncio
|
|
import os.path
|
|
from functools import partial
|
|
|
|
import flet as ft
|
|
|
|
from ...models.recording_model import Recording
|
|
from ...models.recording_status_model import RecordingStatus
|
|
from ...utils import utils
|
|
from ..views.storage_view import StoragePage
|
|
from .card_dialog import CardDialog
|
|
from .recording_dialog import RecordingDialog
|
|
from .video_player import VideoPlayer
|
|
|
|
|
|
class RecordingCardManager:
|
|
def __init__(self, app):
|
|
self.app = app
|
|
self.cards_obj = {}
|
|
self.update_duration_tasks = {}
|
|
self.selected_cards = {}
|
|
self.app.language_manager.add_observer(self)
|
|
self._ = {}
|
|
self.load()
|
|
self.pubsub_subscribe()
|
|
|
|
def load(self):
|
|
language = self.app.language_manager.language
|
|
for key in ("recording_card", "recording_manager", "base", "home_page", "video_quality", "storage_page"):
|
|
self._.update(language.get(key, {}))
|
|
|
|
def pubsub_subscribe(self):
|
|
self.app.page.pubsub.subscribe_topic("update", self.subscribe_update_card)
|
|
self.app.page.pubsub.subscribe_topic("delete", self.subscribe_remove_cards)
|
|
|
|
async def create_card(self, recording: Recording):
|
|
"""Create a card for a given recording."""
|
|
rec_id = recording.rec_id
|
|
if not self.cards_obj.get(rec_id):
|
|
if self.app.recording_enabled:
|
|
self.app.page.run_task(self.app.record_manager.check_if_live, recording)
|
|
else:
|
|
recording.status_info = RecordingStatus.NOT_RECORDING_SPACE
|
|
card_data = self._create_card_components(recording)
|
|
self.cards_obj[rec_id] = card_data
|
|
self.start_update_task(recording)
|
|
return card_data["card"]
|
|
|
|
def _create_card_components(self, recording: Recording):
|
|
"""create card components."""
|
|
speed = recording.speed
|
|
duration_text_label = ft.Text(self.app.record_manager.get_duration(recording), size=12)
|
|
|
|
record_button = ft.IconButton(
|
|
icon=self.get_icon_for_recording_state(recording),
|
|
tooltip=self.get_tip_for_recording_state(recording),
|
|
on_click=partial(self.recording_button_on_click, recording=recording),
|
|
)
|
|
|
|
edit_button = ft.IconButton(
|
|
icon=ft.Icons.EDIT,
|
|
tooltip=self._["edit_record_config"],
|
|
on_click=partial(self.edit_recording_button_click, recording=recording),
|
|
)
|
|
|
|
preview_button = ft.IconButton(
|
|
icon=ft.Icons.VIDEO_LIBRARY,
|
|
tooltip=self._["preview_video"],
|
|
on_click=partial(self.preview_video_button_on_click, recording=recording),
|
|
)
|
|
|
|
monitor_button = ft.IconButton(
|
|
icon=self.get_icon_for_monitor_state(recording),
|
|
tooltip=self.get_tip_for_monitor_state(recording),
|
|
on_click=partial(self.monitor_button_on_click, recording=recording),
|
|
)
|
|
|
|
delete_button = ft.IconButton(
|
|
icon=ft.Icons.DELETE,
|
|
tooltip=self._["delete_monitor"],
|
|
on_click=partial(self.recording_delete_button_click, recording=recording),
|
|
)
|
|
|
|
if recording.monitor_status:
|
|
display_title = recording.title
|
|
else:
|
|
display_title = f"[{self._['monitor_stopped']}] {recording.title}"
|
|
display_title_label = ft.Text(display_title, size=14, selectable=True, max_lines=1, no_wrap=True)
|
|
open_folder_button = ft.IconButton(
|
|
icon=ft.Icons.FOLDER,
|
|
tooltip=self._["open_folder"],
|
|
on_click=partial(self.recording_dir_button_on_click, recording=recording),
|
|
)
|
|
recording_info_button = ft.IconButton(
|
|
icon=ft.Icons.INFO,
|
|
tooltip=self._["recording_info"],
|
|
on_click=partial(self.recording_info_button_on_click, recording=recording),
|
|
)
|
|
speed_text_label = ft.Text(speed, size=12)
|
|
|
|
card_container = ft.Container(
|
|
content=ft.Column(
|
|
[
|
|
display_title_label,
|
|
duration_text_label,
|
|
speed_text_label,
|
|
ft.Row(
|
|
[
|
|
record_button,
|
|
open_folder_button,
|
|
recording_info_button,
|
|
preview_button,
|
|
edit_button,
|
|
delete_button,
|
|
monitor_button
|
|
],
|
|
spacing=5
|
|
),
|
|
],
|
|
spacing=5,
|
|
tight=True
|
|
),
|
|
padding=10,
|
|
on_click=partial(self.recording_card_on_click, recording=recording),
|
|
bgcolor=None,
|
|
border_radius=5,
|
|
|
|
)
|
|
card = ft.Card(key=str(recording.rec_id), content=card_container)
|
|
|
|
return {
|
|
"card": card,
|
|
"display_title_label": display_title_label,
|
|
"duration_label": duration_text_label,
|
|
"speed_label": speed_text_label,
|
|
"record_button": record_button,
|
|
"open_folder_button": open_folder_button,
|
|
"recording_info_button": recording_info_button,
|
|
"edit_button": edit_button,
|
|
"monitor_button": monitor_button,
|
|
}
|
|
|
|
async def update_card(self, recording):
|
|
"""Update only the recordings cards in the scrollable content area."""
|
|
if recording.rec_id in self.cards_obj:
|
|
recording_card = self.cards_obj[recording.rec_id]
|
|
recording_card["display_title_label"].value = recording.display_title
|
|
recording_card["duration_label"].value = self.app.record_manager.get_duration(recording)
|
|
recording_card["speed_label"].value = recording.speed
|
|
recording_card["record_button"].icon = self.get_icon_for_recording_state(recording)
|
|
recording_card["record_button"].tooltip = self.get_tip_for_recording_state(recording)
|
|
recording_card["monitor_button"].icon = self.get_icon_for_monitor_state(recording)
|
|
recording_card["monitor_button"].tooltip = self.get_tip_for_monitor_state(recording)
|
|
recording_card["card"].content.bgcolor = await self.update_record_hover(recording)
|
|
recording_card["card"].update()
|
|
|
|
async def update_monitor_state(self, recording: Recording):
|
|
"""Update the monitor button state based on the current monitoring status."""
|
|
if recording.monitor_status:
|
|
recording.update(
|
|
{
|
|
"recording": False,
|
|
"monitor_status": not recording.monitor_status,
|
|
"status_info": RecordingStatus.STOPPED_MONITORING,
|
|
"display_title": f"[{self._['monitor_stopped']}] {recording.title}",
|
|
}
|
|
)
|
|
self.app.record_manager.stop_recording(recording)
|
|
self.app.page.run_task(self.app.snack_bar.show_snack_bar, self._["stop_monitor_tip"])
|
|
else:
|
|
recording.update(
|
|
{
|
|
"monitor_status": not recording.monitor_status,
|
|
"status_info": RecordingStatus.MONITORING,
|
|
"display_title": f"{recording.title}",
|
|
}
|
|
)
|
|
self.app.page.run_task(self.app.record_manager.check_if_live, recording)
|
|
self.app.page.run_task(self.app.snack_bar.show_snack_bar, self._["start_monitor_tip"], ft.Colors.GREEN)
|
|
|
|
await self.update_card(recording)
|
|
self.app.page.pubsub.send_others_on_topic("update", recording)
|
|
self.app.page.run_task(self.app.record_manager.persist_recordings)
|
|
|
|
async def show_recording_info_dialog(self, recording: Recording):
|
|
"""Display a dialog with detailed information about the recording."""
|
|
dialog = CardDialog(self.app, recording)
|
|
dialog.open = True
|
|
self.app.dialog_area.content = dialog
|
|
self.app.page.update()
|
|
|
|
async def edit_recording_callback(self, recording_list: list[dict]):
|
|
recording_dict = recording_list[0]
|
|
rec_id = recording_dict["rec_id"]
|
|
recording = self.app.record_manager.find_recording_by_id(rec_id)
|
|
|
|
await self.app.record_manager.update_recording_card(recording, updated_info=recording_dict)
|
|
if not recording_dict["monitor_status"]:
|
|
recording.display_title = f"[{self._['monitor_stopped']}] " + recording.title
|
|
|
|
recording.scheduled_time_range = await self.app.record_manager.get_scheduled_time_range(
|
|
recording.scheduled_start_time, recording.monitor_hours)
|
|
|
|
await self.update_card(recording)
|
|
self.app.page.pubsub.send_others_on_topic("update", recording_dict)
|
|
|
|
async def on_toggle_recording(self, recording: Recording):
|
|
"""Toggle the recording state for a specific recording."""
|
|
if recording and self.app.recording_enabled:
|
|
if recording.recording:
|
|
self.app.record_manager.stop_recording(recording)
|
|
await self.app.snack_bar.show_snack_bar(self._["stop_record_tip"])
|
|
else:
|
|
if recording.monitor_status:
|
|
await self.app.record_manager.check_if_live(recording)
|
|
if recording.is_live:
|
|
self.app.record_manager.start_update(recording)
|
|
await self.app.snack_bar.show_snack_bar(self._["pre_record_tip"], bgcolor=ft.Colors.GREEN)
|
|
else:
|
|
await self.app.snack_bar.show_snack_bar(self._["is_not_live_tip"])
|
|
else:
|
|
await self.app.snack_bar.show_snack_bar(self._["please_start_monitor_tip"])
|
|
|
|
await self.update_card(recording)
|
|
self.app.page.pubsub.send_others_on_topic("update", recording)
|
|
|
|
async def on_delete_recording(self, recording: Recording):
|
|
"""Delete a recording from the list and update UI."""
|
|
if recording:
|
|
if recording.recording:
|
|
await self.app.snack_bar.show_snack_bar(self._["please_stop_monitor_tip"])
|
|
return
|
|
await self.app.record_manager.delete_recording_cards([recording])
|
|
await self.app.snack_bar.show_snack_bar(
|
|
self._["delete_recording_success_tip"], bgcolor=ft.Colors.GREEN, duration=2000
|
|
)
|
|
|
|
async def remove_recording_card(self, recordings: list[Recording]):
|
|
home_page = self.app.current_page
|
|
|
|
existing_ids = {rec.rec_id for rec in self.app.record_manager.recordings}
|
|
remove_ids = {rec.rec_id for rec in recordings}
|
|
keep_ids = existing_ids - remove_ids
|
|
|
|
cards_to_remove = [
|
|
card_data["card"]
|
|
for rec_id, card_data in self.cards_obj.items()
|
|
if rec_id not in keep_ids
|
|
]
|
|
|
|
home_page.recording_card_area.controls = [
|
|
control
|
|
for control in home_page.recording_card_area.controls
|
|
if control not in cards_to_remove
|
|
]
|
|
|
|
self.cards_obj = {
|
|
k: v for k, v in self.cards_obj.items()
|
|
if k in keep_ids
|
|
}
|
|
home_page.recording_card_area.update()
|
|
|
|
@staticmethod
|
|
async def update_record_hover(recording: Recording):
|
|
return ft.Colors.GREY_400 if recording.selected else None
|
|
|
|
@staticmethod
|
|
def get_icon_for_recording_state(recording: Recording):
|
|
"""Return the appropriate icon based on the recording's state."""
|
|
return ft.Icons.PLAY_CIRCLE if not recording.recording else ft.Icons.STOP_CIRCLE
|
|
|
|
def get_tip_for_recording_state(self, recording: Recording):
|
|
return self._["stop_record"] if recording.recording else self._["start_record"]
|
|
|
|
@staticmethod
|
|
def get_icon_for_monitor_state(recording: Recording):
|
|
"""Return the appropriate icon based on the monitor's state."""
|
|
return ft.Icons.VISIBILITY if recording.monitor_status else ft.Icons.VISIBILITY_OFF
|
|
|
|
def get_tip_for_monitor_state(self, recording: Recording):
|
|
return self._["stop_monitor"] if recording.monitor_status else self._["start_monitor"]
|
|
|
|
async def update_duration(self, recording: Recording):
|
|
"""Update the duration text periodically."""
|
|
while True:
|
|
await asyncio.sleep(1) # Update every second
|
|
if not recording or recording.rec_id not in self.cards_obj: # Stop task if card is removed
|
|
break
|
|
|
|
if recording.recording:
|
|
duration_label = self.cards_obj[recording.rec_id]["duration_label"]
|
|
duration_label.value = self.app.record_manager.get_duration(recording)
|
|
duration_label.update()
|
|
|
|
def start_update_task(self, recording: Recording):
|
|
"""Start a background task to update the duration text."""
|
|
self.update_duration_tasks[recording.rec_id] = self.app.page.run_task(self.update_duration, recording)
|
|
|
|
async def on_card_click(self, recording: Recording):
|
|
"""Handle card click events."""
|
|
recording.selected = not recording.selected
|
|
self.selected_cards[recording.rec_id] = recording
|
|
self.cards_obj[recording.rec_id]["card"].content.bgcolor = await self.update_record_hover(recording)
|
|
self.cards_obj[recording.rec_id]["card"].update()
|
|
|
|
async def recording_dir_on_click(self, recording: Recording):
|
|
if recording.recording_dir:
|
|
if os.path.exists(recording.recording_dir):
|
|
if not utils.open_folder(recording.recording_dir):
|
|
await self.app.snack_bar.show_snack_bar(self._['no_video_file'])
|
|
else:
|
|
await self.app.snack_bar.show_snack_bar(self._["no_recording_folder"])
|
|
|
|
async def edit_recording_button_click(self, _, recording: Recording):
|
|
"""Handle edit button click by showing the edit dialog with existing recording info."""
|
|
|
|
if recording.recording or recording.monitor_status:
|
|
await self.app.snack_bar.show_snack_bar(self._["please_stop_monitor_tip"])
|
|
return
|
|
|
|
await RecordingDialog(
|
|
self.app,
|
|
on_confirm_callback=self.edit_recording_callback,
|
|
recording=recording,
|
|
).show_dialog()
|
|
|
|
async def recording_delete_button_click(self, _, recording: Recording):
|
|
async def confirm_dlg(_):
|
|
self.app.page.run_task(self.on_delete_recording, recording)
|
|
await close_dialog(None)
|
|
|
|
async def close_dialog(_):
|
|
delete_alert_dialog.open = False
|
|
delete_alert_dialog.update()
|
|
|
|
delete_alert_dialog = ft.AlertDialog(
|
|
title=ft.Text(self._["confirm"]),
|
|
content=ft.Text(self._["delete_confirm_tip"]),
|
|
actions=[
|
|
ft.TextButton(text=self._["cancel"], on_click=close_dialog),
|
|
ft.TextButton(text=self._["sure"], on_click=confirm_dlg),
|
|
],
|
|
actions_alignment=ft.MainAxisAlignment.END,
|
|
modal=False,
|
|
)
|
|
delete_alert_dialog.open = True
|
|
self.app.dialog_area.content = delete_alert_dialog
|
|
self.app.page.update()
|
|
|
|
async def preview_video_button_on_click(self, _, recording: Recording):
|
|
if self.app.page.web and recording.record_url:
|
|
video_player = VideoPlayer(self.app)
|
|
await video_player.preview_video(recording.record_url, is_file_path=False, room_url=recording.url)
|
|
elif recording.recording_dir and os.path.exists(recording.recording_dir):
|
|
video_files = []
|
|
for root, _, files in os.walk(recording.recording_dir):
|
|
for file in files:
|
|
if utils.is_valid_video_file(file):
|
|
video_files.append(os.path.join(root, file))
|
|
|
|
if video_files:
|
|
video_files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
|
|
latest_video = video_files[0]
|
|
await StoragePage(self.app).preview_file(latest_video, recording.url)
|
|
else:
|
|
await self.app.snack_bar.show_snack_bar(self._["no_video_file"])
|
|
else:
|
|
await self.app.snack_bar.show_snack_bar(self._["no_recording_folder"])
|
|
|
|
async def recording_button_on_click(self, _, recording: Recording):
|
|
await self.on_toggle_recording(recording)
|
|
|
|
async def recording_dir_button_on_click(self, _, recording: Recording):
|
|
await self.recording_dir_on_click(recording)
|
|
|
|
async def recording_info_button_on_click(self, _, recording: Recording):
|
|
await self.show_recording_info_dialog(recording)
|
|
|
|
async def monitor_button_on_click(self, _, recording: Recording):
|
|
await self.update_monitor_state(recording)
|
|
|
|
async def recording_card_on_click(self, _, recording: Recording):
|
|
await self.on_card_click(recording)
|
|
|
|
async def subscribe_update_card(self, _, recording: Recording):
|
|
await self.update_card(recording)
|
|
|
|
async def subscribe_remove_cards(self, _, recordings: list[Recording]):
|
|
await self.remove_recording_card(recordings)
|