diff --git a/src/core/configuration.py b/src/core/configuration.py index 4f4fc0e..89bb73d 100644 --- a/src/core/configuration.py +++ b/src/core/configuration.py @@ -15,7 +15,7 @@ class AppConfig(BaseModel): target_platform: str = Field(default="fluxer") # fluxer | stoat | none target_bot_token: Optional[str] = Field(default=None) target_server_id: Optional[str] = Field(default=None) - target_api_url: Optional[str] = Field(default="default") + target_api_url: Optional[str] = Field(default=None) migration: MigrationSettings = Field(default_factory=MigrationSettings) # ── backward‑compat shims (read‑only) ──────────────────────────────── @@ -77,12 +77,12 @@ def load_config(config_path: Union[str, Path] = "config.yaml", create_if_missing data.setdefault("target_platform", "fluxer") data.setdefault("target_bot_token", data["fluxer_bot_token"]) data.setdefault("target_server_id", data.get("fluxer_community_id")) - data.setdefault("target_api_url", data.get("fluxer_api_url", "default")) + data.setdefault("target_api_url", data.get("fluxer_api_url") or "default") elif data.get("stoat_bot_token") and data["stoat_bot_token"] not in ("STOAT_BOT_TOKEN", None): data.setdefault("target_platform", "stoat") data.setdefault("target_bot_token", data["stoat_bot_token"]) data.setdefault("target_server_id", data.get("stoat_server_id")) - data.setdefault("target_api_url", data.get("stoat_api_url", "default")) + data.setdefault("target_api_url", data.get("stoat_api_url") or "default") # Remove legacy keys so they don't conflict with the model for key in ("fluxer_bot_token", "fluxer_community_id", "fluxer_api_url", "stoat_bot_token", "stoat_server_id", "stoat_api_url", diff --git a/src/ui/backup_ops.py b/src/ui/backup_ops.py index f66178d..1e3902f 100644 --- a/src/ui/backup_ops.py +++ b/src/ui/backup_ops.py @@ -73,9 +73,9 @@ class BackupPane(Container): yield Label("", id="bp_lbl_backup") with Vertical(id="bp_actions"): - yield Button("Backup Server Profile", id="bp_backup_profile", disabled=True) - yield Button("Backup Channel Messages", id="bp_backup_msgs", disabled=True, variant="primary") - yield Button("Update Existing Backup", id="bp_backup_sync", disabled=True, variant="success") + yield Button("Backup Server Profile", id="bp_backup_profile", disabled=True, tooltip="Backup Discord server roles, emojis, and channel structure") + yield Button("Backup Channel Messages", id="bp_backup_msgs", disabled=True, variant="primary", tooltip="Select and backup message history from text channels") + yield Button("Update Existing Backup", id="bp_backup_sync", disabled=True, variant="success", tooltip="Scan for new messages\n& Update existing backup") def on_mount(self) -> None: self._validate() diff --git a/src/ui/main_app.py b/src/ui/main_app.py index 6f598d8..5497ad3 100644 --- a/src/ui/main_app.py +++ b/src/ui/main_app.py @@ -39,9 +39,9 @@ class NewConfigModal(ModalScreen[str]): def compose(self) -> ComposeResult: with Vertical(id="new_config_dialog"): yield Label("Enter new configuration name:", id="new_config_title") - yield Input(placeholder="e.g. MyServer", id="new_config_input") + yield Input(placeholder="e.g. MyServer", id="new_config_input", tooltip="Enter a unique name for this config") with Horizontal(id="new_config_buttons"): - yield Button("Create", variant="success", id="btn_create") + yield Button("Create", variant="success", id="btn_create", tooltip="Create config and launch setup") yield Button("Cancel", variant="primary", id="btn_cancel") def _get_sanitized_name(self) -> str: @@ -97,7 +97,7 @@ class ConfigSelectionScreen(Screen): with VerticalScroll(id="config_list_container"): yield ListView(id="config_list") with Horizontal(id="config_sel_actions"): - yield Button("New Config", id="btn_new_config", variant="success") + yield Button("New Config", id="btn_new_config", variant="success", tooltip="Create a new configuration folder") yield Button("Exit", id="btn_exit", variant="error") yield Footer() @@ -219,9 +219,10 @@ class ConfigScreen(Screen): value=self.config.discord_bot_token or "", id="inp_discord_token", password=True, - placeholder="Paste Bot Token here" + placeholder="Paste Bot Token here", + tooltip="Enter your Discord BOT token from the Developer Portal" ) - yield Button("Validate", id="btn_fetch_guilds", variant="primary") + yield Button("Validate", id="btn_fetch_guilds", variant="primary", tooltip="Verify token and fetch available Discord servers") yield Label("Server ID:", classes="field_label") yield Select( @@ -237,17 +238,17 @@ class ConfigScreen(Screen): yield RadioButton( "Shuttle Transfer (direct migration)", id="radio_direct", - value=(cur_mode == "direct_transfer"), + value=(cur_mode == "direct_transfer") ) yield RadioButton( "Backup & Migrate (backup first, then migrate)", id="radio_backup", - value=(cur_mode == "backup_transfer"), + value=(cur_mode == "backup_transfer") ) yield RadioButton( "Backup Only (local backup, no migration)", id="radio_bkonly", - value=(cur_mode == "backup_only"), + value=(cur_mode == "backup_only") ) # ── Target Platform (hidden for backup_only) ───────────── @@ -258,12 +259,12 @@ class ConfigScreen(Screen): yield RadioButton( "Fluxer", id="radio_fluxer", - value=(cur_plat == "fluxer"), + value=(cur_plat == "fluxer") ) yield RadioButton( "Stoat", id="radio_stoat", - value=(cur_plat == "stoat"), + value=(cur_plat == "stoat") ) yield Label("Bot Token:", classes="field_label") with Horizontal(classes="fetch_row"): @@ -271,9 +272,10 @@ class ConfigScreen(Screen): value=self.config.target_bot_token or "", id="inp_target_token", password=True, - placeholder="Paste Target Bot Token" + placeholder="Paste Target Bot Token", + tooltip="Enter the Bot token for the target platform" ) - yield Button("Validate", id="btn_fetch_target_servers", variant="primary") + yield Button("Validate", id="btn_fetch_target_servers", variant="primary", tooltip="Verify token and fetch available communities") yield Label("Community / Server ID:", classes="field_label") yield Select( @@ -284,13 +286,15 @@ class ConfigScreen(Screen): yield Label("Target API URL:", classes="field_label") yield Input( - value=self.config.target_api_url or "default", + value=self.config.target_api_url if (self.config.target_api_url and self.config.target_api_url != "default") else "", id="inp_target_api", + placeholder="Leave this Empty for official instance", + tooltip="Enter the custom API url\nfor self hosted instances" ) yield Rule() with Horizontal(id="cfg_actions"): - yield Button("Save Configuration", variant="success", id="btn_save") + yield Button("Save Configuration", variant="success", id="btn_save", tooltip="Save all changes to config.yaml") yield Button("Back", id="btn_back") yield Footer() @@ -455,7 +459,7 @@ class ConfigScreen(Screen): self.config.target_server_id = None target_api = self.query_one("#inp_target_api", Input).value.strip() - self.config.target_api_url = target_api or "default" + self.config.target_api_url = target_api or None else: self.config.target_platform = "none" diff --git a/src/ui/modals.py b/src/ui/modals.py index 708d1c0..ab192fe 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -101,14 +101,14 @@ class ProgressScreen(Screen[None]): with Vertical(id="prog_actions"): with Horizontal(classes="action_row", id="prog_actions_row1"): - yield Button("Start from First", id="btn_start_first", disabled=True, variant="primary") - yield Button("Continue Migration", id="btn_continue", disabled=True, variant="success") - yield Button("Start from ID", id="btn_start_id", disabled=True, variant="warning") + yield Button("Start from First", id="btn_start_first", disabled=True, variant="primary", tooltip="Start the operation from the beginning") + yield Button("Continue Migration", id="btn_continue", disabled=True, variant="success", tooltip="Resume the operation from the last saved state") + yield Button("Start from ID", id="btn_start_id", disabled=True, variant="warning", tooltip="Start or resume from a specific Discord Message ID") with Horizontal(classes="action_row", id="prog_actions_row2"): yield Button("Back", id="btn_back", disabled=False) yield Button("Main Menu", id="btn_main_menu", disabled=False) with Horizontal(classes="action_row", id="prog_actions_cancel"): - yield Button("Cancel", id="btn_cancel", variant="error") + yield Button("Cancel", id="btn_cancel", variant="error", tooltip="Stop current operation") yield Footer() def __init__(self, log_level: str = "INFO", *args, **kwargs): @@ -232,7 +232,11 @@ class ProgressScreen(Screen[None]): show_id: bool = True, btn_start_label: str = "Start from First", btn_continue_label: str = "Continue Migration", - btn_id_label: str = "Start from ID" + btn_id_label: str = "Start from ID", + btn_start_variant: str = "primary", + btn_start_tooltip: str | None = None, + btn_continue_tooltip: str | None = None, + btn_id_tooltip: str | None = None ): """Phase 2: Wait for user confirmation after analysis.""" try: self.query_one("#prog_loader", LoadingIndicator).display = False @@ -241,12 +245,27 @@ class ProgressScreen(Screen[None]): try: self.query_one("#prog_timer", Label).display = False except Exception: pass - # Update button labels - try: self.query_one("#btn_start_first", Button).label = btn_start_label + # Update button labels, variants and tooltips + try: + btn_start = self.query_one("#btn_start_first", Button) + btn_start.label = btn_start_label + btn_start.variant = btn_start_variant + if btn_start_tooltip: + btn_start.tooltip = btn_start_tooltip except Exception: pass - try: self.query_one("#btn_continue", Button).label = btn_continue_label + + try: + btn_cont = self.query_one("#btn_continue", Button) + btn_cont.label = btn_continue_label + if btn_continue_tooltip: + btn_cont.tooltip = btn_continue_tooltip except Exception: pass - try: self.query_one("#btn_start_id", Button).label = btn_id_label + + try: + btn_id = self.query_one("#btn_start_id", Button) + btn_id.label = btn_id_label + if btn_id_tooltip: + btn_id.tooltip = btn_id_tooltip except Exception: pass # Show confirmation buttons @@ -426,8 +445,8 @@ class OptionSelectModal(ModalScreen[list[str]]): with Vertical(id="opt_dialog"): yield Label(self._title, id="opt_title") with Horizontal(id="opt_batch_buttons"): - yield Button("Select All", id="btn_opt_all", flat=True) - yield Button("Deselect All", id="btn_opt_none", flat=True) + yield Button("Select All", id="btn_opt_all", flat=True, tooltip="Select all available options") + yield Button("Deselect All", id="btn_opt_none", flat=True, tooltip="Deselect all options") with Vertical(id="opt_scroll"): for opt_id, label in self._options: @@ -435,8 +454,8 @@ class OptionSelectModal(ModalScreen[list[str]]): yield Rule() with Horizontal(id="opt_buttons"): - yield Button("Proceed", variant="success", id="btn_opt_ok") - yield Button("Back", id="btn_opt_back") + yield Button("Proceed", variant="success", id="btn_opt_ok", tooltip="Proceed with the selected options") + yield Button("Back", id="btn_opt_back", tooltip="Return to the previous menu") def on_button_pressed(self, event: Button.Pressed): if event.button.id == "btn_opt_back": @@ -568,8 +587,8 @@ class ChannelPickerScreen(Screen[tuple]): yield Rule() with Horizontal(id="chanpick_buttons"): - yield Button("Select", variant="success", id="btn_pick_ok") - yield Button("Back", id="btn_pick_back") + yield Button("Select", variant="success", id="btn_pick_ok", tooltip="Confirm selection and start migration") + yield Button("Back", id="btn_pick_back", tooltip="Cancel selection") yield Footer() def on_mount(self) -> None: @@ -719,17 +738,17 @@ class ChannelSelectScreen(Screen[dict]): yield Label("Note: Channels shown in green have existing backups", classes="label_warning") with Horizontal(id="select_all_buttons"): - yield Button("Select All", id="btn_all") - yield Button("Deselect All", id="btn_none") + yield Button("Select All", id="btn_all", tooltip="Select all channels for backup") + yield Button("Deselect All", id="btn_none", tooltip="Deselect all channels") yield Rule() with Horizontal(id="confirm_buttons"): if self.any_found: - yield Button("Sync", variant="success", id="btn_sync") - yield Button("Force Overwrite", variant="warning", id="btn_force") + yield Button("Sync", variant="success", id="btn_sync", tooltip="Backup new channels\n& update existing backups") + yield Button("Force Overwrite", variant="warning", id="btn_force", tooltip="Overwrite existing backups\nwith fresh data") else: - yield Button("Backup", variant="success", id="btn_backup") - yield Button("Back", id="btn_cancel_chan") + yield Button("Backup", variant="success", id="btn_backup", tooltip="Start backing up selected channels") + yield Button("Back", id="btn_cancel_chan", tooltip="Cancel and go back") yield Footer() def on_button_pressed(self, event: Button.Pressed) -> None: @@ -798,8 +817,8 @@ class MessageIDInputModal(ModalScreen[int | None]): yield Label("Enter an ID and click Verify to preview.", id="lbl_msg_preview") with Horizontal(id="msg_id_buttons"): - yield Button("Verify", variant="primary", id="btn_verify_start", disabled=True) - yield Button("Back", variant="warning", id="btn_cancel_msg_id") + yield Button("Verify", variant="primary", id="btn_verify_start", disabled=True, tooltip="Check if this message ID exists in the channel") + yield Button("Back", variant="warning", id="btn_cancel_msg_id", tooltip="Cancel and go back") def on_input_changed(self, event: Input.Changed) -> None: if event.input.id == "input_msg_id": diff --git a/src/ui/mode_screen.py b/src/ui/mode_screen.py index d767876..b066056 100644 --- a/src/ui/mode_screen.py +++ b/src/ui/mode_screen.py @@ -129,8 +129,8 @@ class ModeScreen(Screen): yield Rule() with Horizontal(id="bottom_actions"): if mode == "backup_transfer": - yield Button("Switch to Migrate ⇄", id="btn_switch", variant="primary") - yield Button("Configuration", id="btn_config", variant="success") + yield Button("Switch to Migrate ⇄", id="btn_switch", variant="primary", tooltip="Switch between\nBackup & Migrate operations") + yield Button("Configuration", id="btn_config", variant="success", tooltip="Change Bot tokens\nand Reaper Mode") yield Button("Exit", id="btn_exit", variant="error") yield Footer() diff --git a/src/ui/shuttle_ops.py b/src/ui/shuttle_ops.py index 9e8307f..82ab680 100644 --- a/src/ui/shuttle_ops.py +++ b/src/ui/shuttle_ops.py @@ -129,11 +129,11 @@ class ShuttlePane(Container): yield Label("", id="sp_lbl_status") with Vertical(id="sp_actions"): - yield Button("Clone Server Template", id="sp_clone", disabled=True) - yield Button("Sync Server Settings", id="sp_sync", disabled=True) - yield Button("Migrate Message History", id="sp_messages", disabled=True, variant="primary") + yield Button("Clone Server Template", id="sp_clone", disabled=True, tooltip="Clone server roles, categories, and channels to the target community") + yield Button("Sync Server Settings", id="sp_sync", disabled=True, tooltip="Sync emojis, stickers, server name, and icon to the target community") + yield Button("Migrate Message History", id="sp_messages", disabled=True, variant="primary", tooltip="Migrate message history from Discord to the target platform") yield Rule() - yield Button("Danger Zone ⚠", id="sp_danger", variant="error", disabled=True, flat=True) + yield Button("Danger Zone ⚠", id="sp_danger", variant="error", disabled=True, flat=True, tooltip="Dangerous operations:\ndelete channels, roles, emojis on target\n(use with caution)") def on_mount(self) -> None: self._rebuild_engine() @@ -459,8 +459,10 @@ class ShuttlePane(Container): choice = await modal.phase_wait_confirm( btn_start_label="Start Cloning", - btn_id_label="Force Clone (may create duplicates)", - show_id=True + btn_id_label="Force Clone", + show_id=True, + btn_start_tooltip="Clone without creating duplicates", + btn_id_tooltip="Force clone everything\n(may create duplicates)" ) if choice == "btn_back": modal.dismiss() @@ -539,7 +541,9 @@ class ShuttlePane(Container): choice = await modal.phase_wait_confirm( btn_start_label="Start Syncing", btn_id_label="Force Sync", - show_id=True + show_id=True, + btn_start_tooltip="Sync new assets only", + btn_id_tooltip="Force sync assets\n(may create duplicates)" ) if choice == "btn_back": modal.dismiss() @@ -905,7 +909,10 @@ class ShuttlePane(Container): show_id=True, btn_start_label="Start from\nFirst Message", btn_continue_label="Continue\nMigration", - btn_id_label="Start from\nmessage ID" + btn_id_label="Start from\nmessage ID", + btn_start_tooltip="Start migrating from the earliest available message", + btn_continue_tooltip="Resume from the last successfully migrated message", + btn_id_tooltip="Start migrating from a specific Discord message ID" ) logger.info(f"User confirmation choice: {choice}") if choice == "btn_back": @@ -1084,7 +1091,12 @@ class ShuttlePane(Container): modal.write(f" [dim](could not fetch list)[/dim]") modal.write("") - choice = await modal.phase_wait_confirm(btn_start_label="WIPE ALL DATA", show_id=False) + choice = await modal.phase_wait_confirm( + btn_start_label="WIPE ALL DATA", + show_id=False, + btn_start_variant="error", + btn_start_tooltip="WARNING\nIrreversible Operation!\nProceed with Caution" + ) if choice == "btn_back": modal.dismiss() self._open_danger_menu()