feat: first ui with almost nothing

This commit is contained in:
Christoph J. Scherr 2024-08-22 23:20:29 +02:00
parent 5da9a360d4
commit c4a7b68c74
9 changed files with 267 additions and 4 deletions

View File

@ -12,13 +12,20 @@ repository = "https://git.cscherr.de/PlexSheep/beatbear"
keywords = ["media", "sound", "music", "player", "jellyfin", "downloads"]
[features]
default = ["backend-fs", "backend-jellyfin"]
backend-fs = []
backend-jellyfin = []
gui = []
tui = []
[dependencies]
anyhow = "1.0.86"
clap = { version = "4.5.16", features = ["derive"] }
eframe = { version = "0.28.1", optional = false }
egui = { version = "0.28.1", optional = false }
human-panic = "2.0.1"
image = "0.25.2"
libpt = { version = "0.6.0", features = ["cli", "full"] }
thiserror = "1.0.63"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
[lib]
name = "beatbaer"

BIN
assets/img/icon-256.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/img/icon-512.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1 @@
pub trait Backend {}

9
src/error/mod.rs Normal file
View File

@ -0,0 +1,9 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Error in the player UI")]
UiError(#[from] eframe::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}

View File

@ -1,2 +1,4 @@
pub mod backend;
pub mod error;
pub mod music;
pub mod player;

View File

@ -1,3 +1,31 @@
fn main() {
println!("Hello, world!");
use beatbaer::error::Error;
use beatbaer::player::Player;
use human_panic::{setup_panic, Metadata};
use libpt::log::info;
fn main() -> Result<(), Error> {
setup_panic!(
Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
.authors(env!("CARGO_PKG_AUTHORS"))
.homepage(env!("CARGO_PKG_HOMEPAGE"))
.support(format!(
"Public issue tracker at: {}\nor alternatively email: {}",
env!("CARGO_PKG_REPOSITORY"),
"software@cscherr.de"
))
);
let mut player = Player::build()?;
info!("starting ui");
eframe::run_native(
beatbaer::player::TITLE,
player.gui_options.clone(),
Box::new(|cc| {
player.init(cc);
Ok(Box::new(player))
}),
)?;
info!("leaving ui");
Ok(())
}

59
src/player/mod.rs Normal file
View File

@ -0,0 +1,59 @@
use clap::Parser;
use eframe::CreationContext;
use libpt::cli::args::VerbosityLevel;
use libpt::log::{debug, info, trace};
use crate::error::Error;
pub mod ui;
pub const TITLE: &str = "Beatbär";
#[derive(Parser)]
pub struct Player {
#[command(flatten)]
verbosity: VerbosityLevel,
#[clap(skip)]
pub gui_options: eframe::NativeOptions,
#[clap(skip)]
show_info_window: bool,
}
impl Player {
pub fn build() -> Result<Self, Error> {
let mut app = Player::parse();
let filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(app.verbosity.level().into())
.from_env()
.expect("could not init logger")
.add_directive(
format!("{}={}", env!("CARGO_PKG_NAME"), app.verbosity.level())
.parse()
.expect("could not init logger"),
);
tracing_subscriber::fmt::Subscriber::builder()
.with_env_filter(filter)
.with_max_level(app.verbosity.level())
.init();
debug!("logging initialized!");
debug!("level: {}", app.verbosity.level());
app.gui_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([400.0, 300.0])
.with_min_inner_size([300.0, 220.0])
.with_icon(Player::load_icon()),
..Default::default()
};
info!("Player ready to start UI");
Ok(app)
}
pub fn init(&mut self, _cc: &CreationContext) {
// we can use the creation context to do some customizing, but idc right now
}
}

157
src/player/ui.rs Normal file
View File

@ -0,0 +1,157 @@
use egui::IconData;
use libpt::log::trace;
use super::*;
const ICON_RAW: &[u8; 36525] = include_bytes!("../../assets/img/icon-512.jpg");
impl Player {
fn top_panel(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
egui::menu::bar(ui, |ui| {
// NOTE: no File->Quit on web pages!
let is_web = cfg!(target_arch = "wasm32");
if !is_web {
ui.menu_button("File", |ui| {
if ui.button("Info").clicked() {
self.show_info_window = true;
}
ui.separator();
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.add_space(16.0);
}
if self.show_info_window {
self.info_diag(ctx);
}
egui::widgets::global_dark_light_mode_buttons(ui);
});
}
fn info_diag(&mut self, ctx: &egui::Context) {
trace!("rendering info dialogue");
ctx.show_viewport_immediate(
egui::ViewportId::from_hash_of(format!("{TITLE}: Information")),
egui::ViewportBuilder::default()
.with_title(format!("{TITLE}: Information"))
.with_inner_size([500.0, 200.0]),
|ctx, class| {
assert!(
class == egui::ViewportClass::Immediate,
"This egui backend doesn't support multiple viewports"
);
egui::CentralPanel::default().show(ctx, |ui| {
ui.label(format!("{TITLE} v{}", env!("CARGO_PKG_VERSION")));
ui.hyperlink_to("Source Code\n", env!("CARGO_PKG_REPOSITORY"));
ui.label(format!("Author: {}", env!("CARGO_PKG_AUTHORS")));
ui.label(format!("License: {}", env!("CARGO_PKG_LICENSE")));
ui.label(format!(
"\n{TITLE} is free software. If you paid for this you were scammed.\n"
));
////////////////////////////////////////////////////////////////
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("Powered by ");
ui.hyperlink_to("egui", "https://github.com/emilk/egui");
ui.label(" and ");
ui.hyperlink_to(
"eframe",
"https://github.com/emilk/egui/tree/master/crates/eframe",
);
ui.label(".");
});
////////////////////////////////////////////////////////////////
});
if ctx.input(|i| i.viewport().close_requested()) {
// Tell parent viewport that we should not show next frame:
self.show_info_window = false;
}
},
);
}
fn bottom_label(ui: &mut egui::Ui) {
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label(format!("{TITLE} v{}", env!("CARGO_PKG_VERSION")));
ui.label(" | Powered by ");
ui.hyperlink_to("egui", "https://github.com/emilk/egui");
ui.label(" and ");
ui.hyperlink_to(
"eframe",
"https://github.com/emilk/egui/tree/master/crates/eframe",
);
ui.label(".");
});
egui::warn_if_debug_build(ui);
});
}
fn main_panel(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
ui.vertical_centered(|ui| {
for i in 0..100 {
ui.horizontal_wrapped(|ui| {
for j in 0..10 {
ui.label(format!("foo-{i}-{j}"));
}
});
}
});
}
fn meta_panel(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
egui::menu::bar(ui, |ui| {
// NOTE: no File->Quit on web pages!
ui.menu_button("File", |ui| {
if ui.button("Info").clicked() {
self.show_info_window = true;
}
ui.separator();
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.add_space(16.0);
});
}
pub(crate) fn load_icon() -> IconData {
let (icon_rgba, icon_width, icon_height) = {
let image = image::load_from_memory(ICON_RAW)
.expect("Failed to open icon path")
.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
IconData {
rgba: icon_rgba,
width: icon_width,
height: icon_height,
}
}
}
impl eframe::App for Player {
/// Called each time the UI needs repainting, which may be many times per second.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
self.top_panel(ui, ctx);
});
egui::TopBottomPanel::top("top_panel2").show(ctx, |ui| {
self.meta_panel(ui, ctx);
});
egui::CentralPanel::default().show(ctx, |ui| {
self.main_panel(ui, ctx);
});
egui::TopBottomPanel::bottom("bot_panel").show(ctx, Self::bottom_label);
}
}