Compare commits

..

26 Commits

Author SHA1 Message Date
Christoph J. Scherr 234e2743ad feat: get the http streamer and file streamer demos to work
cargo devel CI / cargo CI (push) Failing after 1m6s Details
2024-08-24 13:33:56 +02:00
Christoph J. Scherr e068450b6b feat: add a lot of stuff, don't remember what exactly it did lol
cargo devel CI / cargo CI (push) Failing after 1m4s Details
2024-08-24 01:24:39 +02:00
Christoph J. Scherr 6bee508bf3 ci: add libssl dep 2024-08-24 01:23:44 +02:00
Christoph J. Scherr 5dbc2ac6d2 feat(stream): add a demo that actually streams and plays music from my jellyfin server 2024-08-24 01:23:13 +02:00
Christoph J. Scherr 6a5cbcdbea fix(store): store is now actually saved and loaded
cargo devel CI / cargo CI (push) Successful in 3m14s Details
2024-08-23 12:07:14 +02:00
PlexSheep cc0bc95ba2 automatic cargo CI changes 2024-08-23 08:51:21 +00:00
Christoph J. Scherr 876e2d68e0 feat(store): add store
cargo devel CI / cargo CI (push) Successful in 3m9s Details
2024-08-23 10:48:16 +02:00
Christoph J. Scherr af3135462a feat(player): add category and category selector
cargo devel CI / cargo CI (push) Successful in 3m28s Details
2024-08-23 10:23:54 +02:00
Christoph J. Scherr 3dd949e9a5 Merge branch 'devel' of git.cscherr.de:PlexSheep/beatbaer into devel
cargo devel CI / cargo CI (push) Successful in 3m9s Details
2024-08-23 09:43:27 +02:00
Christoph J. Scherr 8f6c6b594f feat(player): imrpove album view 2024-08-23 09:43:16 +02:00
PlexSheep 51c6b318b3 automatic cargo CI changes 2024-08-22 23:31:58 +00:00
Christoph J. Scherr c7b6917af2 feat(player): load img or default
cargo devel CI / cargo CI (push) Successful in 3m20s Details
2024-08-23 01:28:43 +02:00
Christoph J. Scherr 29b6c10fd1 Merge branch 'devel' of git.cscherr.de:PlexSheep/beatbaer into devel
cargo devel CI / cargo CI (push) Failing after 1m50s Details
2024-08-23 01:13:26 +02:00
Christoph J. Scherr 22dbfdd193 feat(player): somewhat of a album view 2024-08-23 01:13:19 +02:00
PlexSheep c5447f58f1 automatic cargo CI changes 2024-08-22 23:04:40 +00:00
Christoph J. Scherr 1e38e17f36 Merge branch 'devel' of git.cscherr.de:PlexSheep/beatbaer into devel
cargo devel CI / cargo CI (push) Successful in 3m23s Details
2024-08-23 01:01:06 +02:00
Christoph J. Scherr 66f70eea52 feat(player): show some image 2024-08-23 01:00:52 +02:00
PlexSheep 23640a3fdf automatic cargo CI changes 2024-08-22 22:27:29 +00:00
Christoph J. Scherr b4667faf5d Merge branch 'devel' of git.cscherr.de:PlexSheep/beatbaer into devel
cargo devel CI / cargo CI (push) Successful in 3m4s Details
2024-08-23 00:24:29 +02:00
Christoph J. Scherr d727b22c0f feat(player): somewhat of a grid 2024-08-23 00:24:27 +02:00
PlexSheep 734b39945e automatic cargo CI changes 2024-08-22 21:23:33 +00:00
Christoph J. Scherr a744147355 Merge branch 'devel' of git.cscherr.de:PlexSheep/beatbaer into devel
cargo devel CI / cargo CI (push) Successful in 3m2s Details
2024-08-22 23:20:35 +02:00
Christoph J. Scherr c4a7b68c74 feat: first ui with almost nothing 2024-08-22 23:20:29 +02:00
PlexSheep b19bc410d5 automatic cargo CI changes 2024-08-22 17:30:11 +00:00
Christoph J. Scherr 5da9a360d4 Merge branch 'master' into devel
cargo devel CI / cargo CI (push) Successful in 1m40s Details
2024-08-22 19:28:32 +02:00
Christoph J. Scherr 676a1836ed feat: modules and cargo features 2024-08-22 19:28:22 +02:00
29 changed files with 834 additions and 3 deletions

View File

