mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-27 18:08:11 +08:00
Restyle and refactor shortcut labels to send hints bar and welcome screen layouts from Rust (#3447)
* Restyle UserInputLabel and refactor its usages to have all input its data sent from Rust * Replace the welcome screen quick buttons with ones sent by backend * Add the ShortcutLabel widget to the backend * Replace hints bar with a backend-controlled layout; show mouse icons in place of mouse labels
This commit is contained in:
@@ -7,7 +7,7 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayProvid
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use crate::messages::tool::utility_types::{HintData, ToolType};
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
use graphene_std::raster::color::Color;
|
||||
|
||||
@@ -190,7 +190,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
||||
|
||||
responses.add(OverlaysMessage::RemoveProvider { provider: ARTBOARD_OVERLAY_PROVIDER });
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data: Default::default() });
|
||||
HintData::clear_layout(responses);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: Default::default() });
|
||||
|
||||
self.tool_is_active = false;
|
||||
|
||||
@@ -601,7 +601,7 @@ impl Fsm for ArtboardToolFsmState {
|
||||
]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -464,7 +464,7 @@ impl Fsm for BrushToolFsmState {
|
||||
BrushToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -138,7 +138,7 @@ impl Fsm for EyedropperToolFsmState {
|
||||
}
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -158,7 +158,7 @@ impl Fsm for FillToolFsmState {
|
||||
FillToolFsmState::Filling => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -346,7 +346,7 @@ impl Fsm for FreehandToolFsmState {
|
||||
FreehandToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -530,7 +530,7 @@ impl Fsm for GradientToolFsmState {
|
||||
]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -166,7 +166,7 @@ impl Fsm for NavigateToolFsmState {
|
||||
]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::consts::{
|
||||
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DEFAULT_STROKE_WIDTH, DOUBLE_CLICK_MILLISECONDS, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD,
|
||||
DRILL_THROUGH_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE,
|
||||
};
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
||||
use crate::messages::input_mapper::utility_types::macros::action_shortcut_manual;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::{path_overlays, selected_segments};
|
||||
@@ -270,7 +270,7 @@ impl LayoutHolder for PathTool {
|
||||
.icon("Dot")
|
||||
.tooltip_label("Point Editing Mode")
|
||||
.tooltip_description("To multi-select modes, perform the shortcut shown.")
|
||||
.tooltip_shortcut(KeysGroup(vec![Key::Shift, Key::MouseLeft]).to_string())
|
||||
.tooltip_shortcut(action_shortcut_manual!(Key::Shift, Key::MouseLeft))
|
||||
.on_update(|_| PathToolMessage::TogglePointEditing.into())
|
||||
.widget_holder();
|
||||
let segment_editing_mode = CheckboxInput::new(self.options.path_editing_mode.segment_editing_mode)
|
||||
@@ -278,7 +278,7 @@ impl LayoutHolder for PathTool {
|
||||
.icon("Remove")
|
||||
.tooltip_label("Segment Editing Mode")
|
||||
.tooltip_description("To multi-select modes, perform the shortcut shown.")
|
||||
.tooltip_shortcut(KeysGroup(vec![Key::Shift, Key::MouseLeft]).to_string())
|
||||
.tooltip_shortcut(action_shortcut_manual!(Key::Shift, Key::MouseLeft))
|
||||
.on_update(|_| PathToolMessage::ToggleSegmentEditing.into())
|
||||
.widget_holder();
|
||||
|
||||
@@ -3589,5 +3589,5 @@ fn update_dynamic_hints(
|
||||
]),
|
||||
PathToolFsmState::SlidingPoint => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
|
||||
};
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
@@ -2288,7 +2288,7 @@ impl Fsm for PenToolFsmState {
|
||||
}
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -1728,7 +1728,7 @@ impl Fsm for SelectToolFsmState {
|
||||
HintInfo::keys([Key::Control, Key::KeyD], "Duplicate").add_mac_keys([Key::Command, Key::KeyD]),
|
||||
]),
|
||||
]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
SelectToolFsmState::Dragging { axis, using_compass, has_dragged, .. } if *has_dragged => {
|
||||
let mut hint_data = vec![
|
||||
@@ -1743,7 +1743,7 @@ impl Fsm for SelectToolFsmState {
|
||||
hint_data.push(HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain to Axis")]));
|
||||
};
|
||||
let hint_data = HintData(hint_data);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
SelectToolFsmState::Drawing { has_drawn, .. } if *has_drawn => {
|
||||
let hint_data = HintData(vec![
|
||||
@@ -1753,7 +1753,7 @@ impl Fsm for SelectToolFsmState {
|
||||
// TODO: (See https://discord.com/channels/731730685944922173/1216976541947531264/1321360311298818048)
|
||||
// HintGroup(vec![HintInfo::keys([Key::Shift], "Extend")])
|
||||
]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
SelectToolFsmState::Drawing { .. } | SelectToolFsmState::Dragging { .. } => {}
|
||||
SelectToolFsmState::ResizingBounds => {
|
||||
@@ -1761,25 +1761,25 @@ impl Fsm for SelectToolFsmState {
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Alt], "From Pivot"), HintInfo::keys([Key::Shift], "Preserve Aspect Ratio")]),
|
||||
]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
SelectToolFsmState::RotatingBounds => {
|
||||
let hint_data = HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]),
|
||||
]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
SelectToolFsmState::SkewingBounds { .. } => {
|
||||
let hint_data = HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Control], "Unlock Slide")]),
|
||||
]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
SelectToolFsmState::DraggingPivot => {
|
||||
let hint_data = HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1187,5 +1187,5 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
|
||||
]),
|
||||
ShapeToolFsmState::ModifyingGizmo => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
|
||||
};
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
@@ -503,7 +503,7 @@ impl Fsm for SplineToolFsmState {
|
||||
SplineToolFsmState::MergingEndpoints => HintData(vec![]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -939,7 +939,7 @@ impl Fsm for TextToolFsmState {
|
||||
]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
hint_data.send_layout(responses);
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -4,9 +4,9 @@ use super::common_functionality::shape_editor::ShapeState;
|
||||
use super::tool_messages::*;
|
||||
use crate::messages::broadcast::BroadcastMessage;
|
||||
use crate::messages::broadcast::event::EventMessage;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, LayoutKeysGroup, MouseMotion};
|
||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, LabeledKeyOrMouseMotion, LabeledShortcut, MouseMotion};
|
||||
use crate::messages::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual};
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
|
||||
use crate::messages::preferences::PreferencesMessageHandler;
|
||||
@@ -125,12 +125,12 @@ impl DocumentToolData {
|
||||
widgets: vec![
|
||||
IconButton::new("SwapVertical", 16)
|
||||
.tooltip_label("Swap")
|
||||
.shortcut_keys(action_keys!(ToolMessageDiscriminant::SwapColors))
|
||||
.tooltip_shortcut(action_shortcut!(ToolMessageDiscriminant::SwapColors))
|
||||
.on_update(|_| ToolMessage::SwapColors.into())
|
||||
.widget_holder(),
|
||||
IconButton::new("WorkingColors", 16)
|
||||
.tooltip_label("Reset")
|
||||
.shortcut_keys(action_keys!(ToolMessageDiscriminant::ResetColors))
|
||||
.tooltip_shortcut(action_shortcut!(ToolMessageDiscriminant::ResetColors))
|
||||
.on_update(|_| ToolMessage::ResetColors.into())
|
||||
.widget_holder(),
|
||||
],
|
||||
@@ -245,12 +245,12 @@ impl LayoutHolder for ToolData {
|
||||
ToolAvailability::Available(tool) =>
|
||||
ToolEntry::new(tool.tool_type(), tool.icon_name())
|
||||
.tooltip_label(tool.tooltip_label())
|
||||
.shortcut_keys(action_keys!(tool_type_to_activate_tool_message(tool.tool_type()))),
|
||||
.tooltip_shortcut(action_shortcut!(tool_type_to_activate_tool_message(tool.tool_type()))),
|
||||
ToolAvailability::AvailableAsShape(shape) =>
|
||||
ToolEntry::new(shape.tool_type(), shape.icon_name())
|
||||
.tooltip_label(shape.tooltip_label())
|
||||
.tooltip_description(shape.tooltip_description())
|
||||
.shortcut_keys(action_keys!(tool_type_to_activate_tool_message(shape.tool_type()))),
|
||||
.tooltip_shortcut(action_shortcut!(tool_type_to_activate_tool_message(shape.tool_type()))),
|
||||
ToolAvailability::ComingSoon(tool) => tool.clone(),
|
||||
}
|
||||
})
|
||||
@@ -258,7 +258,7 @@ impl LayoutHolder for ToolData {
|
||||
)
|
||||
.flat_map(|group| {
|
||||
let separator = std::iter::once(Separator::new(SeparatorType::Section).direction(SeparatorDirection::Vertical).widget_holder());
|
||||
let buttons = group.into_iter().map(|ToolEntry { tooltip_label, tooltip_description, tooltip_shortcut, shortcut_keys, tool_type, icon_name }| {
|
||||
let buttons = group.into_iter().map(|ToolEntry { tooltip_label, tooltip_description, tooltip_shortcut, tool_type, icon_name }| {
|
||||
let coming_soon = tooltip_description.contains("Coming soon.");
|
||||
|
||||
IconButton::new(icon_name, 32)
|
||||
@@ -270,7 +270,6 @@ impl LayoutHolder for ToolData {
|
||||
.tooltip_label(tooltip_label.clone())
|
||||
.tooltip_description(tooltip_description)
|
||||
.tooltip_shortcut(tooltip_shortcut)
|
||||
.shortcut_keys(shortcut_keys)
|
||||
.on_update(move |_| {
|
||||
match tool_type {
|
||||
ToolType::Line => ToolMessage::ActivateToolShapeLine.into(),
|
||||
@@ -306,8 +305,7 @@ pub struct ToolEntry {
|
||||
pub icon_name: String,
|
||||
pub tooltip_label: String,
|
||||
pub tooltip_description: String,
|
||||
pub tooltip_shortcut: String,
|
||||
pub shortcut_keys: Option<ActionKeys>,
|
||||
pub tooltip_shortcut: Option<ActionShortcut>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -429,26 +427,26 @@ fn list_tools_in_groups() -> Vec<Vec<ToolAvailability>> {
|
||||
ToolEntry::new(ToolType::Heal, "RasterHealTool")
|
||||
.tooltip_label("Heal Tool")
|
||||
.tooltip_description("Coming soon.")
|
||||
.tooltip_shortcut(Key::KeyJ.to_string()),
|
||||
.tooltip_shortcut(action_shortcut_manual!(Key::KeyJ)),
|
||||
),
|
||||
ToolAvailability::ComingSoon(
|
||||
ToolEntry::new(ToolType::Clone, "RasterCloneTool")
|
||||
.tooltip_label("Clone Tool")
|
||||
.tooltip_description("Coming soon.")
|
||||
.tooltip_shortcut(Key::KeyC.to_string()),
|
||||
.tooltip_shortcut(action_shortcut_manual!(Key::KeyC)),
|
||||
),
|
||||
ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Patch, "RasterPatchTool").tooltip_label("Patch Tool").tooltip_description("Coming soon.")),
|
||||
ToolAvailability::ComingSoon(
|
||||
ToolEntry::new(ToolType::Detail, "RasterDetailTool")
|
||||
.tooltip_label("Detail Tool")
|
||||
.tooltip_description("Coming soon.")
|
||||
.tooltip_shortcut(Key::KeyD.to_string()),
|
||||
.tooltip_shortcut(action_shortcut_manual!(Key::KeyD)),
|
||||
),
|
||||
ToolAvailability::ComingSoon(
|
||||
ToolEntry::new(ToolType::Relight, "RasterRelightTool")
|
||||
.tooltip_label("Relight Tool")
|
||||
.tooltip_description("Coming soon.")
|
||||
.tooltip_shortcut(Key::KeyO.to_string()),
|
||||
.tooltip_shortcut(action_shortcut_manual!(Key::KeyO)),
|
||||
),
|
||||
],
|
||||
]
|
||||
@@ -518,6 +516,55 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct HintData(pub Vec<HintGroup>);
|
||||
|
||||
impl HintData {
|
||||
pub fn to_layout(&self) -> Layout {
|
||||
let mut widgets = Vec::new();
|
||||
|
||||
for (index, hint_group) in self.0.iter().enumerate() {
|
||||
if index > 0 {
|
||||
widgets.push(Separator::new(SeparatorType::Section).widget_holder());
|
||||
}
|
||||
for hint in &hint_group.0 {
|
||||
if hint.plus {
|
||||
widgets.push(TextLabel::new("+").bold(true).widget_holder());
|
||||
}
|
||||
if hint.slash {
|
||||
widgets.push(TextLabel::new("/").bold(true).widget_holder());
|
||||
}
|
||||
|
||||
for shortcut in &hint.key_groups {
|
||||
widgets.push(ShortcutLabel::new(Some(ActionShortcut::Shortcut(shortcut.clone()))).widget_holder());
|
||||
}
|
||||
if let Some(mouse_movement) = &hint.mouse {
|
||||
let mouse_movement = LabeledShortcut(vec![LabeledKeyOrMouseMotion::MouseMotion(mouse_movement.clone())]);
|
||||
let shortcut = ActionShortcut::Shortcut(mouse_movement);
|
||||
widgets.push(ShortcutLabel::new(Some(shortcut)).widget_holder());
|
||||
}
|
||||
|
||||
if !hint.label.is_empty() {
|
||||
widgets.push(TextLabel::new(hint.label.clone()).widget_holder());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
|
||||
pub fn send_layout(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: self.to_layout(),
|
||||
layout_target: LayoutTarget::StatusBarHints,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clear_layout(responses: &mut VecDeque<Message>) {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
|
||||
layout_target: LayoutTarget::StatusBarHints,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct HintGroup(pub Vec<HintInfo>);
|
||||
|
||||
@@ -526,10 +573,10 @@ pub struct HintInfo {
|
||||
/// A `KeysGroup` specifies all the keys pressed simultaneously to perform an action (like "Ctrl C" to copy).
|
||||
/// Usually at most one is given, but less commonly, multiple can be used to describe additional hotkeys not used simultaneously (like the four different arrow keys to nudge a layer).
|
||||
#[serde(rename = "keyGroups")]
|
||||
pub key_groups: Vec<LayoutKeysGroup>,
|
||||
pub key_groups: Vec<LabeledShortcut>,
|
||||
/// `None` means that the regular `key_groups` should be used for all platforms, `Some` is an override for a Mac-only input hint.
|
||||
#[serde(rename = "keyGroupsMac")]
|
||||
pub key_groups_mac: Option<Vec<LayoutKeysGroup>>,
|
||||
pub key_groups_mac: Option<Vec<LabeledShortcut>>,
|
||||
/// An optional `MouseMotion` that can indicate the mouse action, like which mouse button is used and whether a drag occurs.
|
||||
/// No such icon is shown if `None` is given, and it can be combined with `key_groups` if desired.
|
||||
pub mouse: Option<MouseMotion>,
|
||||
|
||||
Reference in New Issue
Block a user