Add shortcut to reload database and/or library #116
@ -67,7 +67,7 @@ fn with<LIB: ILibrary, DB: IDatabase>(builder: MusicHoardBuilder<LIB, DB>) {
|
||||
let listener = EventListener::new(channel.sender());
|
||||
let handler = EventHandler::new(channel.receiver());
|
||||
|
||||
let app = App::new(music_hoard).expect("failed to initialise application");
|
||||
let app = App::new(music_hoard);
|
||||
let ui = Ui;
|
||||
|
||||
// Run the TUI application.
|
||||
|
164
src/tui/app.rs
164
src/tui/app.rs
@ -1,28 +1,7 @@
|
||||
use std::fmt;
|
||||
|
||||
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
|
||||
use ratatui::widgets::ListState;
|
||||
|
||||
use crate::tui::{lib::IMusicHoard, Error};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppError {
|
||||
Lib(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Self::Lib(ref s) => write!(f, "the musichoard library returned an error: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<musichoard::Error> for AppError {
|
||||
fn from(err: musichoard::Error) -> AppError {
|
||||
AppError::Lib(err.to_string())
|
||||
}
|
||||
}
|
||||
use crate::tui::lib::IMusicHoard;
|
||||
|
||||
pub enum AppState<BS, IS, RS, ES> {
|
||||
Browse(BS),
|
||||
@ -57,8 +36,9 @@ pub trait IAppInteract {
|
||||
|
||||
fn is_running(&self) -> bool;
|
||||
fn quit(&mut self);
|
||||
fn force_quit(&mut self);
|
||||
|
||||
fn save(&mut self) -> Result<(), AppError>;
|
||||
fn save(&mut self);
|
||||
|
||||
fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES>;
|
||||
}
|
||||
@ -337,17 +317,24 @@ pub struct App<MH: IMusicHoard> {
|
||||
}
|
||||
|
||||
impl<MH: IMusicHoard> App<MH> {
|
||||
pub fn new(mut music_hoard: MH) -> Result<Self, Error> {
|
||||
// FIXME: if either returns an error start in an error state
|
||||
music_hoard.load_from_database()?;
|
||||
music_hoard.rescan_library()?;
|
||||
pub fn new(mut music_hoard: MH) -> Self {
|
||||
let state = match Self::init(&mut music_hoard) {
|
||||
Ok(()) => AppState::Browse(()),
|
||||
Err(err) => AppState::Error(err.to_string()),
|
||||
};
|
||||
let selection = Selection::new(Some(music_hoard.get_collection()));
|
||||
Ok(App {
|
||||
App {
|
||||
running: true,
|
||||
music_hoard,
|
||||
selection,
|
||||
state: AppState::Browse(()),
|
||||
})
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
fn init(music_hoard: &mut MH) -> Result<(), musichoard::Error> {
|
||||
music_hoard.load_from_database()?;
|
||||
music_hoard.rescan_library()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -362,11 +349,19 @@ impl<MH: IMusicHoard> IAppInteract for App<MH> {
|
||||
}
|
||||
|
||||
fn quit(&mut self) {
|
||||
if !self.state.is_error() {
|
||||
self.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn force_quit(&mut self) {
|
||||
self.running = false;
|
||||
}
|
||||
|
||||
fn save(&mut self) -> Result<(), AppError> {
|
||||
Ok(self.music_hoard.save_to_database()?)
|
||||
fn save(&mut self) {
|
||||
if let Err(err) = self.music_hoard.save_to_database() {
|
||||
self.state = AppState::Error(err.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES> {
|
||||
@ -642,14 +637,50 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running() {
|
||||
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
||||
fn running_quit() {
|
||||
let mut app = App::new(music_hoard(COLLECTION.to_owned()));
|
||||
assert!(app.is_running());
|
||||
|
||||
app.quit();
|
||||
assert!(!app.is_running());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_quit() {
|
||||
let mut app = App::new(music_hoard(COLLECTION.to_owned()));
|
||||
assert!(app.is_running());
|
||||
|
||||
app.state = AppState::Error(String::from("get rekt"));
|
||||
|
||||
app.quit();
|
||||
assert!(app.is_running());
|
||||
|
||||
app.dismiss_error();
|
||||
|
||||
app.quit();
|
||||
assert!(!app.is_running());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running_force_quit() {
|
||||
let mut app = App::new(music_hoard(COLLECTION.to_owned()));
|
||||
assert!(app.is_running());
|
||||
|
||||
app.force_quit();
|
||||
assert!(!app.is_running());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_force_quit() {
|
||||
let mut app = App::new(music_hoard(COLLECTION.to_owned()));
|
||||
assert!(app.is_running());
|
||||
|
||||
app.state = AppState::Error(String::from("get rekt"));
|
||||
|
||||
app.force_quit();
|
||||
assert!(!app.is_running());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save() {
|
||||
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
||||
@ -659,15 +690,47 @@ mod tests {
|
||||
.times(1)
|
||||
.return_once(|| Ok(()));
|
||||
|
||||
let mut app = App::new(music_hoard).unwrap();
|
||||
let mut app = App::new(music_hoard);
|
||||
|
||||
let result = app.save();
|
||||
assert!(result.is_ok());
|
||||
app.save();
|
||||
assert!(app.state.is_browse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_error() {
|
||||
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
||||
|
||||
music_hoard
|
||||
.expect_save_to_database()
|
||||
.times(1)
|
||||
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
|
||||
|
||||
let mut app = App::new(music_hoard);
|
||||
|
||||
app.save();
|
||||
|
||||
assert!(app.state.is_error());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_error() {
|
||||
let mut music_hoard = MockIMusicHoard::new();
|
||||
|
||||
music_hoard
|
||||
.expect_load_from_database()
|
||||
.times(1)
|
||||
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
|
||||
music_hoard.expect_get_collection().return_const(vec![]);
|
||||
|
||||
let app = App::new(music_hoard);
|
||||
|
||||
assert!(app.is_running());
|
||||
assert!(app.state.is_error());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifiers() {
|
||||
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
||||
let mut app = App::new(music_hoard(COLLECTION.to_owned()));
|
||||
assert!(app.is_running());
|
||||
|
||||
assert_eq!(app.selection.active, Category::Artist);
|
||||
@ -759,7 +822,7 @@ mod tests {
|
||||
let mut collection = COLLECTION.to_owned();
|
||||
collection[0].albums[0].tracks = vec![];
|
||||
|
||||
let mut app = App::new(music_hoard(collection)).unwrap();
|
||||
let mut app = App::new(music_hoard(collection));
|
||||
assert!(app.is_running());
|
||||
|
||||
assert_eq!(app.selection.active, Category::Artist);
|
||||
@ -788,7 +851,7 @@ mod tests {
|
||||
let mut collection = COLLECTION.to_owned();
|
||||
collection[0].albums = vec![];
|
||||
|
||||
let mut app = App::new(music_hoard(collection)).unwrap();
|
||||
let mut app = App::new(music_hoard(collection));
|
||||
assert!(app.is_running());
|
||||
|
||||
assert_eq!(app.selection.active, Category::Artist);
|
||||
@ -827,7 +890,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn no_artists() {
|
||||
let mut app = App::new(music_hoard(vec![])).unwrap();
|
||||
let mut app = App::new(music_hoard(vec![]));
|
||||
assert!(app.is_running());
|
||||
|
||||
assert_eq!(app.selection.active, Category::Artist);
|
||||
@ -880,7 +943,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn info_overlay() {
|
||||
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
||||
let mut app = App::new(music_hoard(COLLECTION.to_owned()));
|
||||
assert!(app.state().is_browse());
|
||||
|
||||
app.show_info_overlay();
|
||||
@ -892,7 +955,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn reload_go_back() {
|
||||
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
||||
let mut app = App::new(music_hoard(COLLECTION.to_owned()));
|
||||
assert!(app.state().is_browse());
|
||||
|
||||
app.show_reload_menu();
|
||||
@ -911,7 +974,7 @@ mod tests {
|
||||
.times(1)
|
||||
.return_once(|| Ok(()));
|
||||
|
||||
let mut app = App::new(music_hoard).unwrap();
|
||||
let mut app = App::new(music_hoard);
|
||||
assert!(app.state().is_browse());
|
||||
|
||||
app.show_reload_menu();
|
||||
@ -930,7 +993,7 @@ mod tests {
|
||||
.times(1)
|
||||
.return_once(|| Ok(()));
|
||||
|
||||
let mut app = App::new(music_hoard).unwrap();
|
||||
let mut app = App::new(music_hoard);
|
||||
assert!(app.state().is_browse());
|
||||
|
||||
app.show_reload_menu();
|
||||
@ -949,7 +1012,7 @@ mod tests {
|
||||
.times(1)
|
||||
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
|
||||
|
||||
let mut app = App::new(music_hoard).unwrap();
|
||||
let mut app = App::new(music_hoard);
|
||||
assert!(app.state().is_browse());
|
||||
|
||||
app.show_reload_menu();
|
||||
@ -961,13 +1024,4 @@ mod tests {
|
||||
app.dismiss_error();
|
||||
assert!(app.state().is_browse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
let app_err: AppError = musichoard::Error::DatabaseError(String::from("get rekt")).into();
|
||||
|
||||
assert!(!app_err.to_string().is_empty());
|
||||
|
||||
assert!(!format!("{:?}", app_err).is_empty());
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,11 @@ use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use std::fmt;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use crate::tui::app::AppError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EventError {
|
||||
Send(Event),
|
||||
Recv,
|
||||
Io(std::io::Error),
|
||||
App(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for EventError {
|
||||
@ -20,12 +17,6 @@ impl fmt::Display for EventError {
|
||||
Self::Io(ref e) => {
|
||||
write!(f, "an I/O error was triggered during event handling: {e}")
|
||||
}
|
||||
Self::App(ref s) => {
|
||||
write!(
|
||||
f,
|
||||
"the application returned an error during event handling: {s}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -42,12 +33,6 @@ impl From<mpsc::RecvError> for EventError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AppError> for EventError {
|
||||
fn from(err: AppError) -> EventError {
|
||||
EventError::App(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Event {
|
||||
Key(KeyEvent),
|
||||
@ -148,16 +133,13 @@ mod tests {
|
||||
}));
|
||||
let recv_err = EventError::Recv;
|
||||
let io_err = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "interrupted"));
|
||||
let app_err: EventError = AppError::Lib(String::from("lib error")).into();
|
||||
|
||||
assert!(!send_err.to_string().is_empty());
|
||||
assert!(!recv_err.to_string().is_empty());
|
||||
assert!(!io_err.to_string().is_empty());
|
||||
assert!(!app_err.to_string().is_empty());
|
||||
|
||||
assert!(!format!("{:?}", send_err).is_empty());
|
||||
assert!(!format!("{:?}", recv_err).is_empty());
|
||||
assert!(!format!("{:?}", io_err).is_empty());
|
||||
assert!(!format!("{:?}", app_err).is_empty());
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
// FIXME: Can code here be less verbose
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
#[cfg(test)]
|
||||
@ -18,24 +17,11 @@ pub trait IEventHandler<APP: IAppInteract> {
|
||||
}
|
||||
|
||||
trait IEventHandlerPrivate<APP: IAppInteract> {
|
||||
fn handle_key_event(app: &mut APP, key_event: KeyEvent) -> Result<(), EventError>;
|
||||
fn handle_browse_key_event(
|
||||
app: &mut <APP as IAppInteract>::BS,
|
||||
key_event: KeyEvent,
|
||||
) -> Result<(), EventError>;
|
||||
fn handle_info_key_event(
|
||||
app: &mut <APP as IAppInteract>::IS,
|
||||
key_event: KeyEvent,
|
||||
) -> Result<(), EventError>;
|
||||
fn handle_reload_key_event(
|
||||
app: &mut <APP as IAppInteract>::RS,
|
||||
key_event: KeyEvent,
|
||||
) -> Result<(), EventError>;
|
||||
fn handle_error_key_event(
|
||||
app: &mut <APP as IAppInteract>::ES,
|
||||
key_event: KeyEvent,
|
||||
) -> Result<(), EventError>;
|
||||
fn quit(app: &mut APP) -> Result<(), EventError>;
|
||||
fn handle_key_event(app: &mut APP, key_event: KeyEvent);
|
||||
fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, key_event: KeyEvent);
|
||||
fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent);
|
||||
fn handle_reload_key_event(app: &mut <APP as IAppInteract>::RS, key_event: KeyEvent);
|
||||
fn handle_error_key_event(app: &mut <APP as IAppInteract>::ES, key_event: KeyEvent);
|
||||
}
|
||||
|
||||
pub struct EventHandler {
|
||||
@ -52,7 +38,7 @@ impl EventHandler {
|
||||
impl<APP: IAppInteract> IEventHandler<APP> for EventHandler {
|
||||
fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> {
|
||||
match self.events.recv()? {
|
||||
Event::Key(key_event) => Self::handle_key_event(app, key_event)?,
|
||||
Event::Key(key_event) => Self::handle_key_event(app, key_event),
|
||||
Event::Mouse(_) => {}
|
||||
Event::Resize(_, _) => {}
|
||||
};
|
||||
@ -61,44 +47,37 @@ impl<APP: IAppInteract> IEventHandler<APP> for EventHandler {
|
||||
}
|
||||
|
||||
impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
|
||||
fn handle_key_event(app: &mut APP, key_event: KeyEvent) -> Result<(), EventError> {
|
||||
fn handle_key_event(app: &mut APP, key_event: KeyEvent) {
|
||||
match key_event.code {
|
||||
// Exit application on `ESC` or `q`.
|
||||
KeyCode::Esc | KeyCode::Char('q') => {
|
||||
Self::quit(app)?;
|
||||
app.save();
|
||||
app.quit();
|
||||
}
|
||||
// Exit application on `Ctrl-C`.
|
||||
KeyCode::Char('c') | KeyCode::Char('C') => {
|
||||
if key_event.modifiers == KeyModifiers::CONTROL {
|
||||
Self::quit(app)?;
|
||||
app.force_quit();
|
||||
}
|
||||
}
|
||||
_ => match app.state() {
|
||||
AppState::Browse(browse) => {
|
||||
<Self as IEventHandlerPrivate<APP>>::handle_browse_key_event(
|
||||
browse, key_event,
|
||||
)?;
|
||||
<Self as IEventHandlerPrivate<APP>>::handle_browse_key_event(browse, key_event);
|
||||
}
|
||||
AppState::Info(info) => {
|
||||
<Self as IEventHandlerPrivate<APP>>::handle_info_key_event(info, key_event)?;
|
||||
<Self as IEventHandlerPrivate<APP>>::handle_info_key_event(info, key_event);
|
||||
}
|
||||
AppState::Reload(reload) => {
|
||||
<Self as IEventHandlerPrivate<APP>>::handle_reload_key_event(
|
||||
reload, key_event,
|
||||
)?;
|
||||
<Self as IEventHandlerPrivate<APP>>::handle_reload_key_event(reload, key_event);
|
||||
}
|
||||
AppState::Error(error) => {
|
||||
<Self as IEventHandlerPrivate<APP>>::handle_error_key_event(error, key_event)?;
|
||||
<Self as IEventHandlerPrivate<APP>>::handle_error_key_event(error, key_event);
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_browse_key_event(
|
||||
app: &mut <APP as IAppInteract>::BS,
|
||||
key_event: KeyEvent,
|
||||
) -> Result<(), EventError> {
|
||||
fn handle_browse_key_event(app: &mut <APP as IAppInteract>::BS, key_event: KeyEvent) {
|
||||
match key_event.code {
|
||||
// Category change.
|
||||
KeyCode::Left => app.decrement_category(),
|
||||
@ -113,28 +92,18 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
|
||||
// Othey keys.
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_info_key_event(
|
||||
app: &mut <APP as IAppInteract>::IS,
|
||||
key_event: KeyEvent,
|
||||
) -> Result<(), EventError> {
|
||||
fn handle_info_key_event(app: &mut <APP as IAppInteract>::IS, key_event: KeyEvent) {
|
||||
match key_event.code {
|
||||
// Toggle overlay.
|
||||
KeyCode::Char('m') | KeyCode::Char('M') => app.hide_info_overlay(),
|
||||
// Othey keys.
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_reload_key_event(
|
||||
app: &mut <APP as IAppInteract>::RS,
|
||||
key_event: KeyEvent,
|
||||
) -> Result<(), EventError> {
|
||||
fn handle_reload_key_event(app: &mut <APP as IAppInteract>::RS, key_event: KeyEvent) {
|
||||
match key_event.code {
|
||||
// Reload keys.
|
||||
KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(),
|
||||
@ -144,23 +113,11 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
|
||||
// Othey keys.
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_error_key_event(
|
||||
app: &mut <APP as IAppInteract>::ES,
|
||||
_key_event: KeyEvent,
|
||||
) -> Result<(), EventError> {
|
||||
fn handle_error_key_event(app: &mut <APP as IAppInteract>::ES, _key_event: KeyEvent) {
|
||||
// Any key dismisses the error.
|
||||
app.dismiss_error();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn quit(app: &mut APP) -> Result<(), EventError> {
|
||||
app.quit();
|
||||
app.save()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// GRCOV_EXCL_STOP
|
||||
|
@ -28,18 +28,11 @@ use crate::tui::{
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
Lib(String),
|
||||
Io(String),
|
||||
Event(String),
|
||||
ListenerPanic,
|
||||
}
|
||||
|
||||
impl From<musichoard::Error> for Error {
|
||||
fn from(err: musichoard::Error) -> Error {
|
||||
Error::Lib(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err.to_string())
|
||||
@ -81,9 +74,6 @@ impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> {
|
||||
handler: impl IEventHandler<APP>,
|
||||
) -> Result<(), Error> {
|
||||
while app.is_running() {
|
||||
// FIXME: rendering logic should be in render.rs - reverse Renderer/Ui relationship so
|
||||
// that TAPP calls are renderer.render(ui, frame); Then rename ui -> app and render -> ui
|
||||
// as it used to be originally.
|
||||
self.terminal.draw(|frame| UI::render(&mut app, frame))?;
|
||||
handler.handle_next_event(&mut app)?;
|
||||
}
|
||||
@ -123,7 +113,7 @@ impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> {
|
||||
Ok(err) => return Err(err.into()),
|
||||
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs
|
||||
// will not produce an error message. The panic error message is printed at
|
||||
// the location of the panic which at the time is hidden by the TAPP.
|
||||
// the location of the panic which at the time is hidden by the TUI.
|
||||
Err(_) => return Err(Error::ListenerPanic),
|
||||
}
|
||||
}
|
||||
@ -211,7 +201,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn app(collection: Collection) -> App<MockIMusicHoard> {
|
||||
App::new(music_hoard(collection)).unwrap()
|
||||
App::new(music_hoard(collection))
|
||||
}
|
||||
|
||||
fn listener() -> MockIEventListener {
|
||||
@ -319,12 +309,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
let lib_err: Error = musichoard::Error::DatabaseError(String::from("")).into();
|
||||
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
|
||||
let event_err: Error = EventError::Recv.into();
|
||||
let listener_err = Error::ListenerPanic;
|
||||
|
||||
assert!(!format!("{:?}", lib_err).is_empty());
|
||||
assert!(!format!("{:?}", io_err).is_empty());
|
||||
assert!(!format!("{:?}", event_err).is_empty());
|
||||
assert!(!format!("{:?}", listener_err).is_empty());
|
||||
|
Loading…
Reference in New Issue
Block a user