@ -21,6 +21,10 @@ jobs:
run: |
rustup component add rustfmt
rustup component add clippy
- name: install additional dependencies
run: |
apt update -y
apt install libssl -y
- name: config custom registry
run: |
mkdir -p ~/.cargo/

View File

@ -22,6 +22,10 @@ jobs:
run: |
rustup component add rustfmt
rustup component add clippy
- name: install additional dependencies
run: |
apt update -y
apt install libssl -y
- name: config custom registry
run: |
mkdir -p ~/.cargo/

View File

@ -11,8 +11,30 @@ homepage = "https://git.cscherr.de/PlexSheep/beatbear"
repository = "https://git.cscherr.de/PlexSheep/beatbear"
keywords = ["media", "sound", "music", "player", "jellyfin", "downloads"]
[features]
default = ["backend-fs", "backend-jellyfin"]
backend-fs = []
backend-jellyfin = []
[dependencies]
anyhow = "1.0.86"
clap = { version = "4.5.16", features = ["derive"] }
directories = "5.0.1"
eframe = { version = "0.28.1", optional = false }
egui = { version = "0.28.1", optional = false }
egui_extras = { version = "0.28.1", features = ["image"] }
human-panic = "2.0.1"
image = { version = "0.25.2", default-features = true, features = [
"jpeg",
"png",
] }
libpt = { version = "0.6.0", features = ["cli", "full"] }
rmp-serde = "1.3.0"
serde = { version = "1.0.208", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
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,7 @@
[package]
name = "play_file_url"
version = "0.1.0"
edition = "2021"
[dependencies]
rodio = { version = "0.19.0", features = ["symphonia-all"] }

View File

@ -0,0 +1,37 @@
use std::fs::File;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use rodio::{Decoder, OutputStream, Source};
fn main() {
const URL: &str = "file:///home/plex/Musik/Plex/.mp3";
let f = open_file_url(URL).unwrap();
let reader = std::io::BufReader::new(f);
// Get an output stream handle to the default physical sound device
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
// Decode that sound file into a source
let source = Decoder::new(reader).unwrap();
// Play the sound directly on the device
stream_handle.play_raw(source.convert_samples()).unwrap();
std::thread::sleep(Duration::from_secs(10)); // should be fine, because tokio says
// spawn_blocking happens in another thread
}
fn open_file_url(url: &str) -> std::io::Result<File> {
if !url.starts_with("file://") {
return Err(std::io::Error::new(std::io::ErrorKind::Unsupported, "bad"));
}
let path = url.replacen("file://", "", 1);
if let Ok(path) = PathBuf::from_str(&path) {
let f = std::fs::File::open(path)?;
Ok(f)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"not a path even without file://",
))
}
}

View File

@ -0,0 +1,10 @@
[package]
name = "try_stream"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = "0.12.7"
rodio = { version = "0.19.0", features = ["symphonia-all"] }
stream-download = { version = "0.7.2", features = ["reqwest-native-tls"] }
tokio = { version = "1.39.3", features = ["rt", "macros"] }

View File

@ -0,0 +1,39 @@
use std::error::Error;
use std::result::Result;
use std::time::Duration;
use reqwest::header::HeaderMap;
use reqwest::Client;
use rodio::{Decoder, OutputStream, Source};
use stream_download::http::HttpStream;
use stream_download::storage::memory::MemoryStorageProvider;
use stream_download::{Settings, StreamDownload};
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
const URL: &str = "https://upload.wikimedia.org/wikipedia/commons/4/48/Aadama.mp3";
let mut headers = HeaderMap::new();
headers.append("User-Agent", "Beatbär Streaming Demo".parse().unwrap());
let client = Client::builder().default_headers(headers).build()?;
let stream = HttpStream::new(client, URL.parse()?).await?;
let reader =
StreamDownload::from_stream(stream, MemoryStorageProvider, Settings::default()).await?;
// now play with rodio, or read it out
tokio::task::spawn_blocking(move || {
// Get an output stream handle to the default physical sound device
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
// Decode that sound file into a source
let source = Decoder::new(reader).unwrap();
// Play the sound directly on the device
stream_handle.play_raw(source.convert_samples()).unwrap();
std::thread::sleep(Duration::from_secs(10)); // should be fine, because tokio says
// spawn_blocking happens in another thread
})
.await?;
Ok(())
}

View File

0
src/backend/jellyfin.rs Normal file
View File

1
src/backend/mod.rs Normal file
View File

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

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

@ -0,0 +1,28 @@
use std::path::PathBuf;
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),
#[error(
r"The system does not seem to have the usual project dirs, like:
Linux: /home/alice/.config/barapp
MS Windows: C:\Users\Alice\AppData\Roaming\Foo Corp\Bar App\config
Mac: /Users/Alice/Library/Application Support/com.Foo-Corp.Bar-App
Closing as to not break your files accidentally."
)]
NoProjDir,
#[error(r"The store file '{0}' where Beatbär stores it's metadata is not a regular file, refusing to save or load to keep your files safe.")]
BadStoreFile(PathBuf),
#[error(r"Could not convert the store to the binary format for saving it to the disk.")]
RmpEncode(#[from] rmp_serde::encode::Error),
#[error(r"Could not decode the store from the binary format for loading it from the disk. Is your store file broken?")]
RmpDecode(#[from] rmp_serde::decode::Error),
#[error(r"Error while reading or writing to the disk. Source: {0}")]
IO(#[from] std::io::Error),
}

View File

@ -1 +1,5 @@
pub mod backend;
pub mod error;
pub mod music;
pub mod player;
pub mod store;

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(())
}

1
src/music/mod.rs Normal file
View File

@ -0,0 +1 @@

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

@ -0,0 +1,119 @@
use clap::Parser;
use eframe::CreationContext;
use egui::Stroke;
use libpt::cli::args::VerbosityLevel;
use libpt::log::{debug, info};
use crate::error::Error;
use crate::store::Store;
use self::ui::details::Details;
use self::ui::entry::{Entry, Kind};
use self::ui::mainpanel::MainPanel;
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,
#[clap(skip)]
kind: Kind,
#[clap(skip)]
store: Option<Store>,
#[clap(skip)]
active_main_panel: MainPanel,
}
impl Player {
#[inline]
pub fn store(&self) -> &Store {
&self.store.as_ref().unwrap()
}
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([900.0, 700.0])
.with_min_inner_size([500.0, 320.0])
.with_icon(Player::load_icon()),
..Default::default()
};
debug!("main panel on start: {:#?}", app.active_main_panel);
info!("Player ready to start UI");
Ok(app)
}
pub fn init(&mut self, _cc: &CreationContext) {
self.store = Some(Store::load().expect("could not load store"));
}
fn set_category(&mut self, kind: Kind) {
self.kind = kind;
match self.kind {
Kind::Album | Kind::Playlist => {
self.set_active_main_panel(MainPanel::Details(Default::default()))
}
Kind::Genre | Kind::Artist | Kind::Overview => {
self.set_active_main_panel(MainPanel::Overview(Default::default()))
}
Kind::Song => {
unreachable!()
}
}
}
fn category(&self) -> Kind {
self.kind
}
fn open(&mut self, entry: &Entry) {
todo!()
}
fn entries(&self) -> Vec<Entry> {
match self.category() {
Kind::Overview => self.store().albums(),
Kind::Album => self.store().albums(),
Kind::Song => self.store().songs(),
Kind::Playlist => self.store().playlists(),
Kind::Artist => self.store().artists(),
Kind::Genre => self.store().genres(),
}
}
pub fn end(&mut self) -> Result<(), Error> {
self.store().save()
}
}

58
src/player/ui/details.rs Normal file
View File

@ -0,0 +1,58 @@
use egui::text::LayoutJob;
use egui::{Color32, FontFamily, FontId, Label, TextFormat};
use super::Entry;
#[derive(Clone, Default, PartialEq)]
pub struct Details {
parent: Entry,
frame: egui::Frame,
}
impl Details {
pub fn new(parent: Entry) -> Self {
let frame = egui::Frame::none();
Self {
parent: parent.to_owned(),
frame,
}
}
pub(crate) fn ui(&self, ui: &mut egui::Ui, ctx: &egui::Context, player: &mut super::Player) {
ui.label("todo");
}
}
impl egui::Widget for Details {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
ui.vertical_centered(|ui| {
let r = ui.add(
egui::Image::new(self.parent.img())
.rounding(5.0)
.bg_fill(Color32::DARK_GRAY)
.shrink_to_fit()
.maintain_aspect_ratio(true),
);
let mut job = LayoutJob::default();
job.append(
&self.parent.title,
0.0,
TextFormat {
font_id: FontId::new(14.0, FontFamily::Proportional),
..Default::default()
},
);
r.union(ui.add(Label::new(job)));
r.union(ui.label(&self.parent.subtitle));
})
.response
}
}
impl std::fmt::Debug for Details {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Details")
.field("parent", &self.parent)
.finish()
}
}

66
src/player/ui/entry.rs Normal file
View File

@ -0,0 +1,66 @@
use egui::ImageSource;
use strum::EnumIter;
#[derive(Hash, Debug, Clone, PartialEq, Eq, Copy, Default, EnumIter)]
pub enum Kind {
Playlist,
#[default]
Album,
Song,
Artist,
Genre,
Overview,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Entry {
pub kind: Kind,
pub title: String,
pub subtitle: String,
/// image url because I can't be bothered to use lifetimes everywhere
pub img_url: Option<String>,
}
impl Entry {
pub fn new(kind: Kind, title: &str, subtitle: &str, img: Option<String>) -> Self {
Self {
kind,
title: title.to_owned(),
subtitle: subtitle.to_owned(),
img_url: img,
}
}
pub fn img(&self) -> ImageSource {
if let Some(url) = &self.img_url {
ImageSource::Uri(std::borrow::Cow::from(url))
} else {
ImageSource::Bytes {
uri: std::borrow::Cow::Borrowed("builtin://icon_256.jpg"),
bytes: egui::load::Bytes::Static(super::ICON_RAW),
}
}
}
}
impl std::fmt::Display for Entry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.title)
}
}
impl std::fmt::Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Song => "Song",
Self::Overview => "Overview",
Self::Genre => "Genre",
Self::Album => "Album",
Self::Playlist => "Playlist",
Self::Artist => "Artist",
}
)
}
}

View File

@ -0,0 +1,26 @@
use egui_extras::{Column, TableBuilder};
use super::details::Details;
use super::overview::Overview;
use super::Player;
#[derive(Clone, PartialEq, Debug)]
pub enum MainPanel {
Details(Details),
Overview(Overview),
}
impl MainPanel {
pub(crate) fn ui(&self, ui: &mut egui::Ui, ctx: &egui::Context, player: &mut Player) {
match self {
Self::Overview(ov) => ov.ui(ui, ctx, player),
Self::Details(dt) => dt.ui(ui, ctx, player),
}
}
}
impl Default for MainPanel {
fn default() -> Self {
Self::Overview(Overview::default())
}
}

170
src/player/ui/mod.rs Normal file
View File

@ -0,0 +1,170 @@
use egui::{IconData, Sense};
use egui_extras::{Column, TableBuilder};
use libpt::log::{error, trace, warn};
use strum::IntoEnumIterator;
pub mod details;
pub mod entry;
pub mod mainpanel;
pub mod overview;
pub mod showbox;
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) {
let bind = self.active_main_panel.clone();
bind.ui(ui, ctx, self)
}
fn meta_panel(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
ui.horizontal_centered(|ui| {
for category in Kind::iter() {
if category == Kind::Song {
continue; // no category for songs, that's just clicked and then played
}
let sl = ui.selectable_label(self.category() == category, category.to_string());
if sl.clicked() {
self.set_category(category);
}
}
});
}
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,
}
}
pub fn set_active_main_panel(&mut self, mp: MainPanel) {
self.active_main_panel = mp;
}
}
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_extras::install_image_loaders(ctx); // apperently it's okay to have this every update
// as it's only done once? sus
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);
}
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
match self.store().save() {
Ok(_) => (),
Err(e) => {
error!("{e}");
}
}
}
}

47
src/player/ui/overview.rs Normal file
View File

@ -0,0 +1,47 @@
use egui::Sense;
use egui_extras::{Column, TableBuilder};
use libpt::log::{debug, warn};
use super::showbox::ShowBox;
use super::Player;
#[derive(Clone, Default, PartialEq)]
pub struct Overview {
frame: egui::Frame,
}
impl Overview {
pub fn ui(&self, ui: &mut egui::Ui, ctx: &egui::Context, player: &mut Player) {
let mut tb = TableBuilder::new(ui);
let entries_per_line: usize = ctx.screen_rect().width() as usize / 256;
for _ in 0..entries_per_line {
tb = tb.column(Column::remainder());
}
tb.body(|mut body| {
for line in player.entries().chunks(entries_per_line) {
body.row(240.0, |mut row| {
for e in line {
row.col(|ui| {
let shobo = ui.add(ShowBox::new(e));
let shobo = shobo.interact(Sense::click());
if shobo.clicked() {
debug!("showbox \"{:#?}\" was clicked", shobo);
warn!("clicking for showbox not defined yet");
player.open(e);
}
});
}
});
}
});
}
}
impl std::fmt::Debug for Overview {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Overview").finish()
}
}

