better syncing
This commit is contained in:
parent
9c1306d8c7
commit
9a72c09fed
6 changed files with 144 additions and 45 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1308,6 +1308,7 @@ name = "cosmic-settings-daemon-config"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cosmic-config",
|
"cosmic-config",
|
||||||
|
"cosmic-theme",
|
||||||
"ron 0.8.1",
|
"ron 0.8.1",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use std::{
|
||||||
pub use cosmic_applets_config::time::TimeAppletConfig;
|
pub use cosmic_applets_config::time::TimeAppletConfig;
|
||||||
pub use cosmic_bg_config::{state::State as BgState, Color, Source as BgSource};
|
pub use cosmic_bg_config::{state::State as BgState, Color, Source as BgSource};
|
||||||
pub use cosmic_comp_config::{CosmicCompConfig, XkbConfig};
|
pub use cosmic_comp_config::{CosmicCompConfig, XkbConfig};
|
||||||
pub use cosmic_theme::Theme;
|
pub use cosmic_theme::{Theme, ThemeBuilder};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct UserData {
|
pub struct UserData {
|
||||||
|
|
@ -17,6 +17,7 @@ pub struct UserData {
|
||||||
pub full_name: String,
|
pub full_name: String,
|
||||||
pub icon_opt: Option<Vec<u8>>,
|
pub icon_opt: Option<Vec<u8>>,
|
||||||
pub theme_opt: Option<Theme>,
|
pub theme_opt: Option<Theme>,
|
||||||
|
pub theme_builder_opt: Option<ThemeBuilder>,
|
||||||
pub bg_state: BgState,
|
pub bg_state: BgState,
|
||||||
pub bg_path_data: BTreeMap<PathBuf, Vec<u8>>,
|
pub bg_path_data: BTreeMap<PathBuf, Vec<u8>>,
|
||||||
pub xkb_config_opt: Option<XkbConfig>,
|
pub xkb_config_opt: Option<XkbConfig>,
|
||||||
|
|
@ -59,6 +60,7 @@ impl UserData {
|
||||||
pub fn load_config_as_user(&mut self) {
|
pub fn load_config_as_user(&mut self) {
|
||||||
self.icon_opt = None;
|
self.icon_opt = None;
|
||||||
self.theme_opt = None;
|
self.theme_opt = None;
|
||||||
|
self.theme_builder_opt = None;
|
||||||
self.bg_state = Default::default();
|
self.bg_state = Default::default();
|
||||||
self.xkb_config_opt = None;
|
self.xkb_config_opt = None;
|
||||||
self.time_applet_config = Default::default();
|
self.time_applet_config = Default::default();
|
||||||
|
|
@ -114,6 +116,28 @@ impl UserData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match if is_dark {
|
||||||
|
cosmic_theme::ThemeBuilder::dark_config()
|
||||||
|
} else {
|
||||||
|
cosmic_theme::ThemeBuilder::light_config()
|
||||||
|
} {
|
||||||
|
Ok(helper) => match cosmic_theme::ThemeBuilder::get_entry(&helper) {
|
||||||
|
Ok(theme) => {
|
||||||
|
self.theme_builder_opt = Some(theme);
|
||||||
|
}
|
||||||
|
Err((errs, theme)) => {
|
||||||
|
log::error!("failed to load cosmic-theme builder config: {:?}", errs);
|
||||||
|
self.theme_builder_opt = Some(theme);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(
|
||||||
|
"failed to create cosmic-theme builder config helper: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: fallback to background config if background state is not set?
|
//TODO: fallback to background config if background state is not set?
|
||||||
match cosmic_bg_config::state::State::state() {
|
match cosmic_bg_config::state::State::state() {
|
||||||
Ok(helper) => match cosmic_bg_config::state::State::get_entry(&helper) {
|
Ok(helper) => match cosmic_bg_config::state::State::get_entry(&helper) {
|
||||||
|
|
|
||||||
3
debian/cosmic-greeter.tmpfiles
vendored
3
debian/cosmic-greeter.tmpfiles
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
# Home directory of cosmic-greeter
|
# Home directory of cosmic-greeter
|
||||||
d /var/lib/cosmic-greeter 0750 cosmic-greeter cosmic-greeter
|
d /var/lib/cosmic-greeter 0750 cosmic-greeter cosmic-greeter
|
||||||
|
d /run/cosmic-greeter 0755 cosmic-greeter cosmic-greeter -
|
||||||
|
|
|
||||||
149
src/greeter.rs
149
src/greeter.rs
|
|
@ -9,7 +9,6 @@ use cosmic::app::{Core, Settings, Task};
|
||||||
use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity;
|
use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity;
|
||||||
use cosmic::iced::{Point, Size};
|
use cosmic::iced::{Point, Size};
|
||||||
use cosmic::iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings;
|
use cosmic::iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings;
|
||||||
use cosmic::surface;
|
|
||||||
use cosmic::widget::text;
|
use cosmic::widget::text;
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
Element,
|
Element,
|
||||||
|
|
@ -29,11 +28,13 @@ use cosmic::{
|
||||||
iced_runtime::core::window::Id as SurfaceId,
|
iced_runtime::core::window::Id as SurfaceId,
|
||||||
theme, widget,
|
theme, widget,
|
||||||
};
|
};
|
||||||
|
use cosmic::{cosmic_theme::{self, CosmicPalette}, surface};
|
||||||
|
use cosmic_config::CosmicConfigEntry;
|
||||||
use cosmic_greeter_config::Config as CosmicGreeterConfig;
|
use cosmic_greeter_config::Config as CosmicGreeterConfig;
|
||||||
use cosmic_greeter_daemon::UserData;
|
use cosmic_greeter_daemon::UserData;
|
||||||
use cosmic_settings_subscriptions::{
|
use cosmic_settings_daemon_config::greeter::GreeterAccessibilityState;
|
||||||
accessibility::{self, DBusRequest, DBusUpdate},
|
use cosmic_settings_subscriptions::cosmic_a11y_manager::{
|
||||||
cosmic_a11y_manager::{AccessibilityEvent, AccessibilityRequest, ColorFilter},
|
AccessibilityEvent, AccessibilityRequest,
|
||||||
};
|
};
|
||||||
use greetd_ipc::Request;
|
use greetd_ipc::Request;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
@ -47,7 +48,8 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use tokio::{sync::mpsc::UnboundedSender, time};
|
use tokio::process::Child;
|
||||||
|
use tokio::time;
|
||||||
use wayland_client::{Proxy, protocol::wl_output::WlOutput};
|
use wayland_client::{Proxy, protocol::wl_output::WlOutput};
|
||||||
use zbus::{Connection, proxy};
|
use zbus::{Connection, proxy};
|
||||||
|
|
||||||
|
|
@ -339,7 +341,6 @@ struct NameIndexPair {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
Common(common::Message),
|
Common(common::Message),
|
||||||
DBusUpdate(DBusUpdate),
|
|
||||||
OutputEvent(OutputEvent, WlOutput),
|
OutputEvent(OutputEvent, WlOutput),
|
||||||
Auth(Option<String>),
|
Auth(Option<String>),
|
||||||
ConfigUpdateUser,
|
ConfigUpdateUser,
|
||||||
|
|
@ -354,6 +355,7 @@ pub enum Message {
|
||||||
KeyboardLayout(usize),
|
KeyboardLayout(usize),
|
||||||
Login,
|
Login,
|
||||||
Reconnect,
|
Reconnect,
|
||||||
|
Reload(cosmic::Theme),
|
||||||
Restart,
|
Restart,
|
||||||
Session(String),
|
Session(String),
|
||||||
Shutdown,
|
Shutdown,
|
||||||
|
|
@ -389,20 +391,20 @@ pub struct App {
|
||||||
dropdown_opt: Option<Dropdown>,
|
dropdown_opt: Option<Dropdown>,
|
||||||
heartbeat_handle: Option<cosmic::iced::task::Handle>,
|
heartbeat_handle: Option<cosmic::iced::task::Handle>,
|
||||||
entering_name: bool,
|
entering_name: bool,
|
||||||
|
theme_builder: cosmic_theme::ThemeBuilder,
|
||||||
|
|
||||||
accessibility: Accessibility,
|
accessibility: Accessibility,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Accessibility {
|
struct Accessibility {
|
||||||
pub dbus_sender: Option<UnboundedSender<DBusRequest>>,
|
|
||||||
pub wayland_sender: Option<calloop::channel::Sender<AccessibilityRequest>>,
|
pub wayland_sender: Option<calloop::channel::Sender<AccessibilityRequest>>,
|
||||||
pub wayland_protocol_version: Option<u32>,
|
pub wayland_protocol_version: Option<u32>,
|
||||||
|
|
||||||
pub state: cosmic_settings_daemon_config::greeter::GreeterAccessibilityState,
|
pub state: cosmic_settings_daemon_config::greeter::GreeterAccessibilityState,
|
||||||
pub helper: Option<cosmic::cosmic_config::Config>,
|
pub helper: Option<cosmic::cosmic_config::Config>,
|
||||||
|
|
||||||
pub screen_reader: bool,
|
pub screen_reader: Option<Child>,
|
||||||
pub magnifier: bool,
|
pub magnifier: bool,
|
||||||
pub high_contrast: bool,
|
pub high_contrast: bool,
|
||||||
pub invert_colors: bool,
|
pub invert_colors: bool,
|
||||||
|
|
@ -592,8 +594,8 @@ impl App {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
items.push(menu_checklist(
|
items.push(menu_checklist(
|
||||||
fl!("accessibility", "screen-reader"),
|
fl!("accessibility", "screen-reader"),
|
||||||
self.accessibility.screen_reader,
|
self.accessibility.screen_reader.is_some(),
|
||||||
Message::ScreenReader(!self.accessibility.screen_reader),
|
Message::ScreenReader(!self.accessibility.screen_reader.is_some()),
|
||||||
));
|
));
|
||||||
items.push(menu_checklist(
|
items.push(menu_checklist(
|
||||||
fl!("accessibility", "magnifier"),
|
fl!("accessibility", "magnifier"),
|
||||||
|
|
@ -907,8 +909,13 @@ impl App {
|
||||||
// Ensure that user's xkb config is used
|
// Ensure that user's xkb config is used
|
||||||
self.common.set_xkb_config(&user_data);
|
self.common.set_xkb_config(&user_data);
|
||||||
|
|
||||||
|
if let Some(builder) = &user_data.theme_builder_opt {
|
||||||
|
self.theme_builder = builder.clone();
|
||||||
|
}
|
||||||
|
|
||||||
match &user_data.theme_opt {
|
match &user_data.theme_opt {
|
||||||
Some(theme) => {
|
Some(theme) => {
|
||||||
|
self.accessibility.high_contrast = theme.is_high_contrast;
|
||||||
cosmic::command::set_theme(cosmic::Theme::custom(Arc::new(theme.clone())))
|
cosmic::command::set_theme(cosmic::Theme::custom(Arc::new(theme.clone())))
|
||||||
}
|
}
|
||||||
None => Task::none(),
|
None => Task::none(),
|
||||||
|
|
@ -940,10 +947,6 @@ impl cosmic::Application for App {
|
||||||
|
|
||||||
/// Creates the application, and optionally emits command on initialize.
|
/// Creates the application, and optionally emits command on initialize.
|
||||||
fn init(core: Core, flags: Self::Flags) -> (Self, Task<Message>) {
|
fn init(core: Core, flags: Self::Flags) -> (Self, Task<Message>) {
|
||||||
// init state that is communicated to cosmic session
|
|
||||||
if let Err(err) = crate::state::init() {
|
|
||||||
log::error!("{err:?}");
|
|
||||||
}
|
|
||||||
let (mut common, common_task) = Common::init(core);
|
let (mut common, common_task) = Common::init(core);
|
||||||
common.on_output_event = Some(Box::new(|output_event, output| {
|
common.on_output_event = Some(Box::new(|output_event, output| {
|
||||||
Message::OutputEvent(output_event, output)
|
Message::OutputEvent(output_event, output)
|
||||||
|
|
@ -993,6 +996,10 @@ impl cosmic::Application for App {
|
||||||
let mut accessibility = Accessibility::default();
|
let mut accessibility = Accessibility::default();
|
||||||
accessibility.helper =
|
accessibility.helper =
|
||||||
cosmic_settings_daemon_config::greeter::GreeterAccessibilityState::config().ok();
|
cosmic_settings_daemon_config::greeter::GreeterAccessibilityState::config().ok();
|
||||||
|
// Reset the state so that only new changes are applied.
|
||||||
|
if let Some(helper) = accessibility.helper.as_ref() {
|
||||||
|
_ = GreeterAccessibilityState::write_entry(&Default::default(), helper);
|
||||||
|
}
|
||||||
|
|
||||||
let app = App {
|
let app = App {
|
||||||
common,
|
common,
|
||||||
|
|
@ -1008,6 +1015,7 @@ impl cosmic::Application for App {
|
||||||
heartbeat_handle: None,
|
heartbeat_handle: None,
|
||||||
entering_name: false,
|
entering_name: false,
|
||||||
accessibility,
|
accessibility,
|
||||||
|
theme_builder: Default::default(),
|
||||||
};
|
};
|
||||||
(app, common_task)
|
(app, common_task)
|
||||||
}
|
}
|
||||||
|
|
@ -1018,20 +1026,7 @@ impl cosmic::Application for App {
|
||||||
Message::Common(common_message) => {
|
Message::Common(common_message) => {
|
||||||
return self.common.update(common_message);
|
return self.common.update(common_message);
|
||||||
}
|
}
|
||||||
Message::DBusUpdate(update) => match update {
|
|
||||||
DBusUpdate::Error(err) => {
|
|
||||||
log::error!("{err}");
|
|
||||||
let _ = self.accessibility.dbus_sender.take();
|
|
||||||
self.accessibility.screen_reader = false;
|
|
||||||
}
|
|
||||||
DBusUpdate::Status(enabled) => {
|
|
||||||
self.accessibility.screen_reader = enabled;
|
|
||||||
}
|
|
||||||
DBusUpdate::Init(enabled, tx) => {
|
|
||||||
self.accessibility.screen_reader = enabled;
|
|
||||||
self.accessibility.dbus_sender = Some(tx);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Message::OutputEvent(output_event, output) => {
|
Message::OutputEvent(output_event, output) => {
|
||||||
match output_event {
|
match output_event {
|
||||||
OutputEvent::Created(output_info_opt) => {
|
OutputEvent::Created(output_info_opt) => {
|
||||||
|
|
@ -1176,6 +1171,12 @@ impl cosmic::Application for App {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::Reload(new) => {
|
||||||
|
|
||||||
|
return cosmic::command::set_theme(
|
||||||
|
new.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
Message::Session(selected_session) => {
|
Message::Session(selected_session) => {
|
||||||
self.selected_session = selected_session;
|
self.selected_session = selected_session;
|
||||||
if self.dropdown_opt == Some(Dropdown::Session) {
|
if self.dropdown_opt == Some(Dropdown::Session) {
|
||||||
|
|
@ -1458,23 +1459,78 @@ impl cosmic::Application for App {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Message::ScreenReader(enabled) => {
|
Message::ScreenReader(enabled) => {
|
||||||
if let Some(tx) = &self.accessibility.dbus_sender.as_ref() {
|
if enabled
|
||||||
self.accessibility.screen_reader = enabled;
|
&& self
|
||||||
let _ = tx.send(DBusRequest::Status(enabled));
|
.accessibility
|
||||||
|
.screen_reader
|
||||||
|
.as_mut()
|
||||||
|
.is_none_or(|c| c.try_wait().is_ok())
|
||||||
|
{
|
||||||
|
self.accessibility.screen_reader =
|
||||||
|
tokio::process::Command::new("/usr/bin/orca").spawn().ok();
|
||||||
} else {
|
} else {
|
||||||
self.accessibility.screen_reader = false;
|
if let Some(mut c) = self.accessibility.screen_reader.take() {
|
||||||
|
return cosmic::task::future::<(), ()>(async move {
|
||||||
|
if let Err(err) = c.kill().await {
|
||||||
|
log::error!("Failed to stop screen reader: {err:?}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.discard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(helper) = self.accessibility.helper.as_ref() {
|
||||||
|
_ = self
|
||||||
|
.accessibility
|
||||||
|
.state
|
||||||
|
.set_screen_reader(&helper, Some(enabled));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Magnifier(enabled) => {
|
Message::Magnifier(enabled) => {
|
||||||
if let Some(tx) = &self.accessibility.wayland_sender {
|
if let Some(tx) = &self.accessibility.wayland_sender {
|
||||||
self.accessibility.magnifier = enabled;
|
self.accessibility.magnifier = enabled;
|
||||||
let _ = tx.send(AccessibilityRequest::Magnifier(enabled));
|
let _ = tx.send(AccessibilityRequest::Magnifier(enabled));
|
||||||
|
if let Some(helper) = self.accessibility.helper.as_ref() {
|
||||||
|
_ = self
|
||||||
|
.accessibility
|
||||||
|
.state
|
||||||
|
.set_magnifier(&helper, Some(enabled));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.accessibility.magnifier = false;
|
self.accessibility.magnifier = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::HighContrast(enabled) => {
|
Message::HighContrast(enabled) => {
|
||||||
self.accessibility.high_contrast = enabled;
|
self.accessibility.high_contrast = enabled;
|
||||||
|
|
||||||
|
if let Some(helper) = self.accessibility.helper.as_ref() {
|
||||||
|
_ = self
|
||||||
|
.accessibility
|
||||||
|
.state
|
||||||
|
.set_high_contrast(&helper, Some(enabled));
|
||||||
|
}
|
||||||
|
let builder = self.theme_builder.clone();
|
||||||
|
|
||||||
|
return cosmic::task::future::<_, _>(async move {
|
||||||
|
let builder = builder.clone();
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
match apply_hc_theme(builder, enabled) {
|
||||||
|
Ok(t) => {
|
||||||
|
_ = tx.send(Some(t));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("{err:?}");
|
||||||
|
_ = tx.send(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Ok(Some(theme)) = rx.await {
|
||||||
|
cosmic::Action::App(Message::Reload(cosmic::Theme::custom(std::sync::Arc::new(theme))))
|
||||||
|
} else {
|
||||||
|
cosmic::Action::None
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Message::InvertColors(enabled) => {
|
Message::InvertColors(enabled) => {
|
||||||
if let Some(tx) = &self.accessibility.wayland_sender {
|
if let Some(tx) = &self.accessibility.wayland_sender {
|
||||||
|
|
@ -1483,6 +1539,12 @@ impl cosmic::Application for App {
|
||||||
inverted: enabled,
|
inverted: enabled,
|
||||||
filter: None,
|
filter: None,
|
||||||
});
|
});
|
||||||
|
if let Some(helper) = self.accessibility.helper.as_ref() {
|
||||||
|
_ = self
|
||||||
|
.accessibility
|
||||||
|
.state
|
||||||
|
.set_invert_colors(&helper, Some(enabled));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.accessibility.invert_colors = false;
|
self.accessibility.invert_colors = false;
|
||||||
}
|
}
|
||||||
|
|
@ -1539,7 +1601,28 @@ impl cosmic::Application for App {
|
||||||
self.common.subscription().map(Message::from),
|
self.common.subscription().map(Message::from),
|
||||||
ipc::subscription(),
|
ipc::subscription(),
|
||||||
wayland::a11y_subscription().map(Message::WaylandUpdate),
|
wayland::a11y_subscription().map(Message::WaylandUpdate),
|
||||||
accessibility::subscription().map(Message::DBusUpdate),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn apply_hc_theme(builder: cosmic_theme::ThemeBuilder, enabled: bool) -> Result<cosmic_theme::Theme, cosmic_config::Error> {
|
||||||
|
let is_dark = builder.palette.is_dark();
|
||||||
|
let mut builder = builder.clone();
|
||||||
|
|
||||||
|
builder.palette = if is_dark {
|
||||||
|
if enabled {
|
||||||
|
CosmicPalette::HighContrastDark(builder.palette.inner())
|
||||||
|
} else {
|
||||||
|
CosmicPalette::Dark(builder.palette.inner())
|
||||||
|
}
|
||||||
|
} else if enabled {
|
||||||
|
CosmicPalette::HighContrastLight(builder.palette.inner())
|
||||||
|
} else {
|
||||||
|
CosmicPalette::Light(builder.palette.inner())
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_theme = builder.build();
|
||||||
|
|
||||||
|
Ok(new_theme)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,4 @@ mod networkmanager;
|
||||||
#[cfg(feature = "upower")]
|
#[cfg(feature = "upower")]
|
||||||
mod upower;
|
mod upower;
|
||||||
|
|
||||||
mod state;
|
|
||||||
|
|
||||||
mod time;
|
mod time;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
use std::os::unix::fs::PermissionsExt as _;
|
|
||||||
|
|
||||||
pub fn init() -> anyhow::Result<()> {
|
|
||||||
let path = cosmic_settings_daemon_config::greeter::GreeterAccessibilityState::path();
|
|
||||||
std::fs::create_dir_all(&path)?;
|
|
||||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue