add extras in channel selection
This commit is contained in:
parent
f5cdf1b1ff
commit
706852e425
3 changed files with 232 additions and 16 deletions
|
|
@ -46,16 +46,16 @@ class FluxerWriter:
|
||||||
|
|
||||||
assert self.client is not None
|
assert self.client is not None
|
||||||
try:
|
try:
|
||||||
# 1. Try to find existing webhook named "Stoat-Migrator"
|
# 1. Try to find existing webhook named "ReapersWebhook"
|
||||||
webhooks_data = await self.client.get_channel_webhooks(channel_id)
|
webhooks_data = await self.client.get_channel_webhooks(channel_id)
|
||||||
for w_data in webhooks_data:
|
for w_data in webhooks_data:
|
||||||
if w_data.get("name") == "Stoat-Migrator":
|
if w_data.get("name") == "ReapersWebhook":
|
||||||
w = Webhook.from_data(w_data, self.client)
|
w = Webhook.from_data(w_data, self.client)
|
||||||
self._webhooks[channel_id] = w
|
self._webhooks[channel_id] = w
|
||||||
return w
|
return w
|
||||||
|
|
||||||
# 2. Create new one if not found
|
# 2. Create new one if not found
|
||||||
w_data = await self.client.create_webhook(channel_id, name="Stoat-Migrator")
|
w_data = await self.client.create_webhook(channel_id, name="ReapersWebhook")
|
||||||
w = Webhook.from_data(w_data, self.client)
|
w = Webhook.from_data(w_data, self.client)
|
||||||
self._webhooks[channel_id] = w
|
self._webhooks[channel_id] = w
|
||||||
return w
|
return w
|
||||||
|
|
|
||||||
188
src/ui/modals.py
188
src/ui/modals.py
|
|
@ -552,13 +552,14 @@ class ChannelPickerScreen(Screen[tuple]):
|
||||||
#chanpick_buttons Button { width: 1fr; margin: 0 1; }
|
#chanpick_buttons Button { width: 1fr; margin: 0 1; }
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, src_channels: list, src_cat_map: dict, tgt_channels: list, tgt_cat_map: dict, tgt_name: str = "Fluxer"):
|
def __init__(self, src_channels: list, src_cat_map: dict, tgt_channels: list, tgt_cat_map: dict, tgt_name: str = "Fluxer", all_tgt_channels: list | None = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.src_channels = src_channels
|
self.src_channels = src_channels
|
||||||
self.src_cat_map = src_cat_map
|
self.src_cat_map = src_cat_map
|
||||||
self.tgt_channels = tgt_channels
|
self.tgt_channels = tgt_channels
|
||||||
self.tgt_cat_map = tgt_cat_map
|
self.tgt_cat_map = tgt_cat_map
|
||||||
self.tgt_name = tgt_name
|
self.tgt_name = tgt_name
|
||||||
|
self.all_tgt_channels = all_tgt_channels or tgt_channels
|
||||||
|
|
||||||
def _render_pane(self, channels, categories, pane_id, prefix):
|
def _render_pane(self, channels, categories, pane_id, prefix):
|
||||||
cat_grouped: dict[int | None, list] = {}
|
cat_grouped: dict[int | None, list] = {}
|
||||||
|
|
@ -586,6 +587,12 @@ class ChannelPickerScreen(Screen[tuple]):
|
||||||
cid = c.id
|
cid = c.id
|
||||||
options.append(Option(name, id=f"{prefix}_{cid}"))
|
options.append(Option(name, id=f"{prefix}_{cid}"))
|
||||||
|
|
||||||
|
# Add "Extras" category to the target pane only
|
||||||
|
if prefix == "tgt":
|
||||||
|
options.append(Option(f"[bold yellow]── Extras ──[/bold yellow]", id="header_extras", disabled=True))
|
||||||
|
options.append(Option("✨ Create New channel+", id="tgt_create_new"))
|
||||||
|
options.append(Option("🔗 Enter Channel ID", id="tgt_enter_id"))
|
||||||
|
|
||||||
yield OptionList(*options, id=f"{prefix}_list", classes="split_pane")
|
yield OptionList(*options, id=f"{prefix}_list", classes="split_pane")
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
|
|
@ -655,10 +662,36 @@ class ChannelPickerScreen(Screen[tuple]):
|
||||||
if opt.id and opt.id.startswith("tgt_"):
|
if opt.id and opt.id.startswith("tgt_"):
|
||||||
tgt_val = opt.id.split("_", 1)[1]
|
tgt_val = opt.id.split("_", 1)[1]
|
||||||
|
|
||||||
if src_val and tgt_val:
|
if not src_val or not tgt_val:
|
||||||
self.dismiss((int(src_val), tgt_val)) # Source is guaranteed Discord ID (int), Target could be UUID/string
|
self.notify("Please select one channel from both lists.", severity="warning")
|
||||||
else:
|
return
|
||||||
self.notify("Please select one channel from both lists.", severity="warning")
|
|
||||||
|
# Handle "Extras" options by pushing sub-modals on top of this screen
|
||||||
|
if tgt_val == "create_new":
|
||||||
|
# Get source channel name for default
|
||||||
|
src_name = ""
|
||||||
|
for c in self.src_channels:
|
||||||
|
cid = c.get("id") if isinstance(c, dict) else c.id
|
||||||
|
if str(cid) == src_val:
|
||||||
|
src_name = c.get("name") if isinstance(c, dict) else c.name
|
||||||
|
break
|
||||||
|
|
||||||
|
def on_name_result(name):
|
||||||
|
if name:
|
||||||
|
self.dismiss((int(src_val), "create_new", name))
|
||||||
|
|
||||||
|
self.app.push_screen(ChannelNameInputModal(default_name=src_name), on_name_result)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif tgt_val == "enter_id":
|
||||||
|
def on_chan_result(chan_dict):
|
||||||
|
if chan_dict:
|
||||||
|
self.dismiss((int(src_val), str(chan_dict.get("id")), chan_dict))
|
||||||
|
|
||||||
|
self.app.push_screen(ChannelIDInputModal(known_channels=self.all_tgt_channels), on_chan_result)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.dismiss((int(src_val), tgt_val)) # Source is guaranteed Discord ID (int), Target could be UUID/string
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -894,3 +927,148 @@ class MessageIDInputModal(ModalScreen[int | None]):
|
||||||
preview.update(f"[bold red]Error fetching message:[/bold red] {e}")
|
preview.update(f"[bold red]Error fetching message:[/bold red] {e}")
|
||||||
finally:
|
finally:
|
||||||
btn.disabled = False
|
btn.disabled = False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ChannelNameInputModal – Input a channel name for creation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ChannelNameInputModal(ModalScreen[str | None]):
|
||||||
|
"""Modal to input a channel name for creation on the target platform."""
|
||||||
|
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
ChannelNameInputModal { align: center middle; }
|
||||||
|
#chan_name_dialog {
|
||||||
|
width: 70%;
|
||||||
|
height: auto;
|
||||||
|
border: solid green;
|
||||||
|
padding: 1 2;
|
||||||
|
background: $surface;
|
||||||
|
}
|
||||||
|
#chan_name_buttons {
|
||||||
|
height: auto;
|
||||||
|
dock: bottom;
|
||||||
|
margin-top: 1;
|
||||||
|
}
|
||||||
|
#chan_name_buttons Button { width: 1fr; margin: 0 1; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, default_name: str = ""):
|
||||||
|
super().__init__()
|
||||||
|
self.default_name = default_name
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Container(id="chan_name_dialog"):
|
||||||
|
yield Label("[bold green]Create New Channel[/bold green]")
|
||||||
|
yield Label("Enter a name for the new channel:")
|
||||||
|
yield Input(value=self.default_name, placeholder="channel-name", id="input_chan_name")
|
||||||
|
|
||||||
|
with Horizontal(id="chan_name_buttons"):
|
||||||
|
yield Button("Create", variant="success", id="btn_create_chan")
|
||||||
|
yield Button("Back", id="btn_cancel_chan_name")
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
if event.button.id == "btn_cancel_chan_name":
|
||||||
|
self.dismiss(None)
|
||||||
|
elif event.button.id == "btn_create_chan":
|
||||||
|
name = self.query_one("#input_chan_name", Input).value.strip()
|
||||||
|
if name:
|
||||||
|
self.dismiss(name)
|
||||||
|
else:
|
||||||
|
self.notify("Please enter a channel name.", severity="warning")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ChannelIDInputModal – Input and verify a channel ID
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ChannelIDInputModal(ModalScreen[dict | None]):
|
||||||
|
"""Modal to input a target channel ID, verify it exists, and select it."""
|
||||||
|
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
ChannelIDInputModal { align: center middle; }
|
||||||
|
#chan_id_dialog {
|
||||||
|
width: 80%;
|
||||||
|
height: auto;
|
||||||
|
border: solid yellow;
|
||||||
|
padding: 1 2;
|
||||||
|
background: $surface;
|
||||||
|
}
|
||||||
|
#chan_id_preview {
|
||||||
|
border: solid $primary;
|
||||||
|
padding: 1 2;
|
||||||
|
margin: 1 0;
|
||||||
|
height: auto;
|
||||||
|
min-height: 3;
|
||||||
|
}
|
||||||
|
#chan_id_buttons {
|
||||||
|
height: auto;
|
||||||
|
dock: bottom;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
#chan_id_buttons Button { width: 1fr; margin: 0 1; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, known_channels: list[dict]):
|
||||||
|
super().__init__()
|
||||||
|
self.known_channels = known_channels # list of channel dicts from writer.get_channels()
|
||||||
|
self.verified_channel: dict | None = None
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Container(id="chan_id_dialog"):
|
||||||
|
yield Label("[bold yellow]Enter Target Channel ID[/bold yellow]")
|
||||||
|
yield Input(placeholder="Enter Channel ID", id="input_chan_id", type="text")
|
||||||
|
with Container(id="chan_id_preview"):
|
||||||
|
yield Label("Enter an ID and click Verify.", id="lbl_chan_preview")
|
||||||
|
|
||||||
|
with Horizontal(id="chan_id_buttons"):
|
||||||
|
yield Button("Verify", variant="primary", id="btn_verify_chan", disabled=True)
|
||||||
|
yield Button("Back", variant="warning", id="btn_cancel_chan_id")
|
||||||
|
|
||||||
|
def on_input_changed(self, event: Input.Changed) -> None:
|
||||||
|
if event.input.id == "input_chan_id":
|
||||||
|
btn = self.query_one("#btn_verify_chan", Button)
|
||||||
|
self.verified_channel = None
|
||||||
|
btn.label = "Verify"
|
||||||
|
btn.variant = "primary"
|
||||||
|
|
||||||
|
val = event.input.value.strip()
|
||||||
|
btn.disabled = not bool(val)
|
||||||
|
|
||||||
|
preview = self.query_one("#lbl_chan_preview", Label)
|
||||||
|
preview.update("Click Verify to check channel.")
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
if event.button.id == "btn_cancel_chan_id":
|
||||||
|
self.dismiss(None)
|
||||||
|
elif event.button.id == "btn_verify_chan":
|
||||||
|
if self.verified_channel is not None:
|
||||||
|
self.dismiss(self.verified_channel)
|
||||||
|
return
|
||||||
|
|
||||||
|
inp = self.query_one("#input_chan_id", Input)
|
||||||
|
preview = self.query_one("#lbl_chan_preview", Label)
|
||||||
|
btn = event.button
|
||||||
|
|
||||||
|
chan_id_str = inp.value.strip()
|
||||||
|
if not chan_id_str:
|
||||||
|
preview.update("[bold red]Please enter a Channel ID.[/bold red]")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Search in known channels
|
||||||
|
found = None
|
||||||
|
for ch in self.known_channels:
|
||||||
|
if str(ch.get("id")) == chan_id_str:
|
||||||
|
found = ch
|
||||||
|
break
|
||||||
|
|
||||||
|
if found:
|
||||||
|
self.verified_channel = found
|
||||||
|
ch_name = found.get("name", "Unknown")
|
||||||
|
ch_type = found.get("type", 0)
|
||||||
|
type_label = {0: "Text", 2: "Voice", 4: "Category"}.get(ch_type, f"Type {ch_type}")
|
||||||
|
preview.update(f"[bold green]Channel Found![/bold green]\n\n Name: [cyan]{ch_name}[/cyan]\n Type: {type_label}\n ID: {chan_id_str}")
|
||||||
|
btn.label = "Select This Channel"
|
||||||
|
btn.variant = "success"
|
||||||
|
else:
|
||||||
|
preview.update(f"[bold red]No channel found with ID: {chan_id_str}[/bold red]\n[dim]Make sure the ID belongs to a channel in the target community.[/dim]")
|
||||||
|
|
|
||||||
|
|
@ -827,20 +827,44 @@ class ShuttlePane(Container):
|
||||||
if not pick_future.done():
|
if not pick_future.done():
|
||||||
pick_future.set_result(result)
|
pick_future.set_result(result)
|
||||||
|
|
||||||
self.app.push_screen(ChannelPickerScreen(d_channels, d_cat_map, f_channels, target_cat_names, platform_name), on_pick)
|
self.app.push_screen(ChannelPickerScreen(d_channels, d_cat_map, f_channels, target_cat_names, platform_name, all_tgt_channels=full_f), on_pick)
|
||||||
res = await pick_future
|
res = await pick_future
|
||||||
|
|
||||||
if res is None:
|
if res is None:
|
||||||
await self.engine.close_connections()
|
await self.engine.close_connections()
|
||||||
return
|
return
|
||||||
|
|
||||||
src_id, tgt_id = res
|
# Handle result from channel picker
|
||||||
source_channel = next(c for c in d_channels if c.id == src_id)
|
# Normal: (src_id, tgt_id) - 2-tuple
|
||||||
target_channel = next(c for c in f_channels if c.get("id") == tgt_id)
|
# Create new: (src_id, "create_new", channel_name) - 3-tuple
|
||||||
|
# Enter ID: (src_id, tgt_id, channel_dict) - 3-tuple
|
||||||
|
|
||||||
# Determine after_id status
|
pending_create_name = None # Deferred channel creation
|
||||||
last_migrated = self.engine.state.get_last_message_id(str(target_channel.get('id')))
|
|
||||||
has_previous = bool(last_migrated)
|
if len(res) == 3 and res[1] == "create_new":
|
||||||
|
src_id, _, chan_name = res
|
||||||
|
source_channel = next(c for c in d_channels if c.id == src_id)
|
||||||
|
# Don't create yet — defer until user confirms migration
|
||||||
|
pending_create_name = chan_name
|
||||||
|
target_channel = {"id": "__pending__", "name": chan_name, "type": 0}
|
||||||
|
|
||||||
|
elif len(res) == 3:
|
||||||
|
src_id, _, chan_dict = res
|
||||||
|
source_channel = next(c for c in d_channels if c.id == src_id)
|
||||||
|
target_channel = chan_dict
|
||||||
|
|
||||||
|
else:
|
||||||
|
src_id, tgt_id = res
|
||||||
|
source_channel = next(c for c in d_channels if c.id == src_id)
|
||||||
|
target_channel = next(c for c in f_channels if c.get("id") == tgt_id)
|
||||||
|
|
||||||
|
# Determine after_id status (skip for pending channels)
|
||||||
|
if pending_create_name:
|
||||||
|
last_migrated = None
|
||||||
|
has_previous = False
|
||||||
|
else:
|
||||||
|
last_migrated = self.engine.state.get_last_message_id(str(target_channel.get('id')))
|
||||||
|
has_previous = bool(last_migrated)
|
||||||
|
|
||||||
# Analyze
|
# Analyze
|
||||||
modal = ProgressScreen(log_level=self.config.log_level)
|
modal = ProgressScreen(log_level=self.config.log_level)
|
||||||
|
|
@ -963,6 +987,20 @@ class ShuttlePane(Container):
|
||||||
# If we are here, we are proceeding with migration
|
# If we are here, we are proceeding with migration
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Create the channel now if it was deferred
|
||||||
|
if pending_create_name:
|
||||||
|
modal.set_status(f"Creating channel [green]#{pending_create_name}[/green]...")
|
||||||
|
try:
|
||||||
|
new_id = await self.engine.writer.create_channel(name=pending_create_name)
|
||||||
|
logger.info(f"Created new channel '{pending_create_name}' with ID: {new_id}")
|
||||||
|
target_channel = {"id": new_id, "name": pending_create_name, "type": 0}
|
||||||
|
f_channels.append(target_channel)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create channel '{pending_create_name}': {e}")
|
||||||
|
modal.write(f"[bold red]Failed to create channel: {e}[/bold red]")
|
||||||
|
modal.phase_report("Channel Creation", status="error")
|
||||||
|
return
|
||||||
|
|
||||||
# Phase 3: Progress
|
# Phase 3: Progress
|
||||||
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
||||||
modal.phase_progress()
|
modal.phase_progress()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue