Monado app switcher, lang update
This commit is contained in:
@@ -2,8 +2,17 @@
|
||||
"ANCHOR": {
|
||||
"CENTER": "Zentrum"
|
||||
},
|
||||
"BAR": {
|
||||
"ADD_MIRROR": "Neuen Spiegel-Overlay hinzufügen"
|
||||
"BAR": {
|
||||
"ADD_MIRROR": "Neuen Spiegel-Overlay hinzufügen",
|
||||
"EDIT_MODE_TOGGLE": "Bearbeitungsmodus umschalten",
|
||||
"ADD_NEW_SET": "Neues Set hinzufügen",
|
||||
"DELETE_CURRENT_SET": "Aktuelles Set löschen",
|
||||
"TOGGLE_VISIBILITY": "Sichtbarkeit umschalten",
|
||||
"RESET_POSITION": "Position zurücksetzen",
|
||||
"RELOAD_FROM_DISK": "XML-Datei von der Festplatte neu laden",
|
||||
"CLOSE_MIRROR": "Spiegel schließen",
|
||||
"CLOSE_APP": "App schließen",
|
||||
"FORCE_CLOSE_APP": "App zwangsweise schließen"
|
||||
},
|
||||
"WATCH": {
|
||||
"RECENTER": "Spielbereich neu zentrieren",
|
||||
@@ -14,7 +23,8 @@
|
||||
"ADD_NEW_SET": "Neuen Satz hinzufügen",
|
||||
"SWITCH_TO_SET": "Zum Satz wechseln",
|
||||
"TOGGLE_FOR_CURRENT_SET": "Sichtbarkeit im aktuellen Satz umschalten",
|
||||
"LONG_PRESS_TO_DELETE_SET": "Lange drücken, um Satz zu löschen"
|
||||
"LONG_PRESS_TO_DELETE_SET": "Lange drücken, um Satz zu löschen",
|
||||
"CLEANUP_MIRRORS": "Spiegel entfernen, die\nderzeit nicht sichtbar sind"
|
||||
},
|
||||
"EDIT_MODE": {
|
||||
"ADJUST_CURVATURE": "Krümmung anpassen",
|
||||
@@ -83,6 +93,8 @@
|
||||
"EMPTY_SET": "Leeres Set!",
|
||||
"LETS_ADD_OVERLAYS": "Lass uns ein paar Overlays von der Uhr hinzufügen!",
|
||||
"FIXING_FLOOR": "Boden wird in 5 Sekunden fixiert...",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "Lege einen Controller auf den Boden!"
|
||||
"ONE_CONTROLLER_ON_FLOOR": "Lege einen Controller auf den Boden!",
|
||||
"CANNOT_ADD_SET": "Satz kann nicht hinzugefügt werden!",
|
||||
"MAXIMUM_SETS_REACHED": "Maximale Anzahl an Sets erreicht."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,16 @@
|
||||
"CENTER": "Centro"
|
||||
},
|
||||
"BAR": {
|
||||
"ADD_MIRROR": "Agregar una nueva superposición de espejo"
|
||||
"ADD_MIRROR": "Agregar una nueva superposición de espejo",
|
||||
"EDIT_MODE_TOGGLE": "Activar/desactivar el modo de edición",
|
||||
"ADD_NEW_SET": "Añadir nuevo set",
|
||||
"DELETE_CURRENT_SET": "Eliminar set actual",
|
||||
"TOGGLE_VISIBILITY": "Alternar visibilidad",
|
||||
"RESET_POSITION": "Restablecer posición",
|
||||
"RELOAD_FROM_DISK": "Volver a cargar XML desde el disco",
|
||||
"CLOSE_MIRROR": "Cerrar espejo",
|
||||
"CLOSE_APP": "Cerrar aplicación",
|
||||
"FORCE_CLOSE_APP": "Forzar cierre de la aplicación"
|
||||
},
|
||||
"WATCH": {
|
||||
"RECENTER": "Recentrar el área de juego",
|
||||
@@ -14,7 +23,8 @@
|
||||
"ADD_NEW_SET": "Añadir un nuevo conjunto",
|
||||
"SWITCH_TO_SET": "Cambiar al conjunto",
|
||||
"TOGGLE_FOR_CURRENT_SET": "Alternar visibilidad en el conjunto actual",
|
||||
"LONG_PRESS_TO_DELETE_SET": "Mantén presionado para eliminar el conjunto"
|
||||
"LONG_PRESS_TO_DELETE_SET": "Mantén presionado para eliminar el conjunto",
|
||||
"CLEANUP_MIRRORS": "Eliminar los espejos que\nno son actualmente visibles"
|
||||
},
|
||||
"EDIT_MODE": {
|
||||
"ADJUST_CURVATURE": "Ajustar curvatura",
|
||||
@@ -83,6 +93,8 @@
|
||||
"EMPTY_SET": "¡Conjunto vacío!",
|
||||
"LETS_ADD_OVERLAYS": "¡Añadamos algunos overlays desde el reloj!",
|
||||
"FIXING_FLOOR": "Fijando el suelo en 5 segundos...",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "¡Coloca un mando en el suelo!"
|
||||
"ONE_CONTROLLER_ON_FLOOR": "¡Coloca un mando en el suelo!",
|
||||
"CANNOT_ADD_SET": "¡No se puede agregar el conjunto!",
|
||||
"MAXIMUM_SETS_REACHED": "Se ha alcanzado el número máximo de sets."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,17 @@
|
||||
"ANCHOR": {
|
||||
"CENTER": "センター"
|
||||
},
|
||||
"BAR": {
|
||||
"ADD_MIRROR": "新しいミラーを追加"
|
||||
"BAR": {
|
||||
"ADD_MIRROR": "新しいミラーを追加",
|
||||
"EDIT_MODE_TOGGLE": "編集モードの切り替え",
|
||||
"ADD_NEW_SET": "新しいセットを追加",
|
||||
"DELETE_CURRENT_SET": "現在のセットを削除",
|
||||
"TOGGLE_VISIBILITY": "表示/非表示の切り替え",
|
||||
"RESET_POSITION": "位置をリセット",
|
||||
"RELOAD_FROM_DISK": "ディスクからXMLを再読み込み",
|
||||
"CLOSE_MIRROR": "ミラーを閉じる",
|
||||
"CLOSE_APP": "アプリを閉じる",
|
||||
"FORCE_CLOSE_APP": "アプリを強制終了"
|
||||
},
|
||||
"WATCH": {
|
||||
"RECENTER": "プレイスペースをリセンター",
|
||||
@@ -14,7 +23,8 @@
|
||||
"ADD_NEW_SET": "新しいセットを追加",
|
||||
"SWITCH_TO_SET": "セットに切り替える",
|
||||
"TOGGLE_FOR_CURRENT_SET": "現在のセットで表示を切り替え",
|
||||
"LONG_PRESS_TO_DELETE_SET": "長押しでセットを削除"
|
||||
"LONG_PRESS_TO_DELETE_SET": "長押しでセットを削除",
|
||||
"CLEANUP_MIRRORS": "現在表示されていないミラーを削除"
|
||||
},
|
||||
"EDIT_MODE": {
|
||||
"ADJUST_CURVATURE": "曲率の調整",
|
||||
@@ -81,6 +91,8 @@
|
||||
"EMPTY_SET": "空のセットです!",
|
||||
"LETS_ADD_OVERLAYS": "ウォッチからオーバーレイを追加しましょう!",
|
||||
"FIXING_FLOOR": "5秒後にフロアを固定します...",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "コントローラーを床に置いてください!"
|
||||
"ONE_CONTROLLER_ON_FLOOR": "コントローラーを床に置いてください!",
|
||||
"CANNOT_ADD_SET": "セットを追加できません!",
|
||||
"MAXIMUM_SETS_REACHED": "最大セット数に達しました。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,17 @@
|
||||
"ANCHOR": {
|
||||
"CENTER": "Centrum"
|
||||
},
|
||||
"BAR": {
|
||||
"ADD_MIRROR": "Dodaj nowy widok lustrzany"
|
||||
"BAR": {
|
||||
"ADD_MIRROR": "Dodaj nowy widok lustrzany",
|
||||
"EDIT_MODE_TOGGLE": "Przełącz tryb edycji",
|
||||
"ADD_NEW_SET": "Dodaj nowy zestaw",
|
||||
"DELETE_CURRENT_SET": "Usuń aktualny zestaw",
|
||||
"TOGGLE_VISIBILITY": "Przełącz widoczność",
|
||||
"RESET_POSITION": "Zresetuj pozycję",
|
||||
"RELOAD_FROM_DISK": "Przeładuj XML z dysku",
|
||||
"CLOSE_MIRROR": "Zamknij lustro",
|
||||
"CLOSE_APP": "Zamknij aplikację",
|
||||
"FORCE_CLOSE_APP": "Wymuś zamknięcie aplikacji"
|
||||
},
|
||||
"WATCH": {
|
||||
"RECENTER": "Wyśrodkuj przestrzeń gry",
|
||||
@@ -14,7 +23,8 @@
|
||||
"ADD_NEW_SET": "Dodaj nowy zestaw",
|
||||
"SWITCH_TO_SET": "Przełącz na zestaw",
|
||||
"TOGGLE_FOR_CURRENT_SET": "Przełącz widoczność w bieżącym zestawie",
|
||||
"LONG_PRESS_TO_DELETE_SET": "Przytrzymaj, aby usunąć zestaw"
|
||||
"LONG_PRESS_TO_DELETE_SET": "Przytrzymaj, aby usunąć zestaw",
|
||||
"CLEANUP_MIRRORS": "Usuń lustra, które\nnie są obecnie widoczne"
|
||||
},
|
||||
"EDIT_MODE": {
|
||||
"ADJUST_CURVATURE": "Dostosuj zakrzywienie",
|
||||
@@ -81,6 +91,8 @@
|
||||
"EMPTY_SET": "Pusty zestaw!",
|
||||
"LETS_ADD_OVERLAYS": "Dodajmy kilka nakładek z zegarka!",
|
||||
"FIXING_FLOOR": "Naprawianie podłogi za 5 sekund...",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "Umieść jeden kontroler na podłodze!"
|
||||
"ONE_CONTROLLER_ON_FLOOR": "Umieść jeden kontroler na podłodze!",
|
||||
"CANNOT_ADD_SET": "Nie można dodać zestawu!",
|
||||
"MAXIMUM_SETS_REACHED": "Osiągnięto maksymalną liczbę zestawów."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,18 @@ impl InputBlocker {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, state: &AppState, watch_id: OverlayID, monado: &mut Monado) {
|
||||
if !state.session.config.block_game_input {
|
||||
pub fn update(&mut self, app: &mut AppState, watch_id: OverlayID) {
|
||||
let Some(monado) = &mut app.monado else {
|
||||
return; // monado not available
|
||||
};
|
||||
|
||||
if !app.session.config.block_game_input {
|
||||
return;
|
||||
}
|
||||
|
||||
let any_hovered = state.input_state.pointers.iter().any(|p| {
|
||||
let any_hovered = app.input_state.pointers.iter().any(|p| {
|
||||
p.interaction.hovered_id.is_some_and(|id| {
|
||||
id != watch_id || !state.session.config.block_game_input_ignore_watch
|
||||
id != watch_id || !app.session.config.block_game_input_ignore_watch
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
|
||||
use wlx_common::config_io;
|
||||
|
||||
use crate::{
|
||||
backend::input::{Haptics, Pointer, TrackedDevice, TrackedDeviceRole},
|
||||
backend::input::{Haptics, InputState, Pointer, TrackedDevice, TrackedDeviceRole},
|
||||
state::{AppSession, AppState},
|
||||
};
|
||||
|
||||
@@ -227,12 +227,12 @@ impl OpenXrInputSource {
|
||||
fn update_device_battery_status(
|
||||
device: &mut mnd::Device,
|
||||
role: TrackedDeviceRole,
|
||||
app: &mut AppState,
|
||||
input_state: &mut InputState,
|
||||
) {
|
||||
if let Ok(status) = device.battery_status()
|
||||
&& status.present
|
||||
{
|
||||
app.input_state.devices.push(TrackedDevice {
|
||||
input_state.devices.push(TrackedDevice {
|
||||
soc: Some(status.charge),
|
||||
charging: status.charging,
|
||||
role,
|
||||
@@ -247,7 +247,11 @@ impl OpenXrInputSource {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_devices(app: &mut AppState, monado: &mut mnd::Monado) -> bool {
|
||||
pub fn update_devices(app: &mut AppState) -> bool {
|
||||
let Some(monado) = &mut app.monado else {
|
||||
return false; // monado not available
|
||||
};
|
||||
|
||||
let old_len = app.input_state.devices.len();
|
||||
app.input_state.devices.clear();
|
||||
|
||||
@@ -267,13 +271,14 @@ impl OpenXrInputSource {
|
||||
),
|
||||
];
|
||||
let mut seen = Vec::<u32>::with_capacity(32);
|
||||
|
||||
for (mnd_role, wlx_role) in roles {
|
||||
let device = monado.device_from_role(mnd_role);
|
||||
if let Ok(mut device) = device
|
||||
&& !seen.contains(&device.index)
|
||||
{
|
||||
seen.push(device.index);
|
||||
Self::update_device_battery_status(&mut device, wlx_role, app);
|
||||
Self::update_device_battery_status(&mut device, wlx_role, &mut app.input_state);
|
||||
}
|
||||
}
|
||||
if let Ok(devices) = monado.devices() {
|
||||
@@ -284,7 +289,7 @@ impl OpenXrInputSource {
|
||||
} else {
|
||||
TrackedDeviceRole::None
|
||||
};
|
||||
Self::update_device_battery_status(&mut device, role, app);
|
||||
Self::update_device_battery_status(&mut device, role, &mut app.input_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use std::{
|
||||
|
||||
use glam::{Affine3A, Vec3};
|
||||
use input::OpenXrInputSource;
|
||||
use libmonado::Monado;
|
||||
use openxr as xr;
|
||||
use skybox::create_skybox;
|
||||
use vulkano::{Handle, VulkanObject};
|
||||
@@ -98,17 +97,15 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
|
||||
|
||||
let mut delete_queue = vec![];
|
||||
|
||||
let mut monado = Monado::auto_connect()
|
||||
.map_err(|e| log::warn!("Will not use libmonado: {e}"))
|
||||
.ok();
|
||||
app.monado_init();
|
||||
|
||||
let mut playspace = monado.as_mut().and_then(|m| {
|
||||
let mut playspace = app.monado.as_mut().and_then(|m| {
|
||||
playspace::PlayspaceMover::new(m)
|
||||
.map_err(|e| log::warn!("Will not use Monado playspace mover: {e}"))
|
||||
.ok()
|
||||
});
|
||||
|
||||
let mut blocker = monado.is_some().then(blocker::InputBlocker::new);
|
||||
let mut blocker = app.monado.is_some().then(blocker::InputBlocker::new);
|
||||
|
||||
let (session, mut frame_wait, mut frame_stream) = unsafe {
|
||||
let raw_session = helpers::create_overlay_session(
|
||||
@@ -223,10 +220,8 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
|
||||
}
|
||||
}
|
||||
|
||||
if next_device_update <= Instant::now()
|
||||
&& let Some(monado) = &mut monado
|
||||
{
|
||||
let changed = OpenXrInputSource::update_devices(&mut app, monado);
|
||||
if app.monado.is_some() && next_device_update <= Instant::now() {
|
||||
let changed = OpenXrInputSource::update_devices(&mut app);
|
||||
if changed {
|
||||
overlays.devices_changed(&mut app)?;
|
||||
}
|
||||
@@ -278,11 +273,7 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
|
||||
app.input_state.post_update(&app.session);
|
||||
|
||||
if let Some(ref mut blocker) = blocker {
|
||||
blocker.update(
|
||||
&app,
|
||||
watch_id,
|
||||
monado.as_mut().unwrap(), // safe
|
||||
);
|
||||
blocker.update(&mut app, watch_id);
|
||||
}
|
||||
|
||||
if app
|
||||
@@ -307,11 +298,7 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
|
||||
|
||||
watch_fade(&mut app, overlays.mut_by_id(watch_id).unwrap()); // want panic
|
||||
if let Some(ref mut space_mover) = playspace {
|
||||
space_mover.update(
|
||||
&mut overlays,
|
||||
&app,
|
||||
monado.as_mut().unwrap(), // safe
|
||||
);
|
||||
space_mover.update(&mut overlays, &mut app);
|
||||
}
|
||||
|
||||
for o in overlays.values_mut() {
|
||||
@@ -489,8 +476,8 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
|
||||
overlays.handle_task(&mut app, task)?;
|
||||
}
|
||||
TaskType::Playspace(task) => {
|
||||
if let (Some(playspace), Some(monado)) = (playspace.as_mut(), monado.as_mut()) {
|
||||
playspace.handle_task(&app, monado, task);
|
||||
if let Some(playspace) = playspace.as_mut() {
|
||||
playspace.handle_task(&mut app, task);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "openvr")]
|
||||
|
||||
@@ -43,7 +43,11 @@ impl PlayspaceMover {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn handle_task(&mut self, app: &AppState, monado: &mut Monado, task: PlayspaceTask) {
|
||||
pub fn handle_task(&mut self, app: &mut AppState, task: PlayspaceTask) {
|
||||
let Some(monado) = &mut app.monado else {
|
||||
return; // monado not available
|
||||
};
|
||||
|
||||
match task {
|
||||
PlayspaceTask::FixFloor => {
|
||||
self.fix_floor(&app.input_state, monado);
|
||||
@@ -60,9 +64,12 @@ impl PlayspaceMover {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
overlays: &mut OverlayWindowManager<OpenXrOverlayData>,
|
||||
app: &AppState,
|
||||
monado: &mut Monado,
|
||||
app: &mut AppState,
|
||||
) {
|
||||
let Some(monado) = &mut app.monado else {
|
||||
return; // monado not available
|
||||
};
|
||||
|
||||
for pointer in &app.input_state.pointers {
|
||||
if pointer.now.space_reset {
|
||||
if !pointer.before.space_reset {
|
||||
|
||||
@@ -16,7 +16,7 @@ use wgui::{
|
||||
widget::EventResult,
|
||||
};
|
||||
use wlx_common::{
|
||||
dash_interface::DashInterface,
|
||||
dash_interface::{self, DashInterface},
|
||||
overlays::{BackendAttrib, BackendAttribValue},
|
||||
};
|
||||
use wlx_common::{
|
||||
@@ -444,4 +444,92 @@ impl DashInterface<AppState> for DashInterfaceLive {
|
||||
RUNNING.store(false, Ordering::Relaxed);
|
||||
RESTART.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn monado_client_list(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
) -> anyhow::Result<Vec<dash_interface::MonadoClient>> {
|
||||
let Some(monado) = &mut app.monado else {
|
||||
return Ok(Vec::new()); // no monado available
|
||||
};
|
||||
|
||||
let clients = monado_list_clients_filtered(monado)?;
|
||||
|
||||
let mut res = Vec::<dash_interface::MonadoClient>::new();
|
||||
|
||||
for mut client in clients {
|
||||
let name = client.name()?;
|
||||
let state = client.state()?;
|
||||
|
||||
res.push(dash_interface::MonadoClient {
|
||||
name,
|
||||
is_primary: state.contains(libmonado::ClientState::ClientPrimaryApp),
|
||||
is_active: state.contains(libmonado::ClientState::ClientSessionActive),
|
||||
is_visible: state.contains(libmonado::ClientState::ClientSessionVisible),
|
||||
is_focused: state.contains(libmonado::ClientState::ClientSessionFocused),
|
||||
is_overlay: state.contains(libmonado::ClientState::ClientSessionOverlay),
|
||||
is_io_active: state.contains(libmonado::ClientState::ClientIoActive),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn monado_client_focus(&mut self, app: &mut AppState, name: &str) -> anyhow::Result<()> {
|
||||
let Some(monado) = &mut app.monado else {
|
||||
return Ok(()); // no monado avoilable
|
||||
};
|
||||
|
||||
monado_client_focus(monado, name)?;
|
||||
|
||||
// Restart monado (BUG!)
|
||||
// https://gitlab.freedesktop.org/monado/monado/-/issues/497
|
||||
app.monado_init();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const CLIENT_NAME_BLACKLIST: [&str; 2] = ["wlx-overlay-s", "libmonado"];
|
||||
|
||||
fn monado_list_clients_filtered(
|
||||
monado: &mut libmonado::Monado,
|
||||
) -> anyhow::Result<Vec<libmonado::Client<'_>>> {
|
||||
let mut clients: Vec<_> = monado.clients()?.into_iter().collect();
|
||||
|
||||
let clients: Vec<_> = clients
|
||||
.iter_mut()
|
||||
.filter_map(|client| {
|
||||
let Ok(name) = client.name() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
for cell in CLIENT_NAME_BLACKLIST {
|
||||
if cell == name {
|
||||
// blacklisted!
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(client.clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(clients)
|
||||
}
|
||||
|
||||
fn monado_client_focus(monado: &mut libmonado::Monado, name: &str) -> anyhow::Result<()> {
|
||||
let clients = monado_list_clients_filtered(monado)?;
|
||||
|
||||
for mut client in clients {
|
||||
let client_name = client.name()?;
|
||||
if client_name != name {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::info!("Monado focus set to {client_name}");
|
||||
client.set_primary()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -64,6 +64,9 @@ pub struct AppState {
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
pub wvr_server: Option<WvrServerState>,
|
||||
|
||||
#[cfg(feature = "openxr")]
|
||||
pub monado: Option<libmonado::Monado>,
|
||||
}
|
||||
|
||||
#[allow(unused_mut)]
|
||||
@@ -163,8 +166,20 @@ impl AppState {
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
wvr_server,
|
||||
|
||||
#[cfg(feature = "openxr")]
|
||||
monado: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "openxr")]
|
||||
pub fn monado_init(&mut self) {
|
||||
log::debug!("Connecting to Monado IPC");
|
||||
self.monado = None; // stop connection first
|
||||
self.monado = libmonado::Monado::auto_connect()
|
||||
.map_err(|e| log::warn!("Will not use libmonado: {e}"))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppSession {
|
||||
|
||||
Reference in New Issue
Block a user