mod gobject; mod relm; mod zipline; use std::{borrow::Cow, path::PathBuf, time::Duration}; use color_eyre::eyre::{OptionExt, Result, bail}; use gobject::GtkZiplineFolder; use relm::{Dialog, DialogInput}; use relm4::{ Component, ComponentController, ComponentParts, Controller, RelmApp, SimpleComponent, adw::{self, prelude::*}, gtk::{self, gio, glib::clone}, }; use tracing::Level; use tracing_subscriber::EnvFilter; use urlencoding::Encoded; use zipline::ZiplineFolder; #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] struct Config { zipline_url: String, zipline_token: String, } impl Config { fn fixed_url(&self) -> String { let mut url = self.zipline_url.clone(); if !url.ends_with("/") { url += "/"; } if url.ends_with("api/") { url.truncate(url.len() - 3); } url } } #[derive(Debug)] enum Message { SetPath(PathBuf), SetFolder(ZiplineFolder), StartTheProcess, Nothing, } struct Widgets { file_picker_row: adw::ActionRow, send_button: gtk::Button, } struct Tyrolienne { config: Config, video_path: Option, folder: Option, dialog: Controller, } impl Tyrolienne { fn display_video_path(&self) -> Cow<'_, str> { self.video_path .as_ref() .map(|p| p.to_string_lossy()) .unwrap_or(Cow::Borrowed("None")) } } impl SimpleComponent for Tyrolienne { type Input = Message; type Output = (); type Init = (Config, Vec); type Root = adw::ApplicationWindow; type Widgets = Widgets; fn init_root() -> Self::Root { adw::ApplicationWindow::builder() .title("Tyrolienne") .default_width(500) .build() } fn init( (config, folders): Self::Init, root: Self::Root, sender: relm4::ComponentSender, ) -> relm4::ComponentParts { let model = Tyrolienne { config, video_path: None, folder: None, dialog: Dialog::builder() .launch(root.clone()) .forward(sender.input_sender(), |_| Message::Nothing), }; let file_picker_row = adw::ActionRow::builder() .activatable(true) .title("Video file") .subtitle(model.display_video_path()) .css_classes(["property"]) .build(); file_picker_row.connect_activated(clone!( #[strong] sender, move |_| { let file = rfd::FileDialog::new() .add_filter("Video", &["mp4", "mkv", "webm"]) .pick_file(); if let Some(path) = file { sender.input(Message::SetPath(path)); } } )); let mut gtk_folders = folders .iter() .map(GtkZiplineFolder::from_folder) .collect::>(); gtk_folders.insert(0, GtkZiplineFolder::none()); let store = gio::ListStore::new::(); store.extend_from_slice(>k_folders); let folder_row = adw::ComboRow::builder() .title("Folder") .model(&store) .expression(gtk::PropertyExpression::new( GtkZiplineFolder::r#type(), None::<>k::Expression>, "name", )) .build(); folder_row.connect_activated(clone!( #[strong] sender, move |r| { if let Some(item) = r .selected_item() .and_then(|i| i.downcast::().ok()) { sender.input(Message::SetFolder(item.as_folder())); } } )); let send_button = gtk::Button::builder() .label("Send") .sensitive(false) .margin_start(32) .margin_end(32) .margin_bottom(32) .build(); send_button.connect_clicked(clone!( #[strong] sender, move |_| sender.input(Message::StartTheProcess) )); let list = gtk::ListBox::builder() .margin_top(32) .margin_end(32) .margin_bottom(32) .margin_start(32) .selection_mode(gtk::SelectionMode::None) .css_classes(["boxed-list"]) .build(); list.append(&file_picker_row); list.append(&folder_row); let root_box = gtk::Box::new(gtk::Orientation::Vertical, 0); root_box.append(&adw::HeaderBar::new()); root_box.append(&list); root_box.append(&send_button); root.set_content(Some(&root_box)); let widgets = Widgets { file_picker_row, send_button, }; ComponentParts { model, widgets } } fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender) { match message { Message::Nothing => {} Message::SetPath(path) => self.video_path = Some(path), Message::SetFolder(folder) => { self.folder = (folder.id != GtkZiplineFolder::NONE_ID).then_some(folder) } Message::StartTheProcess => match the_process(self) { Ok(url) => tracing::info!("{url}"), Err(e) => self.dialog.emit(DialogInput::Show { heading: "An error occurred".into(), body: e.to_string(), }), }, } } fn update_view(&self, widgets: &mut Self::Widgets, _sender: relm4::ComponentSender) { widgets .file_picker_row .set_subtitle(&self.display_video_path()); widgets.send_button.set_sensitive(self.video_path.is_some()); } } fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env().add_directive(Level::INFO.into())) .init(); color_eyre::install()?; // TODO: show dialog in case these error let config = get_config()?; let folders = zipline::get_folders(&config)?; let app = RelmApp::new("net.uku3lig.Tyrolienne"); app.run::((config, folders)); Ok(()) } fn get_config() -> Result { let Some(config_dir) = dirs::config_dir() else { bail!("could not get config dir!"); }; std::fs::create_dir_all(&config_dir)?; let config_file_path = config_dir.join("tyrolienne.toml"); let config_contents = match std::fs::read_to_string(&config_file_path) { Ok(str) => str, Err(e) if e.kind() == std::io::ErrorKind::NotFound => { let default_cfg = toml::to_string(&Config::default())?; std::fs::write(&config_file_path, &default_cfg)?; bail!("config file was not found, wrote default one at {config_file_path:?}"); } Err(e) => bail!(e), }; let config: Config = toml::from_str(&config_contents)?; if config.zipline_token.is_empty() { bail!("zipline token is empty! please set it at {config_file_path:?}"); } if config.zipline_url.is_empty() { bail!("zipline url is empty! please set it at {config_file_path:?}"); } Ok(config) } fn the_process(app: &Tyrolienne) -> Result { if let Some(folder) = app.folder.as_ref() { tracing::info!("uploading to folder '{}'...", folder.name); } else { tracing::info!("uploading video..."); } let res = zipline::upload_file( &app.config, app.folder.as_ref(), app.video_path.as_ref().unwrap(), )?; let zp_file = &res.files[0]; tracing::info!("recalculating thumbnails..."); zipline::recalc_thumbnails(&app.config)?; std::thread::sleep(Duration::from_secs(2)); tracing::info!("fetching thumbnail url..."); let res = zipline::get_file_details(&app.config, &zp_file.id)?; let thumbnail_url = res .thumbnail_url(&app.config) .ok_or_eyre("could not get thumbnail url")?; // TODO get w&h from video Ok(format!( "https://autocompressor.net/av1?v={}&i={}&w=1920&h=1080", Encoded(&zp_file.url), Encoded(&thumbnail_url) )) }