Compare commits
63 commits
Author | SHA1 | Date | |
---|---|---|---|
3e1bc8c6ac | |||
47db86ba59 | |||
ab61924e5f | |||
3e95544fb3 | |||
5d0116e9de | |||
578865abe7 | |||
ca21d5c640 | |||
075754fcd0 | |||
c145bdbd9c | |||
502795c6cf | |||
e8b170e0e2 | |||
d276482c8b | |||
08b78467b5 | |||
0ddd33a898 | |||
8ebeac0499 | |||
8e6305d934 | |||
1b92da1ab2 | |||
00240e4205 | |||
e4cdccce63 | |||
015da43d1b | |||
773587b664 | |||
2f23e63349 | |||
8cd599aadd | |||
2865352f7a | |||
180898103a | |||
d90c86c894 | |||
f42fadd403 | |||
f2f58047a5 | |||
d457098f2a | |||
d0beff5a98 | |||
7a860e323a | |||
ef234f0ec0 | |||
391949cde6 | |||
f66880f13d | |||
83bb20c246 | |||
dd868969cc | |||
1dc28d7b07 | |||
f7ffa62e02 | |||
bcea959381 | |||
aa6fb750e7 | |||
a1706913e6 | |||
e28d20a5da | |||
4653399fe9 | |||
e0be69df81 | |||
8643542b7a | |||
e9e57c9dff | |||
d1590bee0a | |||
56f6784892 | |||
a223d8b530 | |||
fad82f6448 | |||
934aa1a610 | |||
6d400cd7c7 | |||
c902f0fcf3 | |||
e025a48735 | |||
74a84d9f7a | |||
f2f93f5393 | |||
1a623f451d | |||
ef07d7936a | |||
7f6a24545c | |||
34d3e47863 | |||
a9441536c4 | |||
a1fe8c3120 | |||
ce45a40df5 |
10 changed files with 2702 additions and 1782 deletions
3783
Cargo.lock
generated
3783
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
@ -1,21 +1,23 @@
|
|||
[package]
|
||||
name = "mpd_info_screen"
|
||||
version = "0.3.7"
|
||||
edition = "2018"
|
||||
version = "0.4.17"
|
||||
edition = "2021"
|
||||
description = "Displays info on currently playing music from an MPD daemon"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/Stephen-Seo/mpd_info_screen"
|
||||
resolver = "2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
structopt = "0.3"
|
||||
image = "0.24"
|
||||
ggez = "0.7"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
image = "0.25"
|
||||
ggez = "0.9.3"
|
||||
freetype = { version = "0.7", optional = true }
|
||||
wgpu-types = "0.16"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = { version = "0.53", optional = true }
|
||||
bindgen = { version = "0.69", optional = true }
|
||||
|
||||
[features]
|
||||
unicode_support = ["dep:freetype", "dep:bindgen"]
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021-2022 Stephen Seo
|
||||
Copyright (c) 2021-2024 Stephen Seo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
65
README.md
65
README.md
|
@ -12,6 +12,11 @@ A Rust program that displays info about the currently running MPD server.
|
|||
The window shows albumart (may be embedded in the audio file, or is a "cover.jpg" in the same directory as the song file), a "time-remaining"
|
||||
counter, and the filename currently being played
|
||||
|
||||
## Known Bugs ❗❗
|
||||
|
||||
Currently there are no known bugs. Please report any bugs you find to the
|
||||
[issue tracker](https://github.com/Stephen-Seo/mpd_info_screen/issues).
|
||||
|
||||
## Unicode Support
|
||||
|
||||
By default, unicode characters will not display properly. Build the project with
|
||||
|
@ -23,32 +28,50 @@ installed already).
|
|||
|
||||
cargo build --release --features unicode_support
|
||||
|
||||
or through crates.io:
|
||||
|
||||
cargo install --features unicode_support mpd_info_screen
|
||||
|
||||
# Usage
|
||||
|
||||
|
||||
mpd_info_screen 0.3.7
|
||||
Displays info on currently playing music from an MPD daemon
|
||||
|
||||
USAGE:
|
||||
mpd_info_screen [FLAGS] [OPTIONS] <host> [port]
|
||||
Usage: mpd_info_screen [OPTIONS] <HOST> [PORT]
|
||||
|
||||
FLAGS:
|
||||
--disable-show-album disable album display
|
||||
--disable-show-artist disable artist display
|
||||
--disable-show-filename disable filename display
|
||||
--disable-show-title disable title display
|
||||
--no-scale-fill don't scale-fill the album art to the window
|
||||
--pprompt input password via prompt
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
Arguments:
|
||||
<HOST>
|
||||
[PORT] [default: 6600]
|
||||
|
||||
OPTIONS:
|
||||
-l, --log-level <log-level> [default: Error] [possible values: Error, Warning, Debug, Verbose]
|
||||
-p <password>
|
||||
-t, --text-bg-opacity <text-bg-opacity> sets the opacity of the text background (0-255) [default: 190]
|
||||
Options:
|
||||
-p <PASSWORD>
|
||||
|
||||
ARGS:
|
||||
<host>
|
||||
<port> [default: 6600]
|
||||
--disable-show-title
|
||||
disable title display
|
||||
--disable-show-artist
|
||||
disable artist display
|
||||
--disable-show-album
|
||||
disable album display
|
||||
--disable-show-filename
|
||||
disable filename display
|
||||
--disable-show-percentage
|
||||
disable percentage display
|
||||
--force-text-height-scale <FORCE_TEXT_HEIGHT_SCALE>
|
||||
force-set text height relative to window height as a ratio (default 0.12)
|
||||
--pprompt
|
||||
input password via prompt
|
||||
--pfile <PASSWORD_FILE>
|
||||
read password from file
|
||||
--no-scale-fill
|
||||
don't scale-fill the album art to the window
|
||||
-l, --log-level <LOG_LEVEL>
|
||||
[default: error] [possible values: error, warning, debug, verbose]
|
||||
-t, --text-bg-opacity <TEXT_BG_OPACITY>
|
||||
sets the opacity of the text background (0-255) [default: 190]
|
||||
-h, --help
|
||||
Print help
|
||||
-V, --version
|
||||
Print version
|
||||
|
||||
|
||||
Note that presing the Escape key when the window is focused closes the program.
|
||||
|
@ -77,8 +100,8 @@ MIT license.
|
|||
Uses dependency [image](https://crates.io/crates/image) which is licensed under
|
||||
MIT license.
|
||||
|
||||
Uses dependency [structopt](https://crates.io/crates/structopt) which is
|
||||
licensed under Apache-2.0 or MIT licenses.
|
||||
Uses dependency [clap](https://crates.io/crates/clap) which is licensed under
|
||||
Apache-2.0 or MIT licenses.
|
||||
|
||||
## Unicode Support Dependencies
|
||||
|
||||
|
|
2
build.rs
2
build.rs
|
@ -14,7 +14,7 @@ fn main() {
|
|||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("src/bindgen_wrapper.h")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use clap::ValueEnum;
|
||||
use std::fmt::Display;
|
||||
use structopt::clap::arg_enum;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LogState {
|
||||
Error,
|
||||
Warning,
|
||||
|
@ -9,14 +9,12 @@ pub enum LogState {
|
|||
Verbose,
|
||||
}
|
||||
|
||||
arg_enum! {
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum LogLevel {
|
||||
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LogLevel {
|
||||
Error,
|
||||
Warning,
|
||||
Debug,
|
||||
Verbose,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log<T>(msg: T, state: LogState, level: LogLevel)
|
||||
|
@ -46,26 +44,26 @@ pub fn log_error<T>(msg: T)
|
|||
where
|
||||
T: Display,
|
||||
{
|
||||
println!("Error: {}", msg);
|
||||
println!("Error: {msg}");
|
||||
}
|
||||
|
||||
pub fn log_warning<T>(msg: T)
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
println!("Warning: {}", msg);
|
||||
println!("Warning: {msg}");
|
||||
}
|
||||
|
||||
pub fn log_debug<T>(msg: T)
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
println!("Debug: {}", msg);
|
||||
println!("Debug: {msg}");
|
||||
}
|
||||
|
||||
pub fn log_verbose<T>(msg: T)
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
println!("Verbose: {}", msg);
|
||||
println!("Verbose: {msg}");
|
||||
}
|
||||
|
|
386
src/display.rs
386
src/display.rs
|
@ -1,13 +1,16 @@
|
|||
use crate::debug_log::{self, log};
|
||||
use crate::mpd_handler::{InfoFromShared, MPDHandler, MPDHandlerState, MPDPlayState};
|
||||
use crate::Opt;
|
||||
use ggez::event::{self, EventHandler};
|
||||
use ggez::event::EventHandler;
|
||||
use ggez::graphics::{
|
||||
self, Color, DrawMode, DrawParam, Drawable, Font, Image, Mesh, MeshBuilder, PxScale, Rect,
|
||||
Text, TextFragment, Transform,
|
||||
self, Color, DrawMode, DrawParam, Drawable, Image, Mesh, MeshBuilder, PxScale, Rect, Text,
|
||||
TextFragment, Transform,
|
||||
};
|
||||
use ggez::{timer, Context, GameError, GameResult};
|
||||
use image::io::Reader as ImageReader;
|
||||
use ggez::input::keyboard::{self, KeyInput};
|
||||
use ggez::mint::Vector2;
|
||||
use ggez::{Context, GameError, GameResult};
|
||||
use image::DynamicImage;
|
||||
use image::ImageReader;
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
@ -16,14 +19,16 @@ use std::thread;
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
const POLL_TIME: Duration = Duration::from_millis(333);
|
||||
const INIT_FONT_SIZE_X: f32 = 24.0;
|
||||
const INIT_FONT_SIZE_Y: f32 = 34.0;
|
||||
const INIT_FONT_SIZE_RATIO: f32 = 1.4167;
|
||||
const INIT_FONT_SIZE_X: f32 = 36.0;
|
||||
const INIT_FONT_SIZE_Y: f32 = INIT_FONT_SIZE_X * INIT_FONT_SIZE_RATIO;
|
||||
const TEXT_X_OFFSET: f32 = 0.3;
|
||||
const TEXT_OFFSET_Y_SPACING: f32 = 0.4;
|
||||
const TEXT_HEIGHT_SCALE: f32 = 0.1;
|
||||
const ARTIST_HEIGHT_SCALE: f32 = 0.08;
|
||||
const ALBUM_HEIGHT_SCALE: f32 = 0.08;
|
||||
const TIMER_HEIGHT_SCALE: f32 = 0.07;
|
||||
const TEXT_HEIGHT_SCALE: f32 = 0.12;
|
||||
const ARTIST_HEIGHT_SCALE: f32 = 0.12;
|
||||
const ALBUM_HEIGHT_SCALE: f32 = 0.12;
|
||||
const TIMER_HEIGHT_SCALE_RATIO: f32 = 0.875;
|
||||
const TIMER_HEIGHT_SCALE: f32 = TEXT_HEIGHT_SCALE * TIMER_HEIGHT_SCALE_RATIO;
|
||||
const MIN_WIDTH_RATIO: f32 = 4.0 / 5.0;
|
||||
const INCREASE_AMT: f32 = 6.0 / 5.0;
|
||||
const DECREASE_AMT: f32 = 5.0 / 6.0;
|
||||
|
@ -51,11 +56,15 @@ fn seconds_to_time(seconds: f64) -> String {
|
|||
result
|
||||
}
|
||||
|
||||
fn time_to_percentage(total: f64, current: f64) -> String {
|
||||
((100.0f64 * current / total).round() as i32).to_string() + "%"
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unicode_support"))]
|
||||
#[allow(clippy::ptr_arg)]
|
||||
fn string_to_text(
|
||||
string: String,
|
||||
_loaded_fonts: &mut Vec<(PathBuf, Font)>,
|
||||
_loaded_fonts: &mut Vec<(PathBuf, String)>,
|
||||
_ctx: &mut Context,
|
||||
) -> Text {
|
||||
Text::new(TextFragment::from(string))
|
||||
|
@ -64,7 +73,7 @@ fn string_to_text(
|
|||
#[cfg(feature = "unicode_support")]
|
||||
fn string_to_text(
|
||||
string: String,
|
||||
loaded_fonts: &mut Vec<(PathBuf, Font)>,
|
||||
loaded_fonts: &mut Vec<(PathBuf, String)>,
|
||||
ctx: &mut Context,
|
||||
) -> Text {
|
||||
use super::unicode_support;
|
||||
|
@ -79,7 +88,7 @@ fn string_to_text(
|
|||
}
|
||||
|
||||
let find_font =
|
||||
|c: char, loaded_fonts: &mut Vec<(PathBuf, Font)>, ctx: &mut Context| -> Option<usize> {
|
||||
|c: char, loaded_fonts: &mut Vec<(PathBuf, String)>, ctx: &mut Context| -> Option<usize> {
|
||||
for (idx, (path, _)) in loaded_fonts.iter().enumerate() {
|
||||
let result = unicode_support::font_has_char(c, path);
|
||||
if result.is_ok() && result.unwrap() {
|
||||
|
@ -89,9 +98,16 @@ fn string_to_text(
|
|||
|
||||
let find_result = unicode_support::get_matching_font_from_char(c);
|
||||
if let Ok(path) = find_result {
|
||||
let new_font = Font::new(ctx, &path);
|
||||
let new_font = ggez::graphics::FontData::from_path(ctx, &path);
|
||||
if let Ok(font) = new_font {
|
||||
loaded_fonts.push((path, font));
|
||||
let font_name: String = path
|
||||
.file_name()
|
||||
.expect("Should be valid filename at end of Font path.")
|
||||
.to_str()
|
||||
.expect("Font filename should be valid unicode.")
|
||||
.to_owned();
|
||||
ctx.gfx.add_font(&font_name, font);
|
||||
loaded_fonts.push((path, font_name));
|
||||
return Some(loaded_fonts.len() - 1);
|
||||
} else {
|
||||
log(
|
||||
|
@ -102,7 +118,7 @@ fn string_to_text(
|
|||
}
|
||||
} else {
|
||||
log(
|
||||
format!("Failed to find font for {}", c),
|
||||
format!("Failed to find font for {c}"),
|
||||
debug_log::LogState::Error,
|
||||
debug_log::LogLevel::Error,
|
||||
);
|
||||
|
@ -132,13 +148,13 @@ fn string_to_text(
|
|||
text.add(current_fragment);
|
||||
current_fragment = Default::default();
|
||||
}
|
||||
let (_, font) = loaded_fonts[idx];
|
||||
current_fragment.font = Some(font);
|
||||
let (_, font) = &loaded_fonts[idx];
|
||||
current_fragment.font = Some(font.clone());
|
||||
}
|
||||
current_fragment.text.push(c);
|
||||
} else if let Some(idx) = idx_opt {
|
||||
let font = loaded_fonts[idx].1;
|
||||
if let Some(current_font) = current_fragment.font {
|
||||
let font = &loaded_fonts[idx].1;
|
||||
if let Some(current_font) = current_fragment.font.as_ref() {
|
||||
if current_font == font {
|
||||
current_fragment.text.push(c);
|
||||
} else {
|
||||
|
@ -147,17 +163,17 @@ fn string_to_text(
|
|||
current_fragment = Default::default();
|
||||
}
|
||||
current_fragment.text.push(c);
|
||||
current_fragment.font = Some(font);
|
||||
current_fragment.font = Some(font.clone());
|
||||
}
|
||||
} else if current_fragment.text.is_empty() {
|
||||
current_fragment.text.push(c);
|
||||
current_fragment.font = Some(font);
|
||||
current_fragment.font = Some(font.clone());
|
||||
} else {
|
||||
text.add(current_fragment);
|
||||
current_fragment = Default::default();
|
||||
|
||||
current_fragment.text.push(c);
|
||||
current_fragment.font = Some(font);
|
||||
current_fragment.font = Some(font.clone());
|
||||
}
|
||||
} else {
|
||||
if !current_fragment.text.is_empty() && current_fragment.font.is_some() {
|
||||
|
@ -203,16 +219,23 @@ pub struct MPDDisplay {
|
|||
album_string_cache: String,
|
||||
album_transform: Transform,
|
||||
timer_text: Text,
|
||||
timer_text_len: usize,
|
||||
timer_transform: Transform,
|
||||
timer_x: f32,
|
||||
timer_y: f32,
|
||||
timer: f64,
|
||||
length: f64,
|
||||
cached_filename_y: f32,
|
||||
cached_album_y: f32,
|
||||
cached_artist_y: f32,
|
||||
cached_title_y: f32,
|
||||
cached_timer_y: f32,
|
||||
text_bg_mesh: Option<Mesh>,
|
||||
hide_text: bool,
|
||||
tried_album_art_in_dir: bool,
|
||||
prev_mpd_play_state: MPDPlayState,
|
||||
mpd_play_state: MPDPlayState,
|
||||
loaded_fonts: Vec<(PathBuf, Font)>,
|
||||
loaded_fonts: Vec<(PathBuf, String)>,
|
||||
}
|
||||
|
||||
impl MPDDisplay {
|
||||
|
@ -224,7 +247,7 @@ impl MPDDisplay {
|
|||
is_initialized: false,
|
||||
is_authenticated: false,
|
||||
notice_text: Text::default(),
|
||||
poll_instant: Instant::now() - POLL_TIME,
|
||||
poll_instant: Instant::now().checked_sub(POLL_TIME).unwrap(),
|
||||
shared: None,
|
||||
password_entered: false,
|
||||
dirty_flag: None,
|
||||
|
@ -237,14 +260,21 @@ impl MPDDisplay {
|
|||
title_text: Text::default(),
|
||||
title_transform: Transform::default(),
|
||||
timer_text: Text::new("0"),
|
||||
timer_text_len: 0,
|
||||
timer_transform: Transform::default(),
|
||||
timer_x: INIT_FONT_SIZE_X,
|
||||
timer_y: INIT_FONT_SIZE_Y,
|
||||
timer: 0.0,
|
||||
length: 0.0,
|
||||
cached_filename_y: 0.0f32,
|
||||
cached_album_y: 0.0f32,
|
||||
cached_artist_y: 0.0f32,
|
||||
cached_title_y: 0.0f32,
|
||||
cached_timer_y: 0.0f32,
|
||||
text_bg_mesh: None,
|
||||
hide_text: false,
|
||||
tried_album_art_in_dir: false,
|
||||
prev_mpd_play_state: MPDPlayState::Playing,
|
||||
mpd_play_state: MPDPlayState::Playing,
|
||||
loaded_fonts: Vec::new(),
|
||||
filename_string_cache: String::new(),
|
||||
|
@ -291,24 +321,24 @@ impl MPDDisplay {
|
|||
fn get_album_art_transform(&mut self, ctx: &mut Context, fill_scaled: bool) {
|
||||
if fill_scaled {
|
||||
if let Some(image) = &self.album_art {
|
||||
let screen_coords: Rect = graphics::screen_coordinates(ctx);
|
||||
let art_rect: Rect = image.dimensions();
|
||||
let drawable_size = ctx.gfx.drawable_size();
|
||||
let art_rect: Rect = image.dimensions(ctx).expect("Image should have dimensions");
|
||||
|
||||
// try to fit to width first
|
||||
let mut x_scale = screen_coords.w / art_rect.w;
|
||||
let mut x_scale = drawable_size.0 / art_rect.w;
|
||||
let mut y_scale = x_scale;
|
||||
let mut new_width = art_rect.w * x_scale;
|
||||
let mut new_height = art_rect.h * y_scale;
|
||||
if new_height > screen_coords.h.abs() {
|
||||
if new_height > drawable_size.1.abs() {
|
||||
// fit to height instead
|
||||
y_scale = screen_coords.h.abs() / art_rect.h;
|
||||
y_scale = drawable_size.1.abs() / art_rect.h;
|
||||
x_scale = y_scale;
|
||||
new_width = art_rect.w * x_scale;
|
||||
new_height = art_rect.h * y_scale;
|
||||
}
|
||||
|
||||
let offset_x: f32 = (screen_coords.w.abs() - new_width) / 2.0f32;
|
||||
let offset_y: f32 = (screen_coords.h.abs() - new_height) / 2.0f32;
|
||||
let offset_x: f32 = (drawable_size.0.abs() - new_width) / 2.0f32;
|
||||
let offset_y: f32 = (drawable_size.1.abs() - new_height) / 2.0f32;
|
||||
|
||||
self.album_art_draw_transform = Some(Transform::Values {
|
||||
dest: [offset_x, offset_y].into(),
|
||||
|
@ -320,10 +350,10 @@ impl MPDDisplay {
|
|||
self.album_art_draw_transform = None;
|
||||
}
|
||||
} else if let Some(image) = &self.album_art {
|
||||
let screen_coords: Rect = graphics::screen_coordinates(ctx);
|
||||
let art_rect: Rect = image.dimensions();
|
||||
let offset_x: f32 = (screen_coords.w.abs() - art_rect.w.abs()) / 2.0f32;
|
||||
let offset_y: f32 = (screen_coords.h.abs() - art_rect.h.abs()) / 2.0f32;
|
||||
let drawable_size = ctx.gfx.drawable_size();
|
||||
let art_rect: Rect = image.dimensions(ctx).expect("Image should have dimensions");
|
||||
let offset_x: f32 = (drawable_size.0.abs() - art_rect.w.abs()) / 2.0f32;
|
||||
let offset_y: f32 = (drawable_size.1.abs() - art_rect.h.abs()) / 2.0f32;
|
||||
self.album_art_draw_transform = Some(Transform::Values {
|
||||
dest: [offset_x, offset_y].into(),
|
||||
rotation: 0.0f32,
|
||||
|
@ -398,20 +428,23 @@ impl MPDDisplay {
|
|||
}
|
||||
|
||||
let img_result = if is_unknown_format {
|
||||
let mut reader = ImageReader::new(Cursor::new(&image_ref));
|
||||
reader = reader
|
||||
let reader = ImageReader::new(Cursor::new(image_ref));
|
||||
let guessed_reader = reader
|
||||
.with_guessed_format()
|
||||
.map_err(|e| format!("Error: Failed to guess format of album art image: {}", e))?;
|
||||
.map_err(|e| format!("Error: Failed to guess format of album art image: {e}"));
|
||||
if let Ok(reader) = guessed_reader {
|
||||
reader.decode().map_err(|e| {
|
||||
format!(
|
||||
"Error: Failed to decode album art image (guessed format): {}",
|
||||
e
|
||||
)
|
||||
format!("Error: Failed to decode album art image (guessed format): {e}")
|
||||
})
|
||||
} else {
|
||||
ImageReader::with_format(Cursor::new(&image_ref), image_format)
|
||||
// Convert Ok(_) to Ok(DynamicImage) which will never be used
|
||||
// since the if statement covers it.
|
||||
guessed_reader.map(|_| -> DynamicImage { unreachable!() })
|
||||
}
|
||||
} else {
|
||||
ImageReader::with_format(Cursor::new(image_ref), image_format)
|
||||
.decode()
|
||||
.map_err(|e| format!("Error: Failed to decode album art image: {}", e))
|
||||
.map_err(|e| format!("Error: Failed to decode album art image: {e}"))
|
||||
};
|
||||
if img_result.is_err() && !self.tried_album_art_in_dir {
|
||||
return try_second_art_fetch_method(
|
||||
|
@ -423,13 +456,13 @@ impl MPDDisplay {
|
|||
}
|
||||
let img = img_result?;
|
||||
let rgba8 = img.to_rgba8();
|
||||
let ggez_img = Image::from_rgba8(
|
||||
let ggez_img = Image::from_pixels(
|
||||
ctx,
|
||||
rgba8.width() as u16,
|
||||
rgba8.height() as u16,
|
||||
rgba8.as_raw(),
|
||||
)
|
||||
.map_err(|e| format!("Error: Failed to load album art image in ggez Image: {}", e))?;
|
||||
wgpu_types::TextureFormat::Rgba8UnormSrgb,
|
||||
rgba8.width(),
|
||||
rgba8.height(),
|
||||
);
|
||||
|
||||
self.album_art = Some(ggez_img);
|
||||
|
||||
|
@ -437,20 +470,31 @@ impl MPDDisplay {
|
|||
}
|
||||
|
||||
fn refresh_text_transforms(&mut self, ctx: &mut Context) -> GameResult<()> {
|
||||
let screen_coords: Rect = graphics::screen_coordinates(ctx);
|
||||
let drawable_size = ctx.gfx.drawable_size();
|
||||
|
||||
let text_height_limit = TEXT_HEIGHT_SCALE * screen_coords.h.abs();
|
||||
let album_height_limit = ALBUM_HEIGHT_SCALE * screen_coords.h.abs();
|
||||
let artist_height_limit = ARTIST_HEIGHT_SCALE * screen_coords.h.abs();
|
||||
let timer_height = TIMER_HEIGHT_SCALE * screen_coords.h.abs();
|
||||
let text_height_scale: f32;
|
||||
let album_height_scale: f32;
|
||||
let artist_height_scale: f32;
|
||||
let timer_height_scale: f32;
|
||||
|
||||
let mut offset_y: f32 = screen_coords.h;
|
||||
if let Some(forced_scale) = &self.opts.force_text_height_scale {
|
||||
text_height_scale = *forced_scale;
|
||||
album_height_scale = *forced_scale;
|
||||
artist_height_scale = *forced_scale;
|
||||
timer_height_scale = *forced_scale * TIMER_HEIGHT_SCALE_RATIO;
|
||||
} else {
|
||||
text_height_scale = TEXT_HEIGHT_SCALE;
|
||||
album_height_scale = ALBUM_HEIGHT_SCALE;
|
||||
artist_height_scale = ARTIST_HEIGHT_SCALE;
|
||||
timer_height_scale = TIMER_HEIGHT_SCALE;
|
||||
}
|
||||
|
||||
let mut filename_y: f32 = 0.0;
|
||||
let mut album_y: f32 = 0.0;
|
||||
let mut artist_y: f32 = 0.0;
|
||||
let mut title_y: f32 = 0.0;
|
||||
let mut timer_y: f32 = 0.0;
|
||||
let text_height_limit = text_height_scale * drawable_size.1.abs();
|
||||
let album_height_limit = album_height_scale * drawable_size.1.abs();
|
||||
let artist_height_limit = artist_height_scale * drawable_size.1.abs();
|
||||
let timer_height = timer_height_scale * drawable_size.1.abs();
|
||||
|
||||
let mut offset_y: f32 = drawable_size.1;
|
||||
|
||||
let set_transform = |text: &mut Text,
|
||||
transform: &mut Transform,
|
||||
|
@ -463,8 +507,7 @@ impl MPDDisplay {
|
|||
timer_y: &mut f32| {
|
||||
let mut current_x = INIT_FONT_SIZE_X;
|
||||
let mut current_y = INIT_FONT_SIZE_Y;
|
||||
let mut width: f32;
|
||||
let mut height: f32 = 0.0;
|
||||
let mut width_height: Vector2<f32> = Vector2 { x: 0.0, y: 0.0 };
|
||||
let mut iteration_count: u8 = 0;
|
||||
loop {
|
||||
iteration_count += 1;
|
||||
|
@ -478,12 +521,13 @@ impl MPDDisplay {
|
|||
y: current_y,
|
||||
});
|
||||
}
|
||||
width = text.width(ctx);
|
||||
height = text.height(ctx);
|
||||
width_height = text
|
||||
.measure(ctx)
|
||||
.expect("Should be able to get width/height of text.");
|
||||
|
||||
if is_string {
|
||||
if screen_coords.w < width
|
||||
|| height
|
||||
if drawable_size.0 < width_height.x
|
||||
|| width_height.y
|
||||
>= (if is_artist {
|
||||
artist_height_limit
|
||||
} else if is_album {
|
||||
|
@ -495,7 +539,7 @@ impl MPDDisplay {
|
|||
current_x *= DECREASE_AMT;
|
||||
current_y *= DECREASE_AMT;
|
||||
continue;
|
||||
} else if screen_coords.w * MIN_WIDTH_RATIO > width {
|
||||
} else if drawable_size.0 * MIN_WIDTH_RATIO > width_height.x {
|
||||
current_x *= INCREASE_AMT;
|
||||
current_y *= INCREASE_AMT;
|
||||
continue;
|
||||
|
@ -503,7 +547,7 @@ impl MPDDisplay {
|
|||
break;
|
||||
}
|
||||
} else {
|
||||
let diff_scale_y = current_y / height * timer_height;
|
||||
let diff_scale_y = current_y / width_height.y * timer_height;
|
||||
let current_x = current_x * diff_scale_y / current_y;
|
||||
for fragment in text.fragments_mut() {
|
||||
fragment.scale = Some(PxScale {
|
||||
|
@ -514,20 +558,23 @@ impl MPDDisplay {
|
|||
*timer_x = current_x;
|
||||
*timer_y = diff_scale_y;
|
||||
// width = text.width(ctx); // not really used after this
|
||||
height = text.height(ctx);
|
||||
width_height.y = text
|
||||
.measure(ctx)
|
||||
.expect("Should be able to get width/height of text.")
|
||||
.y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
*y = *offset_y - height;
|
||||
*y = *offset_y - width_height.y;
|
||||
*transform = Transform::Values {
|
||||
dest: [TEXT_X_OFFSET, *offset_y - height].into(),
|
||||
dest: [TEXT_X_OFFSET, *offset_y - width_height.y].into(),
|
||||
rotation: 0.0,
|
||||
scale: [1.0, 1.0].into(),
|
||||
offset: [0.0, 0.0].into(),
|
||||
};
|
||||
|
||||
*offset_y -= height + TEXT_OFFSET_Y_SPACING;
|
||||
*offset_y -= width_height.y + TEXT_OFFSET_Y_SPACING;
|
||||
};
|
||||
|
||||
if !self.filename_text.contents().is_empty() && !self.opts.disable_show_filename {
|
||||
|
@ -535,7 +582,7 @@ impl MPDDisplay {
|
|||
&mut self.filename_text,
|
||||
&mut self.filename_transform,
|
||||
&mut offset_y,
|
||||
&mut filename_y,
|
||||
&mut self.cached_filename_y,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
|
@ -555,7 +602,7 @@ impl MPDDisplay {
|
|||
&mut self.album_text,
|
||||
&mut self.album_transform,
|
||||
&mut offset_y,
|
||||
&mut album_y,
|
||||
&mut self.cached_album_y,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
|
@ -569,7 +616,7 @@ impl MPDDisplay {
|
|||
&mut self.artist_text,
|
||||
&mut self.artist_transform,
|
||||
&mut offset_y,
|
||||
&mut artist_y,
|
||||
&mut self.cached_artist_y,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
|
@ -589,7 +636,7 @@ impl MPDDisplay {
|
|||
&mut self.title_text,
|
||||
&mut self.title_transform,
|
||||
&mut offset_y,
|
||||
&mut title_y,
|
||||
&mut self.cached_title_y,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
|
@ -608,7 +655,7 @@ impl MPDDisplay {
|
|||
&mut self.timer_text,
|
||||
&mut self.timer_transform,
|
||||
&mut offset_y,
|
||||
&mut timer_y,
|
||||
&mut self.cached_timer_y,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
|
@ -616,11 +663,32 @@ impl MPDDisplay {
|
|||
&mut self.timer_y,
|
||||
);
|
||||
|
||||
let filename_dimensions = self.filename_text.dimensions(ctx);
|
||||
let album_dimensions = self.album_text.dimensions(ctx);
|
||||
let artist_dimensions = self.artist_text.dimensions(ctx);
|
||||
let title_dimensions = self.title_text.dimensions(ctx);
|
||||
let timer_dimensions = self.timer_text.dimensions(ctx);
|
||||
self.update_bg_mesh(ctx)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_bg_mesh(&mut self, ctx: &mut Context) -> GameResult<()> {
|
||||
let filename_dimensions = self
|
||||
.filename_text
|
||||
.dimensions(ctx)
|
||||
.expect("Should be able to get dimensions of Text.");
|
||||
let album_dimensions = self
|
||||
.album_text
|
||||
.dimensions(ctx)
|
||||
.expect("Should be able to get dimensions of Text.");
|
||||
let artist_dimensions = self
|
||||
.artist_text
|
||||
.dimensions(ctx)
|
||||
.expect("Should be able to get dimensions of Text.");
|
||||
let title_dimensions = self
|
||||
.title_text
|
||||
.dimensions(ctx)
|
||||
.expect("Should be able to get dimensions of Text.");
|
||||
let timer_dimensions = self
|
||||
.timer_text
|
||||
.dimensions(ctx)
|
||||
.expect("Should be able to get dimensions of Text.");
|
||||
|
||||
let mut mesh_builder: MeshBuilder = MeshBuilder::new();
|
||||
if !self.opts.disable_show_filename {
|
||||
|
@ -628,7 +696,7 @@ impl MPDDisplay {
|
|||
DrawMode::fill(),
|
||||
Rect {
|
||||
x: TEXT_X_OFFSET,
|
||||
y: filename_y,
|
||||
y: self.cached_filename_y,
|
||||
w: filename_dimensions.w,
|
||||
h: filename_dimensions.h,
|
||||
},
|
||||
|
@ -640,7 +708,7 @@ impl MPDDisplay {
|
|||
DrawMode::fill(),
|
||||
Rect {
|
||||
x: TEXT_X_OFFSET,
|
||||
y: album_y,
|
||||
y: self.cached_album_y,
|
||||
w: album_dimensions.w,
|
||||
h: album_dimensions.h,
|
||||
},
|
||||
|
@ -652,7 +720,7 @@ impl MPDDisplay {
|
|||
DrawMode::fill(),
|
||||
Rect {
|
||||
x: TEXT_X_OFFSET,
|
||||
y: artist_y,
|
||||
y: self.cached_artist_y,
|
||||
w: artist_dimensions.w,
|
||||
h: artist_dimensions.h,
|
||||
},
|
||||
|
@ -664,25 +732,26 @@ impl MPDDisplay {
|
|||
DrawMode::fill(),
|
||||
Rect {
|
||||
x: TEXT_X_OFFSET,
|
||||
y: title_y,
|
||||
y: self.cached_title_y,
|
||||
w: title_dimensions.w,
|
||||
h: title_dimensions.h,
|
||||
},
|
||||
Color::from_rgba(0, 0, 0, self.opts.text_bg_opacity),
|
||||
)?;
|
||||
}
|
||||
let mesh: Mesh = mesh_builder
|
||||
.rectangle(
|
||||
if self.mpd_play_state == MPDPlayState::Playing {
|
||||
mesh_builder.rectangle(
|
||||
DrawMode::fill(),
|
||||
Rect {
|
||||
x: TEXT_X_OFFSET,
|
||||
y: timer_y,
|
||||
y: self.cached_timer_y,
|
||||
w: timer_dimensions.w,
|
||||
h: timer_dimensions.h,
|
||||
},
|
||||
Color::from_rgba(0, 0, 0, self.opts.text_bg_opacity),
|
||||
)?
|
||||
.build(ctx)?;
|
||||
)?;
|
||||
}
|
||||
let mesh: Mesh = Mesh::from_data(ctx, mesh_builder.build());
|
||||
|
||||
self.text_bg_mesh = Some(mesh);
|
||||
|
||||
|
@ -695,8 +764,7 @@ impl EventHandler for MPDDisplay {
|
|||
if !self.is_valid {
|
||||
if let Err(mpd_handler_error) = &self.mpd_handler {
|
||||
return Err(GameError::EventLoopError(format!(
|
||||
"Failed to initialize MPDHandler: {}",
|
||||
mpd_handler_error
|
||||
"Failed to initialize MPDHandler: {mpd_handler_error}"
|
||||
)));
|
||||
} else {
|
||||
return Err(GameError::EventLoopError(
|
||||
|
@ -752,6 +820,8 @@ impl EventHandler for MPDDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
self.prev_mpd_play_state = self.mpd_play_state;
|
||||
|
||||
if self.is_valid && self.is_initialized && self.poll_instant.elapsed() > POLL_TIME {
|
||||
self.poll_instant = Instant::now();
|
||||
if self.dirty_flag.is_some()
|
||||
|
@ -759,7 +829,7 @@ impl EventHandler for MPDDisplay {
|
|||
.dirty_flag
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.swap(false, Ordering::Relaxed)
|
||||
.swap(false, Ordering::AcqRel)
|
||||
{
|
||||
log(
|
||||
"dirty_flag cleared, acquiring shared data...",
|
||||
|
@ -807,7 +877,7 @@ impl EventHandler for MPDDisplay {
|
|||
self.dirty_flag
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.store(true, Ordering::Relaxed);
|
||||
.store(true, Ordering::Release);
|
||||
}
|
||||
if !shared.artist.is_empty() {
|
||||
if shared.artist != self.artist_string_cache {
|
||||
|
@ -822,7 +892,7 @@ impl EventHandler for MPDDisplay {
|
|||
self.dirty_flag
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.store(true, Ordering::Relaxed);
|
||||
.store(true, Ordering::Release);
|
||||
}
|
||||
if !shared.album.is_empty() {
|
||||
if shared.album != self.album_string_cache {
|
||||
|
@ -837,7 +907,7 @@ impl EventHandler for MPDDisplay {
|
|||
self.dirty_flag
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.store(true, Ordering::Relaxed);
|
||||
.store(true, Ordering::Release);
|
||||
}
|
||||
if !shared.filename.is_empty() {
|
||||
if shared.filename != self.filename_string_cache {
|
||||
|
@ -856,7 +926,7 @@ impl EventHandler for MPDDisplay {
|
|||
self.dirty_flag
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.store(true, Ordering::Relaxed);
|
||||
.store(true, Ordering::Release);
|
||||
}
|
||||
self.timer = shared.pos;
|
||||
self.length = shared.length;
|
||||
|
@ -882,102 +952,111 @@ impl EventHandler for MPDDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
let delta = timer::delta(ctx);
|
||||
let delta = ctx.time.delta();
|
||||
self.timer += delta.as_secs_f64();
|
||||
let timer_diff = seconds_to_time(self.length - self.timer);
|
||||
let mut timer_diff = seconds_to_time(self.length - self.timer);
|
||||
if !self.opts.disable_show_percentage {
|
||||
timer_diff = timer_diff + " " + &time_to_percentage(self.length, self.timer);
|
||||
}
|
||||
let timer_diff_len = timer_diff.len();
|
||||
self.timer_text = Text::new(timer_diff);
|
||||
self.timer_text.set_font(
|
||||
Font::default(),
|
||||
PxScale {
|
||||
self.timer_text.set_scale(PxScale {
|
||||
x: self.timer_x,
|
||||
y: self.timer_y,
|
||||
},
|
||||
);
|
||||
});
|
||||
if timer_diff_len != self.timer_text_len {
|
||||
self.timer_text_len = timer_diff_len;
|
||||
self.update_bg_mesh(ctx)?;
|
||||
} else if self.mpd_play_state != MPDPlayState::Playing
|
||||
&& self.prev_mpd_play_state == MPDPlayState::Playing
|
||||
{
|
||||
self.update_bg_mesh(ctx)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self, ctx: &mut ggez::Context) -> Result<(), GameError> {
|
||||
graphics::clear(ctx, Color::BLACK);
|
||||
let mut canvas = graphics::Canvas::from_frame(ctx, Color::BLACK);
|
||||
|
||||
if self.mpd_play_state != MPDPlayState::Stopped
|
||||
&& self.album_art.is_some()
|
||||
&& self.album_art_draw_transform.is_some()
|
||||
{
|
||||
self.album_art.as_ref().unwrap().draw(
|
||||
ctx,
|
||||
canvas.draw(
|
||||
self.album_art.as_ref().unwrap(),
|
||||
DrawParam {
|
||||
trans: self.album_art_draw_transform.unwrap(),
|
||||
transform: self.album_art_draw_transform.unwrap(),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
);
|
||||
}
|
||||
|
||||
if !self.hide_text {
|
||||
self.notice_text.draw(ctx, DrawParam::default())?;
|
||||
canvas.draw(&self.notice_text, DrawParam::default());
|
||||
|
||||
if self.mpd_play_state != MPDPlayState::Stopped && self.is_valid && self.is_initialized
|
||||
{
|
||||
if let Some(mesh) = &self.text_bg_mesh {
|
||||
mesh.draw(ctx, DrawParam::default())?;
|
||||
canvas.draw(mesh, DrawParam::default());
|
||||
}
|
||||
|
||||
if !self.opts.disable_show_filename {
|
||||
self.filename_text.draw(
|
||||
ctx,
|
||||
canvas.draw(
|
||||
&self.filename_text,
|
||||
DrawParam {
|
||||
trans: self.filename_transform,
|
||||
transform: self.filename_transform,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
);
|
||||
}
|
||||
|
||||
if !self.opts.disable_show_album {
|
||||
self.album_text.draw(
|
||||
ctx,
|
||||
canvas.draw(
|
||||
&self.album_text,
|
||||
DrawParam {
|
||||
trans: self.album_transform,
|
||||
transform: self.album_transform,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
);
|
||||
}
|
||||
|
||||
if !self.opts.disable_show_artist {
|
||||
self.artist_text.draw(
|
||||
ctx,
|
||||
canvas.draw(
|
||||
&self.artist_text,
|
||||
DrawParam {
|
||||
trans: self.artist_transform,
|
||||
transform: self.artist_transform,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
);
|
||||
}
|
||||
|
||||
if !self.opts.disable_show_title {
|
||||
self.title_text.draw(
|
||||
ctx,
|
||||
canvas.draw(
|
||||
&self.title_text,
|
||||
DrawParam {
|
||||
trans: self.title_transform,
|
||||
transform: self.title_transform,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
);
|
||||
}
|
||||
|
||||
if self.mpd_play_state == MPDPlayState::Playing {
|
||||
self.timer_text.draw(
|
||||
ctx,
|
||||
canvas.draw(
|
||||
&self.timer_text,
|
||||
DrawParam {
|
||||
trans: self.timer_transform,
|
||||
transform: self.timer_transform,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graphics::present(ctx)
|
||||
canvas.finish(ctx)
|
||||
}
|
||||
|
||||
fn text_input_event(&mut self, _ctx: &mut Context, character: char) {
|
||||
fn text_input_event(&mut self, _ctx: &mut Context, character: char) -> Result<(), GameError> {
|
||||
if !self.is_initialized && self.opts.enable_prompt_password && !character.is_control() {
|
||||
if self.opts.password.is_none() {
|
||||
let s = String::from(character);
|
||||
|
@ -988,17 +1067,18 @@ impl EventHandler for MPDDisplay {
|
|||
self.notice_text.add('*');
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn key_down_event(
|
||||
&mut self,
|
||||
_ctx: &mut Context,
|
||||
keycode: event::KeyCode,
|
||||
_keymods: event::KeyMods,
|
||||
input: KeyInput,
|
||||
_repeat: bool,
|
||||
) {
|
||||
) -> Result<(), GameError> {
|
||||
if !self.is_initialized && self.opts.enable_prompt_password {
|
||||
if keycode == event::KeyCode::Back {
|
||||
if input.keycode == Some(keyboard::KeyCode::Back) {
|
||||
let s: String = self.notice_text.contents();
|
||||
|
||||
if s.ends_with('*') {
|
||||
|
@ -1008,28 +1088,34 @@ impl EventHandler for MPDDisplay {
|
|||
if let Some(input_p) = &mut self.opts.password {
|
||||
input_p.pop();
|
||||
}
|
||||
} else if keycode == event::KeyCode::Return {
|
||||
} else if input.keycode == Some(keyboard::KeyCode::Return) {
|
||||
self.password_entered = true;
|
||||
}
|
||||
} else if keycode == event::KeyCode::H {
|
||||
} else if input.keycode == Some(keyboard::KeyCode::H) {
|
||||
self.hide_text = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn key_up_event(
|
||||
&mut self,
|
||||
_ctx: &mut Context,
|
||||
keycode: event::KeyCode,
|
||||
_keymods: event::KeyMods,
|
||||
) {
|
||||
if keycode == event::KeyCode::H {
|
||||
fn key_up_event(&mut self, _ctx: &mut Context, input: KeyInput) -> Result<(), GameError> {
|
||||
if input.keycode == Some(keyboard::KeyCode::H) {
|
||||
self.hide_text = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resize_event(&mut self, ctx: &mut Context, _width: f32, _height: f32) {
|
||||
fn resize_event(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
_width: f32,
|
||||
_height: f32,
|
||||
) -> Result<(), GameError> {
|
||||
self.get_album_art_transform(ctx, !self.opts.do_not_fill_scale_album_art);
|
||||
self.refresh_text_transforms(ctx)
|
||||
.expect("Failed to set text transforms");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
119
src/main.rs
119
src/main.rs
|
@ -4,52 +4,56 @@ mod mpd_handler;
|
|||
#[cfg(feature = "unicode_support")]
|
||||
mod unicode_support;
|
||||
|
||||
use clap::Parser;
|
||||
use ggez::conf::{WindowMode, WindowSetup};
|
||||
use ggez::event::winit_event::{ElementState, KeyboardInput, ModifiersState};
|
||||
use ggez::event::{self, ControlFlow, EventHandler};
|
||||
use ggez::filesystem::mount;
|
||||
use ggez::graphics::{self, Rect};
|
||||
use ggez::input::keyboard::{self, KeyInput};
|
||||
use ggez::{ContextBuilder, GameError};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use structopt::StructOpt;
|
||||
|
||||
use debug_log::log;
|
||||
|
||||
#[derive(StructOpt, Debug, Clone)]
|
||||
#[structopt(name = "mpd_info_screen")]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Opt {
|
||||
host: Ipv4Addr,
|
||||
#[structopt(default_value = "6600")]
|
||||
#[arg(default_value = "6600")]
|
||||
port: u16,
|
||||
#[structopt(short = "p")]
|
||||
#[arg(short = 'p')]
|
||||
password: Option<String>,
|
||||
#[structopt(long = "disable-show-title", help = "disable title display")]
|
||||
#[arg(long = "disable-show-title", help = "disable title display")]
|
||||
disable_show_title: bool,
|
||||
#[structopt(long = "disable-show-artist", help = "disable artist display")]
|
||||
#[arg(long = "disable-show-artist", help = "disable artist display")]
|
||||
disable_show_artist: bool,
|
||||
#[structopt(long = "disable-show-album", help = "disable album display")]
|
||||
#[arg(long = "disable-show-album", help = "disable album display")]
|
||||
disable_show_album: bool,
|
||||
#[structopt(long = "disable-show-filename", help = "disable filename display")]
|
||||
#[arg(long = "disable-show-filename", help = "disable filename display")]
|
||||
disable_show_filename: bool,
|
||||
#[structopt(long = "pprompt", help = "input password via prompt")]
|
||||
#[arg(long = "disable-show-percentage", help = "disable percentage display")]
|
||||
disable_show_percentage: bool,
|
||||
#[arg(
|
||||
long = "force-text-height-scale",
|
||||
help = "force-set text height relative to window height as a ratio (default 0.12)"
|
||||
)]
|
||||
force_text_height_scale: Option<f32>,
|
||||
#[arg(long = "pprompt", help = "input password via prompt")]
|
||||
enable_prompt_password: bool,
|
||||
#[structopt(
|
||||
#[arg(long = "pfile", help = "read password from file")]
|
||||
password_file: Option<PathBuf>,
|
||||
#[arg(
|
||||
long = "no-scale-fill",
|
||||
help = "don't scale-fill the album art to the window"
|
||||
)]
|
||||
do_not_fill_scale_album_art: bool,
|
||||
#[structopt(
|
||||
short = "l",
|
||||
long = "log-level",
|
||||
possible_values = &debug_log::LogLevel::variants(),
|
||||
default_value = "Error",
|
||||
case_insensitive = true,
|
||||
)]
|
||||
#[arg(short = 'l', long = "log-level", default_value = "error")]
|
||||
log_level: debug_log::LogLevel,
|
||||
#[structopt(
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "sets the opacity of the text background (0-255)",
|
||||
|
@ -59,9 +63,35 @@ pub struct Opt {
|
|||
}
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
let opt = Opt::from_args();
|
||||
let mut opt = Opt::parse();
|
||||
if let Some(forced_scale) = &mut opt.force_text_height_scale {
|
||||
if *forced_scale < 0.01 {
|
||||
*forced_scale = 0.01;
|
||||
println!("WARNING: Clamped \"force-text-height-scale\" to minimum of 0.01!");
|
||||
} else if *forced_scale > 0.5 {
|
||||
*forced_scale = 0.5;
|
||||
println!("WARNING: Clamped \"force-text-height-scale\" to maximum of 0.5!");
|
||||
}
|
||||
}
|
||||
println!("Got host addr == {}, port == {}", opt.host, opt.port);
|
||||
|
||||
// Read password from file if exists, error otherwise.
|
||||
if let Some(psswd_file_path) = opt.password_file.as_ref() {
|
||||
let mut file = File::open(psswd_file_path).expect("pfile/password_file should exist");
|
||||
let mut content: String = String::new();
|
||||
|
||||
file.read_to_string(&mut content)
|
||||
.expect("Should be able to read from pfile/password_file");
|
||||
|
||||
if content.ends_with("\r\n") {
|
||||
content.truncate(content.len() - 2);
|
||||
} else if content.ends_with('\n') {
|
||||
content.truncate(content.len() - 1);
|
||||
}
|
||||
|
||||
opt.password = Some(content);
|
||||
}
|
||||
|
||||
let (mut ctx, event_loop) = ContextBuilder::new("mpd_info_screen", "Stephen Seo")
|
||||
.window_setup(WindowSetup {
|
||||
title: "mpd info screen".into(),
|
||||
|
@ -69,20 +99,21 @@ fn main() -> Result<(), String> {
|
|||
})
|
||||
.window_mode(WindowMode {
|
||||
resizable: true,
|
||||
resize_on_scale_factor_change: true,
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.expect("Failed to create ggez context");
|
||||
|
||||
// mount "/" read-only so that fonts can be loaded via absolute paths
|
||||
mount(&mut ctx, &PathBuf::from("/"), true);
|
||||
ctx.fs.mount(&PathBuf::from("/"), true);
|
||||
|
||||
let mut display = display::MPDDisplay::new(&mut ctx, opt.clone());
|
||||
|
||||
let mut modifiers_state: ModifiersState = ModifiersState::default();
|
||||
|
||||
event_loop.run(move |mut event, _window_target, control_flow| {
|
||||
if !ctx.continuing {
|
||||
if !ctx.continuing || ctx.quit_requested {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
|
@ -94,7 +125,7 @@ fn main() -> Result<(), String> {
|
|||
event::process_event(ctx, &mut event);
|
||||
match event {
|
||||
event::winit_event::Event::WindowEvent { event, .. } => match event {
|
||||
event::winit_event::WindowEvent::CloseRequested => event::quit(ctx),
|
||||
event::winit_event::WindowEvent::CloseRequested => ctx.request_quit(),
|
||||
event::winit_event::WindowEvent::ModifiersChanged(state) => {
|
||||
modifiers_state = state;
|
||||
}
|
||||
|
@ -108,40 +139,37 @@ fn main() -> Result<(), String> {
|
|||
},
|
||||
is_synthetic: _,
|
||||
} => {
|
||||
if keycode == event::KeyCode::Escape {
|
||||
if keycode == keyboard::KeyCode::Escape {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
let ki = KeyInput {
|
||||
scancode: 0,
|
||||
keycode: Some(keycode),
|
||||
mods: From::from(modifiers_state),
|
||||
};
|
||||
if state == ElementState::Pressed {
|
||||
display.key_down_event(ctx, keycode, modifiers_state.into(), false);
|
||||
display.key_down_event(ctx, ki, false).ok();
|
||||
} else {
|
||||
display.key_up_event(ctx, keycode, modifiers_state.into());
|
||||
display.key_up_event(ctx, ki).ok();
|
||||
}
|
||||
}
|
||||
event::winit_event::WindowEvent::Resized(phys_size) => {
|
||||
graphics::set_screen_coordinates(
|
||||
ctx,
|
||||
Rect {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
w: phys_size.width as f32,
|
||||
h: phys_size.height as f32,
|
||||
},
|
||||
)
|
||||
.expect("Failed to handle resizing window");
|
||||
display.resize_event(ctx, phys_size.width as f32, phys_size.height as f32);
|
||||
display
|
||||
.resize_event(ctx, phys_size.width as f32, phys_size.height as f32)
|
||||
.ok();
|
||||
}
|
||||
event::winit_event::WindowEvent::ReceivedCharacter(ch) => {
|
||||
display.text_input_event(ctx, ch);
|
||||
display.text_input_event(ctx, ch).ok();
|
||||
}
|
||||
x => log(
|
||||
format!("Other window event fired: {:?}", x),
|
||||
format!("Other window event fired: {x:?}"),
|
||||
debug_log::LogState::Verbose,
|
||||
opt.log_level,
|
||||
),
|
||||
},
|
||||
event::winit_event::Event::MainEventsCleared => {
|
||||
ctx.timer_context.tick();
|
||||
ctx.time.tick();
|
||||
|
||||
let mut game_result: Result<(), GameError> = display.update(ctx);
|
||||
if game_result.is_err() {
|
||||
|
@ -149,21 +177,24 @@ fn main() -> Result<(), String> {
|
|||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.gfx.begin_frame().unwrap();
|
||||
game_result = display.draw(ctx);
|
||||
if game_result.is_err() {
|
||||
println!("Error draw: {}", game_result.unwrap_err());
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
ctx.gfx.end_frame().unwrap();
|
||||
|
||||
ctx.mouse_context.reset_delta();
|
||||
ctx.mouse.reset_delta();
|
||||
|
||||
// sleep to force ~5 fps
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
ggez::timer::yield_now();
|
||||
}
|
||||
x => log(
|
||||
format!("Device event fired: {:?}", x),
|
||||
format!("Device event fired: {x:?}"),
|
||||
debug_log::LogState::Verbose,
|
||||
opt.log_level,
|
||||
),
|
||||
|
|
|
@ -250,6 +250,8 @@ impl MPDHandler {
|
|||
)
|
||||
.map_err(|_| String::from("Failed to get TCP connection (is MPD running?)"))?;
|
||||
|
||||
let password_is_empty = password.is_empty();
|
||||
|
||||
let s = MPDHandler {
|
||||
state: Arc::new(RwLock::new(MPDHandlerState {
|
||||
art_data: Vec::new(),
|
||||
|
@ -266,7 +268,7 @@ impl MPDHandler {
|
|||
password,
|
||||
error_text: String::new(),
|
||||
can_authenticate: true,
|
||||
is_authenticated: false,
|
||||
is_authenticated: password_is_empty,
|
||||
can_get_album_art: true,
|
||||
can_get_album_art_in_dir: true,
|
||||
can_get_status: true,
|
||||
|
@ -274,9 +276,9 @@ impl MPDHandler {
|
|||
did_check_overtime: false,
|
||||
force_get_status: false,
|
||||
force_get_current_song: false,
|
||||
song_title_get_time: Instant::now() - Duration::from_secs(10),
|
||||
song_pos_get_time: Instant::now() - Duration::from_secs(10),
|
||||
song_length_get_time: Instant::now() - Duration::from_secs(10),
|
||||
song_title_get_time: Instant::now().checked_sub(Duration::from_secs(10)).unwrap(),
|
||||
song_pos_get_time: Instant::now().checked_sub(Duration::from_secs(10)).unwrap(),
|
||||
song_length_get_time: Instant::now().checked_sub(Duration::from_secs(10)).unwrap(),
|
||||
self_thread: None,
|
||||
dirty_flag: Arc::new(AtomicBool::new(true)),
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
|
@ -330,7 +332,7 @@ impl MPDHandler {
|
|||
#[allow(dead_code)]
|
||||
pub fn is_dirty(&self) -> Result<bool, ()> {
|
||||
if let Ok(write_lock) = self.state.try_write() {
|
||||
return Ok(write_lock.dirty_flag.swap(false, Ordering::Relaxed));
|
||||
return Ok(write_lock.dirty_flag.swap(false, Ordering::AcqRel));
|
||||
}
|
||||
|
||||
Err(())
|
||||
|
@ -370,7 +372,7 @@ impl MPDHandler {
|
|||
|
||||
pub fn stop_thread(&self) -> Result<(), ()> {
|
||||
let read_handle = self.state.try_read().map_err(|_| ())?;
|
||||
read_handle.stop_flag.store(true, Ordering::Relaxed);
|
||||
read_handle.stop_flag.store(true, Ordering::Release);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -426,20 +428,20 @@ impl MPDHandler {
|
|||
|
||||
if let Err(err_string) = self.handler_read_block(&mut buf, &mut saved, &mut saved_str) {
|
||||
log(
|
||||
format!("read_block error: {}", err_string),
|
||||
format!("read_block error: {err_string}"),
|
||||
LogState::Warning,
|
||||
log_level,
|
||||
);
|
||||
} else if let Err(err_string) = self.handler_write_block() {
|
||||
log(
|
||||
format!("write_block error: {}", err_string),
|
||||
format!("write_block error: {err_string}"),
|
||||
LogState::Warning,
|
||||
log_level,
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(read_handle) = self.state.try_read() {
|
||||
if read_handle.stop_flag.load(Ordering::Relaxed) || !read_handle.can_authenticate {
|
||||
if read_handle.stop_flag.load(Ordering::Acquire) || !read_handle.can_authenticate {
|
||||
break 'main;
|
||||
}
|
||||
}
|
||||
|
@ -477,7 +479,7 @@ impl MPDHandler {
|
|||
let read_result = write_handle.stream.read(buf);
|
||||
if let Err(io_err) = read_result {
|
||||
if io_err.kind() != io::ErrorKind::WouldBlock {
|
||||
return Err(format!("TCP stream error: {}", io_err));
|
||||
return Err(format!("TCP stream error: {io_err}"));
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -509,7 +511,7 @@ impl MPDHandler {
|
|||
write_handle.log_level,
|
||||
);
|
||||
if write_handle.art_data.len() == write_handle.art_data_size {
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
}
|
||||
} else {
|
||||
write_handle.art_data.extend_from_slice(&buf_vec);
|
||||
|
@ -524,7 +526,7 @@ impl MPDHandler {
|
|||
write_handle.log_level,
|
||||
);
|
||||
if write_handle.art_data.len() == write_handle.art_data_size {
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
}
|
||||
break 'handle_buf;
|
||||
}
|
||||
|
@ -559,7 +561,7 @@ impl MPDHandler {
|
|||
PollState::ReadPicture => {
|
||||
if write_handle.art_data.is_empty() {
|
||||
write_handle.can_get_album_art = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
log(
|
||||
"No embedded album art",
|
||||
LogState::Warning,
|
||||
|
@ -570,7 +572,7 @@ impl MPDHandler {
|
|||
PollState::ReadPictureInDir => {
|
||||
if write_handle.art_data.is_empty() {
|
||||
write_handle.can_get_album_art_in_dir = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
log(
|
||||
"No album art in dir",
|
||||
LogState::Warning,
|
||||
|
@ -587,13 +589,13 @@ impl MPDHandler {
|
|||
match write_handle.poll_state {
|
||||
PollState::Password => {
|
||||
write_handle.can_authenticate = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
write_handle.error_text = "Failed to authenticate to MPD".into();
|
||||
write_handle.stop_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.stop_flag.store(true, Ordering::Release);
|
||||
}
|
||||
PollState::CurrentSong | PollState::Status => {
|
||||
write_handle.can_get_status = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
write_handle.error_text = "Failed to get MPD status".into();
|
||||
if line.contains("don't have permission") {
|
||||
write_handle.can_authenticate = false;
|
||||
|
@ -602,7 +604,7 @@ impl MPDHandler {
|
|||
}
|
||||
PollState::ReadPicture => {
|
||||
write_handle.can_get_album_art = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
log(
|
||||
"Failed to get readpicture",
|
||||
LogState::Warning,
|
||||
|
@ -613,7 +615,7 @@ impl MPDHandler {
|
|||
}
|
||||
PollState::ReadPictureInDir => {
|
||||
write_handle.can_get_album_art_in_dir = false;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
log(
|
||||
"Failed to get albumart",
|
||||
LogState::Warning,
|
||||
|
@ -649,9 +651,9 @@ impl MPDHandler {
|
|||
MPDPlayState::Paused
|
||||
};
|
||||
write_handle.error_text.clear();
|
||||
write!(&mut write_handle.error_text, "MPD has {:?}", got_mpd_state).ok();
|
||||
write!(&mut write_handle.error_text, "MPD has {got_mpd_state:?}").ok();
|
||||
log(
|
||||
format!("MPD is {:?}", got_mpd_state),
|
||||
format!("MPD is {got_mpd_state:?}"),
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
|
@ -675,13 +677,13 @@ impl MPDHandler {
|
|||
write_handle.force_get_status = true;
|
||||
write_handle.error_text.clear();
|
||||
}
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
write_handle.song_title_get_time = Instant::now();
|
||||
} else if line.starts_with("elapsed: ") {
|
||||
let parse_pos_result = f64::from_str(&line.split_off(9));
|
||||
if let Ok(value) = parse_pos_result {
|
||||
write_handle.current_song_position = value;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
write_handle.song_pos_get_time = Instant::now();
|
||||
} else {
|
||||
log(
|
||||
|
@ -694,7 +696,7 @@ impl MPDHandler {
|
|||
let parse_pos_result = f64::from_str(&line.split_off(10));
|
||||
if let Ok(value) = parse_pos_result {
|
||||
write_handle.current_song_length = value;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
write_handle.song_length_get_time = Instant::now();
|
||||
} else {
|
||||
log(
|
||||
|
@ -707,7 +709,7 @@ impl MPDHandler {
|
|||
let parse_artsize_result = usize::from_str(&line.split_off(6));
|
||||
if let Ok(value) = parse_artsize_result {
|
||||
write_handle.art_data_size = value;
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
} else {
|
||||
log(
|
||||
"Failed to parse album art byte size",
|
||||
|
@ -736,7 +738,7 @@ impl MPDHandler {
|
|||
write_handle.art_data_type = line.split_off(6);
|
||||
} else {
|
||||
log(
|
||||
format!("Got unrecognized/ignored line: {}", line),
|
||||
format!("Got unrecognized/ignored line: {line}"),
|
||||
LogState::Warning,
|
||||
write_handle.log_level,
|
||||
);
|
||||
|
@ -760,7 +762,7 @@ impl MPDHandler {
|
|||
} // 'handle_buf: loop
|
||||
|
||||
if got_mpd_state != write_handle.mpd_play_state {
|
||||
write_handle.dirty_flag.store(true, Ordering::Relaxed);
|
||||
write_handle.dirty_flag.store(true, Ordering::Release);
|
||||
if got_mpd_state == MPDPlayState::Playing {
|
||||
write_handle.error_text.clear();
|
||||
}
|
||||
|
@ -799,12 +801,12 @@ impl MPDHandler {
|
|||
let p = write_handle.password.clone();
|
||||
let write_result = write_handle
|
||||
.stream
|
||||
.write(format!("password {}\n", p).as_bytes());
|
||||
.write(format!("password {p}\n").as_bytes());
|
||||
if write_result.is_ok() {
|
||||
write_handle.poll_state = PollState::Password;
|
||||
} else if let Err(e) = write_result {
|
||||
log(
|
||||
format!("Failed to send password for authentication: {}", e),
|
||||
format!("Failed to send password for authentication: {e}"),
|
||||
LogState::Error,
|
||||
write_handle.log_level,
|
||||
);
|
||||
|
@ -820,7 +822,7 @@ impl MPDHandler {
|
|||
write_handle.poll_state = PollState::CurrentSong;
|
||||
} else if let Err(e) = write_result {
|
||||
log(
|
||||
format!("Failed to request song info over stream: {}", e),
|
||||
format!("Failed to request song info over stream: {e}"),
|
||||
LogState::Error,
|
||||
write_handle.log_level,
|
||||
);
|
||||
|
@ -836,7 +838,7 @@ impl MPDHandler {
|
|||
write_handle.poll_state = PollState::Status;
|
||||
} else if let Err(e) = write_result {
|
||||
log(
|
||||
format!("Failed to request status over stream: {}", e),
|
||||
format!("Failed to request status over stream: {e}"),
|
||||
LogState::Error,
|
||||
write_handle.log_level,
|
||||
);
|
||||
|
@ -848,14 +850,14 @@ impl MPDHandler {
|
|||
let title = write_handle.current_song_filename.clone();
|
||||
let art_data_length = write_handle.art_data.len();
|
||||
if write_handle.can_get_album_art {
|
||||
let write_result = write_handle.stream.write(
|
||||
format!("readpicture \"{}\" {}\n", title, art_data_length).as_bytes(),
|
||||
);
|
||||
let write_result = write_handle
|
||||
.stream
|
||||
.write(format!("readpicture \"{title}\" {art_data_length}\n").as_bytes());
|
||||
if write_result.is_ok() {
|
||||
write_handle.poll_state = PollState::ReadPicture;
|
||||
} else if let Err(e) = write_result {
|
||||
log(
|
||||
format!("Failed to request album art: {}", e),
|
||||
format!("Failed to request album art: {e}"),
|
||||
LogState::Error,
|
||||
write_handle.log_level,
|
||||
);
|
||||
|
@ -863,12 +865,12 @@ impl MPDHandler {
|
|||
} else if write_handle.can_get_album_art_in_dir {
|
||||
let write_result = write_handle
|
||||
.stream
|
||||
.write(format!("albumart \"{}\" {}\n", title, art_data_length).as_bytes());
|
||||
.write(format!("albumart \"{title}\" {art_data_length}\n").as_bytes());
|
||||
if write_result.is_ok() {
|
||||
write_handle.poll_state = PollState::ReadPictureInDir;
|
||||
} else if let Err(e) = write_result {
|
||||
log(
|
||||
format!("Failed to request album art in dir: {}", e),
|
||||
format!("Failed to request album art in dir: {e}"),
|
||||
LogState::Error,
|
||||
write_handle.log_level,
|
||||
);
|
||||
|
|
|
@ -67,8 +67,7 @@ mod ffi {
|
|||
return Err(String::from("Failed to FcFontMatch (FcResult is not FcResultMatch; result_pattern is not null)"));
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Failed to FcFontMatch (FcResult is not FcResultMatch; {:?})",
|
||||
result
|
||||
"Failed to FcFontMatch (FcResult is not FcResultMatch; {result:?})"
|
||||
));
|
||||
}
|
||||
} else if result_pattern.is_null() {
|
||||
|
|
Loading…
Reference in a new issue