Implement cursor asset caching for improved performance
Key improvements: 1. Add image caching to CursorInfo struct to avoid repeated disk reads and decoding 2. Implement Windows cursor handle caching to minimize GDI resource allocation 3. Update all code paths to use cached resources for better performance 4. Remove deprecated/unused functions for cleaner codebase This change significantly reduces CPU usage and improves cursor switching performance.
This commit is contained in:
+35
-12
@@ -12,6 +12,9 @@ use crate::platform;
|
|||||||
pub struct CursorInfo {
|
pub struct CursorInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
pub image: Option<::image::RgbaImage>,
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub win_cursor: Option<crate::platform::SyncHCURSOR>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to load all PNG files from the assets directory
|
// Function to load all PNG files from the assets directory
|
||||||
@@ -36,9 +39,25 @@ pub fn load_cursor_files() -> Vec<CursorInfo> {
|
|||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
println!("Adding cursor: {} at path {}", name, path.display());
|
println!("Adding cursor: {} at path {}", name, path.display());
|
||||||
|
|
||||||
|
// Try to load the image during initialization
|
||||||
|
let image = match image_open(path) {
|
||||||
|
Ok(img) => {
|
||||||
|
println!("Successfully pre-loaded image for cursor: {}", name);
|
||||||
|
Some(img.to_rgba8())
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to pre-load image for cursor {}: {}", name, e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
cursors.push(CursorInfo {
|
cursors.push(CursorInfo {
|
||||||
name,
|
name,
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
|
image,
|
||||||
|
#[cfg(windows)]
|
||||||
|
win_cursor: None, // Windows cursor will be created on first use
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,17 +91,21 @@ pub fn validate_cursor_image(path: &Path) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the cursor at the given path
|
// Apply the cursor from CursorInfo
|
||||||
pub fn apply_cursor(path: &Path) -> bool {
|
pub fn apply_cursor_info(cursor_info: &mut CursorInfo) -> bool {
|
||||||
// Validate the cursor image
|
// Validate the cursor image if not already cached
|
||||||
if validate_cursor_image(path) {
|
if cursor_info.image.is_none() && !validate_cursor_image(&cursor_info.path) {
|
||||||
// Use platform-specific cursor image update (not hiding system cursor again)
|
return false;
|
||||||
if platform::update_cursor_image(path) {
|
}
|
||||||
println!("Successfully updated cursor image");
|
|
||||||
return true;
|
// Use platform-specific cursor image update with the cursor info
|
||||||
} else {
|
if platform::update_cursor_info(cursor_info) {
|
||||||
eprintln!("Failed to update cursor image");
|
println!("Successfully updated cursor image: {}", cursor_info.name);
|
||||||
}
|
return true;
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to update cursor image: {}", cursor_info.name);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ mod cursor;
|
|||||||
|
|
||||||
pub use state::CursorRush;
|
pub use state::CursorRush;
|
||||||
pub use messages::Message;
|
pub use messages::Message;
|
||||||
pub use cursor::{CursorInfo, load_cursor_files, apply_cursor};
|
pub use cursor::{CursorInfo, load_cursor_files, apply_cursor_info};
|
||||||
pub use state::SharedState;
|
pub use state::SharedState;
|
||||||
|
|
||||||
use iced::{Subscription, Task};
|
use iced::{Subscription, Task};
|
||||||
|
|||||||
+14
-22
@@ -22,20 +22,16 @@ pub fn update_app(state: &mut CursorRush, message: Message) -> Task<Message> {
|
|||||||
state.current_index = (state.current_index + 1) % state.cursors.len();
|
state.current_index = (state.current_index + 1) % state.cursors.len();
|
||||||
|
|
||||||
// Apply the new cursor
|
// Apply the new cursor
|
||||||
if let Some(cursor) = state.cursors.get(state.current_index) {
|
if let Some(cursor) = state.cursors.get_mut(state.current_index) {
|
||||||
apply_cursor(&cursor.path);
|
// Use the cached version with the cursor info
|
||||||
|
apply_cursor_info(cursor);
|
||||||
// Load the new cursor image
|
|
||||||
let current_cursor_image = match image::open(&cursor.path) {
|
|
||||||
Ok(img) => Some(img.to_rgba8()),
|
|
||||||
Err(_) => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update shared state
|
// Update shared state
|
||||||
if let Some(shared) = &state.shared_state {
|
if let Some(shared) = &state.shared_state {
|
||||||
if let Ok(mut shared_state) = shared.lock() {
|
if let Ok(mut shared_state) = shared.lock() {
|
||||||
shared_state.current_index = state.current_index;
|
shared_state.current_index = state.current_index;
|
||||||
shared_state.current_cursor_image = current_cursor_image;
|
// Use the cached image directly
|
||||||
|
shared_state.current_cursor_image = cursor.image.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,20 +43,16 @@ pub fn update_app(state: &mut CursorRush, message: Message) -> Task<Message> {
|
|||||||
state.current_index = idx;
|
state.current_index = idx;
|
||||||
|
|
||||||
// Apply the selected cursor
|
// Apply the selected cursor
|
||||||
if let Some(cursor) = state.cursors.get(state.current_index) {
|
if let Some(cursor) = state.cursors.get_mut(state.current_index) {
|
||||||
apply_cursor(&cursor.path);
|
// Use the cached version with the cursor info
|
||||||
|
apply_cursor_info(cursor);
|
||||||
// Load the new cursor image
|
|
||||||
let current_cursor_image = match image_open(&cursor.path) {
|
|
||||||
Ok(img) => Some(img.to_rgba8()),
|
|
||||||
Err(_) => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update shared state
|
// Update shared state
|
||||||
if let Some(shared) = &state.shared_state {
|
if let Some(shared) = &state.shared_state {
|
||||||
if let Ok(mut shared_state) = shared.lock() {
|
if let Ok(mut shared_state) = shared.lock() {
|
||||||
shared_state.current_index = state.current_index;
|
shared_state.current_index = state.current_index;
|
||||||
shared_state.current_cursor_image = current_cursor_image;
|
// Use the cached image directly
|
||||||
|
shared_state.current_cursor_image = cursor.image.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,7 +204,7 @@ pub fn update_app(state: &mut CursorRush, message: Message) -> Task<Message> {
|
|||||||
|
|
||||||
use crate::platform;
|
use crate::platform;
|
||||||
use crate::ui::tray::setup_tray_icon;
|
use crate::ui::tray::setup_tray_icon;
|
||||||
use crate::app::{Message, CursorInfo, load_cursor_files, apply_cursor, TRAY_SENDER};
|
use crate::app::{Message, CursorInfo, load_cursor_files, apply_cursor_info, TRAY_SENDER};
|
||||||
|
|
||||||
// Shared state between the UI and the tray icon
|
// Shared state between the UI and the tray icon
|
||||||
pub struct SharedState {
|
pub struct SharedState {
|
||||||
@@ -313,7 +305,7 @@ pub fn app_subscription(_state: &CursorRush) -> Subscription<Message> {
|
|||||||
// Boot function implementation
|
// Boot function implementation
|
||||||
pub fn boot_app() -> (CursorRush, Task<Message>) {
|
pub fn boot_app() -> (CursorRush, Task<Message>) {
|
||||||
// Load cursor files from assets directory
|
// Load cursor files from assets directory
|
||||||
let cursors = load_cursor_files();
|
let mut cursors = load_cursor_files();
|
||||||
|
|
||||||
if cursors.is_empty() {
|
if cursors.is_empty() {
|
||||||
eprintln!("No cursor PNG files found in assets directory!");
|
eprintln!("No cursor PNG files found in assets directory!");
|
||||||
@@ -336,8 +328,8 @@ pub fn boot_app() -> (CursorRush, Task<Message>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply the first cursor by default
|
// Apply the first cursor by default
|
||||||
if let Some(cursor) = cursors.first() {
|
if let Some(cursor) = cursors.first_mut() {
|
||||||
apply_cursor(&cursor.path);
|
apply_cursor_info(cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create shared state
|
// Create shared state
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Linux cursor management
|
// Linux cursor management
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use crate::platform::CursorInfo;
|
||||||
|
|
||||||
// Hide the system cursor
|
// Hide the system cursor
|
||||||
pub fn hide_system_cursor() -> bool {
|
pub fn hide_system_cursor() -> bool {
|
||||||
@@ -8,12 +9,14 @@ pub fn hide_system_cursor() -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cursor image
|
// Update the cursor image using CursorInfo (with caching)
|
||||||
pub fn update_cursor_image(_path: &Path) -> bool {
|
pub fn update_cursor_info(_cursor_info: &mut CursorInfo) -> bool {
|
||||||
println!("Linux cursor image update not yet implemented");
|
println!("Linux cursor image update with caching not yet implemented");
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Restore the original cursor
|
// Restore the original cursor
|
||||||
pub fn restore_cursor() {
|
pub fn restore_cursor() {
|
||||||
println!("Linux cursor restoration not yet implemented");
|
println!("Linux cursor restoration not yet implemented");
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ mod cursor;
|
|||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
// Re-export the public API
|
// Re-export the public API
|
||||||
pub use cursor::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
|
pub use cursor::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// macOS cursor management
|
// macOS cursor management
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use crate::platform::CursorInfo;
|
||||||
|
|
||||||
// Hide the system cursor
|
// Hide the system cursor
|
||||||
pub fn hide_system_cursor() -> bool {
|
pub fn hide_system_cursor() -> bool {
|
||||||
@@ -8,12 +9,14 @@ pub fn hide_system_cursor() -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cursor image
|
// Update the cursor image using CursorInfo (with caching)
|
||||||
pub fn update_cursor_image(_path: &Path) -> bool {
|
pub fn update_cursor_info(_cursor_info: &mut CursorInfo) -> bool {
|
||||||
println!("macOS cursor image update not yet implemented");
|
println!("macOS cursor image update with caching not yet implemented");
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Restore the original cursor
|
// Restore the original cursor
|
||||||
pub fn restore_cursor() {
|
pub fn restore_cursor() {
|
||||||
println!("macOS cursor restoration not yet implemented");
|
println!("macOS cursor restoration not yet implemented");
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ mod cursor;
|
|||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
// Re-export the public API
|
// Re-export the public API
|
||||||
pub use cursor::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
|
pub use cursor::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||||
|
|||||||
+23
-4
@@ -16,18 +16,37 @@ mod linux;
|
|||||||
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
|
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
|
||||||
mod unsupported;
|
mod unsupported;
|
||||||
|
|
||||||
|
// --- Platform-independent types ---
|
||||||
|
|
||||||
|
// Define a cross-platform cursor handle type
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub use windows::types::SyncHCURSOR;
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct SyncHCURSOR(pub usize);
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
unsafe impl Send for SyncHCURSOR {}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
unsafe impl Sync for SyncHCURSOR {}
|
||||||
|
|
||||||
// --- Public Exports ---
|
// --- Public Exports ---
|
||||||
|
|
||||||
// Export the platform-specific functions
|
// Export the platform-specific functions
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub use windows::{hide_system_cursor, update_cursor_image, restore_cursor};
|
pub use windows::{hide_system_cursor, update_cursor_info, restore_cursor};
|
||||||
|
|
||||||
|
// Re-export CursorInfo for platform-specific implementations
|
||||||
|
pub use crate::app::CursorInfo;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub use macos::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
|
pub use macos::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub use linux::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
|
pub use linux::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||||
|
|
||||||
// Default implementation for other platforms
|
// Default implementation for other platforms
|
||||||
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
|
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
|
||||||
pub use unsupported::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
|
pub use unsupported::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Unsupported platform cursor management
|
// Unsupported platform cursor management
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use crate::platform::CursorInfo;
|
||||||
|
|
||||||
// Hide the system cursor
|
// Hide the system cursor
|
||||||
pub fn hide_system_cursor() -> bool {
|
pub fn hide_system_cursor() -> bool {
|
||||||
@@ -8,12 +9,14 @@ pub fn hide_system_cursor() -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cursor image
|
// Update the cursor image using CursorInfo (with caching)
|
||||||
pub fn update_cursor_image(_path: &Path) -> bool {
|
pub fn update_cursor_info(_cursor_info: &mut CursorInfo) -> bool {
|
||||||
println!("Cursor image update not supported");
|
println!("Cursor image update with caching not supported");
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Restore the original cursor
|
// Restore the original cursor
|
||||||
pub fn restore_cursor() {
|
pub fn restore_cursor() {
|
||||||
println!("Cursor restoration not supported");
|
println!("Cursor restoration not supported");
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ mod cursor;
|
|||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
// Re-export the public API
|
// Re-export the public API
|
||||||
pub use cursor::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
|
pub use cursor::{hide_system_cursor, update_cursor_info, restore_cursor, render_cursor};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Windows platform cursor management
|
// Windows platform cursor management
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
use std::ptr::null_mut;
|
use std::ptr::null_mut;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use winapi::{
|
use winapi::{
|
||||||
@@ -290,21 +289,48 @@ pub fn hide_system_cursor() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cursor image - creates and sets a custom cursor for all system cursors
|
// Update the cursor image using CursorInfo (with caching)
|
||||||
pub fn update_cursor_image(path: &Path) -> bool {
|
pub fn update_cursor_info(cursor_info: &mut crate::platform::CursorInfo) -> bool {
|
||||||
// Load the image
|
// Use cached image if available, otherwise load from disk
|
||||||
let image = match image_open(path) {
|
let image = if let Some(ref cached_image) = cursor_info.image {
|
||||||
Ok(img) => img.to_rgba8(),
|
println!("Using cached image for cursor: {}", cursor_info.name);
|
||||||
Err(e) => {
|
cached_image.clone()
|
||||||
eprintln!("Failed to load cursor image: {}", e);
|
} else {
|
||||||
return false;
|
println!("Loading image from disk for cursor: {}", cursor_info.name);
|
||||||
|
match image_open(&cursor_info.path) {
|
||||||
|
Ok(img) => {
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
// Cache the image for future use
|
||||||
|
cursor_info.image = Some(rgba.clone());
|
||||||
|
rgba
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to load cursor image: {}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store the image for reference
|
// Store the image in the global state for reference
|
||||||
*CURRENT_CURSOR_IMAGE.lock().unwrap() = Some(image.clone());
|
*CURRENT_CURSOR_IMAGE.lock().unwrap() = Some(image.clone());
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
// Use cached Windows cursor if available
|
||||||
|
#[cfg(windows)]
|
||||||
|
if let Some(cached_cursor) = cursor_info.win_cursor {
|
||||||
|
println!("Using cached Windows cursor handle for: {}", cursor_info.name);
|
||||||
|
|
||||||
|
// Apply the cached cursor to all system cursors
|
||||||
|
let success = apply_cursor_to_all_system_cursors(cached_cursor.0);
|
||||||
|
if !success {
|
||||||
|
eprintln!("Failed to apply cached cursor to all system cursors");
|
||||||
|
// Even if we failed to set all system cursors, try to at least set the current cursor
|
||||||
|
SetCursor(cached_cursor.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up old cursor if it exists
|
// Clean up old cursor if it exists
|
||||||
let mut cursor_guard = CUSTOM_CURSOR.lock().unwrap();
|
let mut cursor_guard = CUSTOM_CURSOR.lock().unwrap();
|
||||||
if let Some(cursor_wrapper) = cursor_guard.take() {
|
if let Some(cursor_wrapper) = cursor_guard.take() {
|
||||||
@@ -316,6 +342,13 @@ pub fn update_cursor_image(path: &Path) -> bool {
|
|||||||
// Store the cursor for later use
|
// Store the cursor for later use
|
||||||
*cursor_guard = Some(cursor_wrapper);
|
*cursor_guard = Some(cursor_wrapper);
|
||||||
|
|
||||||
|
// Cache the Windows cursor handle in the CursorInfo
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
println!("Caching Windows cursor handle for: {}", cursor_info.name);
|
||||||
|
cursor_info.win_cursor = Some(cursor_wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the custom cursor to all system cursors
|
// Apply the custom cursor to all system cursors
|
||||||
let success = apply_cursor_to_all_system_cursors(cursor_wrapper.0);
|
let success = apply_cursor_to_all_system_cursors(cursor_wrapper.0);
|
||||||
if !success {
|
if !success {
|
||||||
@@ -332,6 +365,8 @@ pub fn update_cursor_image(path: &Path) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Apply a cursor to all system cursors
|
// Apply a cursor to all system cursors
|
||||||
unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCURSOR) -> bool {
|
unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCURSOR) -> bool {
|
||||||
let mut success = true;
|
let mut success = true;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Windows platform implementation module
|
// Windows platform implementation module
|
||||||
|
|
||||||
mod types;
|
pub mod types;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
// Re-export the public API
|
// Re-export the public API
|
||||||
pub use cursor::{hide_system_cursor, update_cursor_image, restore_cursor};
|
pub use cursor::{hide_system_cursor, update_cursor_info, restore_cursor};
|
||||||
|
|||||||
+5
-3
@@ -83,10 +83,12 @@ pub fn setup_tray_icon(
|
|||||||
Ok(mut state) => {
|
Ok(mut state) => {
|
||||||
if !state.cursors.is_empty() {
|
if !state.cursors.is_empty() {
|
||||||
state.current_index = (state.current_index + 1) % state.cursors.len();
|
state.current_index = (state.current_index + 1) % state.cursors.len();
|
||||||
if let Some(cursor) = state.cursors.get(state.current_index) {
|
let current_index = state.current_index;
|
||||||
|
if let Some(cursor) = state.cursors.get_mut(current_index) {
|
||||||
println!("Tray: Updating cursor image: {}", cursor.name);
|
println!("Tray: Updating cursor image: {}", cursor.name);
|
||||||
crate::app::apply_cursor(&cursor.path);
|
crate::app::apply_cursor_info(cursor);
|
||||||
state.current_cursor_image = image_open(&cursor.path).ok().map(|img| img.to_rgba8());
|
// Use the cached image directly
|
||||||
|
state.current_cursor_image = cursor.image.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user