concord 2.4.3

A terminal user interface client for Discord
Documentation
use crate::discord::{
    AppCommand, AttachmentInfo, AttachmentMediaType, DownloadAttachmentSource, InlinePreviewInfo,
    MediaPlaybackSource, MediaPlaybackTarget,
    ids::{Id, marker::MessageMarker},
};

use super::super::{AttachmentViewerItem, DashboardState};
use crate::tui::state::popups::{
    ActiveModalPopupKind, AttachmentViewerState, AttachmentViewerZoom, ModalPopup,
};

struct SelectedAttachmentViewerAttachment<'a> {
    message_id: Id<MessageMarker>,
    index: usize,
    total: usize,
    attachment: &'a AttachmentInfo,
}

impl DashboardState {
    pub fn open_attachment_viewer_for_selected_message(&mut self) -> bool {
        let Some(message) = self.selected_message_state() else {
            return false;
        };
        if message.attachments_in_display_order().next().is_none() {
            return false;
        }

        self.popups.modal = Some(ModalPopup::AttachmentViewer(AttachmentViewerState {
            message_id: message.id,
            selection: Default::default(),
            zoom: AttachmentViewerZoom::default(),
        }));
        true
    }

    pub fn close_attachment_viewer(&mut self) {
        if self.is_active_modal_popup(ActiveModalPopupKind::AttachmentViewer) {
            self.popups.clear_modal();
        }
    }

    pub fn attachment_viewer_zoom(&self) -> AttachmentViewerZoom {
        self.popups
            .attachment_viewer()
            .map(|viewer| viewer.zoom)
            .unwrap_or_default()
    }

    pub fn toggle_attachment_viewer_fullscreen(&mut self) {
        if let Some(viewer) = self.popups.attachment_viewer_mut() {
            viewer.zoom = viewer.zoom.toggle_fullscreen();
        }
    }

    pub fn zoom_attachment_viewer_in(&mut self) {
        if let Some(viewer) = self.popups.attachment_viewer_mut() {
            viewer.zoom = viewer.zoom.zoom_in();
        }
    }

    pub fn zoom_attachment_viewer_out(&mut self) {
        if let Some(viewer) = self.popups.attachment_viewer_mut() {
            viewer.zoom = viewer.zoom.zoom_out();
        }
    }

    pub fn move_attachment_viewer_previous(&mut self) {
        if let Some(viewer) = self.popups.attachment_viewer_mut() {
            viewer.selection.move_up();
        }
    }

    pub fn move_attachment_viewer_next(&mut self) {
        let Some((message_id, selected)) = self
            .popups
            .attachment_viewer()
            .map(|viewer| (viewer.message_id, viewer.selection.selected()))
        else {
            return;
        };
        let count = self
            .attachment_viewer_attachments(message_id)
            .map_or(0, |attachments| attachments.len());
        if count == 0 {
            self.close_attachment_viewer();
            return;
        }
        if let Some(viewer) = self.popups.attachment_viewer_mut() {
            viewer
                .selection
                .select(selected.saturating_add(1).min(count.saturating_sub(1)));
        }
    }

    pub fn selected_attachment_viewer_item(&self) -> Option<AttachmentViewerItem> {
        let selected = self.selected_attachment_viewer_attachment()?;
        Some(AttachmentViewerItem {
            index: selected.index.saturating_add(1),
            total: selected.total,
            filename: selected.attachment.filename.clone(),
            url: selected.attachment.preferred_url().map(str::to_owned),
            size_bytes: selected.attachment.size,
            media_type: selected.attachment.media_type(),
        })
    }

    pub(in crate::tui) fn selected_attachment_viewer_preview(
        &self,
    ) -> Option<(Id<MessageMarker>, usize, InlinePreviewInfo<'_>)> {
        let selected = self.selected_attachment_viewer_attachment()?;
        let preview = selected.attachment.inline_preview_info()?;
        Some((selected.message_id, selected.index, preview))
    }

    pub fn download_selected_attachment_viewer_attachment(&mut self) -> Option<AppCommand> {
        let item = self.selected_attachment_viewer_item()?;
        let url = item.url?;
        let id = self.next_attachment_download_id();
        Some(AppCommand::DownloadAttachment {
            id,
            url,
            filename: item.filename,
            source: DownloadAttachmentSource::AttachmentViewer,
        })
    }

    pub fn play_selected_attachment_viewer_attachment(&mut self) -> Option<AppCommand> {
        if !self.media_playback_enabled() {
            self.show_media_playback_disabled_toast(std::time::Instant::now());
            return None;
        }
        let item = self.selected_attachment_viewer_item()?;
        if !matches!(
            item.media_type,
            Some(AttachmentMediaType::Video | AttachmentMediaType::Audio)
        ) {
            return None;
        }
        Some(AppCommand::PlayMedia {
            target: MediaPlaybackTarget {
                url: item.url?,
                label: item.filename,
                source: MediaPlaybackSource::AttachmentViewer,
            },
            request_id: None,
        })
    }

    fn attachment_viewer_attachments(
        &self,
        message_id: Id<MessageMarker>,
    ) -> Option<Vec<&AttachmentInfo>> {
        self.messages()
            .into_iter()
            .find(|message| message.id == message_id)
            .map(|message| message.attachments_in_display_order().collect())
    }

    fn selected_attachment_viewer_attachment(
        &self,
    ) -> Option<SelectedAttachmentViewerAttachment<'_>> {
        let viewer = self.popups.attachment_viewer()?;
        let attachments = self.attachment_viewer_attachments(viewer.message_id)?;
        let index = viewer.selection.selected_for_len(attachments.len());
        let attachment = *attachments.get(index)?;
        Some(SelectedAttachmentViewerAttachment {
            message_id: viewer.message_id,
            index,
            total: attachments.len(),
            attachment,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::AttachmentViewerZoom;

    #[test]
    fn zoom_in_steps_default_large_fullscreen_and_caps() {
        let zoom = AttachmentViewerZoom::Default;
        let zoom = zoom.zoom_in();
        assert_eq!(zoom, AttachmentViewerZoom::Large);
        let zoom = zoom.zoom_in();
        assert_eq!(zoom, AttachmentViewerZoom::Fullscreen);
        let zoom = zoom.zoom_in();
        assert_eq!(zoom, AttachmentViewerZoom::Fullscreen);
    }

    #[test]
    fn zoom_out_steps_fullscreen_large_default_and_caps() {
        let zoom = AttachmentViewerZoom::Fullscreen;
        let zoom = zoom.zoom_out();
        assert_eq!(zoom, AttachmentViewerZoom::Large);
        let zoom = zoom.zoom_out();
        assert_eq!(zoom, AttachmentViewerZoom::Default);
        let zoom = zoom.zoom_out();
        assert_eq!(zoom, AttachmentViewerZoom::Default);
    }

    #[test]
    fn toggle_fullscreen_round_trips() {
        let zoom = AttachmentViewerZoom::Default;
        let zoom = zoom.toggle_fullscreen();
        assert_eq!(zoom, AttachmentViewerZoom::Fullscreen);
        let zoom = zoom.toggle_fullscreen();
        assert_eq!(zoom, AttachmentViewerZoom::Default);

        let zoom = AttachmentViewerZoom::Large.toggle_fullscreen();
        assert_eq!(zoom, AttachmentViewerZoom::Fullscreen);
    }
}