Merge pull request #76 from Herbstein/commander
Move to enum-based commands
This commit is contained in:
219
src/command.rs
Normal file
219
src/command.rs
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
use queue::RepeatSetting;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum PlaylistCommands {
|
||||||
|
Update,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum SeekInterval {
|
||||||
|
Forward,
|
||||||
|
Backwards,
|
||||||
|
Custom(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum TargetMode {
|
||||||
|
Current,
|
||||||
|
Selected,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum MoveMode {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum ShiftMode {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum GotoMode {
|
||||||
|
Album,
|
||||||
|
Artist,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum SeekDirection {
|
||||||
|
Relative(i32),
|
||||||
|
Absolute(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum Command {
|
||||||
|
Quit,
|
||||||
|
TogglePlay,
|
||||||
|
Playlists(PlaylistCommands),
|
||||||
|
Stop,
|
||||||
|
Previous,
|
||||||
|
Next,
|
||||||
|
Clear,
|
||||||
|
Queue,
|
||||||
|
Play,
|
||||||
|
Save,
|
||||||
|
SaveQueue,
|
||||||
|
Delete,
|
||||||
|
Focus(String),
|
||||||
|
Seek(SeekDirection),
|
||||||
|
Repeat(Option<RepeatSetting>),
|
||||||
|
Shuffle(Option<bool>),
|
||||||
|
Share(TargetMode),
|
||||||
|
Back,
|
||||||
|
Open,
|
||||||
|
Goto(GotoMode),
|
||||||
|
Move(MoveMode, Option<i32>),
|
||||||
|
Shift(ShiftMode, Option<i32>),
|
||||||
|
Search(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_aliases(map: &mut HashMap<&str, &str>, cmd: &'static str, names: Vec<&'static str>) {
|
||||||
|
for a in names {
|
||||||
|
map.insert(a, cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref ALIASES: HashMap<&'static str, &'static str> = {
|
||||||
|
let mut m = HashMap::new();
|
||||||
|
|
||||||
|
register_aliases(&mut m, "quit", vec!["q", "x"]);
|
||||||
|
register_aliases(
|
||||||
|
&mut m,
|
||||||
|
"playpause",
|
||||||
|
vec!["pause", "toggleplay", "toggleplayback"],
|
||||||
|
);
|
||||||
|
register_aliases(&mut m, "repeat", vec!["loop"]);
|
||||||
|
|
||||||
|
m.insert("1", "foo");
|
||||||
|
m.insert("2", "bar");
|
||||||
|
m.insert("3", "baz");
|
||||||
|
m
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_aliases(input: &str) -> &str {
|
||||||
|
if let Some(cmd) = ALIASES.get(input) {
|
||||||
|
handle_aliases(cmd)
|
||||||
|
} else {
|
||||||
|
input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(input: &str) -> Option<Command> {
|
||||||
|
let components: Vec<_> = input.trim().split(' ').collect();
|
||||||
|
|
||||||
|
let command = handle_aliases(&components[0]);
|
||||||
|
let args = components[1..].to_vec();
|
||||||
|
|
||||||
|
match command {
|
||||||
|
"quit" => Some(Command::Quit),
|
||||||
|
"playpause" => Some(Command::TogglePlay),
|
||||||
|
"stop" => Some(Command::Stop),
|
||||||
|
"previous" => Some(Command::Previous),
|
||||||
|
"next" => Some(Command::Next),
|
||||||
|
"clear" => Some(Command::Clear),
|
||||||
|
"queue" => Some(Command::Queue),
|
||||||
|
"play" => Some(Command::Play),
|
||||||
|
"delete" => Some(Command::Delete),
|
||||||
|
"back" => Some(Command::Back),
|
||||||
|
"open" => Some(Command::Open),
|
||||||
|
"search" => args.get(0).map(|query| Command::Search(query.to_string())),
|
||||||
|
"shift" => {
|
||||||
|
let amount = args.get(1).and_then(|amount| amount.parse().ok());
|
||||||
|
|
||||||
|
args.get(0)
|
||||||
|
.and_then(|direction| match *direction {
|
||||||
|
"up" => Some(ShiftMode::Up),
|
||||||
|
"down" => Some(ShiftMode::Down),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(|mode| Command::Shift(mode, amount))
|
||||||
|
}
|
||||||
|
"move" => {
|
||||||
|
let amount = args.get(1).and_then(|amount| amount.parse().ok());
|
||||||
|
|
||||||
|
args.get(0)
|
||||||
|
.and_then(|direction| match *direction {
|
||||||
|
"up" => Some(MoveMode::Up),
|
||||||
|
"down" => Some(MoveMode::Down),
|
||||||
|
"left" => Some(MoveMode::Left),
|
||||||
|
"right" => Some(MoveMode::Right),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(|mode| Command::Move(mode, amount))
|
||||||
|
}
|
||||||
|
"goto" => args
|
||||||
|
.get(0)
|
||||||
|
.and_then(|mode| match *mode {
|
||||||
|
"album" => Some(GotoMode::Album),
|
||||||
|
"artist" => Some(GotoMode::Artist),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(Command::Goto),
|
||||||
|
"share" => args
|
||||||
|
.get(0)
|
||||||
|
.and_then(|target| match *target {
|
||||||
|
"selected" => Some(TargetMode::Selected),
|
||||||
|
"current" => Some(TargetMode::Current),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(Command::Share),
|
||||||
|
"shuffle" => {
|
||||||
|
let shuffle = args.get(0).and_then(|mode| match *mode {
|
||||||
|
"on" => Some(true),
|
||||||
|
"off" => Some(false),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(Command::Shuffle(shuffle))
|
||||||
|
}
|
||||||
|
"repeat" => {
|
||||||
|
let mode = args.get(0).and_then(|mode| match *mode {
|
||||||
|
"list" | "playlist" | "queue" => Some(RepeatSetting::RepeatPlaylist),
|
||||||
|
"track" | "once" => Some(RepeatSetting::RepeatTrack),
|
||||||
|
"none" | "off" => Some(RepeatSetting::None),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(Command::Repeat(mode))
|
||||||
|
}
|
||||||
|
"seek" => args.get(0).and_then(|arg| match arg.chars().nth(0) {
|
||||||
|
Some(x) if x == '-' || x == '+' => String::from_iter(arg.chars().skip(1))
|
||||||
|
.parse::<i32>()
|
||||||
|
.ok()
|
||||||
|
.map(|amount| {
|
||||||
|
Command::Seek(SeekDirection::Relative(
|
||||||
|
amount
|
||||||
|
* match x {
|
||||||
|
'-' => -1,
|
||||||
|
_ => 1,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
_ => String::from_iter(arg.chars())
|
||||||
|
.parse()
|
||||||
|
.ok()
|
||||||
|
.map(|amount| Command::Seek(SeekDirection::Absolute(amount))),
|
||||||
|
}),
|
||||||
|
"focus" => args.get(0).map(|target| Command::Focus(target.to_string())),
|
||||||
|
"playlists" => args
|
||||||
|
.get(0)
|
||||||
|
.and_then(|action| match *action {
|
||||||
|
"update" => Some(PlaylistCommands::Update),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(Command::Playlists),
|
||||||
|
"save" => args.get(0).map(|target| match *target {
|
||||||
|
"queue" => Command::SaveQueue,
|
||||||
|
_ => Command::Save,
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
382
src/commands.rs
382
src/commands.rs
@@ -2,18 +2,18 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use command::{
|
||||||
|
Command, GotoMode, MoveMode, PlaylistCommands, SeekDirection, ShiftMode, TargetMode,
|
||||||
|
};
|
||||||
use cursive::event::{Event, Key};
|
use cursive::event::{Event, Key};
|
||||||
use cursive::views::ViewRef;
|
use cursive::views::ViewRef;
|
||||||
use cursive::Cursive;
|
use cursive::Cursive;
|
||||||
|
|
||||||
use library::Library;
|
use library::Library;
|
||||||
use queue::{Queue, RepeatSetting};
|
use queue::{Queue, RepeatSetting};
|
||||||
use spotify::Spotify;
|
use spotify::Spotify;
|
||||||
use traits::ViewExt;
|
use traits::ViewExt;
|
||||||
use ui::layout::Layout;
|
use ui::layout::Layout;
|
||||||
|
|
||||||
type CommandCb = dyn Fn(&mut Cursive, &[String]) -> Result<Option<String>, String>;
|
|
||||||
|
|
||||||
pub enum CommandResult {
|
pub enum CommandResult {
|
||||||
Consumed(Option<String>),
|
Consumed(Option<String>),
|
||||||
View(Box<dyn ViewExt>),
|
View(Box<dyn ViewExt>),
|
||||||
@@ -21,22 +21,22 @@ pub enum CommandResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct CommandManager {
|
pub struct CommandManager {
|
||||||
callbacks: HashMap<String, Option<Box<CommandCb>>>,
|
|
||||||
aliases: HashMap<String, String>,
|
aliases: HashMap<String, String>,
|
||||||
|
spotify: Arc<Spotify>,
|
||||||
|
queue: Arc<Queue>,
|
||||||
|
library: Arc<Library>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandManager {
|
impl CommandManager {
|
||||||
pub fn new() -> CommandManager {
|
pub fn new(spotify: Arc<Spotify>, queue: Arc<Queue>, library: Arc<Library>) -> CommandManager {
|
||||||
CommandManager {
|
CommandManager {
|
||||||
callbacks: HashMap::new(),
|
|
||||||
aliases: HashMap::new(),
|
aliases: HashMap::new(),
|
||||||
|
spotify,
|
||||||
|
queue,
|
||||||
|
library,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_command<S: Into<String>>(&mut self, name: S, cb: Option<Box<CommandCb>>) {
|
|
||||||
self.callbacks.insert(name.into(), cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register_aliases<S: Into<String>>(&mut self, name: S, aliases: Vec<S>) {
|
pub fn register_aliases<S: Into<String>>(&mut self, name: S, aliases: Vec<S>) {
|
||||||
let name = name.into();
|
let name = name.into();
|
||||||
for a in aliases {
|
for a in aliases {
|
||||||
@@ -44,198 +44,92 @@ impl CommandManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_all(
|
pub fn register_all(&mut self) {
|
||||||
&mut self,
|
|
||||||
spotify: Arc<Spotify>,
|
|
||||||
queue: Arc<Queue>,
|
|
||||||
library: Arc<Library>,
|
|
||||||
) {
|
|
||||||
self.register_aliases("quit", vec!["q", "x"]);
|
self.register_aliases("quit", vec!["q", "x"]);
|
||||||
self.register_aliases("playpause", vec!["pause", "toggleplay", "toggleplayback"]);
|
self.register_aliases("playpause", vec!["pause", "toggleplay", "toggleplayback"]);
|
||||||
self.register_aliases("repeat", vec!["loop"]);
|
self.register_aliases("repeat", vec!["loop"]);
|
||||||
|
|
||||||
self.register_command("search", None);
|
|
||||||
self.register_command("move", None);
|
|
||||||
self.register_command("shift", None);
|
|
||||||
self.register_command("play", None);
|
|
||||||
self.register_command("queue", None);
|
|
||||||
self.register_command("save", None);
|
|
||||||
self.register_command("delete", None);
|
|
||||||
self.register_command("back", None);
|
|
||||||
self.register_command("open", None);
|
|
||||||
self.register_command("goto", None);
|
|
||||||
|
|
||||||
self.register_command(
|
|
||||||
"quit",
|
|
||||||
Some(Box::new(move |s, _args| {
|
|
||||||
s.quit();
|
|
||||||
Ok(None)
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
{
|
|
||||||
let queue = queue.clone();
|
|
||||||
self.register_command(
|
|
||||||
"stop",
|
|
||||||
Some(Box::new(move |_s, _args| {
|
|
||||||
queue.stop();
|
|
||||||
Ok(None)
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let queue = queue.clone();
|
|
||||||
let spotify = spotify.clone();
|
|
||||||
self.register_command(
|
|
||||||
"previous",
|
|
||||||
Some(Box::new(move |_s, _args| {
|
|
||||||
if spotify.get_current_progress() < Duration::from_secs(5) {
|
|
||||||
queue.previous();
|
|
||||||
} else {
|
|
||||||
spotify.seek(0);
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let queue = queue.clone();
|
|
||||||
self.register_command(
|
|
||||||
"next",
|
|
||||||
Some(Box::new(move |_s, _args| {
|
|
||||||
queue.next(true);
|
|
||||||
Ok(None)
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let queue = queue.clone();
|
|
||||||
self.register_command(
|
|
||||||
"clear",
|
|
||||||
Some(Box::new(move |_s, _args| {
|
|
||||||
queue.clear();
|
|
||||||
Ok(None)
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let library = library.clone();
|
|
||||||
self.register_command(
|
|
||||||
"playlists",
|
|
||||||
Some(Box::new(move |_s, args| {
|
|
||||||
if let Some(arg) = args.get(0) {
|
|
||||||
if arg == "update" {
|
|
||||||
library.update_playlists();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let queue = queue.clone();
|
|
||||||
self.register_command(
|
|
||||||
"playpause",
|
|
||||||
Some(Box::new(move |_s, _args| {
|
|
||||||
queue.toggleplayback();
|
|
||||||
Ok(None)
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let queue = queue.clone();
|
|
||||||
self.register_command(
|
|
||||||
"shuffle",
|
|
||||||
Some(Box::new(move |_s, args| {
|
|
||||||
if let Some(arg) = args.get(0) {
|
|
||||||
queue.set_shuffle(match arg.as_ref() {
|
|
||||||
"on" => true,
|
|
||||||
"off" => false,
|
|
||||||
_ => {
|
|
||||||
return Err("Unknown shuffle setting.".to_string());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
queue.set_shuffle(!queue.get_shuffle());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let queue = queue.clone();
|
|
||||||
self.register_command(
|
|
||||||
"repeat",
|
|
||||||
Some(Box::new(move |_s, args| {
|
|
||||||
if let Some(arg) = args.get(0) {
|
|
||||||
queue.set_repeat(match arg.as_ref() {
|
|
||||||
"list" | "playlist" | "queue" => RepeatSetting::RepeatPlaylist,
|
|
||||||
"track" | "once" => RepeatSetting::RepeatTrack,
|
|
||||||
"none" | "off" => RepeatSetting::None,
|
|
||||||
_ => {
|
|
||||||
return Err("Unknown loop setting.".to_string());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
queue.set_repeat(match queue.get_repeat() {
|
|
||||||
RepeatSetting::None => RepeatSetting::RepeatPlaylist,
|
|
||||||
RepeatSetting::RepeatPlaylist => RepeatSetting::RepeatTrack,
|
|
||||||
RepeatSetting::RepeatTrack => RepeatSetting::None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let spotify = spotify.clone();
|
|
||||||
self.register_command(
|
|
||||||
"seek",
|
|
||||||
Some(Box::new(move |_s, args| {
|
|
||||||
if let Some(arg) = args.get(0) {
|
|
||||||
match arg.chars().next().unwrap() {
|
|
||||||
'+' | '-' => {
|
|
||||||
spotify.seek_relative(arg.parse::<i32>().unwrap_or(0));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
spotify.seek(arg.parse::<u32>().unwrap_or(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_aliases(&self, name: &str) -> String {
|
fn handle_default_commands(
|
||||||
if let Some(s) = self.aliases.get(name) {
|
|
||||||
self.handle_aliases(s)
|
|
||||||
} else {
|
|
||||||
name.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_callbacks(
|
|
||||||
&self,
|
&self,
|
||||||
s: &mut Cursive,
|
s: &mut Cursive,
|
||||||
cmd: &str,
|
cmd: &Command,
|
||||||
args: &[String],
|
|
||||||
) -> Result<Option<String>, String> {
|
) -> Result<Option<String>, String> {
|
||||||
|
match cmd {
|
||||||
|
Command::Quit => {
|
||||||
|
s.quit();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Command::Stop => {
|
||||||
|
self.queue.stop();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Command::Previous => {
|
||||||
|
if self.spotify.get_current_progress() < Duration::from_secs(5) {
|
||||||
|
self.queue.previous();
|
||||||
|
} else {
|
||||||
|
self.spotify.seek(0);
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Command::Next => {
|
||||||
|
self.queue.next(true);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Command::Clear => {
|
||||||
|
self.queue.clear();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Command::Playlists(mode) => {
|
||||||
|
match mode {
|
||||||
|
PlaylistCommands::Update => self.library.update_playlists(),
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Command::TogglePlay => {
|
||||||
|
self.queue.toggleplayback();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Command::Shuffle(mode) => {
|
||||||
|
let mode = mode.unwrap_or_else(|| !self.queue.get_shuffle());
|
||||||
|
self.queue.set_shuffle(mode);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Command::Repeat(mode) => {
|
||||||
|
let mode = mode.unwrap_or_else(|| match self.queue.get_repeat() {
|
||||||
|
RepeatSetting::None => RepeatSetting::RepeatPlaylist,
|
||||||
|
RepeatSetting::RepeatPlaylist => RepeatSetting::RepeatTrack,
|
||||||
|
RepeatSetting::RepeatTrack => RepeatSetting::None,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.queue.set_repeat(mode);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Command::Seek(direction) => {
|
||||||
|
match *direction {
|
||||||
|
SeekDirection::Relative(rel) => self.spotify.seek_relative(rel),
|
||||||
|
SeekDirection::Absolute(abs) => self.spotify.seek(abs),
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Command::Search(_)
|
||||||
|
| Command::Move(_, _)
|
||||||
|
| Command::Shift(_, _)
|
||||||
|
| Command::Play
|
||||||
|
| Command::Queue
|
||||||
|
| Command::Save
|
||||||
|
| Command::Delete
|
||||||
|
| Command::Back
|
||||||
|
| Command::Open
|
||||||
|
| Command::Goto(_) => Ok(None),
|
||||||
|
_ => Err("Unknown Command".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_callbacks(&self, s: &mut Cursive, cmd: &Command) -> Result<Option<String>, String> {
|
||||||
let local = {
|
let local = {
|
||||||
let mut main: ViewRef<Layout> = s.find_id("main").unwrap();
|
let mut main: ViewRef<Layout> = s.find_id("main").unwrap();
|
||||||
main.on_command(s, cmd, args)?
|
main.on_command(s, cmd)?
|
||||||
};
|
};
|
||||||
|
|
||||||
if let CommandResult::Consumed(output) = local {
|
if let CommandResult::Consumed(output) = local {
|
||||||
@@ -246,24 +140,13 @@ impl CommandManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else if let Some(callback) = self.callbacks.get(cmd) {
|
|
||||||
callback.as_ref().map(|cb| cb(s, args)).unwrap_or(Ok(None))
|
|
||||||
} else {
|
} else {
|
||||||
Err("Unknown command.".to_string())
|
self.handle_default_commands(s, cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(&self, s: &mut Cursive, cmd: String) {
|
pub fn handle(&self, s: &mut Cursive, cmd: Command) {
|
||||||
let components: Vec<String> = cmd
|
let result = self.handle_callbacks(s, &cmd);
|
||||||
.trim()
|
|
||||||
.split(' ')
|
|
||||||
.map(std::string::ToString::to_string)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let cmd = self.handle_aliases(&components[0]);
|
|
||||||
let args = components[1..].to_vec();
|
|
||||||
|
|
||||||
let result = self.handle_callbacks(s, &cmd, &args);
|
|
||||||
|
|
||||||
s.call_on_id("main", |v: &mut Layout| {
|
s.call_on_id("main", |v: &mut Layout| {
|
||||||
v.set_result(result);
|
v.set_result(result);
|
||||||
@@ -272,22 +155,21 @@ impl CommandManager {
|
|||||||
s.on_event(Event::Refresh);
|
s.on_event(Event::Refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_keybinding<E: Into<cursive::event::Event>, S: Into<String>>(
|
pub fn register_keybinding<E: Into<cursive::event::Event>>(
|
||||||
this: Arc<Self>,
|
this: Arc<Self>,
|
||||||
cursive: &mut Cursive,
|
cursive: &mut Cursive,
|
||||||
event: E,
|
event: E,
|
||||||
command: S,
|
command: Command,
|
||||||
) {
|
) {
|
||||||
let cmd = command.into();
|
|
||||||
cursive.add_global_callback(event, move |s| {
|
cursive.add_global_callback(event, move |s| {
|
||||||
this.handle(s, cmd.clone());
|
this.handle(s, command.clone());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_keybindings(
|
pub fn register_keybindings(
|
||||||
this: Arc<Self>,
|
this: Arc<Self>,
|
||||||
cursive: &mut Cursive,
|
cursive: &mut Cursive,
|
||||||
keybindings: Option<HashMap<String, String>>,
|
keybindings: Option<HashMap<String, Command>>,
|
||||||
) {
|
) {
|
||||||
let mut kb = Self::default_keybindings();
|
let mut kb = Self::default_keybindings();
|
||||||
kb.extend(keybindings.unwrap_or_default());
|
kb.extend(keybindings.unwrap_or_default());
|
||||||
@@ -301,51 +183,51 @@ impl CommandManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_keybindings() -> HashMap<String, String> {
|
fn default_keybindings() -> HashMap<String, Command> {
|
||||||
let mut kb = HashMap::new();
|
let mut kb = HashMap::new();
|
||||||
|
|
||||||
kb.insert("q".into(), "quit".into());
|
kb.insert("q".into(), Command::Quit);
|
||||||
kb.insert("P".into(), "toggleplay".into());
|
kb.insert("P".into(), Command::TogglePlay);
|
||||||
kb.insert("R".into(), "playlists update".into());
|
kb.insert("R".into(), Command::Playlists(PlaylistCommands::Update));
|
||||||
kb.insert("S".into(), "stop".into());
|
kb.insert("S".into(), Command::Stop);
|
||||||
kb.insert("<".into(), "previous".into());
|
kb.insert("<".into(), Command::Previous);
|
||||||
kb.insert(">".into(), "next".into());
|
kb.insert(">".into(), Command::Next);
|
||||||
kb.insert("c".into(), "clear".into());
|
kb.insert("c".into(), Command::Clear);
|
||||||
kb.insert(" ".into(), "queue".into());
|
kb.insert(" ".into(), Command::Queue);
|
||||||
kb.insert("Enter".into(), "play".into());
|
kb.insert("Enter".into(), Command::Play);
|
||||||
kb.insert("s".into(), "save".into());
|
kb.insert("s".into(), Command::Save);
|
||||||
kb.insert("Ctrl+s".into(), "save queue".into());
|
kb.insert("Ctrl+s".into(), Command::SaveQueue);
|
||||||
kb.insert("d".into(), "delete".into());
|
kb.insert("d".into(), Command::Delete);
|
||||||
kb.insert("/".into(), "focus search".into());
|
kb.insert("/".into(), Command::Focus("search".into()));
|
||||||
kb.insert(".".into(), "seek +500".into());
|
kb.insert(".".into(), Command::Seek(SeekDirection::Relative(500)));
|
||||||
kb.insert(",".into(), "seek -500".into());
|
kb.insert(",".into(), Command::Seek(SeekDirection::Relative(-500)));
|
||||||
kb.insert("r".into(), "repeat".into());
|
kb.insert("r".into(), Command::Repeat(None));
|
||||||
kb.insert("z".into(), "shuffle".into());
|
kb.insert("z".into(), Command::Shuffle(None));
|
||||||
kb.insert("x".into(), "share current".into());
|
kb.insert("x".into(), Command::Share(TargetMode::Current));
|
||||||
kb.insert("Shift+x".into(), "share selected".into());
|
kb.insert("Shift+x".into(), Command::Share(TargetMode::Selected));
|
||||||
|
|
||||||
kb.insert("F1".into(), "focus queue".into());
|
kb.insert("F1".into(), Command::Focus("queue".into()));
|
||||||
kb.insert("F2".into(), "focus search".into());
|
kb.insert("F2".into(), Command::Focus("search".into()));
|
||||||
kb.insert("F3".into(), "focus library".into());
|
kb.insert("F3".into(), Command::Focus("library".into()));
|
||||||
kb.insert("Backspace".into(), "back".into());
|
kb.insert("Backspace".into(), Command::Back);
|
||||||
|
|
||||||
kb.insert("o".into(), "open".into());
|
kb.insert("o".into(), Command::Open);
|
||||||
kb.insert("a".into(), "goto album".into());
|
kb.insert("a".into(), Command::Goto(GotoMode::Album));
|
||||||
kb.insert("A".into(), "goto artist".into());
|
kb.insert("A".into(), Command::Goto(GotoMode::Artist));
|
||||||
|
|
||||||
kb.insert("Up".into(), "move up".into());
|
kb.insert("Up".into(), Command::Move(MoveMode::Up, None));
|
||||||
kb.insert("Down".into(), "move down".into());
|
kb.insert("Down".into(), Command::Move(MoveMode::Down, None));
|
||||||
kb.insert("Left".into(), "move left".into());
|
kb.insert("Left".into(), Command::Move(MoveMode::Left, None));
|
||||||
kb.insert("Right".into(), "move right".into());
|
kb.insert("Right".into(), Command::Move(MoveMode::Right, None));
|
||||||
kb.insert("PageUp".into(), "move up 5".into());
|
kb.insert("PageUp".into(), Command::Move(MoveMode::Up, Some(5)));
|
||||||
kb.insert("PageDown".into(), "move down 5".into());
|
kb.insert("PageDown".into(), Command::Move(MoveMode::Down, Some(5)));
|
||||||
kb.insert("k".into(), "move up".into());
|
kb.insert("k".into(), Command::Move(MoveMode::Up, None));
|
||||||
kb.insert("j".into(), "move down".into());
|
kb.insert("j".into(), Command::Move(MoveMode::Down, None));
|
||||||
kb.insert("h".into(), "move left".into());
|
kb.insert("h".into(), Command::Move(MoveMode::Left, None));
|
||||||
kb.insert("l".into(), "move right".into());
|
kb.insert("l".into(), Command::Move(MoveMode::Right, None));
|
||||||
|
|
||||||
kb.insert("Shift+Up".into(), "shift up".into());
|
kb.insert("Shift+Up".into(), Command::Shift(ShiftMode::Up, None));
|
||||||
kb.insert("Shift+Down".into(), "shift down".into());
|
kb.insert("Shift+Down".into(), Command::Shift(ShiftMode::Down, None));
|
||||||
|
|
||||||
kb
|
kb
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use command::Command;
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
|
|
||||||
pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b";
|
pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b";
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub keybindings: Option<HashMap<String, String>>,
|
pub keybindings: Option<HashMap<String, Command>>,
|
||||||
pub theme: Option<ConfigTheme>,
|
pub theme: Option<ConfigTheme>,
|
||||||
pub use_nerdfont: Option<bool>,
|
pub use_nerdfont: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/main.rs
11
src/main.rs
@@ -46,6 +46,7 @@ use librespot::core::authentication::Credentials;
|
|||||||
mod album;
|
mod album;
|
||||||
mod artist;
|
mod artist;
|
||||||
mod authentication;
|
mod authentication;
|
||||||
|
mod command;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod events;
|
mod events;
|
||||||
@@ -183,8 +184,8 @@ fn main() {
|
|||||||
cfg.use_nerdfont.unwrap_or(false),
|
cfg.use_nerdfont.unwrap_or(false),
|
||||||
));
|
));
|
||||||
|
|
||||||
let mut cmd_manager = CommandManager::new();
|
let mut cmd_manager = CommandManager::new(spotify.clone(), queue.clone(), library.clone());
|
||||||
cmd_manager.register_all(spotify.clone(), queue.clone(), library.clone());
|
cmd_manager.register_all();
|
||||||
|
|
||||||
let cmd_manager = Arc::new(cmd_manager);
|
let cmd_manager = Arc::new(cmd_manager);
|
||||||
CommandManager::register_keybindings(
|
CommandManager::register_keybindings(
|
||||||
@@ -240,7 +241,11 @@ fn main() {
|
|||||||
let mut main = s.find_id::<ui::layout::Layout>("main").unwrap();
|
let mut main = s.find_id::<ui::layout::Layout>("main").unwrap();
|
||||||
main.clear_cmdline();
|
main.clear_cmdline();
|
||||||
}
|
}
|
||||||
cmd_manager.handle(s, cmd.to_string()[1..].to_string());
|
let c = &cmd[1..];
|
||||||
|
let parsed = command::parse(c);
|
||||||
|
if let Some(parsed) = parsed {
|
||||||
|
cmd_manager.handle(s, parsed);
|
||||||
|
}
|
||||||
ev.trigger();
|
ev.trigger();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use rand::prelude::*;
|
|||||||
use spotify::Spotify;
|
use spotify::Spotify;
|
||||||
use track::Track;
|
use track::Track;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
pub enum RepeatSetting {
|
pub enum RepeatSetting {
|
||||||
None,
|
None,
|
||||||
RepeatPlaylist,
|
RepeatPlaylist,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use cursive::Cursive;
|
|||||||
|
|
||||||
use album::Album;
|
use album::Album;
|
||||||
use artist::Artist;
|
use artist::Artist;
|
||||||
|
use command::Command;
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
use library::Library;
|
use library::Library;
|
||||||
use queue::Queue;
|
use queue::Queue;
|
||||||
@@ -34,25 +35,14 @@ pub trait ViewExt: View {
|
|||||||
"".into()
|
"".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_command(
|
fn on_command(&mut self, _s: &mut Cursive, _cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
|
||||||
_s: &mut Cursive,
|
|
||||||
_cmd: &str,
|
|
||||||
_args: &[String],
|
|
||||||
) -> Result<CommandResult, String> {
|
|
||||||
Ok(CommandResult::Ignored)
|
Ok(CommandResult::Ignored)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: ViewExt> ViewExt for IdView<V> {
|
impl<V: ViewExt> ViewExt for IdView<V> {
|
||||||
fn on_command(
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
self.with_view_mut(move |v| v.on_command(s, cmd)).unwrap()
|
||||||
s: &mut Cursive,
|
|
||||||
cmd: &str,
|
|
||||||
args: &[String],
|
|
||||||
) -> Result<CommandResult, String> {
|
|
||||||
self.with_view_mut(move |v| v.on_command(s, cmd, args))
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use cursive::Cursive;
|
|||||||
|
|
||||||
use album::Album;
|
use album::Album;
|
||||||
use artist::Artist;
|
use artist::Artist;
|
||||||
|
use command::Command;
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
use library::Library;
|
use library::Library;
|
||||||
use queue::Queue;
|
use queue::Queue;
|
||||||
@@ -69,12 +70,7 @@ impl ViewExt for AlbumView {
|
|||||||
format!("{} ({})", self.album.title, self.album.year)
|
format!("{} ({})", self.album.title, self.album.year)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_command(
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
self.tabs.on_command(s, cmd)
|
||||||
s: &mut Cursive,
|
|
||||||
cmd: &str,
|
|
||||||
args: &[String],
|
|
||||||
) -> Result<CommandResult, String> {
|
|
||||||
self.tabs.on_command(s, cmd, args)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use cursive::view::ViewWrapper;
|
|||||||
use cursive::Cursive;
|
use cursive::Cursive;
|
||||||
|
|
||||||
use artist::Artist;
|
use artist::Artist;
|
||||||
|
use command::Command;
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
use library::Library;
|
use library::Library;
|
||||||
use queue::Queue;
|
use queue::Queue;
|
||||||
@@ -110,12 +111,7 @@ impl ViewExt for ArtistView {
|
|||||||
self.artist.name.clone()
|
self.artist.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_command(
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
self.tabs.on_command(s, cmd)
|
||||||
s: &mut Cursive,
|
|
||||||
cmd: &str,
|
|
||||||
args: &[String],
|
|
||||||
) -> Result<CommandResult, String> {
|
|
||||||
self.tabs.on_command(s, cmd, args)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use cursive::views::EditView;
|
|||||||
use cursive::{Cursive, Printer};
|
use cursive::{Cursive, Printer};
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use command::Command;
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
use events;
|
use events;
|
||||||
use traits::{IntoBoxedViewExt, ViewExt};
|
use traits::{IntoBoxedViewExt, ViewExt};
|
||||||
@@ -205,6 +206,25 @@ impl View for Layout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn layout(&mut self, size: Vec2) {
|
||||||
|
self.last_size = size;
|
||||||
|
|
||||||
|
self.statusbar.layout(Vec2::new(size.x, 2));
|
||||||
|
|
||||||
|
self.cmdline.layout(Vec2::new(size.x, 1));
|
||||||
|
|
||||||
|
if let Some(screen) = self.get_current_screen_mut() {
|
||||||
|
screen.view.layout(Vec2::new(size.x, size.y - 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
// the focus view has changed, let the views know so they can redraw
|
||||||
|
// their items
|
||||||
|
if self.screenchange {
|
||||||
|
debug!("layout: new screen selected: {:?}", self.focus);
|
||||||
|
self.screenchange = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||||
Vec2::new(constraint.x, constraint.y)
|
Vec2::new(constraint.x, constraint.y)
|
||||||
}
|
}
|
||||||
@@ -244,25 +264,6 @@ impl View for Layout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(&mut self, size: Vec2) {
|
|
||||||
self.last_size = size;
|
|
||||||
|
|
||||||
self.statusbar.layout(Vec2::new(size.x, 2));
|
|
||||||
|
|
||||||
self.cmdline.layout(Vec2::new(size.x, 1));
|
|
||||||
|
|
||||||
if let Some(screen) = self.get_current_screen_mut() {
|
|
||||||
screen.view.layout(Vec2::new(size.x, size.y - 3));
|
|
||||||
}
|
|
||||||
|
|
||||||
// the focus view has changed, let the views know so they can redraw
|
|
||||||
// their items
|
|
||||||
if self.screenchange {
|
|
||||||
debug!("layout: new screen selected: {:?}", self.focus);
|
|
||||||
self.screenchange = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call_on_any<'a>(&mut self, s: &Selector, c: AnyCb<'a>) {
|
fn call_on_any<'a>(&mut self, s: &Selector, c: AnyCb<'a>) {
|
||||||
if let Some(screen) = self.get_current_screen_mut() {
|
if let Some(screen) = self.get_current_screen_mut() {
|
||||||
screen.view.call_on_any(s, c);
|
screen.view.call_on_any(s, c);
|
||||||
@@ -283,29 +284,28 @@ impl View for Layout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ViewExt for Layout {
|
impl ViewExt for Layout {
|
||||||
fn on_command(
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
match cmd {
|
||||||
s: &mut Cursive,
|
Command::Focus(view) => {
|
||||||
cmd: &str,
|
|
||||||
args: &[String],
|
|
||||||
) -> Result<CommandResult, String> {
|
|
||||||
if cmd == "focus" {
|
|
||||||
if let Some(view) = args.get(0) {
|
|
||||||
if self.views.keys().any(|k| k == view) {
|
if self.views.keys().any(|k| k == view) {
|
||||||
self.set_view(view.clone());
|
self.set_view(view.clone());
|
||||||
let screen = self.views.get_mut(view).unwrap();
|
let screen = self.views.get_mut(view).unwrap();
|
||||||
screen.view.on_command(s, cmd, args)?;
|
screen.view.on_command(s, cmd)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CommandResult::Consumed(None))
|
||||||
|
}
|
||||||
|
Command::Back => {
|
||||||
|
self.pop_view();
|
||||||
|
Ok(CommandResult::Consumed(None))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(screen) = self.get_current_screen_mut() {
|
||||||
|
screen.view.on_command(s, cmd)
|
||||||
|
} else {
|
||||||
|
Ok(CommandResult::Ignored)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(CommandResult::Consumed(None))
|
|
||||||
} else if cmd == "back" {
|
|
||||||
self.pop_view();
|
|
||||||
Ok(CommandResult::Consumed(None))
|
|
||||||
} else if let Some(screen) = self.get_current_screen_mut() {
|
|
||||||
screen.view.on_command(s, cmd, args)
|
|
||||||
} else {
|
|
||||||
Ok(CommandResult::Ignored)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::sync::Arc;
|
|||||||
use cursive::view::ViewWrapper;
|
use cursive::view::ViewWrapper;
|
||||||
use cursive::Cursive;
|
use cursive::Cursive;
|
||||||
|
|
||||||
|
use command::Command;
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
use library::Library;
|
use library::Library;
|
||||||
use queue::Queue;
|
use queue::Queue;
|
||||||
@@ -48,12 +49,7 @@ impl ViewWrapper for LibraryView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ViewExt for LibraryView {
|
impl ViewExt for LibraryView {
|
||||||
fn on_command(
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
self.tabs.on_command(s, cmd)
|
||||||
s: &mut Cursive,
|
|
||||||
cmd: &str,
|
|
||||||
args: &[String],
|
|
||||||
) -> Result<CommandResult, String> {
|
|
||||||
self.tabs.on_command(s, cmd, args)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use cursive::{Cursive, Printer, Rect, Vec2};
|
|||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use clipboard::{ClipboardContext, ClipboardProvider};
|
use clipboard::{ClipboardContext, ClipboardProvider};
|
||||||
|
use command::{Command, GotoMode, MoveMode, TargetMode};
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
use library::Library;
|
use library::Library;
|
||||||
use queue::Queue;
|
use queue::Queue;
|
||||||
@@ -277,123 +278,116 @@ impl<I: ListItem> View for ListView<I> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<I: ListItem + Clone> ViewExt for ListView<I> {
|
impl<I: ListItem + Clone> ViewExt for ListView<I> {
|
||||||
fn on_command(
|
fn on_command(&mut self, _s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
match cmd {
|
||||||
_s: &mut Cursive,
|
Command::Play => {
|
||||||
cmd: &str,
|
self.queue.clear();
|
||||||
args: &[String],
|
|
||||||
) -> Result<CommandResult, String> {
|
|
||||||
if cmd == "play" {
|
|
||||||
self.queue.clear();
|
|
||||||
|
|
||||||
if !self.attempt_play_all_tracks() {
|
if !self.attempt_play_all_tracks() {
|
||||||
|
let mut content = self.content.write().unwrap();
|
||||||
|
if let Some(item) = content.get_mut(self.selected) {
|
||||||
|
item.play(self.queue.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(CommandResult::Consumed(None));
|
||||||
|
}
|
||||||
|
Command::Queue => {
|
||||||
let mut content = self.content.write().unwrap();
|
let mut content = self.content.write().unwrap();
|
||||||
if let Some(item) = content.get_mut(self.selected) {
|
if let Some(item) = content.get_mut(self.selected) {
|
||||||
item.play(self.queue.clone());
|
item.queue(self.queue.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(CommandResult::Consumed(None));
|
||||||
|
}
|
||||||
|
Command::Save => {
|
||||||
|
let mut item = {
|
||||||
|
let content = self.content.read().unwrap();
|
||||||
|
content.get(self.selected).cloned()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(item) = item.as_mut() {
|
||||||
|
item.toggle_saved(self.library.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(CommandResult::Consumed(None));
|
Command::Share(mode) => {
|
||||||
}
|
let url = match mode {
|
||||||
|
TargetMode::Selected => self.content.read().ok().and_then(|content| {
|
||||||
if cmd == "queue" {
|
|
||||||
let mut content = self.content.write().unwrap();
|
|
||||||
if let Some(item) = content.get_mut(self.selected) {
|
|
||||||
item.queue(self.queue.clone());
|
|
||||||
}
|
|
||||||
return Ok(CommandResult::Consumed(None));
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd == "save" {
|
|
||||||
let mut item = {
|
|
||||||
let content = self.content.read().unwrap();
|
|
||||||
content.get(self.selected).cloned()
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(item) = item.as_mut() {
|
|
||||||
item.toggle_saved(self.library.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd == "share" {
|
|
||||||
let source = args.get(0);
|
|
||||||
let url =
|
|
||||||
source.and_then(|source| match source.as_str() {
|
|
||||||
"selected" => self.content.read().ok().and_then(|content| {
|
|
||||||
content.get(self.selected).and_then(ListItem::share_url)
|
content.get(self.selected).and_then(ListItem::share_url)
|
||||||
}),
|
}),
|
||||||
"current" => self.queue.get_current().and_then(|t| t.share_url()),
|
TargetMode::Current => self.queue.get_current().and_then(|t| t.share_url()),
|
||||||
_ => None,
|
};
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(url) = url {
|
if let Some(url) = url {
|
||||||
ClipboardProvider::new()
|
ClipboardProvider::new()
|
||||||
.and_then(|mut ctx: ClipboardContext| ctx.set_contents(url))
|
.and_then(|mut ctx: ClipboardContext| ctx.set_contents(url))
|
||||||
.ok();
|
.ok();
|
||||||
};
|
}
|
||||||
|
|
||||||
return Ok(CommandResult::Consumed(None));
|
return Ok(CommandResult::Consumed(None));
|
||||||
}
|
}
|
||||||
|
Command::Move(mode, amount) => {
|
||||||
if cmd == "move" {
|
let amount = match amount {
|
||||||
if let Some(dir) = args.get(0) {
|
Some(amount) => *amount,
|
||||||
let amount: usize = args
|
_ => 1,
|
||||||
.get(1)
|
};
|
||||||
.unwrap_or(&"1".to_string())
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
|
||||||
|
|
||||||
let len = self.content.read().unwrap().len();
|
let len = self.content.read().unwrap().len();
|
||||||
|
|
||||||
if dir == "up" && self.selected > 0 {
|
match mode {
|
||||||
self.move_focus(-(amount as i32));
|
MoveMode::Up if self.selected > 0 => {
|
||||||
return Ok(CommandResult::Consumed(None));
|
self.move_focus(-(amount as i32));
|
||||||
}
|
return Ok(CommandResult::Consumed(None));
|
||||||
|
}
|
||||||
if dir == "down" {
|
MoveMode::Down if self.selected < len.saturating_sub(1) => {
|
||||||
if self.selected < len.saturating_sub(1) {
|
|
||||||
self.move_focus(amount as i32);
|
self.move_focus(amount as i32);
|
||||||
return Ok(CommandResult::Consumed(None));
|
return Ok(CommandResult::Consumed(None));
|
||||||
} else if self.selected == len.saturating_sub(1) && self.can_paginate() {
|
}
|
||||||
|
MoveMode::Down
|
||||||
|
if self.selected == len.saturating_sub(1) && self.can_paginate() =>
|
||||||
|
{
|
||||||
self.pagination.call(&self.content);
|
self.pagination.call(&self.content);
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Command::Open => {
|
||||||
|
let mut content = self.content.write().unwrap();
|
||||||
if cmd == "open" {
|
if let Some(item) = content.get_mut(self.selected) {
|
||||||
let mut content = self.content.write().unwrap();
|
let queue = self.queue.clone();
|
||||||
if let Some(item) = content.get_mut(self.selected) {
|
let library = self.library.clone();
|
||||||
let queue = self.queue.clone();
|
if let Some(view) = item.open(queue, library) {
|
||||||
let library = self.library.clone();
|
|
||||||
if let Some(view) = item.open(queue, library) {
|
|
||||||
return Ok(CommandResult::View(view));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd == "goto" {
|
|
||||||
let mut content = self.content.write().unwrap();
|
|
||||||
if let Some(item) = content.get_mut(self.selected) {
|
|
||||||
let queue = self.queue.clone();
|
|
||||||
let library = self.library.clone();
|
|
||||||
let arg = args.get(0).cloned().unwrap_or_default();
|
|
||||||
|
|
||||||
if arg == "album" {
|
|
||||||
if let Some(album) = item.album(queue.clone()) {
|
|
||||||
let view = AlbumView::new(queue, library, &album).as_boxed_view_ext();
|
|
||||||
return Ok(CommandResult::View(view));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if arg == "artist" {
|
|
||||||
if let Some(artist) = item.artist() {
|
|
||||||
let view = ArtistView::new(queue, library, &artist).as_boxed_view_ext();
|
|
||||||
return Ok(CommandResult::View(view));
|
return Ok(CommandResult::View(view));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Command::Goto(mode) => {
|
||||||
|
let mut content = self.content.write().unwrap();
|
||||||
|
if let Some(item) = content.get_mut(self.selected) {
|
||||||
|
let queue = self.queue.clone();
|
||||||
|
let library = self.library.clone();
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
GotoMode::Album => {
|
||||||
|
if let Some(album) = item.album(queue.clone()) {
|
||||||
|
let view =
|
||||||
|
AlbumView::new(queue, library, &album).as_boxed_view_ext();
|
||||||
|
return Ok(CommandResult::View(view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GotoMode::Artist => {
|
||||||
|
if let Some(artist) = item.artist() {
|
||||||
|
let view =
|
||||||
|
ArtistView::new(queue, library, &artist).as_boxed_view_ext();
|
||||||
|
return Ok(CommandResult::View(view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(CommandResult::Ignored)
|
Ok(CommandResult::Ignored)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::sync::{Arc, RwLock};
|
|||||||
use cursive::view::ViewWrapper;
|
use cursive::view::ViewWrapper;
|
||||||
use cursive::Cursive;
|
use cursive::Cursive;
|
||||||
|
|
||||||
|
use command::Command;
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
use library::Library;
|
use library::Library;
|
||||||
use playlist::Playlist;
|
use playlist::Playlist;
|
||||||
@@ -43,12 +44,7 @@ impl ViewExt for PlaylistView {
|
|||||||
self.playlist.name.clone()
|
self.playlist.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_command(
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
self.list.on_command(s, cmd)
|
||||||
s: &mut Cursive,
|
|
||||||
cmd: &str,
|
|
||||||
args: &[String],
|
|
||||||
) -> Result<CommandResult, String> {
|
|
||||||
self.list.on_command(s, cmd, args)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use cursive::view::ViewWrapper;
|
|||||||
use cursive::views::Dialog;
|
use cursive::views::Dialog;
|
||||||
use cursive::Cursive;
|
use cursive::Cursive;
|
||||||
|
|
||||||
|
use command::Command;
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
use library::Library;
|
use library::Library;
|
||||||
use playlist::Playlist;
|
use playlist::Playlist;
|
||||||
@@ -52,19 +53,14 @@ impl ViewWrapper for PlaylistsView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ViewExt for PlaylistsView {
|
impl ViewExt for PlaylistsView {
|
||||||
fn on_command(
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
if let Command::Delete = cmd {
|
||||||
s: &mut Cursive,
|
|
||||||
cmd: &str,
|
|
||||||
args: &[String],
|
|
||||||
) -> Result<CommandResult, String> {
|
|
||||||
if cmd == "delete" {
|
|
||||||
if let Some(dialog) = self.delete_dialog() {
|
if let Some(dialog) = self.delete_dialog() {
|
||||||
s.add_layer(dialog);
|
s.add_layer(dialog);
|
||||||
}
|
}
|
||||||
return Ok(CommandResult::Consumed(None));
|
return Ok(CommandResult::Consumed(None));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.list.on_command(s, cmd, args)
|
self.list.on_command(s, cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use cursive::Cursive;
|
|||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use command::{Command, ShiftMode};
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
use library::Library;
|
use library::Library;
|
||||||
use queue::Queue;
|
use queue::Queue;
|
||||||
@@ -88,55 +89,52 @@ impl ViewWrapper for QueueView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ViewExt for QueueView {
|
impl ViewExt for QueueView {
|
||||||
fn on_command(
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
match cmd {
|
||||||
s: &mut Cursive,
|
Command::Play => {
|
||||||
cmd: &str,
|
self.queue.play(self.list.get_selected_index(), true);
|
||||||
args: &[String],
|
return Ok(CommandResult::Consumed(None));
|
||||||
) -> Result<CommandResult, String> {
|
}
|
||||||
if cmd == "play" {
|
Command::Queue => {
|
||||||
self.queue.play(self.list.get_selected_index(), true);
|
return Ok(CommandResult::Ignored);
|
||||||
return Ok(CommandResult::Consumed(None));
|
}
|
||||||
}
|
Command::Delete => {
|
||||||
|
self.queue.remove(self.list.get_selected_index());
|
||||||
|
return Ok(CommandResult::Consumed(None));
|
||||||
|
}
|
||||||
|
Command::Shift(mode, amount) => {
|
||||||
|
let amount = match amount {
|
||||||
|
Some(amount) => *amount,
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
|
||||||
if cmd == "queue" {
|
|
||||||
return Ok(CommandResult::Ignored);
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd == "delete" {
|
|
||||||
self.queue.remove(self.list.get_selected_index());
|
|
||||||
return Ok(CommandResult::Consumed(None));
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd == "shift" {
|
|
||||||
if let Some(dir) = args.get(0) {
|
|
||||||
let amount: usize = args
|
|
||||||
.get(1)
|
|
||||||
.unwrap_or(&"1".to_string())
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
|
||||||
let selected = self.list.get_selected_index();
|
let selected = self.list.get_selected_index();
|
||||||
let len = self.queue.len();
|
let len = self.queue.len();
|
||||||
if dir == "up" && selected > 0 {
|
|
||||||
self.queue.shift(selected, selected.saturating_sub(amount));
|
match mode {
|
||||||
self.list.move_focus(-(amount as i32));
|
ShiftMode::Up if selected > 0 => {
|
||||||
return Ok(CommandResult::Consumed(None));
|
self.queue
|
||||||
} else if dir == "down" && selected < len.saturating_sub(1) {
|
.shift(selected, (selected as i32).saturating_sub(amount) as usize);
|
||||||
self.queue
|
self.list.move_focus(-(amount as i32));
|
||||||
.shift(selected, min(selected + amount as usize, len - 1));
|
return Ok(CommandResult::Consumed(None));
|
||||||
self.list.move_focus(amount as i32);
|
}
|
||||||
return Ok(CommandResult::Consumed(None));
|
ShiftMode::Down if selected < len.saturating_sub(1) => {
|
||||||
|
self.queue
|
||||||
|
.shift(selected, min(selected + amount as usize, len - 1));
|
||||||
|
self.list.move_focus(amount as i32);
|
||||||
|
return Ok(CommandResult::Consumed(None));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Command::SaveQueue => {
|
||||||
|
let dialog = Self::save_dialog(self.queue.clone(), self.library.clone());
|
||||||
|
s.add_layer(dialog);
|
||||||
|
return Ok(CommandResult::Consumed(None));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd == "save" && args.get(0).unwrap_or(&"".to_string()) == "queue" {
|
self.with_view_mut(move |v| v.on_command(s, cmd)).unwrap()
|
||||||
let dialog = Self::save_dialog(self.queue.clone(), self.library.clone());
|
|
||||||
s.add_layer(dialog);
|
|
||||||
return Ok(CommandResult::Consumed(None));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.with_view_mut(move |v| v.on_command(s, cmd, args))
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use std::sync::{Arc, Mutex, RwLock};
|
|||||||
|
|
||||||
use album::Album;
|
use album::Album;
|
||||||
use artist::Artist;
|
use artist::Artist;
|
||||||
|
use command::{Command, MoveMode};
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
use events::EventManager;
|
use events::EventManager;
|
||||||
use library::Library;
|
use library::Library;
|
||||||
@@ -411,6 +412,14 @@ impl View for SearchView {
|
|||||||
self.tabs.layout(Vec2::new(size.x, size.y - 1));
|
self.tabs.layout(Vec2::new(size.x, size.y - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_event(&mut self, event: Event) -> EventResult {
|
||||||
|
if self.edit_focused {
|
||||||
|
self.edit.on_event(event)
|
||||||
|
} else {
|
||||||
|
self.tabs.on_event(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn call_on_any<'a>(&mut self, selector: &Selector<'_>, mut callback: AnyCb<'a>) {
|
fn call_on_any<'a>(&mut self, selector: &Selector<'_>, mut callback: AnyCb<'a>) {
|
||||||
self.edit.call_on_any(selector, Box::new(|v| callback(v)));
|
self.edit.call_on_any(selector, Box::new(|v| callback(v)));
|
||||||
self.tabs.call_on_any(selector, Box::new(|v| callback(v)));
|
self.tabs.call_on_any(selector, Box::new(|v| callback(v)));
|
||||||
@@ -424,52 +433,38 @@ impl View for SearchView {
|
|||||||
Err(())
|
Err(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_event(&mut self, event: Event) -> EventResult {
|
|
||||||
if self.edit_focused {
|
|
||||||
self.edit.on_event(event)
|
|
||||||
} else {
|
|
||||||
self.tabs.on_event(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewExt for SearchView {
|
impl ViewExt for SearchView {
|
||||||
fn on_command(
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
match cmd {
|
||||||
s: &mut Cursive,
|
Command::Search(query) => self.run_search(query.to_string()),
|
||||||
cmd: &str,
|
Command::Focus(_) => {
|
||||||
args: &[String],
|
self.edit_focused = true;
|
||||||
) -> Result<CommandResult, String> {
|
self.clear();
|
||||||
if cmd == "search" && !args.is_empty() {
|
return Ok(CommandResult::Consumed(None));
|
||||||
self.run_search(args.join(" "));
|
}
|
||||||
return Ok(CommandResult::Consumed(None));
|
_ => {}
|
||||||
}
|
|
||||||
|
|
||||||
if cmd == "focus" {
|
|
||||||
self.edit_focused = true;
|
|
||||||
self.clear();
|
|
||||||
return Ok(CommandResult::Consumed(None));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = if !self.edit_focused {
|
let result = if !self.edit_focused {
|
||||||
self.tabs.on_command(s, cmd, args)?
|
self.tabs.on_command(s, cmd)?
|
||||||
} else {
|
} else {
|
||||||
CommandResult::Ignored
|
CommandResult::Ignored
|
||||||
};
|
};
|
||||||
|
|
||||||
if let CommandResult::Ignored = result {
|
if let CommandResult::Ignored = result {
|
||||||
if cmd == "move" {
|
if let Command::Move(mode, _) = cmd {
|
||||||
if let Some(dir) = args.get(0) {
|
match mode {
|
||||||
if dir == "up" && !self.edit_focused {
|
MoveMode::Up if !self.edit_focused => {
|
||||||
self.edit_focused = true;
|
self.edit_focused = true;
|
||||||
return Ok(CommandResult::Consumed(None));
|
return Ok(CommandResult::Consumed(None));
|
||||||
}
|
}
|
||||||
|
MoveMode::Down if self.edit_focused => {
|
||||||
if dir == "down" && self.edit_focused {
|
|
||||||
self.edit_focused = false;
|
self.edit_focused = false;
|
||||||
return Ok(CommandResult::Consumed(None));
|
return Ok(CommandResult::Consumed(None));
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use cursive::traits::View;
|
|||||||
use cursive::{Cursive, Printer, Vec2};
|
use cursive::{Cursive, Printer, Vec2};
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use command::{Command, MoveMode};
|
||||||
use commands::CommandResult;
|
use commands::CommandResult;
|
||||||
use traits::{IntoBoxedViewExt, ViewExt};
|
use traits::{IntoBoxedViewExt, ViewExt};
|
||||||
|
|
||||||
@@ -102,36 +103,30 @@ impl View for TabView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ViewExt for TabView {
|
impl ViewExt for TabView {
|
||||||
fn on_command(
|
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
|
||||||
&mut self,
|
if let Command::Move(mode, amount) = cmd {
|
||||||
s: &mut Cursive,
|
let amount = match amount {
|
||||||
cmd: &str,
|
Some(amount) => *amount,
|
||||||
args: &[String],
|
_ => 1,
|
||||||
) -> Result<CommandResult, String> {
|
};
|
||||||
if cmd == "move" {
|
|
||||||
if let Some(dir) = args.get(0) {
|
|
||||||
let amount: i32 = args
|
|
||||||
.get(1)
|
|
||||||
.unwrap_or(&"1".to_string())
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
|
||||||
|
|
||||||
let len = self.tabs.len();
|
let len = self.tabs.len();
|
||||||
|
|
||||||
if dir == "left" && self.selected > 0 {
|
match mode {
|
||||||
self.move_focus(-amount);
|
MoveMode::Left if self.selected > 0 => {
|
||||||
|
self.move_focus(-(amount as i32));
|
||||||
return Ok(CommandResult::Consumed(None));
|
return Ok(CommandResult::Consumed(None));
|
||||||
}
|
}
|
||||||
|
MoveMode::Right if self.selected < len - 1 => {
|
||||||
if dir == "right" && self.selected < len - 1 {
|
self.move_focus(amount as i32);
|
||||||
self.move_focus(amount);
|
|
||||||
return Ok(CommandResult::Consumed(None));
|
return Ok(CommandResult::Consumed(None));
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(tab) = self.tabs.get_mut(self.selected) {
|
if let Some(tab) = self.tabs.get_mut(self.selected) {
|
||||||
tab.view.on_command(s, cmd, args)
|
tab.view.on_command(s, cmd)
|
||||||
} else {
|
} else {
|
||||||
Ok(CommandResult::Ignored)
|
Ok(CommandResult::Ignored)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user