53
src/player/ui/showbox.rs Normal file
View File

@ -0,0 +1,53 @@
use egui::text::LayoutJob;
use egui::{Color32, FontFamily, FontId, Label, TextFormat};
use super::Entry;
pub struct ShowBox {
entry: Entry,
frame: egui::Frame,
}
impl ShowBox {
pub fn new(entry: &Entry) -> Self {
let frame = egui::Frame::none();
Self {
entry: entry.to_owned(),
frame,
}
}
}
impl egui::Widget for ShowBox {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
ui.vertical_centered(|ui| {
let r = ui.add(
egui::Image::new(self.entry.img())
.rounding(5.0)
.bg_fill(Color32::DARK_GRAY)
.shrink_to_fit()
.maintain_aspect_ratio(true),
);
let mut job = LayoutJob::default();
job.append(
&self.entry.title,
0.0,
TextFormat {
font_id: FontId::new(14.0, FontFamily::Proportional),
..Default::default()
},
);
r.union(ui.add(Label::new(job)));
r.union(ui.label(&self.entry.subtitle));
})
.response
}
}
impl std::fmt::Debug for ShowBox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShowBox")
.field("entry", &self.entry)
.finish()
}
}

0
src/store/album.rs Normal file
View File

0
src/store/artist.rs Normal file
View File

0
src/store/genre.rs Normal file
View File

104
src/store/mod.rs Normal file
View File

@ -0,0 +1,104 @@
use std::fs::File;
use std::io::{BufReader, Write};
use std::path::PathBuf;
use libpt::log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use crate::error::Error;
#[derive(Serialize, Deserialize, Default, Debug)]
pub struct Store {
dummy: String,
}
impl Store {
pub fn projdir() -> Option<directories::ProjectDirs> {
directories::ProjectDirs::from("de.cscherr", "Beatbär", "Beatbär")
}
pub fn genres(&self) -> Vec<crate::player::ui::entry::Entry> {
vec![]
}
pub fn artists(&self) -> Vec<crate::player::ui::entry::Entry> {
vec![]
}
pub fn playlists(&self) -> Vec<crate::player::ui::entry::Entry> {
vec![]
}
pub fn songs(&self) -> Vec<crate::player::ui::entry::Entry> {
vec![]
}
pub fn albums(&self) -> Vec<crate::player::ui::entry::Entry> {
vec![]
}
pub fn save(&self) -> Result<(), Error> {
info!("saving the store to file");
debug!("Store: {self:#?}");
if let Some(dirs) = Self::projdir() {
let mut store_path: PathBuf = dirs.data_local_dir().into();
store_path.push("store.msgpack");
if !store_path.exists() {
std::fs::create_dir_all(
store_path
.parent()
.expect("beatbär storefile has no parent????"),
)?;
warn!("The Beatbär store at '{}' does not exist. Creating it anew. This is normal if you start Beatbär for the first time.", store_path.as_path().to_string_lossy());
} else if !store_path.is_file() {
let e = Error::BadStoreFile(store_path);
error!("{e}");
return Err(e);
}
let mut file = File::options()
.write(true)
.append(false)
.create(true)
.truncate(true)
.open(store_path)?;
let repr = rmp_serde::to_vec(self)?;
file.write_all(&repr)?;
info!("store file was written");
Ok(())
} else {
Err(Error::NoProjDir)
}
}
pub fn load() -> Result<Self, Error> {
info!("loading the store to file");
if let Some(dirs) = Self::projdir() {
let mut store_path: PathBuf = dirs.data_local_dir().into();
store_path.push("store.msgpack");
if !store_path.exists() {
warn!(
"The Beatbär store at '{}' does not exist. Loading an empty store instead.",
store_path.as_path().to_string_lossy()
);
return Ok(Self::default());
} else if !store_path.is_file() {
let e = Error::BadStoreFile(store_path);
error!("{e}");
return Err(e);
}
let file = File::options().read(true).write(false).open(store_path)?;
let store = rmp_serde::from_read(file)?;
info!("store file was loaded");
debug!("Store: {store:#?}");
Ok(store)
} else {
let e = Error::NoProjDir;
error!("{}", e);
Err(e)
}
}
}

1
src/store/playlist.rs Normal file
View File

@ -0,0 +1 @@
pub struct Playlist;

2
src/store/song.rs Normal file
View File

@ -0,0 +1,2 @@
#[derive(Clone, Debug, Hash)]
pub struct Song {}