Use GlobalState instead of passing play_status/logged_in_user everywhere
This commit is contained in:
parent
d42737f856
commit
f0f34d4abe
41
src/app.rs
41
src/app.rs
@ -1,40 +1,23 @@
|
||||
use crate::playbar::PlayBar;
|
||||
use crate::playbar::CustomTitle;
|
||||
use crate::playstatus::PlayStatus;
|
||||
use crate::queue::Queue;
|
||||
use leptos::*;
|
||||
use leptos::logging::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use crate::pages::login::*;
|
||||
use crate::pages::signup::*;
|
||||
use crate::pages::profile::*;
|
||||
use crate::error_template::{AppError, ErrorTemplate};
|
||||
use crate::auth::get_logged_in_user;
|
||||
use crate::models::User;
|
||||
|
||||
pub type LoggedInUserResource = Resource<(), Option<User>>;
|
||||
use crate::util::state::GlobalState;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
|
||||
let play_status = PlayStatus::default();
|
||||
let play_status = create_rw_signal(play_status);
|
||||
let upload_open = create_rw_signal(false);
|
||||
provide_context(GlobalState::new());
|
||||
|
||||
// A resource that fetches the logged in user
|
||||
// This will not automatically refetch, so any login/logout related code
|
||||
// should call `refetch` on this resource
|
||||
let logged_in_user: LoggedInUserResource = create_resource(|| (), |_| async {
|
||||
get_logged_in_user().await
|
||||
.inspect_err(|e| {
|
||||
error!("Error getting logged in user: {:?}", e);
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
let upload_open = create_rw_signal(false);
|
||||
|
||||
view! {
|
||||
// injects a stylesheet into the document <head>
|
||||
@ -42,7 +25,7 @@ pub fn App() -> impl IntoView {
|
||||
<Stylesheet id="leptos" href="/pkg/libretunes.css"/>
|
||||
|
||||
// sets the document title
|
||||
<CustomTitle play_status=play_status/>
|
||||
<CustomTitle />
|
||||
|
||||
// content for this welcome page
|
||||
<Router fallback=|| {
|
||||
@ -55,15 +38,15 @@ pub fn App() -> impl IntoView {
|
||||
}>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=move || view! { <HomePage play_status=play_status upload_open=upload_open/> }>
|
||||
<Route path="" view=move || view! { <HomePage upload_open=upload_open/> }>
|
||||
<Route path="" view=Dashboard />
|
||||
<Route path="dashboard" view=Dashboard />
|
||||
<Route path="search" view=Search />
|
||||
<Route path="user/:id" view=move || view!{ <Profile logged_in_user /> } />
|
||||
<Route path="user" view=move || view!{ <Profile logged_in_user /> } />
|
||||
<Route path="user/:id" view=Profile />
|
||||
<Route path="user" view=Profile />
|
||||
</Route>
|
||||
<Route path="/login" view=move || view!{ <Login user=logged_in_user /> } />
|
||||
<Route path="/signup" view=move || view!{ <Signup user=logged_in_user /> } />
|
||||
<Route path="/login" view=Login />
|
||||
<Route path="/signup" view=Signup />
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@ -78,7 +61,7 @@ use crate::components::upload::*;
|
||||
|
||||
/// Renders the home page of your application.
|
||||
#[component]
|
||||
fn HomePage(play_status: RwSignal<PlayStatus>, upload_open: RwSignal<bool>) -> impl IntoView {
|
||||
fn HomePage(upload_open: RwSignal<bool>) -> impl IntoView {
|
||||
view! {
|
||||
<div class="home-container">
|
||||
<Upload open=upload_open/>
|
||||
@ -86,8 +69,8 @@ fn HomePage(play_status: RwSignal<PlayStatus>, upload_open: RwSignal<bool>) -> i
|
||||
// This <Outlet /> will render the child route components
|
||||
<Outlet />
|
||||
<Personal />
|
||||
<PlayBar status=play_status/>
|
||||
<Queue status=play_status/>
|
||||
<PlayBar />
|
||||
<Queue />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
use crate::auth::login;
|
||||
use crate::util::state::GlobalState;
|
||||
use leptos::leptos_dom::*;
|
||||
use leptos::*;
|
||||
use leptos_icons::*;
|
||||
use crate::users::UserCredentials;
|
||||
use crate::app::LoggedInUserResource;
|
||||
|
||||
#[component]
|
||||
pub fn Login(user: LoggedInUserResource) -> impl IntoView {
|
||||
pub fn Login() -> impl IntoView {
|
||||
let (username_or_email, set_username_or_email) = create_signal("".to_string());
|
||||
let (password, set_password) = create_signal("".to_string());
|
||||
|
||||
@ -28,6 +28,8 @@ pub fn Login(user: LoggedInUserResource) -> impl IntoView {
|
||||
username_or_email: username_or_email1,
|
||||
password: password1
|
||||
};
|
||||
|
||||
let user = GlobalState::logged_in_user();
|
||||
|
||||
let login_result = login(user_credentials).await;
|
||||
if let Err(err) = login_result {
|
||||
|
@ -11,9 +11,9 @@ use crate::components::error::*;
|
||||
|
||||
use crate::api::profile::*;
|
||||
|
||||
use crate::app::LoggedInUserResource;
|
||||
use crate::models::User;
|
||||
use crate::users::get_user_by_id;
|
||||
use crate::util::state::GlobalState;
|
||||
|
||||
/// Duration in seconds backwards from now to aggregate history data for
|
||||
const HISTORY_SECS: i64 = 60 * 60 * 24 * 30;
|
||||
@ -29,7 +29,7 @@ const TOP_ARTISTS_COUNT: i64 = 10;
|
||||
/// Profile page
|
||||
/// Shows the current user's profile if no id is specified, or a user's profile if an id is specified in the path
|
||||
#[component]
|
||||
pub fn Profile(logged_in_user: LoggedInUserResource) -> impl IntoView {
|
||||
pub fn Profile() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
|
||||
view! {
|
||||
@ -38,7 +38,7 @@ pub fn Profile(logged_in_user: LoggedInUserResource) -> impl IntoView {
|
||||
match params.get("id").map(|id| id.parse::<i32>()) {
|
||||
None => {
|
||||
// No id specified, show the current user's profile
|
||||
view! { <OwnProfile logged_in_user /> }.into_view()
|
||||
view! { <OwnProfile /> }.into_view()
|
||||
},
|
||||
Some(Ok(id)) => {
|
||||
// Id specified, get the user and show their profile
|
||||
@ -61,12 +61,12 @@ pub fn Profile(logged_in_user: LoggedInUserResource) -> impl IntoView {
|
||||
|
||||
/// Show the logged in user's profile
|
||||
#[component]
|
||||
fn OwnProfile(logged_in_user: LoggedInUserResource) -> impl IntoView {
|
||||
fn OwnProfile() -> impl IntoView {
|
||||
view! {
|
||||
<Transition
|
||||
fallback=move || view! { <LoadingPage /> }
|
||||
>
|
||||
{move || logged_in_user.get().map(|user| {
|
||||
{move || GlobalState::logged_in_user().get().map(|user| {
|
||||
match user {
|
||||
Some(user) => {
|
||||
let user_id = user.id.unwrap();
|
||||
|
@ -1,12 +1,12 @@
|
||||
use crate::auth::signup;
|
||||
use crate::models::User;
|
||||
use crate::util::state::GlobalState;
|
||||
use leptos::leptos_dom::*;
|
||||
use leptos::*;
|
||||
use leptos_icons::*;
|
||||
use crate::app::LoggedInUserResource;
|
||||
|
||||
#[component]
|
||||
pub fn Signup(user: LoggedInUserResource) -> impl IntoView {
|
||||
pub fn Signup() -> impl IntoView {
|
||||
let (username, set_username) = create_signal("".to_string());
|
||||
let (email, set_email) = create_signal("".to_string());
|
||||
let (password, set_password) = create_signal("".to_string());
|
||||
@ -30,6 +30,8 @@ pub fn Signup(user: LoggedInUserResource) -> impl IntoView {
|
||||
};
|
||||
log!("new user: {:?}", new_user);
|
||||
|
||||
let user = GlobalState::logged_in_user();
|
||||
|
||||
spawn_local(async move {
|
||||
if let Err(err) = signup(new_user.clone()).await {
|
||||
// Handle the error here, e.g., log it or display to the user
|
||||
|
100
src/playbar.rs
100
src/playbar.rs
@ -1,7 +1,7 @@
|
||||
use crate::models::Artist;
|
||||
use crate::playstatus::PlayStatus;
|
||||
use crate::songdata::SongData;
|
||||
use crate::api::songs;
|
||||
use crate::util::state::GlobalState;
|
||||
use leptos::ev::MouseEvent;
|
||||
use leptos::html::{Audio, Div};
|
||||
use leptos::leptos_dom::*;
|
||||
@ -40,8 +40,8 @@ const HISTORY_LISTEN_THRESHOLD: u64 = MIN_SKIP_BACK_TIME as u64;
|
||||
/// * `None` if the audio element is not available
|
||||
/// * `Some((current_time, duration))` if the audio element is available
|
||||
///
|
||||
pub fn get_song_time_duration(status: impl SignalWithUntracked<Value = PlayStatus>) -> Option<(f64, f64)> {
|
||||
status.with_untracked(|status| {
|
||||
pub fn get_song_time_duration() -> Option<(f64, f64)> {
|
||||
GlobalState::play_status().with_untracked(|status| {
|
||||
if let Some(audio) = status.get_audio() {
|
||||
Some((audio.current_time(), audio.duration()))
|
||||
} else {
|
||||
@ -61,13 +61,13 @@ pub fn get_song_time_duration(status: impl SignalWithUntracked<Value = PlayStatu
|
||||
/// * `status` - The `PlayStatus` to get the audio element from, as a signal
|
||||
/// * `time` - The time to skip to, in seconds
|
||||
///
|
||||
pub fn skip_to(status: impl SignalUpdate<Value = PlayStatus>, time: f64) {
|
||||
pub fn skip_to(time: f64) {
|
||||
if time.is_infinite() || time.is_nan() {
|
||||
error!("Unable to skip to non-finite time: {}", time);
|
||||
return
|
||||
}
|
||||
|
||||
status.update(|status| {
|
||||
GlobalState::play_status().update(|status| {
|
||||
if let Some(audio) = status.get_audio() {
|
||||
audio.set_current_time(time);
|
||||
log!("Player skipped to time: {}", time);
|
||||
@ -85,8 +85,8 @@ pub fn skip_to(status: impl SignalUpdate<Value = PlayStatus>, time: f64) {
|
||||
/// * `status` - The `PlayStatus` to get the audio element from, as a signal
|
||||
/// * `play` - `true` to play the song, `false` to pause it
|
||||
///
|
||||
pub fn set_playing(status: impl SignalUpdate<Value = PlayStatus>, play: bool) {
|
||||
status.update(|status| {
|
||||
pub fn set_playing(play: bool) {
|
||||
GlobalState::play_status().update(|status| {
|
||||
if let Some(audio) = status.get_audio() {
|
||||
if play {
|
||||
if let Err(e) = audio.play() {
|
||||
@ -109,8 +109,8 @@ pub fn set_playing(status: impl SignalUpdate<Value = PlayStatus>, play: bool) {
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_queue(status: impl SignalUpdate<Value = PlayStatus>) {
|
||||
status.update(|status| {
|
||||
fn toggle_queue() {
|
||||
GlobalState::play_status().update(|status| {
|
||||
status.queue_open = !status.queue_open;
|
||||
});
|
||||
|
||||
@ -126,8 +126,8 @@ fn toggle_queue(status: impl SignalUpdate<Value = PlayStatus>) {
|
||||
/// * `status` - The `PlayStatus` to get the audio element from, as a signal
|
||||
/// * `src` - The source to set the audio player to
|
||||
///
|
||||
fn set_play_src(status: impl SignalUpdate<Value = PlayStatus>, src: String) {
|
||||
status.update(|status| {
|
||||
fn set_play_src(src: String) {
|
||||
GlobalState::play_status().update(|status| {
|
||||
if let Some(audio) = status.get_audio() {
|
||||
audio.set_src(&src);
|
||||
log!("Player set src to: {}", src);
|
||||
@ -139,11 +139,13 @@ fn set_play_src(status: impl SignalUpdate<Value = PlayStatus>, src: String) {
|
||||
|
||||
/// The play, pause, and skip buttons
|
||||
#[component]
|
||||
fn PlayControls(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
fn PlayControls() -> impl IntoView {
|
||||
let status = GlobalState::play_status();
|
||||
|
||||
// On click handlers for the skip and play/pause buttons
|
||||
|
||||
let skip_back = move |_| {
|
||||
if let Some(duration) = get_song_time_duration(status) {
|
||||
if let Some(duration) = get_song_time_duration() {
|
||||
// Skip to previous song if the current song is near the start
|
||||
// Also skip to the previous song if we're at the end of the current song
|
||||
// This is because after running out of songs in the queue, the current song will be at the end
|
||||
@ -160,8 +162,8 @@ fn PlayControls(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
// Push the popped song to the front of the queue, and play it
|
||||
let next_src = last_played_song.song_path.clone();
|
||||
status.update(|status| status.queue.push_front(last_played_song));
|
||||
set_play_src(status, next_src);
|
||||
set_playing(status, true);
|
||||
set_play_src(next_src);
|
||||
set_playing(true);
|
||||
} else {
|
||||
warn!("Unable to skip back: No previous song");
|
||||
}
|
||||
@ -170,14 +172,14 @@ fn PlayControls(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
|
||||
// Default to skipping to start of current song, and playing
|
||||
log!("Skipping to start of current song");
|
||||
skip_to(status, 0.0);
|
||||
set_playing(status, true);
|
||||
skip_to(0.0);
|
||||
set_playing(true);
|
||||
};
|
||||
|
||||
let skip_forward = move |_| {
|
||||
if let Some(duration) = get_song_time_duration(status) {
|
||||
skip_to(status, duration.1);
|
||||
set_playing(status, true);
|
||||
if let Some(duration) = get_song_time_duration() {
|
||||
skip_to(duration.1);
|
||||
set_playing(true);
|
||||
} else {
|
||||
error!("Unable to skip forward: Unable to get current duration");
|
||||
}
|
||||
@ -185,7 +187,7 @@ fn PlayControls(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
|
||||
let toggle_play = move |_| {
|
||||
let playing = status.with_untracked(|status| { status.playing });
|
||||
set_playing(status, !playing);
|
||||
set_playing(!playing);
|
||||
};
|
||||
|
||||
// We use this to prevent the buttons from being focused when clicked
|
||||
@ -248,7 +250,9 @@ fn PlayDuration(elapsed_secs: MaybeSignal<i64>, total_secs: MaybeSignal<i64>) ->
|
||||
|
||||
/// The name, artist, and album of the current song
|
||||
#[component]
|
||||
fn MediaInfo(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
fn MediaInfo() -> impl IntoView {
|
||||
let status = GlobalState::play_status();
|
||||
|
||||
let name = Signal::derive(move || {
|
||||
status.with(|status| {
|
||||
status.queue.front().map_or("No media playing".into(), |song| song.title.clone())
|
||||
@ -287,7 +291,9 @@ fn MediaInfo(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
|
||||
/// The like and dislike buttons
|
||||
#[component]
|
||||
fn LikeDislike(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
fn LikeDislike() -> impl IntoView {
|
||||
let status = GlobalState::play_status();
|
||||
|
||||
let like_icon = Signal::derive(move || {
|
||||
status.with(|status| {
|
||||
match status.queue.front() {
|
||||
@ -400,7 +406,7 @@ fn LikeDislike(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
|
||||
/// The play progress bar, and click handler for skipping to a certain time in the song
|
||||
#[component]
|
||||
fn ProgressBar(percentage: MaybeSignal<f64>, status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
fn ProgressBar(percentage: MaybeSignal<f64>) -> impl IntoView {
|
||||
// Keep a reference to the progress bar div so we can get its width and calculate the time to skip to
|
||||
let progress_bar_ref = create_node_ref::<Div>();
|
||||
|
||||
@ -412,10 +418,10 @@ fn ProgressBar(percentage: MaybeSignal<f64>, status: RwSignal<PlayStatus>) -> im
|
||||
let width = progress_bar.offset_width() as f64;
|
||||
let percentage = x_click_pos / width * 100.0;
|
||||
|
||||
if let Some(duration) = get_song_time_duration(status) {
|
||||
if let Some(duration) = get_song_time_duration() {
|
||||
let time = duration.1 * percentage / 100.0;
|
||||
skip_to(status, time);
|
||||
set_playing(status, true);
|
||||
skip_to(time);
|
||||
set_playing(true);
|
||||
} else {
|
||||
error!("Unable to skip to time: Unable to get current duration");
|
||||
}
|
||||
@ -438,11 +444,11 @@ fn ProgressBar(percentage: MaybeSignal<f64>, status: RwSignal<PlayStatus>) -> im
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn QueueToggle(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
|
||||
fn QueueToggle() -> impl IntoView {
|
||||
let update_queue = move |_| {
|
||||
toggle_queue(status);
|
||||
log!("queue button pressed, queue status: {:?}", status.with_untracked(|status| status.queue_open));
|
||||
toggle_queue();
|
||||
log!("queue button pressed, queue status: {:?}",
|
||||
GlobalState::play_status().with_untracked(|status| status.queue_open));
|
||||
};
|
||||
|
||||
// We use this to prevent the buttons from being focused when clicked
|
||||
@ -463,9 +469,9 @@ fn QueueToggle(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
|
||||
/// Renders the title of the page based on the currently playing song
|
||||
#[component]
|
||||
pub fn CustomTitle(play_status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
pub fn CustomTitle() -> impl IntoView {
|
||||
let title = create_memo(move |_| {
|
||||
play_status.with(|play_status| {
|
||||
GlobalState::play_status().with(|play_status| {
|
||||
play_status.queue.front().map_or("LibreTunes".to_string(), |song_data| {
|
||||
format!("{} - {} | {}",song_data.title.clone(),Artist::display_list(&song_data.artists), "LibreTunes")
|
||||
})
|
||||
@ -478,18 +484,20 @@ pub fn CustomTitle(play_status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
|
||||
/// The main play bar component, containing the progress bar, media info, play controls, and play duration
|
||||
#[component]
|
||||
pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
pub fn PlayBar() -> impl IntoView {
|
||||
let status = GlobalState::play_status();
|
||||
|
||||
// Listen for key down events -- arrow keys don't seem to trigger key press events
|
||||
let _arrow_key_handle = window_event_listener(ev::keydown, move |e: ev::KeyboardEvent| {
|
||||
if e.key() == "ArrowRight" {
|
||||
e.prevent_default();
|
||||
log!("Right arrow key pressed, skipping forward by {} seconds", ARROW_KEY_SKIP_TIME);
|
||||
|
||||
if let Some(duration) = get_song_time_duration(status) {
|
||||
if let Some(duration) = get_song_time_duration() {
|
||||
let mut time = duration.0 + ARROW_KEY_SKIP_TIME;
|
||||
time = time.clamp(0.0, duration.1);
|
||||
skip_to(status, time);
|
||||
set_playing(status, true);
|
||||
skip_to(time);
|
||||
set_playing(true);
|
||||
} else {
|
||||
error!("Unable to skip forward: Unable to get current duration");
|
||||
}
|
||||
@ -498,11 +506,11 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
e.prevent_default();
|
||||
log!("Left arrow key pressed, skipping backward by {} seconds", ARROW_KEY_SKIP_TIME);
|
||||
|
||||
if let Some(duration) = get_song_time_duration(status) {
|
||||
if let Some(duration) = get_song_time_duration() {
|
||||
let mut time = duration.0 - ARROW_KEY_SKIP_TIME;
|
||||
time = time.clamp(0.0, duration.1);
|
||||
skip_to(status, time);
|
||||
set_playing(status, true);
|
||||
skip_to(time);
|
||||
set_playing(true);
|
||||
} else {
|
||||
error!("Unable to skip backward: Unable to get current duration");
|
||||
}
|
||||
@ -516,7 +524,7 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
log!("Space bar pressed, toggling play/pause");
|
||||
|
||||
let playing = status.with_untracked(|status| status.playing);
|
||||
set_playing(status, !playing);
|
||||
set_playing(!playing);
|
||||
}
|
||||
});
|
||||
|
||||
@ -659,14 +667,14 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
<audio _ref=audio_ref on:play=on_play on:pause=on_pause
|
||||
on:timeupdate=on_time_update on:ended=on_end type="audio/mpeg" />
|
||||
<div class="playbar">
|
||||
<ProgressBar percentage=percentage.into() status=status />
|
||||
<ProgressBar percentage=percentage.into() />
|
||||
<div class="playbar-left-group">
|
||||
<MediaInfo status=status />
|
||||
<LikeDislike status=status />
|
||||
<MediaInfo />
|
||||
<LikeDislike />
|
||||
</div>
|
||||
<PlayControls status=status />
|
||||
<PlayControls />
|
||||
<PlayDuration elapsed_secs=elapsed_secs.into() total_secs=total_secs.into() />
|
||||
<QueueToggle status=status />
|
||||
<QueueToggle />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
11
src/queue.rs
11
src/queue.rs
@ -1,6 +1,6 @@
|
||||
use crate::models::Artist;
|
||||
use crate::playstatus::PlayStatus;
|
||||
use crate::song::Song;
|
||||
use crate::util::state::GlobalState;
|
||||
use leptos::ev::MouseEvent;
|
||||
use leptos::leptos_dom::*;
|
||||
use leptos::*;
|
||||
@ -9,22 +9,23 @@ use leptos::ev::DragEvent;
|
||||
|
||||
const RM_BTN_SIZE: &str = "2.5rem";
|
||||
|
||||
fn remove_song_fn(index: usize, status: RwSignal<PlayStatus>) {
|
||||
fn remove_song_fn(index: usize) {
|
||||
if index == 0 {
|
||||
log!("Error: Trying to remove currently playing song (index 0) from queue");
|
||||
} else {
|
||||
log!("Remove Song from Queue: Song is not currently playing, deleting song from queue and not adding to history");
|
||||
status.update(|status| {
|
||||
GlobalState::play_status().update(|status| {
|
||||
status.queue.remove(index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Queue(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
pub fn Queue() -> impl IntoView {
|
||||
let status = GlobalState::play_status();
|
||||
|
||||
let remove_song = move |index: usize| {
|
||||
remove_song_fn(index, status);
|
||||
remove_song_fn(index);
|
||||
log!("Removed song {}", index + 1);
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user