mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 02:58:13 +08:00
Event broadcasting system (#692)
* broadcast system implemented but not everywhere * unused types * code review with keavon * - optional signal mappings - tool.rs simplification * Cleanup * reduced code duplication in `tool.rs` * ran cargo fmt * code review changes * fix merge error Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
0ad6e41a59
commit
535b4fff85
@@ -41,7 +41,7 @@ impl MessageHandler<ArtboardMessage, &FontCache> for ArtboardMessageHandler {
|
||||
DocumentResponse::DocumentChanged => responses.push_back(ArtboardMessage::RenderArtboards.into()),
|
||||
_ => {}
|
||||
};
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
|
||||
@@ -102,7 +102,6 @@ pub enum DocumentMessage {
|
||||
RollbackTransaction,
|
||||
SaveDocument,
|
||||
SelectAllLayers,
|
||||
SelectionChanged,
|
||||
SelectLayer {
|
||||
layer_path: Vec<LayerId>,
|
||||
ctrl: bool,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::clipboards::Clipboard;
|
||||
use super::layer_panel::{layer_panel_entry, LayerMetadata, LayerPanelEntry, RawBuffer};
|
||||
use super::properties_panel_message_handler::PropertiesPanelMessageHandlerData;
|
||||
use super::utility_types::DocumentMode;
|
||||
use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis};
|
||||
use super::utility_types::{DocumentMode, TargetDocument};
|
||||
use super::{vectorize_layer_metadata, PropertiesPanelMessageHandler};
|
||||
use super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandler, TransformLayerMessageHandler};
|
||||
use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR};
|
||||
@@ -864,7 +864,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
}
|
||||
DocumentResponse::DocumentChanged => responses.push_back(RenderDocument.into()),
|
||||
};
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("DocumentError: {:?}", e),
|
||||
@@ -894,6 +894,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
PropertiesPanelMessageHandlerData {
|
||||
artwork_document: &self.graphene_document,
|
||||
artboard_document: &self.artboard_message_handler.artboards_graphene_document,
|
||||
selected_layers: &mut self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice())),
|
||||
font_cache,
|
||||
},
|
||||
responses,
|
||||
@@ -910,22 +911,9 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
responses.extend(self.select_layer(layer_path, font_cache));
|
||||
}
|
||||
|
||||
let selected_paths: Vec<Vec<u64>> = self.selected_layers().map(|path| path.to_vec()).collect();
|
||||
if selected_paths.is_empty() {
|
||||
responses.push_back(PropertiesPanelMessage::ClearSelection.into())
|
||||
} else {
|
||||
responses.push_back(
|
||||
PropertiesPanelMessage::SetActiveLayers {
|
||||
paths: selected_paths,
|
||||
document: TargetDocument::Artwork,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Correctly update layer panel in clear_selection instead of here
|
||||
responses.push_back(FolderChanged { affected_folder_path: vec![] }.into());
|
||||
responses.push_back(DocumentMessage::SelectionChanged.into());
|
||||
responses.push_back(BroadcastSignal::SelectionChanged.into());
|
||||
|
||||
self.update_layer_tree_options_bar_widgets(responses, font_cache);
|
||||
}
|
||||
@@ -963,7 +951,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
}
|
||||
BooleanOperation(op) => {
|
||||
@@ -997,7 +985,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
}
|
||||
DeleteLayer { layer_path } => {
|
||||
responses.push_front(DocumentOperation::DeleteLayer { path: layer_path.clone() }.into());
|
||||
responses.push_front(ToolMessage::AbortCurrentTool.into());
|
||||
responses.push_front(BroadcastSignal::ToolAbort.into());
|
||||
responses.push_back(PropertiesPanelMessage::CheckSelectedWasDeleted { path: layer_path }.into());
|
||||
}
|
||||
DeleteSelectedLayers => {
|
||||
@@ -1007,7 +995,8 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
responses.push_front(DocumentMessage::DeleteLayer { layer_path: path.to_vec() }.into());
|
||||
}
|
||||
|
||||
responses.push_front(DocumentMessage::SelectionChanged.into());
|
||||
responses.push_front(BroadcastSignal::SelectionChanged.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
DeselectAllLayers => {
|
||||
responses.push_front(SetSelectedLayers { replacement_selected_layers: vec![] }.into());
|
||||
@@ -1106,7 +1095,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
}
|
||||
FolderChanged { affected_folder_path } => {
|
||||
@@ -1183,7 +1172,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
};
|
||||
responses.push_back(operation.into());
|
||||
}
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
PasteImage { mime, image_data, mouse } => {
|
||||
let path = vec![generate_uuid()];
|
||||
@@ -1221,7 +1210,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
Redo => {
|
||||
responses.push_back(SelectToolMessage::Abort.into());
|
||||
responses.push_back(DocumentHistoryForward.into());
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
responses.push_back(RenderDocument.into());
|
||||
responses.push_back(FolderChanged { affected_folder_path: vec![] }.into());
|
||||
}
|
||||
@@ -1353,11 +1342,6 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
let all = self.all_layers().map(|path| path.to_vec()).collect();
|
||||
responses.push_front(SetSelectedLayers { replacement_selected_layers: all }.into());
|
||||
}
|
||||
SelectionChanged => {
|
||||
// TODO: Hoist this duplicated code into wider system
|
||||
responses.push_back(ToolMessage::SelectionChanged.into());
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
}
|
||||
SelectLayer { layer_path, ctrl, shift } => {
|
||||
let mut paths = vec![];
|
||||
let last_selection_exists = !self.layer_range_selection_reference.is_empty();
|
||||
@@ -1382,7 +1366,7 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(DocumentMessage::SelectionChanged.into());
|
||||
responses.push_back(BroadcastSignal::SelectionChanged.into());
|
||||
} else {
|
||||
paths.push(layer_path.clone());
|
||||
}
|
||||
@@ -1476,12 +1460,12 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
|
||||
}
|
||||
ToggleLayerVisibility { layer_path } => {
|
||||
responses.push_back(DocumentOperation::ToggleLayerVisibility { path: layer_path }.into());
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
Undo => {
|
||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
responses.push_back(DocumentHistoryBackward.into());
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
responses.push_back(RenderDocument.into());
|
||||
responses.push_back(FolderChanged { affected_folder_path: vec![] }.into());
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
||||
self.zoom = 1.
|
||||
}
|
||||
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
|
||||
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
|
||||
self.create_document_transform(&ipp.viewport_bounds, responses);
|
||||
@@ -243,12 +243,12 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
||||
SetCanvasRotation { angle_radians } => {
|
||||
self.tilt = angle_radians;
|
||||
self.create_document_transform(&ipp.viewport_bounds, responses);
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
|
||||
}
|
||||
SetCanvasZoom { zoom_factor } => {
|
||||
self.zoom = zoom_factor.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into());
|
||||
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
|
||||
self.create_document_transform(&ipp.viewport_bounds, responses);
|
||||
@@ -256,7 +256,7 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
||||
TransformCanvasEnd => {
|
||||
self.tilt = self.snapped_angle();
|
||||
self.zoom = self.snapped_scale();
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
responses.push_back(ToolMessage::UpdateCursor.into());
|
||||
responses.push_back(ToolMessage::UpdateHints.into());
|
||||
self.snap_tilt = false;
|
||||
@@ -270,7 +270,7 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
||||
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
|
||||
|
||||
self.pan += transformed_delta;
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
self.create_document_transform(&ipp.viewport_bounds, responses);
|
||||
}
|
||||
TranslateCanvasBegin => {
|
||||
@@ -284,7 +284,7 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
||||
let transformed_delta = document.root.transform.inverse().transform_vector2(delta * ipp.viewport_bounds.size());
|
||||
|
||||
self.pan += transformed_delta;
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
self.create_document_transform(&ipp.viewport_bounds, responses);
|
||||
}
|
||||
WheelCanvasTranslate { use_y_as_x } => {
|
||||
|
||||
@@ -58,7 +58,7 @@ impl PortfolioMessageHandler {
|
||||
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: u64, replace_first_empty: bool, responses: &mut VecDeque<Message>) {
|
||||
// Special case when loading a document on an empty page
|
||||
if replace_first_empty && self.active_document().is_unmodified_default() {
|
||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
responses.push_back(PortfolioMessage::CloseDocument { document_id: self.active_document_id }.into());
|
||||
|
||||
let active_document_index = self
|
||||
@@ -168,7 +168,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
self.document_ids.push(new_document_id);
|
||||
self.active_document_id = new_document_id;
|
||||
|
||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into());
|
||||
responses.push_back(PortfolioMessage::SelectDocument { document_id: new_document_id }.into())
|
||||
}
|
||||
@@ -208,7 +208,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
CloseDocumentWithConfirmation { document_id } => {
|
||||
let target_document = self.documents.get(&document_id).unwrap();
|
||||
if target_document.is_saved() {
|
||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
responses.push_back(PortfolioMessage::CloseDocument { document_id }.into());
|
||||
} else {
|
||||
let dialog = dialog::CloseDocument {
|
||||
@@ -274,13 +274,13 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
let name = self.generate_new_document_name();
|
||||
let new_document = DocumentMessageHandler::with_name(name, ipp);
|
||||
let document_id = generate_uuid();
|
||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
self.load_document(new_document, document_id, false, responses);
|
||||
}
|
||||
NewDocumentWithName { name } => {
|
||||
let new_document = DocumentMessageHandler::with_name(name, ipp);
|
||||
let document_id = generate_uuid();
|
||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
self.load_document(new_document, document_id, false, responses);
|
||||
}
|
||||
NextDocument => {
|
||||
@@ -431,7 +431,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
if !active_document.is_saved() {
|
||||
responses.push_back(PortfolioMessage::AutoSaveDocument { document_id: self.active_document_id }.into());
|
||||
}
|
||||
responses.push_back(ToolMessage::AbortCurrentTool.into());
|
||||
responses.push_back(BroadcastSignal::ToolAbort.into());
|
||||
responses.push_back(SetActiveDocument { document_id }.into());
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateActiveDocument { document_id }.into());
|
||||
@@ -440,7 +440,8 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
|
||||
for layer in self.documents.get(&document_id).unwrap().layer_metadata.keys() {
|
||||
responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into());
|
||||
}
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::SelectionChanged.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
responses.push_back(PortfolioMessage::UpdateDocumentWidgets.into());
|
||||
}
|
||||
SetActiveDocument { document_id } => {
|
||||
|
||||
@@ -12,6 +12,7 @@ pub enum PropertiesPanelMessage {
|
||||
CheckSelectedWasDeleted { path: Vec<LayerId> },
|
||||
CheckSelectedWasUpdated { path: Vec<LayerId> },
|
||||
ClearSelection,
|
||||
Init,
|
||||
ModifyFill { fill: Fill },
|
||||
ModifyFont { font_family: String, font_style: String, size: f64 },
|
||||
ModifyName { name: String },
|
||||
@@ -20,6 +21,7 @@ pub enum PropertiesPanelMessage {
|
||||
ModifyTransform { value: f64, transform_op: TransformOp },
|
||||
ResendActiveProperties,
|
||||
SetActiveLayers { paths: Vec<Vec<LayerId>>, document: TargetDocument },
|
||||
UpdateSelectedDocumentProperties,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -110,6 +110,7 @@ impl PropertiesPanelMessageHandler {
|
||||
pub struct PropertiesPanelMessageHandlerData<'a> {
|
||||
pub artwork_document: &'a GrapheneDocument,
|
||||
pub artboard_document: &'a GrapheneDocument,
|
||||
pub selected_layers: &'a mut dyn Iterator<Item = &'a [LayerId]>,
|
||||
pub font_cache: &'a FontCache,
|
||||
}
|
||||
|
||||
@@ -119,6 +120,7 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
||||
let PropertiesPanelMessageHandlerData {
|
||||
artwork_document,
|
||||
artboard_document,
|
||||
selected_layers,
|
||||
font_cache,
|
||||
} = data;
|
||||
let get_document = |document_selector: TargetDocument| match document_selector {
|
||||
@@ -154,6 +156,13 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
||||
);
|
||||
self.active_selection = None;
|
||||
}
|
||||
Init => responses.push_back(
|
||||
BroadcastMessage::SubscribeSignal {
|
||||
on: BroadcastSignal::SelectionChanged,
|
||||
send: Box::new(PropertiesPanelMessage::UpdateSelectedDocumentProperties.into()),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
ModifyFont { font_family, font_style, size } => {
|
||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
|
||||
@@ -233,6 +242,13 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateSelectedDocumentProperties => responses.push_back(
|
||||
PropertiesPanelMessage::SetActiveLayers {
|
||||
paths: selected_layers.map(|path| path.to_vec()).collect(),
|
||||
document: TargetDocument::Artwork,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
|
||||
|
||||
self.transform_operation = TransformOperation::None;
|
||||
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
BeginGrab => {
|
||||
if let TransformOperation::Grabbing(_) = self.transform_operation {
|
||||
@@ -66,7 +66,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
|
||||
|
||||
self.transform_operation = TransformOperation::Grabbing(Default::default());
|
||||
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
BeginRotate => {
|
||||
if let TransformOperation::Rotating(_) = self.transform_operation {
|
||||
@@ -77,7 +77,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
|
||||
|
||||
self.transform_operation = TransformOperation::Rotating(Default::default());
|
||||
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
BeginScale => {
|
||||
if let TransformOperation::Scaling(_) = self.transform_operation {
|
||||
@@ -89,7 +89,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
|
||||
self.transform_operation = TransformOperation::Scaling(Default::default());
|
||||
self.transform_operation.apply_transform_operation(&mut selected, self.snap);
|
||||
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
CancelTransformOperation => {
|
||||
selected.revert_operation();
|
||||
@@ -99,7 +99,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
|
||||
|
||||
self.transform_operation = TransformOperation::None;
|
||||
|
||||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
ConstrainX => self.transform_operation.constrain_axis(Axis::X, &mut selected, self.snap),
|
||||
ConstrainY => self.transform_operation.constrain_axis(Axis::Y, &mut selected, self.snap),
|
||||
|
||||
@@ -258,7 +258,7 @@ impl<'a> Selected<'a> {
|
||||
);
|
||||
}
|
||||
|
||||
self.responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
self.responses.push_back(BroadcastSignal::DocumentIsDirty.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user