generated from PlexSheep/rs-base
Compare commits
No commits in common. "devel" and "master" have entirely different histories.
|
@ -21,10 +21,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
rustup component add rustfmt
|
rustup component add rustfmt
|
||||||
rustup component add clippy
|
rustup component add clippy
|
||||||
- name: install additional dependencies
|
|
||||||
run: |
|
|
||||||
apt update -y
|
|
||||||
apt install libssl -y
|
|
||||||
- name: config custom registry
|
- name: config custom registry
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.cargo/
|
mkdir -p ~/.cargo/
|
||||||
|
|
|
@ -22,10 +22,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
rustup component add rustfmt
|
rustup component add rustfmt
|
||||||
rustup component add clippy
|
rustup component add clippy
|
||||||
- name: install additional dependencies
|
|
||||||
run: |
|
|
||||||
apt update -y
|
|
||||||
apt install libssl -y
|
|
||||||
- name: config custom registry
|
- name: config custom registry
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.cargo/
|
mkdir -p ~/.cargo/
|
||||||
|
|
22
Cargo.toml
22
Cargo.toml
|
@ -11,30 +11,8 @@ homepage = "https://git.cscherr.de/PlexSheep/beatbear"
|
||||||
repository = "https://git.cscherr.de/PlexSheep/beatbear"
|
repository = "https://git.cscherr.de/PlexSheep/beatbear"
|
||||||
keywords = ["media", "sound", "music", "player", "jellyfin", "downloads"]
|
keywords = ["media", "sound", "music", "player", "jellyfin", "downloads"]
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["backend-fs", "backend-jellyfin"]
|
|
||||||
backend-fs = []
|
|
||||||
backend-jellyfin = []
|
|
||||||
|
|
||||||
[dependencies]
|
[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]
|
[lib]
|
||||||
name = "beatbaer"
|
name = "beatbaer"
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 36 KiB |
|
@ -1,7 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "play_file_url"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rodio = { version = "0.19.0", features = ["symphonia-all"] }
|
|
|
@ -1,37 +0,0 @@
|
||||||
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/moonwater.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://",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
[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"] }
|
|
|
@ -1,39 +0,0 @@
|
||||||
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(())
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub trait Backend {}
|
|
|
@ -1,28 +0,0 @@
|
||||||
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),
|
|
||||||
}
|
|
|
@ -1,5 +1 @@
|
||||||
pub mod backend;
|
|
||||||
pub mod error;
|
|
||||||
pub mod music;
|
|
||||||
pub mod player;
|
|
||||||
pub mod store;
|
|
||||||
|
|
32
src/main.rs
32
src/main.rs
|
@ -1,31 +1,3 @@
|
||||||
use beatbaer::error::Error;
|
fn main() {
|
||||||
use beatbaer::player::Player;
|
println!("Hello, world!");
|
||||||
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 +0,0 @@
|
||||||
|
|
|
@ -1,119 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
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",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,170 +0,0 @@
|
||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
104
src/store/mod.rs
104
src/store/mod.rs
|
@ -1,104 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
pub struct Playlist;
|
|
|
@ -1,2 +0,0 @@
|
||||||
#[derive(Clone, Debug, Hash)]
|
|
||||||
pub struct Song {}
|
|
Loading…
Reference in New Issue