ontv 0.1.3

A rich desktop application for tracking tv shows
Documentation
use serde::{Deserialize, Serialize};

use crate::prelude::*;
use crate::queue::{TaskKind, TaskRef};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct State {
    pub(crate) id: SeriesId,
}

pub(crate) fn page(id: SeriesId) -> Page {
    Page::Series(State { id })
}

#[derive(Debug, Clone)]
pub(crate) enum Message {
    OpenRemote(RemoteSeriesId),
    SeriesActions(comps::series_actions::Message),
    Navigate(Page),
    SeasonInfo(usize, comps::season_info::Message),
    SeriesBanner(comps::series_banner::Message),
    SwitchSeries(SeriesId, RemoteSeriesId),
}

pub(crate) struct Series {
    series: comps::SeriesActions,
    seasons: Vec<comps::SeasonInfo>,
    banner: comps::SeriesBanner,
}

impl Series {
    #[inline]
    pub(crate) fn new(state: &State) -> Self {
        Self {
            series: comps::SeriesActions::new(state.id),
            seasons: Vec::new(),
            banner: comps::SeriesBanner::default(),
        }
    }

    pub(crate) fn prepare(&mut self, cx: &mut Ctxt<'_>, state: &State) {
        self.seasons.init_from_iter(
            cx.service
                .seasons(&state.id)
                .map(|s| (*s.series(), s.number)),
        );

        self.banner.prepare(cx, &state.id);

        if let Some(series) = cx.service.series(&state.id) {
            cx.assets.mark_with_hint(
                cx.service
                    .seasons(&state.id)
                    .flat_map(|season| season.into_season().poster().or(series.poster())),
                POSTER_HINT,
            );
        }
    }

    pub(crate) fn update(&mut self, cx: &mut Ctxt<'_>, message: Message) {
        match message {
            Message::OpenRemote(remote_id) => {
                let url = remote_id.url();
                let _ = webbrowser::open_browser(webbrowser::Browser::Default, &url);
            }
            Message::SeriesActions(message) => {
                self.series.update(cx, message);
            }
            Message::Navigate(page) => {
                cx.push_history(page);
            }
            Message::SeasonInfo(index, message) => {
                if let Some(season_info) = self.seasons.get_mut(index) {
                    season_info.update(cx, message);
                }
            }
            Message::SeriesBanner(message) => {
                self.banner.update(cx, message);
            }
            Message::SwitchSeries(series_id, remote_id) => {
                cx.service
                    .push_task_without_delay(TaskKind::DownloadSeries {
                        series_id,
                        remote_id,
                        last_modified: None,
                        force: true,
                    });
            }
        }
    }

    pub(crate) fn view(
        &self,
        cx: &CtxtRef<'_>,
        state: &State,
    ) -> Result<Element<'static, Message>> {
        let Some(series) = cx.service.series(&state.id) else {
            bail!("missing series {}", state.id);
        };

        let mut top =
            w::Column::new().push(self.banner.view(cx, series).map(Message::SeriesBanner));

        let remote_ids = cx.service.remotes_by_series(&series.id);

        if remote_ids.len() > 0 {
            let mut remotes = w::Row::new();

            for remote_id in remote_ids {
                let mut row = w::Row::new().push(
                    w::button(w::text(&remote_id).size(SMALL))
                        .style(theme::Button::Primary)
                        .on_press(Message::OpenRemote(remote_id)),
                );

                if series.remote_id.as_ref() == Some(&remote_id) {
                    row = row.push(w::button(w::text("Current").size(SMALL)));
                } else if remote_id.is_supported() {
                    let button =
                        w::button(w::text("Switch").size(SMALL)).style(theme::Button::Positive);

                    let status = cx.service.task_status_any([
                        TaskRef::RemoteSeries { remote_id },
                        TaskRef::Series {
                            series_id: series.id,
                        },
                    ]);

                    let button = if status.is_none() {
                        button.on_press(Message::SwitchSeries(series.id, remote_id))
                    } else {
                        button
                    };

                    row = row.push(button);
                }

                remotes = remotes.push(row);
            }

            top = top.push(remotes.spacing(GAP));
        }

        let mut cols = w::Column::new();

        for (index, (season, c)) in cx
            .service
            .seasons(&series.id)
            .zip(&self.seasons)
            .enumerate()
        {
            let poster = match season
                .graphics
                .poster
                .as_ref()
                .or(series.poster())
                .and_then(|i| cx.assets.image_with_hint(i, POSTER_HINT))
            {
                Some(poster) => poster,
                None => cx.missing_poster(),
            };

            let graphic = link(w::image(poster).height(IMAGE_HEIGHT)).on_press(Message::Navigate(
                page::season::page(series.id, season.number),
            ));

            let title = link(w::text(season.number).size(SUBTITLE_SIZE)).on_press(
                Message::Navigate(page::season::page(series.id, season.number)),
            );

            let mut column = w::Column::new().spacing(SPACE).push(title);

            let mut iter = cx.service.episodes_by_season(&series.id, &season.number);
            let first = iter.next();
            let last = iter.next_back().or(first);

            if let (Some(start), end) = (first.and_then(|e| e.aired), last.and_then(|e| e.aired)) {
                let text = if let Some(end) = end {
                    w::text(format_args!("{start} - {end}")).size(SMALL)
                } else {
                    w::text(format_args!("{start} -")).size(SMALL)
                };

                column = column.push(text);
            }

            cols = cols.push(
                centered(
                    w::Row::new()
                        .push(graphic)
                        .push(column.push(c.view(cx).map(move |m| Message::SeasonInfo(index, m))))
                        .spacing(GAP),
                    Some(style::weak),
                )
                .padding(GAP),
            );
        }

        let info = match cx.service.episodes(&series.id).len() {
            0 => w::text("No episodes"),
            1 => w::text("One episode"),
            count => w::text(format!("{count} episodes")),
        };

        let mut header = w::Column::new()
            .push(top.align_items(Alignment::Center).spacing(GAP))
            .push(self.series.view(cx, series).map(Message::SeriesActions))
            .push(info);

        if !series.overview.is_empty() {
            header = header.push(w::text(&series.overview));
        }

        let header = centered(header.spacing(GAP), None).padding(GAP);

        Ok(w::Column::new()
            .push(header)
            .push(cols.spacing(GAP2))
            .width(Length::Fill)
            .spacing(GAP2)
            .into())
    }
}