2023-10-24 16:33:00 -05:00
|
|
|
use cosmic::applet::{menu_button, padded_control};
|
2023-11-21 17:29:49 -05:00
|
|
|
use cosmic::cctk::sctk::reexports::calloop;
|
2023-10-22 15:23:07 -05:00
|
|
|
use cosmic::iced::wayland::popup::{destroy_popup, get_popup};
|
|
|
|
|
use cosmic::iced::{
|
2024-02-27 13:51:42 -08:00
|
|
|
subscription,
|
2023-10-22 15:23:07 -05:00
|
|
|
widget::{column, row, text, vertical_space},
|
|
|
|
|
window, Alignment, Length, Rectangle, Subscription,
|
|
|
|
|
};
|
|
|
|
|
use cosmic::iced_core::alignment::{Horizontal, Vertical};
|
|
|
|
|
use cosmic::iced_style::application;
|
2024-03-28 19:10:59 -04:00
|
|
|
use cosmic::widget::{button, container, divider, grid, horizontal_space, Button, Grid, Space};
|
2023-10-22 15:23:07 -05:00
|
|
|
use cosmic::{app, applet::cosmic_panel_config::PanelAnchor, Command};
|
|
|
|
|
use cosmic::{
|
|
|
|
|
widget::{icon, rectangle_tracker::*},
|
|
|
|
|
Element, Theme,
|
|
|
|
|
};
|
|
|
|
|
|
2024-03-28 19:10:59 -04:00
|
|
|
use chrono::{DateTime, Datelike, DurationRound, Local, Months, NaiveDate, Weekday};
|
2023-10-22 15:23:07 -05:00
|
|
|
|
2024-03-22 17:22:43 +01:00
|
|
|
use crate::config::TimeAppletConfig;
|
2023-10-22 15:23:07 -05:00
|
|
|
use crate::fl;
|
2023-10-23 13:34:19 -05:00
|
|
|
use crate::time::get_calender_first;
|
2023-11-21 17:29:49 -05:00
|
|
|
use cosmic::applet::token::subscription::{
|
|
|
|
|
activation_token_subscription, TokenRequest, TokenUpdate,
|
|
|
|
|
};
|
2023-10-22 15:23:07 -05:00
|
|
|
|
|
|
|
|
#[allow(dead_code)]
|
2024-02-27 13:51:42 -08:00
|
|
|
#[derive(Debug, Clone, Copy)]
|
2023-10-22 15:23:07 -05:00
|
|
|
enum Every {
|
|
|
|
|
Minute,
|
|
|
|
|
Second,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Window {
|
|
|
|
|
core: cosmic::app::Core,
|
|
|
|
|
popup: Option<window::Id>,
|
|
|
|
|
update_at: Every,
|
|
|
|
|
now: DateTime<Local>,
|
2024-02-26 10:24:07 -08:00
|
|
|
date_selected: NaiveDate,
|
2023-10-22 15:23:07 -05:00
|
|
|
rectangle_tracker: Option<RectangleTracker<u32>>,
|
|
|
|
|
rectangle: Rectangle,
|
2023-11-21 17:29:49 -05:00
|
|
|
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
2024-03-22 17:22:43 +01:00
|
|
|
config: TimeAppletConfig,
|
2023-10-22 15:23:07 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub enum Message {
|
|
|
|
|
TogglePopup,
|
|
|
|
|
CloseRequested(window::Id),
|
|
|
|
|
Tick,
|
|
|
|
|
Rectangle(RectangleUpdate<u32>),
|
|
|
|
|
SelectDay(u32),
|
|
|
|
|
PreviousMonth,
|
|
|
|
|
NextMonth,
|
2023-10-23 12:55:48 -05:00
|
|
|
OpenDateTimeSettings,
|
2023-11-21 17:29:49 -05:00
|
|
|
Token(TokenUpdate),
|
2024-03-22 17:22:43 +01:00
|
|
|
ConfigChanged(TimeAppletConfig),
|
2023-10-22 15:23:07 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl cosmic::Application for Window {
|
|
|
|
|
type Message = Message;
|
|
|
|
|
type Executor = cosmic::SingleThreadExecutor;
|
|
|
|
|
type Flags = ();
|
|
|
|
|
const APP_ID: &'static str = "com.system76.CosmicAppletTime";
|
|
|
|
|
|
|
|
|
|
fn init(
|
|
|
|
|
core: app::Core,
|
|
|
|
|
_flags: Self::Flags,
|
|
|
|
|
) -> (Self, cosmic::iced::Command<app::Message<Self::Message>>) {
|
2024-02-26 10:24:07 -08:00
|
|
|
let now = Local::now();
|
2023-10-22 15:23:07 -05:00
|
|
|
(
|
2023-11-16 18:32:31 +00:00
|
|
|
Self {
|
2023-10-22 15:23:07 -05:00
|
|
|
core,
|
|
|
|
|
popup: None,
|
|
|
|
|
update_at: Every::Minute,
|
2024-02-26 10:24:07 -08:00
|
|
|
now,
|
|
|
|
|
date_selected: NaiveDate::from(now.naive_local()),
|
2023-10-22 15:23:07 -05:00
|
|
|
rectangle_tracker: None,
|
|
|
|
|
rectangle: Rectangle::default(),
|
2023-11-21 17:29:49 -05:00
|
|
|
token_tx: None,
|
2024-03-22 17:22:43 +01:00
|
|
|
config: TimeAppletConfig::default(),
|
2023-10-22 15:23:07 -05:00
|
|
|
},
|
|
|
|
|
Command::none(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn core(&self) -> &cosmic::app::Core {
|
|
|
|
|
&self.core
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn core_mut(&mut self) -> &mut cosmic::app::Core {
|
|
|
|
|
&mut self.core
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
|
|
|
|
|
Some(cosmic::applet::style())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn subscription(&self) -> Subscription<Message> {
|
|
|
|
|
Subscription::batch(vec![
|
|
|
|
|
rectangle_tracker_subscription(0).map(|e| Message::Rectangle(e.1)),
|
2024-02-27 13:51:42 -08:00
|
|
|
time_subscription(self.update_at).map(|_| Message::Tick),
|
2023-11-21 17:29:49 -05:00
|
|
|
activation_token_subscription(0).map(Message::Token),
|
2024-03-22 17:22:43 +01:00
|
|
|
self.core.watch_config(Self::APP_ID).map(|u| {
|
|
|
|
|
for err in u.errors {
|
|
|
|
|
tracing::error!(?err, "Error watching config");
|
|
|
|
|
}
|
|
|
|
|
Message::ConfigChanged(u.config)
|
|
|
|
|
}),
|
2023-10-22 15:23:07 -05:00
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update(
|
|
|
|
|
&mut self,
|
|
|
|
|
message: Self::Message,
|
|
|
|
|
) -> cosmic::iced::Command<app::Message<Self::Message>> {
|
|
|
|
|
match message {
|
|
|
|
|
Message::TogglePopup => {
|
|
|
|
|
if let Some(p) = self.popup.take() {
|
|
|
|
|
destroy_popup(p)
|
|
|
|
|
} else {
|
2023-12-11 14:45:36 -05:00
|
|
|
let new_id = window::Id::unique();
|
2023-10-22 15:23:07 -05:00
|
|
|
self.popup.replace(new_id);
|
|
|
|
|
|
|
|
|
|
let mut popup_settings = self.core.applet.get_popup_settings(
|
2023-12-11 14:45:36 -05:00
|
|
|
window::Id::MAIN,
|
2023-10-22 15:23:07 -05:00
|
|
|
new_id,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
let Rectangle {
|
|
|
|
|
x,
|
|
|
|
|
y,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
} = self.rectangle;
|
|
|
|
|
popup_settings.positioner.anchor_rect = Rectangle::<i32> {
|
|
|
|
|
x: x as i32,
|
|
|
|
|
y: y as i32,
|
|
|
|
|
width: width as i32,
|
|
|
|
|
height: height as i32,
|
|
|
|
|
};
|
|
|
|
|
get_popup(popup_settings)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::Tick => {
|
|
|
|
|
self.now = Local::now();
|
|
|
|
|
Command::none()
|
|
|
|
|
}
|
|
|
|
|
Message::Rectangle(u) => {
|
|
|
|
|
match u {
|
|
|
|
|
RectangleUpdate::Rectangle(r) => {
|
|
|
|
|
self.rectangle = r.1;
|
|
|
|
|
}
|
|
|
|
|
RectangleUpdate::Init(tracker) => {
|
|
|
|
|
self.rectangle_tracker = Some(tracker);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Command::none()
|
|
|
|
|
}
|
|
|
|
|
Message::CloseRequested(id) => {
|
|
|
|
|
if Some(id) == self.popup {
|
|
|
|
|
self.popup = None;
|
|
|
|
|
}
|
|
|
|
|
Command::none()
|
|
|
|
|
}
|
2023-10-30 14:43:17 -04:00
|
|
|
Message::SelectDay(_day) => {
|
2023-10-22 15:23:07 -05:00
|
|
|
// TODO
|
|
|
|
|
Command::none()
|
|
|
|
|
}
|
|
|
|
|
Message::PreviousMonth => {
|
2024-02-26 10:24:07 -08:00
|
|
|
self.date_selected = self
|
|
|
|
|
.date_selected
|
|
|
|
|
.checked_sub_months(Months::new(1))
|
|
|
|
|
.expect("valid naivedate");
|
2023-10-22 15:23:07 -05:00
|
|
|
Command::none()
|
|
|
|
|
}
|
|
|
|
|
Message::NextMonth => {
|
2024-02-26 10:24:07 -08:00
|
|
|
self.date_selected = self
|
|
|
|
|
.date_selected
|
|
|
|
|
.checked_add_months(Months::new(1))
|
|
|
|
|
.expect("valid naivedate");
|
2023-10-22 15:23:07 -05:00
|
|
|
Command::none()
|
|
|
|
|
}
|
2023-10-23 12:55:48 -05:00
|
|
|
Message::OpenDateTimeSettings => {
|
2023-11-21 17:29:49 -05:00
|
|
|
let exec = "cosmic-settings time".to_string();
|
|
|
|
|
if let Some(tx) = self.token_tx.as_ref() {
|
|
|
|
|
let _ = tx.send(TokenRequest {
|
|
|
|
|
app_id: Self::APP_ID.to_string(),
|
|
|
|
|
exec,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
tracing::error!("Wayland tx is None");
|
|
|
|
|
};
|
|
|
|
|
Command::none()
|
|
|
|
|
}
|
|
|
|
|
Message::Token(u) => {
|
|
|
|
|
match u {
|
|
|
|
|
TokenUpdate::Init(tx) => {
|
|
|
|
|
self.token_tx = Some(tx);
|
|
|
|
|
}
|
|
|
|
|
TokenUpdate::Finished => {
|
|
|
|
|
self.token_tx = None;
|
|
|
|
|
}
|
|
|
|
|
TokenUpdate::ActivationToken { token, .. } => {
|
|
|
|
|
let mut cmd = std::process::Command::new("cosmic-settings");
|
|
|
|
|
cmd.arg("time");
|
|
|
|
|
if let Some(token) = token {
|
|
|
|
|
cmd.env("XDG_ACTIVATION_TOKEN", &token);
|
|
|
|
|
cmd.env("DESKTOP_STARTUP_ID", &token);
|
|
|
|
|
}
|
|
|
|
|
cosmic::process::spawn(cmd);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-10-23 12:55:48 -05:00
|
|
|
Command::none()
|
|
|
|
|
}
|
2024-03-22 17:22:43 +01:00
|
|
|
Message::ConfigChanged(c) => {
|
|
|
|
|
self.config = c;
|
|
|
|
|
Command::none()
|
|
|
|
|
}
|
2023-10-22 15:23:07 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn view(&self) -> Element<Message> {
|
2024-03-28 19:10:59 -04:00
|
|
|
let horizontal = matches!(
|
|
|
|
|
self.core.applet.anchor,
|
|
|
|
|
PanelAnchor::Top | PanelAnchor::Bottom
|
|
|
|
|
);
|
|
|
|
|
let button = cosmic::widget::button(if horizontal {
|
2024-03-22 17:22:43 +01:00
|
|
|
let format = match (
|
|
|
|
|
self.config.military_time,
|
|
|
|
|
self.config.show_date_in_top_panel,
|
|
|
|
|
) {
|
|
|
|
|
(true, true) => "%b %-d %H:%M",
|
|
|
|
|
(true, false) => "%H:%M",
|
|
|
|
|
(false, true) => "%b %-d %-I:%M %p",
|
|
|
|
|
(false, false) => "%-I:%M %p",
|
|
|
|
|
};
|
2024-03-28 19:10:59 -04:00
|
|
|
Element::from(
|
|
|
|
|
row!(
|
2024-03-22 17:22:43 +01:00
|
|
|
cosmic::widget::text(self.now.format(format).to_string()).size(14),
|
2024-03-28 19:10:59 -04:00
|
|
|
container(vertical_space(Length::Fixed(
|
|
|
|
|
(self.core.applet.suggested_size().1
|
|
|
|
|
+ 2 * self.core.applet.suggested_padding())
|
|
|
|
|
as f32
|
|
|
|
|
)))
|
2023-10-30 14:43:17 -04:00
|
|
|
)
|
2024-03-28 19:10:59 -04:00
|
|
|
.align_items(Alignment::Center),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
2024-03-22 17:22:43 +01:00
|
|
|
let mut date_time_col = if self.config.military_time {
|
|
|
|
|
column![
|
|
|
|
|
text(self.now.format("%H").to_string()).size(14),
|
|
|
|
|
text(self.now.format("%M").to_string()).size(14),
|
|
|
|
|
]
|
|
|
|
|
} else {
|
|
|
|
|
column![
|
|
|
|
|
text(self.now.format("%I").to_string()).size(14),
|
|
|
|
|
text(self.now.format("%M").to_string()).size(14),
|
|
|
|
|
text(self.now.format("%p").to_string()).size(14),
|
|
|
|
|
]
|
|
|
|
|
}
|
2024-03-28 19:10:59 -04:00
|
|
|
.align_items(Alignment::Center)
|
|
|
|
|
.spacing(4);
|
2024-03-22 17:22:43 +01:00
|
|
|
if self.config.show_date_in_top_panel {
|
|
|
|
|
date_time_col = date_time_col.push(vertical_space(Length::Fixed(4.0)));
|
|
|
|
|
date_time_col = date_time_col.push(
|
|
|
|
|
// TODO better calendar icon?
|
|
|
|
|
icon::from_name("calendar-go-today-symbolic")
|
|
|
|
|
.size(self.core.applet.suggested_size().0)
|
|
|
|
|
.symbolic(true),
|
|
|
|
|
);
|
|
|
|
|
for d in self.now.format("%x").to_string().split('/') {
|
|
|
|
|
date_time_col = date_time_col.push(text(d.to_string()).size(14));
|
|
|
|
|
}
|
2024-03-28 19:10:59 -04:00
|
|
|
}
|
|
|
|
|
Element::from(
|
|
|
|
|
column!(
|
|
|
|
|
date_time_col,
|
|
|
|
|
horizontal_space(Length::Fixed(
|
|
|
|
|
(self.core.applet.suggested_size().0
|
|
|
|
|
+ 2 * self.core.applet.suggested_padding())
|
|
|
|
|
as f32
|
|
|
|
|
))
|
|
|
|
|
)
|
|
|
|
|
.align_items(Alignment::Center),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.padding(if horizontal {
|
|
|
|
|
[0, self.core.applet.suggested_padding()]
|
|
|
|
|
} else {
|
|
|
|
|
[self.core.applet.suggested_padding(), 0]
|
|
|
|
|
})
|
2023-10-22 15:23:07 -05:00
|
|
|
.on_press(Message::TogglePopup)
|
2023-10-24 16:33:00 -05:00
|
|
|
.style(cosmic::theme::Button::AppletIcon);
|
2023-10-22 15:23:07 -05:00
|
|
|
|
|
|
|
|
if let Some(tracker) = self.rectangle_tracker.as_ref() {
|
2023-10-30 14:43:17 -04:00
|
|
|
tracker.container(0, button).ignore_bounds(true).into()
|
2023-10-22 15:23:07 -05:00
|
|
|
} else {
|
|
|
|
|
button.into()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn view_window(&self, _id: window::Id) -> Element<Message> {
|
2024-02-26 10:24:07 -08:00
|
|
|
let date = text(self.date_selected.format("%B %-d, %Y").to_string()).size(18);
|
|
|
|
|
let day_of_week = text(self.date_selected.format("%A").to_string()).size(14);
|
2023-10-22 15:23:07 -05:00
|
|
|
|
|
|
|
|
let month_controls = row![
|
2023-10-23 12:55:48 -05:00
|
|
|
button::icon(icon::from_name("go-previous-symbolic"))
|
|
|
|
|
.padding([0, 12])
|
|
|
|
|
.on_press(Message::PreviousMonth),
|
|
|
|
|
button::icon(icon::from_name("go-next-symbolic"))
|
|
|
|
|
.padding([0, 12])
|
|
|
|
|
.on_press(Message::NextMonth)
|
2023-10-22 15:23:07 -05:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Calender
|
|
|
|
|
let mut calender: Grid<'_, Message> = grid().width(Length::Fill);
|
2024-03-22 17:34:24 +01:00
|
|
|
let mut first_day_of_week =
|
|
|
|
|
Weekday::try_from(self.config.first_day_of_week).unwrap_or(Weekday::Sun);
|
2023-10-23 13:34:19 -05:00
|
|
|
for _ in 0..7 {
|
2023-10-23 12:55:48 -05:00
|
|
|
calender = calender.push(
|
2023-10-23 13:34:19 -05:00
|
|
|
text(first_day_of_week)
|
2023-10-23 12:55:48 -05:00
|
|
|
.size(12)
|
|
|
|
|
.width(Length::Fixed(36.0))
|
|
|
|
|
.horizontal_alignment(Horizontal::Center),
|
|
|
|
|
);
|
2023-10-23 13:34:19 -05:00
|
|
|
|
|
|
|
|
first_day_of_week = first_day_of_week.succ();
|
2023-10-23 12:55:48 -05:00
|
|
|
}
|
2023-10-22 15:23:07 -05:00
|
|
|
calender = calender.insert_row();
|
|
|
|
|
|
2024-02-26 10:24:07 -08:00
|
|
|
let monday = get_calender_first(
|
|
|
|
|
self.date_selected.year(),
|
|
|
|
|
self.date_selected.month(),
|
|
|
|
|
first_day_of_week,
|
|
|
|
|
);
|
2023-10-23 12:55:48 -05:00
|
|
|
let mut day_iter = monday.iter_days();
|
2024-02-26 10:24:07 -08:00
|
|
|
for i in 0..42 {
|
2023-10-22 15:23:07 -05:00
|
|
|
if i > 0 && i % 7 == 0 {
|
|
|
|
|
calender = calender.insert_row();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let date = day_iter.next().unwrap();
|
2024-02-26 10:24:07 -08:00
|
|
|
let is_month = date.month() == self.date_selected.month()
|
|
|
|
|
&& date.year_ce() == self.date_selected.year_ce();
|
|
|
|
|
let is_day = date.day() == self.date_selected.day() && is_month;
|
2023-10-23 12:55:48 -05:00
|
|
|
|
|
|
|
|
calender = calender.push(date_button(date.day(), is_month, is_day));
|
2023-10-22 15:23:07 -05:00
|
|
|
}
|
|
|
|
|
|
2023-10-23 12:55:48 -05:00
|
|
|
// content
|
|
|
|
|
let content_list = column![
|
|
|
|
|
row![
|
|
|
|
|
column![date, day_of_week],
|
|
|
|
|
Space::with_width(Length::Fill),
|
|
|
|
|
month_controls,
|
|
|
|
|
]
|
|
|
|
|
.padding([12, 20]),
|
|
|
|
|
calender.padding([0, 12].into()),
|
2023-10-24 16:33:00 -05:00
|
|
|
padded_control(divider::horizontal::default()),
|
|
|
|
|
menu_button(text(fl!("datetime-settings")).size(14))
|
2023-10-23 12:55:48 -05:00
|
|
|
.on_press(Message::OpenDateTimeSettings),
|
|
|
|
|
]
|
|
|
|
|
.padding([8, 0]);
|
2023-10-22 15:23:07 -05:00
|
|
|
|
|
|
|
|
self.core
|
|
|
|
|
.applet
|
2023-10-23 12:55:48 -05:00
|
|
|
.popup_container(container(content_list))
|
2023-10-22 15:23:07 -05:00
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn on_close_requested(&self, id: window::Id) -> Option<Message> {
|
|
|
|
|
Some(Message::CloseRequested(id))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn date_button(
|
|
|
|
|
day: u32,
|
|
|
|
|
is_month: bool,
|
|
|
|
|
is_day: bool,
|
2024-02-05 19:24:19 -05:00
|
|
|
) -> Button<'static, Message, cosmic::Theme, cosmic::Renderer> {
|
2023-10-22 15:23:07 -05:00
|
|
|
let style = if is_day {
|
|
|
|
|
cosmic::widget::button::Style::Suggested
|
|
|
|
|
} else {
|
|
|
|
|
cosmic::widget::button::Style::Text
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let button = button(
|
|
|
|
|
text(format!("{day}"))
|
|
|
|
|
.size(14.0)
|
|
|
|
|
.horizontal_alignment(Horizontal::Center)
|
|
|
|
|
.vertical_alignment(Vertical::Center),
|
|
|
|
|
)
|
|
|
|
|
.style(style)
|
|
|
|
|
.height(Length::Fixed(36.0))
|
|
|
|
|
.width(Length::Fixed(36.0));
|
|
|
|
|
|
|
|
|
|
if is_month {
|
|
|
|
|
button.on_press(Message::SelectDay(day))
|
|
|
|
|
} else {
|
|
|
|
|
button
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-27 13:51:42 -08:00
|
|
|
|
|
|
|
|
fn time_subscription(update_at: Every) -> Subscription<()> {
|
|
|
|
|
subscription::unfold("time-sub", (), move |()| async move {
|
|
|
|
|
let now = Local::now();
|
|
|
|
|
let update_delay = match update_at {
|
|
|
|
|
Every::Minute => chrono::TimeDelta::minutes(1),
|
|
|
|
|
Every::Second => chrono::TimeDelta::seconds(1),
|
|
|
|
|
};
|
|
|
|
|
let duration = ((now + update_delay).duration_trunc(update_delay).unwrap() - now)
|
|
|
|
|
.to_std()
|
|
|
|
|
.unwrap();
|
|
|
|
|
tokio::time::sleep(duration).await;
|
|
|
|
|
((), ())
|
|
|
|
|
})
|
|
|
|
|
}
|