generated from PlexSheep/rs-base
feat: first ui with almost nothing
This commit is contained in:
parent
5da9a360d4
commit
c4a7b68c74
11
Cargo.toml
11
Cargo.toml
|
@ -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"
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
|
@ -0,0 +1 @@
|
|||
pub trait Backend {}
|
|
@ -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),
|
||||
}
|
|
@ -1,2 +1,4 @@
|
|||
pub mod backend;
|
||||
pub mod error;
|
||||
pub mod music;
|
||||
pub mod player;
|
||||
|
|
32
src/main.rs
32
src/main.rs
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue