Compare commits

...
2 Commits
Author SHA1 Message Date
firewire 0abe9a8206 Menu bar + settings window + about window 2026-09-16 22:26:22 -04:00
firewire 5fc42bd0fb Add video, data, and playlist structs 2026-09-16 21:40:44 -04:00
7 changed files with 239 additions and 53 deletions
Generated
+1
View File
@@ -6338,6 +6338,7 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"webbrowser",
"yt-dlp", "yt-dlp",
] ]
+2 -1
View File
@@ -13,6 +13,7 @@ serde_json = "1.0.151"
tokio = {version = "1.53.1", features = ["macros", "rt-multi-thread"]} tokio = {version = "1.53.1", features = ["macros", "rt-multi-thread"]}
tracing = "0.1.44" tracing = "0.1.44"
tracing-subscriber = {version = "0.3.23", features = ["env-filter"]} tracing-subscriber = {version = "0.3.23", features = ["env-filter"]}
webbrowser = "1.2.4"
yt-dlp = "2.8.3" yt-dlp = "2.8.3"
[profile.release] [profile.release]
@@ -24,4 +25,4 @@ debug-assertions = false
incremental = false incremental = false
rpath = false rpath = false
codegen-units = 1 codegen-units = 1
strip = true strip = true
+66 -15
View File
@@ -1,19 +1,52 @@
use std::time::Duration;
use eframe::{Frame, Storage, CreationContext};
use eframe::egui::{Context, Ui}; use eframe::egui::{Context, Ui};
use eframe::{CreationContext, Frame, Storage};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration;
use yt_dlp::model::VideoQuality;
pub(crate) struct Application { pub(crate) struct Application {
settings: AppSettings, pub settings: AppSettings,
layers: Vec<Box<dyn crate::layer::Layer>>, layers: Vec<Box<dyn crate::layer::Layer>>,
data: Data,
} }
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default)]
struct AppSettings { pub struct AppSettings {
use_cookies: bool, pub(crate) use_firefox_cookies: bool,
use_firefox_cookies: bool, pub(crate) use_chrome_cookies: bool,
use_chrome_cookies: bool, pub(crate) save_thumbnails: bool,
save_thumbnails: bool, pub(crate) video_download_quality: crate::video_quality::VideoDownloadQuality,
}
#[derive(Serialize, Deserialize, Default)]
struct Data {
playlists: Vec<Playlist>,
videos: Vec<Video>,
root_path: String,
}
#[derive(Serialize, Deserialize, Default)]
struct Video {
name: String,
uuid: String,
original_url: String,
video_path: String,
has_thumbnail: bool,
thumbnail_path: String,
thumbnail_path_low_quality: String,
video_quality: VideoQuality,
}
#[derive(Serialize, Deserialize, Default)]
struct Playlist {
name: String,
uuid: String,
original_url: String,
videos: Vec<Video>,
creator: String,
has_thumbnail: bool,
thumbnail_path: String,
thumbnail_path_low_quality: String,
} }
impl Application { impl Application {
@@ -27,12 +60,24 @@ impl Application {
} }
} }
Self { settings, layers } let mut data = Data::default();
if let Some(storage) = cc.storage {
if let Some(stored_str) = storage.get_string("app_data") {
if let Ok(parsed) = serde_json::from_str(&stored_str) {
data = parsed;
}
}
}
Self {
settings,
layers,
data,
}
} }
} }
impl eframe::App for Application { impl eframe::App for Application {
fn logic(&mut self, ctx: &Context, frame: &mut Frame) { fn logic(&mut self, ctx: &Context, frame: &mut Frame) {
let mut layers = std::mem::take(&mut self.layers); let mut layers = std::mem::take(&mut self.layers);
for layer in &mut layers { for layer in &mut layers {
@@ -53,15 +98,21 @@ impl eframe::App for Application {
if let Ok(serialized) = serde_json::to_string(&self.settings) { if let Ok(serialized) = serde_json::to_string(&self.settings) {
_storage.set_string("app_settings", serialized); _storage.set_string("app_settings", serialized);
} }
if let Ok(serialized) = serde_json::to_string(&self.data) {
_storage.set_string("app_data", serialized);
}
} }
fn on_exit(&mut self) { fn on_exit(&mut self) {
let mut layers = std::mem::take(&mut self.layers);
for layer in &mut layers {
layer.on_exit(self);
}
self.layers = layers;
} }
fn auto_save_interval(&self) -> Duration { fn auto_save_interval(&self) -> Duration {
Duration::from_secs(5) Duration::from_secs(5)
} }
}
}
+18 -12
View File
@@ -1,14 +1,15 @@
use crate::top_bar_layer::TopBarLayer;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use tracing::{debug, error}; use tracing::{debug, error};
use crate::test_layer::TestLayer;
mod logging;
mod args;
mod app; mod app;
mod args;
mod layer; mod layer;
mod test_layer; mod logging;
mod top_bar_layer;
mod video_quality;
fn main() { fn main() {
let args = args::parse_args(); let args = args::parse_args();
@@ -16,7 +17,9 @@ fn main() {
debug!("Using args: \n{:#?}\n", args); debug!("Using args: \n{:#?}\n", args);
let data_dir_path = PathBuf::from(&args.data_dir); let data_dir_path = PathBuf::from(&args.data_dir);
if data_dir_path.is_file() { if data_dir_path.is_file() {
error!("data_dir is a file... If you have set the data_dir manually, please pass in a directory. If you didn't pass in a path at all, please report this."); error!(
"data_dir is a file... If you have set the data_dir manually, please pass in a directory. If you didn't pass in a path at all, please report this."
);
error!("{:#?}", args); error!("{:#?}", args);
exit(1); exit(1);
} }
@@ -28,15 +31,18 @@ fn main() {
} }
let mut layers: Vec<Box<dyn layer::Layer>> = Vec::new(); let mut layers: Vec<Box<dyn layer::Layer>> = Vec::new();
layers.push(Box::new(TestLayer::default())); layers.push(Box::new(TopBarLayer::default()));
let mut options = eframe::NativeOptions::default(); let mut options = eframe::NativeOptions::default();
options.persistence_path = Some(args.state.parse().expect("Failed to parse state config file path into path")); options.persistence_path = Some(
args.state
.parse()
.expect("Failed to parse state config file path into path"),
);
eframe::run_native( eframe::run_native(
"yt-offline", "yt-offline",
options, options,
Box::new(|cc| { Box::new(|cc| Ok(Box::new(app::Application::new(cc, layers)))),
Ok(Box::new(app::Application::new(cc, layers))) )
})).expect("TODO: panic message"); .expect("TODO: panic message");
} }
-25
View File
@@ -1,25 +0,0 @@
use eframe::egui::{Context, Ui};
use eframe::{egui, Frame};
use crate::app;
use crate::layer::Layer;
#[derive(Default)]
pub struct TestLayer {
test_string: String,
}
impl Layer for TestLayer {
fn logic(&mut self, application: &mut app::Application, ctx: &Context, frame: &mut Frame) {
}
fn ui(&mut self, application: &mut app::Application, ui: &mut Ui, frame: &mut Frame) {
egui::CentralPanel::default().show(ui, |ui| {
ui.heading("Hello from the test_layer!");
ui.text_edit_singleline(&mut self.test_string);
if ui.button("Print test_string in layer").clicked() {
println!("{}", self.test_string);
}
});
}
}
+118
View File
@@ -0,0 +1,118 @@
use crate::app::Application;
use crate::layer::Layer;
use crate::video_quality::VideoDownloadQuality;
use eframe::egui::{Context, Ui};
use eframe::{Frame, egui};
use tracing::warn;
#[derive(Default)]
pub struct TopBarLayer {
show_settings_window: bool,
show_about_window: bool,
}
impl Layer for TopBarLayer {
fn logic(&mut self, application: &mut Application, ctx: &Context, frame: &mut Frame) {}
fn ui(&mut self, application: &mut Application, ui: &mut Ui, frame: &mut Frame) {
egui::MenuBar::new().ui(ui, |ui: &mut Ui| {
ui.menu_button("File", |ui: &mut Ui| {
if ui.button("Settings").clicked() {
self.show_settings_window = true;
}
if ui.button("Exit").clicked() {
ui.send_viewport_cmd(eframe::egui::ViewportCommand::Close);
}
});
ui.menu_button("Help", |ui: &mut Ui| {
if ui.button("About").clicked() {
self.show_about_window = true;
}
if ui.button("Git").clicked() {
if let Err(e) =
webbrowser::open("https://git.catgirls.fish/firewire/yt-offline")
{
warn!("Failed to open web browser: {}", e);
}
}
});
});
if self.show_settings_window {
egui::Window::new("Settings")
.open(&mut self.show_settings_window)
.show(ui, |ui: &mut Ui| {
if ui
.checkbox(
&mut application.settings.use_firefox_cookies,
"Use cookies from Firefox",
)
.clicked()
{
if application.settings.use_chrome_cookies {
application.settings.use_chrome_cookies = false;
}
};
if ui
.checkbox(
&mut application.settings.use_chrome_cookies,
"Use cookies from Chrome",
)
.clicked()
{
if application.settings.use_firefox_cookies {
application.settings.use_firefox_cookies = false;
}
};
ui.checkbox(&mut application.settings.save_thumbnails, "Save thumbnails");
ui.menu_button("Video download quality", |ui: &mut Ui| {
ui.radio_value(
&mut application.settings.video_download_quality,
VideoDownloadQuality::Best,
"Best",
);
ui.radio_value(
&mut application.settings.video_download_quality,
VideoDownloadQuality::High,
"High",
);
ui.radio_value(
&mut application.settings.video_download_quality,
VideoDownloadQuality::Medium,
"Medium",
);
ui.radio_value(
&mut application.settings.video_download_quality,
VideoDownloadQuality::Low,
"Low",
);
ui.radio_value(
&mut application.settings.video_download_quality,
VideoDownloadQuality::Worst,
"Worst",
);
});
});
}
if self.show_about_window {
egui::Window::new("About")
.open(&mut self.show_about_window)
.show(ui, |ui: &mut Ui| {
ui.heading("yt-offline");
ui.label("A simple unified offline YouTube downloader + viewer");
ui.label("Created by Lyra in Rust <3");
ui.separator();
ui.label("Honorable mentions:");
ui.horizontal(|ui| {
ui.label("yt-dlp");
ui.hyperlink("https://github.com/yt-dlp/yt-dlp");
});
ui.horizontal(|ui| {
ui.label("egui");
ui.hyperlink("https://github.com/emilk/egui");
});
});
}
}
}
+34
View File
@@ -0,0 +1,34 @@
use serde::{Deserialize, Serialize};
use yt_dlp::model::VideoQuality;
#[derive(Deserialize, Serialize, PartialEq, Default)]
pub enum VideoDownloadQuality {
#[default]
Best,
High,
Medium,
Low,
Worst,
}
impl VideoDownloadQuality {
pub fn to_ytdlp(&self) -> VideoQuality {
match self {
VideoDownloadQuality::Best => VideoQuality::Best,
VideoDownloadQuality::High => VideoQuality::High,
VideoDownloadQuality::Medium => VideoQuality::Medium,
VideoDownloadQuality::Low => VideoQuality::Low,
VideoDownloadQuality::Worst => VideoQuality::Worst,
}
}
pub fn to_string(&self) -> String {
match self {
VideoDownloadQuality::Best => "best".to_string(),
VideoDownloadQuality::High => "high".to_string(),
VideoDownloadQuality::Medium => "medium".to_string(),
VideoDownloadQuality::Low => "low".to_string(),
VideoDownloadQuality::Worst => "worst".to_string(),
}
}
}