feat: Detached pages and fix navigation actions
This commit is contained in:
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@@ -2,6 +2,5 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/backend" vcs="Git" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -14,6 +14,8 @@
|
|||||||
import { getReiverrApiClient } from './lib/apis/reiverr/reiverr-api';
|
import { getReiverrApiClient } from './lib/apis/reiverr/reiverr-api';
|
||||||
import { appState } from './lib/stores/app-state.store';
|
import { appState } from './lib/stores/app-state.store';
|
||||||
import MoviePage from './lib/pages/MoviePage.svelte';
|
import MoviePage from './lib/pages/MoviePage.svelte';
|
||||||
|
import DetatchedPage from './lib/components/DetatchedPage/DetatchedPage.svelte';
|
||||||
|
import Button from './lib/components/Button.svelte';
|
||||||
|
|
||||||
getReiverrApiClient()
|
getReiverrApiClient()
|
||||||
.GET('/user', {})
|
.GET('/user', {})
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<I18n />
|
<I18n />
|
||||||
<Container direction="horizontal" class="bg-stone-950 text-white flex flex-1 w-screen">
|
<Container class="bg-stone-950 text-white flex flex-1 w-screen">
|
||||||
{#if $appState.user === undefined}
|
{#if $appState.user === undefined}
|
||||||
<div class="h-screen w-screen flex flex-col items-center justify-center">
|
<div class="h-screen w-screen flex flex-col items-center justify-center">
|
||||||
<div class="flex items-center justify-center hover:text-inherit selectable rounded-sm mb-2">
|
<div class="flex items-center justify-center hover:text-inherit selectable rounded-sm mb-2">
|
||||||
@@ -36,13 +38,12 @@
|
|||||||
<LoginPage />
|
<LoginPage />
|
||||||
{:else}
|
{:else}
|
||||||
<Router>
|
<Router>
|
||||||
<Sidebar />
|
<Container class="flex-1 flex flex-col min-w-0" direction="horizontal" trapFocus>
|
||||||
|
<Sidebar />
|
||||||
<Container class="flex-1 flex flex-col min-w-0">
|
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<BrowseSeriesPage />
|
<BrowseSeriesPage />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="movies">
|
<Route path="movies/*">
|
||||||
<MoviesPage />
|
<MoviesPage />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="library">
|
<Route path="library">
|
||||||
@@ -61,6 +62,16 @@
|
|||||||
</Route>
|
</Route>
|
||||||
</Container>
|
</Container>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
||||||
|
<Router>
|
||||||
|
<Route path="movies/movie/:id">
|
||||||
|
<DetatchedPage>
|
||||||
|
<Button>Button 1</Button>
|
||||||
|
<Button>Button 2</Button>
|
||||||
|
<Button on:click={() => history.back()}>Back</Button>
|
||||||
|
</DetatchedPage>
|
||||||
|
</Route>
|
||||||
|
</Router>
|
||||||
{/if}
|
{/if}
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
export let direction: 'vertical' | 'horizontal' | 'grid' = 'vertical';
|
export let direction: 'vertical' | 'horizontal' | 'grid' = 'vertical';
|
||||||
export let gridCols: number = 0;
|
export let gridCols: number = 0;
|
||||||
export let focusOnMount = false;
|
export let focusOnMount = false;
|
||||||
|
export let trapFocus = false;
|
||||||
export let debugOutline = false;
|
export let debugOutline = false;
|
||||||
export let revealStrategy: RevealStrategy | undefined = undefined;
|
export let revealStrategy: RevealStrategy | undefined = undefined;
|
||||||
export let childrenRevealStrategy: RevealStrategy | undefined = undefined;
|
export let childrenRevealStrategy: RevealStrategy | undefined = undefined;
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
.setNavigationActions(navigationActions)
|
.setNavigationActions(navigationActions)
|
||||||
.setRevealStrategy(revealStrategy)
|
.setRevealStrategy(revealStrategy)
|
||||||
.setChildrenRevealStrategy(childrenRevealStrategy)
|
.setChildrenRevealStrategy(childrenRevealStrategy)
|
||||||
|
.setTrapFocus(trapFocus)
|
||||||
.getStores();
|
.getStores();
|
||||||
export const container = rest.container;
|
export const container = rest.container;
|
||||||
export const hasFocus = rest.hasFocus;
|
export const hasFocus = rest.hasFocus;
|
||||||
|
|||||||
@@ -63,6 +63,16 @@ export class TmdbApi implements Api<paths> {
|
|||||||
}
|
}
|
||||||
}).then((res) => res.data as TmdbMovieFull2 | undefined);
|
}).then((res) => res.data as TmdbMovieFull2 | undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPopularMovies = () =>
|
||||||
|
TmdbApiOpen.GET('/3/movie/popular', {
|
||||||
|
params: {
|
||||||
|
query: {
|
||||||
|
language: get(settings)?.language,
|
||||||
|
region: get(settings)?.discover.region
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then((res) => res.data?.results || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tmdbApi = new TmdbApi();
|
export const tmdbApi = new TmdbApi();
|
||||||
@@ -231,16 +241,6 @@ export const getTmdbMoviePoster = async (tmdbId: number) =>
|
|||||||
|
|
||||||
/** Discover */
|
/** Discover */
|
||||||
|
|
||||||
export const getTmdbPopularMovies = () =>
|
|
||||||
TmdbApiOpen.GET('/3/movie/popular', {
|
|
||||||
params: {
|
|
||||||
query: {
|
|
||||||
language: get(settings)?.language,
|
|
||||||
region: get(settings)?.discover.region
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).then((res) => res.data?.results || []);
|
|
||||||
|
|
||||||
export const getTmdbPopularSeries = () =>
|
export const getTmdbPopularSeries = () =>
|
||||||
TmdbApiOpen.GET('/3/tv/popular', {
|
TmdbApiOpen.GET('/3/tv/popular', {
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class={classNames(
|
class={classNames(
|
||||||
'fixed inset-0 justify-center items-center z-20 overflow-hidden flex transition-opacity reltaive',
|
'fixed inset-0 justify-center items-center z-20 overflow-hidden flex transition-opacity',
|
||||||
{
|
{
|
||||||
'opacity-0': hidden
|
'opacity-0': hidden
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
export let tmdbId: number | undefined = undefined;
|
export let tmdbId: number | undefined = undefined;
|
||||||
export let tvdbId: number | undefined = undefined;
|
export let tvdbId: number | undefined = undefined;
|
||||||
export let openInModal = true;
|
|
||||||
export let jellyfinId: string = '';
|
export let jellyfinId: string = '';
|
||||||
export let type: TitleType = 'movie';
|
export let type: TitleType = 'movie';
|
||||||
export let backdropUrl: string;
|
export let backdropUrl: string;
|
||||||
@@ -32,14 +31,8 @@
|
|||||||
<Container
|
<Container
|
||||||
active={focusable}
|
active={focusable}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (openInModal) {
|
if (tmdbId || tvdbId) {
|
||||||
if (tmdbId) {
|
navigate(`${type}/${tmdbId || tvdbId}`);
|
||||||
//openTitleModal({ type, id: tmdbId, provider: 'tmdb' });
|
|
||||||
} else if (tvdbId) {
|
|
||||||
//openTitleModal({ type, id: tvdbId, provider: 'tvdb' });
|
|
||||||
}
|
|
||||||
} else if (tmdbId || tvdbId) {
|
|
||||||
navigate(`/${type}/${tmdbId || tvdbId}`);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
class={classNames(
|
class={classNames(
|
||||||
|
|||||||
19
src/lib/components/Card/TmdbCard.svelte
Normal file
19
src/lib/components/Card/TmdbCard.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Card from './Card.svelte';
|
||||||
|
import type { TmdbMovie2 } from '../../apis/tmdb/tmdb-api';
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import { TMDB_POSTER_SMALL } from '../../constants';
|
||||||
|
|
||||||
|
export let item: TmdbMovie2;
|
||||||
|
const props: ComponentProps<Card> = {
|
||||||
|
tmdbId: item.id,
|
||||||
|
title: item.title,
|
||||||
|
subtitle: item.release_date,
|
||||||
|
backdropUrl: TMDB_POSTER_SMALL + item.poster_path,
|
||||||
|
type: 'movie',
|
||||||
|
orientation: 'portrait',
|
||||||
|
rating: item.vote_average
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card {...props} />
|
||||||
18
src/lib/components/DetatchedPage/DetatchedPage.svelte
Normal file
18
src/lib/components/DetatchedPage/DetatchedPage.svelte
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script>
|
||||||
|
import Container from '../../../Container.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Container
|
||||||
|
navigationActions={{
|
||||||
|
left: () => {
|
||||||
|
console.log('Not called?');
|
||||||
|
history.back();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
focusOnMount
|
||||||
|
trapFocus
|
||||||
|
class="fixed inset-0"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Container>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
function onPrevious() {
|
function onPrevious() {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
Selectable.focusLeft();
|
return false;
|
||||||
} else {
|
} else {
|
||||||
index = (index - 1 + length) % length;
|
index = (index - 1 + length) % length;
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
navigationActions={{
|
navigationActions={{
|
||||||
right: onNext,
|
right: onNext,
|
||||||
left: onPrevious,
|
left: onPrevious,
|
||||||
up: () => Selectable.focusLeft() || true
|
up: () => Selectable.giveFocus('left') || true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-1 z-10 p-4">
|
<div class="flex flex-1 z-10 p-4">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
function onPrevious() {
|
function onPrevious() {
|
||||||
if (showcaseIndex === 0) {
|
if (showcaseIndex === 0) {
|
||||||
Selectable.focusLeft();
|
Selectable.giveFocus('left');
|
||||||
} else {
|
} else {
|
||||||
showcaseIndex = (showcaseIndex - 1 + showcaseLength) % showcaseLength;
|
showcaseIndex = (showcaseIndex - 1 + showcaseLength) % showcaseLength;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<Laptop class="w-8 h-8" slot="icon" />
|
<Laptop class="w-8 h-8" slot="icon" />
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
<Container on:click={() => navigate('/movie/359410')}>
|
<Container on:click={() => navigate('movies')}>
|
||||||
<div class={itemContainer(1, $focusIndex)}>
|
<div class={itemContainer(1, $focusIndex)}>
|
||||||
<CardStack class="w-8 h-8" slot="icon" />
|
<CardStack class="w-8 h-8" slot="icon" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Container from '../../Container.svelte';
|
import Container from '../../Container.svelte';
|
||||||
import { getPosterProps, getTmdbPopularMovies, TmdbApiOpen } from '../apis/tmdb/tmdb-api';
|
import { getPosterProps, tmdbApi, TmdbApiOpen } from '../apis/tmdb/tmdb-api';
|
||||||
import { formatDateToYearMonthDay } from '../utils';
|
import { formatDateToYearMonthDay } from '../utils';
|
||||||
import { settings } from '../stores/settings.store';
|
import { settings } from '../stores/settings.store';
|
||||||
import type { TitleType } from '../types';
|
import type { TitleType } from '../types';
|
||||||
@@ -62,8 +62,6 @@
|
|||||||
.then((res) => res.data?.results || [])
|
.then((res) => res.data?.results || [])
|
||||||
.then((i) => fetchCardProps(i, 'series'));
|
.then((i) => fetchCardProps(i, 'series'));
|
||||||
|
|
||||||
const fetchPopularMovies = () => getTmdbPopularMovies();
|
|
||||||
|
|
||||||
// const fetchLibraryItems = async () => {
|
// const fetchLibraryItems = async () => {
|
||||||
// const items = await getJellyfinItems();
|
// const items = await getJellyfinItems();
|
||||||
// const props = await fetchCardProps(items, 'series');
|
// const props = await fetchCardProps(items, 'series');
|
||||||
@@ -78,7 +76,7 @@
|
|||||||
|
|
||||||
<Container focusOnMount>
|
<Container focusOnMount>
|
||||||
<div class="h-screen flex flex-col">
|
<div class="h-screen flex flex-col">
|
||||||
<HeroShowcase items={getTmdbPopularMovies().then(getShowcasePropsFromTmdb)} />
|
<HeroShowcase items={tmdbApi.getPopularMovies().then(getShowcasePropsFromTmdb)} />
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<Carousel scrollClass="">
|
<Carousel scrollClass="">
|
||||||
<SidebarMargin slot="title" class="mx-4">
|
<SidebarMargin slot="title" class="mx-4">
|
||||||
|
|||||||
@@ -1,5 +1,41 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Container from '../../Container.svelte';
|
import Container from '../../Container.svelte';
|
||||||
|
import HeroShowcase from '../components/HeroShowcase/HeroShowcase.svelte';
|
||||||
|
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
||||||
|
import { getShowcasePropsFromTmdb } from '../components/HeroShowcase/HeroShowcase';
|
||||||
|
import Carousel from '../components/Carousel/Carousel.svelte';
|
||||||
|
import SidebarMargin from '../components/SidebarMargin.svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
|
||||||
|
import TmdbCard from '../components/Card/TmdbCard.svelte';
|
||||||
|
import Button from '../components/Button.svelte';
|
||||||
|
import { useNavigate } from 'svelte-navigator';
|
||||||
|
|
||||||
|
const popularMovies = tmdbApi.getPopularMovies();
|
||||||
|
const navigate = useNavigate();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Container focusOnMount>MoviesPage</Container>
|
<Container focusOnMount class="flex flex-col">
|
||||||
|
<div class="flex flex-col h-screen">
|
||||||
|
<HeroShowcase items={popularMovies.then(getShowcasePropsFromTmdb)} />
|
||||||
|
<div class="mt-8">
|
||||||
|
<Carousel>
|
||||||
|
<SidebarMargin slot="title" class="mx-4">
|
||||||
|
<div class="text-xl font-semibold text-zinc-300">
|
||||||
|
{$_('discover.streamingNow')}
|
||||||
|
</div>
|
||||||
|
</SidebarMargin>
|
||||||
|
{#await popularMovies}
|
||||||
|
<CarouselPlaceholderItems />
|
||||||
|
{:then items}
|
||||||
|
<div class="w-[4.5rem] h-1 shrink-0" />
|
||||||
|
{#each items as item (item.id)}
|
||||||
|
<div class="m-2">
|
||||||
|
<TmdbCard {item} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/await}
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export class Selectable {
|
|||||||
right: undefined
|
right: undefined
|
||||||
};
|
};
|
||||||
private focusByDefault: boolean = false;
|
private focusByDefault: boolean = false;
|
||||||
|
private trapFocus: boolean = false;
|
||||||
private isInitialized: boolean = false;
|
private isInitialized: boolean = false;
|
||||||
private navigationActions: NavigationActions = {};
|
private navigationActions: NavigationActions = {};
|
||||||
private isActive: boolean = true;
|
private isActive: boolean = true;
|
||||||
@@ -256,15 +257,33 @@ export class Selectable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (this.navigationActions[direction]) {
|
||||||
|
// return this;
|
||||||
|
// } else
|
||||||
if (this.neighbors[direction]?.isFocusable()) {
|
if (this.neighbors[direction]?.isFocusable()) {
|
||||||
return this.neighbors[direction];
|
return this.neighbors[direction];
|
||||||
} else {
|
} else if (!this.trapFocus) {
|
||||||
return this.parent?.getFocusableNeighbor(direction);
|
return this.parent?.getFocusableNeighbor(direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private giveFocus(direction: Direction) {
|
private giveFocus(direction: Direction): boolean {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
let selectable: Selectable | undefined = this;
|
||||||
|
while (selectable) {
|
||||||
|
const action = selectable.navigationActions[direction];
|
||||||
|
if (action && action(this)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
selectable = selectable.parent;
|
||||||
|
}
|
||||||
|
|
||||||
const neighbor = this.getFocusableNeighbor(direction);
|
const neighbor = this.getFocusableNeighbor(direction);
|
||||||
|
// if (neighbor?.navigationActions?.[direction] && neighbor.navigationActions[direction]?.(this)) {
|
||||||
|
// return true;
|
||||||
|
// } else
|
||||||
if (neighbor) {
|
if (neighbor) {
|
||||||
neighbor.focus();
|
neighbor.focus();
|
||||||
return true;
|
return true;
|
||||||
@@ -273,24 +292,9 @@ export class Selectable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static focusUp() {
|
static giveFocus(direction: Direction) {
|
||||||
const currentlyFocusedObject = get(Selectable.focusedObject);
|
const currentlyFocusedObject = get(Selectable.focusedObject);
|
||||||
return currentlyFocusedObject?.giveFocus('up');
|
return currentlyFocusedObject?.giveFocus(direction);
|
||||||
}
|
|
||||||
|
|
||||||
static focusDown() {
|
|
||||||
const currentlyFocusedObject = get(Selectable.focusedObject);
|
|
||||||
return currentlyFocusedObject?.giveFocus('down');
|
|
||||||
}
|
|
||||||
|
|
||||||
static focusLeft() {
|
|
||||||
const currentlyFocusedObject = get(Selectable.focusedObject);
|
|
||||||
return currentlyFocusedObject?.giveFocus('left');
|
|
||||||
}
|
|
||||||
|
|
||||||
static focusRight() {
|
|
||||||
const currentlyFocusedObject = get(Selectable.focusedObject);
|
|
||||||
return currentlyFocusedObject?.giveFocus('right');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_initializeSelectable() {
|
_initializeSelectable() {
|
||||||
@@ -454,6 +458,11 @@ export class Selectable {
|
|||||||
getScrollChildrenIntoView() {
|
getScrollChildrenIntoView() {
|
||||||
return this.scrollChildrenIntoView;
|
return this.scrollChildrenIntoView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTrapFocus(trapFocus: boolean) {
|
||||||
|
this.trapFocus = trapFocus;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleKeyboardNavigation(event: KeyboardEvent) {
|
export function handleKeyboardNavigation(event: KeyboardEvent) {
|
||||||
@@ -472,21 +481,13 @@ export function handleKeyboardNavigation(event: KeyboardEvent) {
|
|||||||
|
|
||||||
const navigationActions = currentlyFocusedObject.getNavigationActions();
|
const navigationActions = currentlyFocusedObject.getNavigationActions();
|
||||||
if (event.key === 'ArrowUp') {
|
if (event.key === 'ArrowUp') {
|
||||||
if (navigationActions.up && navigationActions.up(currentlyFocusedObject))
|
if (Selectable.giveFocus('up')) event.preventDefault();
|
||||||
event.preventDefault();
|
|
||||||
else if (Selectable.focusUp()) event.preventDefault();
|
|
||||||
} else if (event.key === 'ArrowDown') {
|
} else if (event.key === 'ArrowDown') {
|
||||||
if (navigationActions.down && navigationActions.down(currentlyFocusedObject))
|
if (Selectable.giveFocus('down')) event.preventDefault();
|
||||||
event.preventDefault();
|
|
||||||
else if (Selectable.focusDown()) event.preventDefault();
|
|
||||||
} else if (event.key === 'ArrowLeft') {
|
} else if (event.key === 'ArrowLeft') {
|
||||||
if (navigationActions.left && navigationActions.left(currentlyFocusedObject))
|
if (Selectable.giveFocus('left')) event.preventDefault();
|
||||||
event.preventDefault();
|
|
||||||
else if (Selectable.focusLeft()) event.preventDefault();
|
|
||||||
} else if (event.key === 'ArrowRight') {
|
} else if (event.key === 'ArrowRight') {
|
||||||
if (navigationActions.right && navigationActions.right(currentlyFocusedObject))
|
if (Selectable.giveFocus('right')) event.preventDefault();
|
||||||
event.preventDefault();
|
|
||||||
else if (Selectable.focusRight()) event.preventDefault();
|
|
||||||
} else if (event.key === 'Enter') {
|
} else if (event.key === 'Enter') {
|
||||||
if (navigationActions.enter && navigationActions.enter(currentlyFocusedObject))
|
if (navigationActions.enter && navigationActions.enter(currentlyFocusedObject))
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
Reference in New Issue
Block a user