mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
feat(xtream): add favorites list
This commit is contained in:
1 parent
d57fd52ac5
commit
3e008aa3f2
14 files changed
+312
-23
No files matched your search
@@ -14,6 +14,8 @@ import {
|
||||
createFavoritesPlaylist,
|
||||
createPlaylistObject,
|
||||
} from '../../../shared/playlist.utils';
|
||||
import { XtreamItem } from '../../../shared/xtream-item.interface';
|
||||
import { XtreamSerieItem } from '../../../shared/xtream-serie-item.interface';
|
||||
import { DbStores } from '../indexed-db.config';
|
||||
import { PlaylistMeta } from '../shared/playlist-meta.type';
|
||||
|
||||
@@ -118,6 +120,43 @@ export class PlaylistsService {
|
||||
);
|
||||
}
|
||||
|
||||
getPortalFavorites(portalId: string) {
|
||||
return this.dbService
|
||||
.getByID<{ favorites: Partial<XtreamItem>[] }>(
|
||||
DbStores.Playlists,
|
||||
portalId
|
||||
)
|
||||
.pipe(map((item) => item.favorites ?? []));
|
||||
}
|
||||
|
||||
addPortalFavorite(portalId: string, item: any) {
|
||||
return this.getPlaylistById(portalId).pipe(
|
||||
switchMap((portal) =>
|
||||
this.dbService.update(DbStores.Playlists, {
|
||||
...portal,
|
||||
favorites: [...(portal.favorites ?? []), item],
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
removeFromPortalFavorites(portalId: string, favoriteId: number) {
|
||||
return this.getPlaylistById(portalId).pipe(
|
||||
switchMap((portal) =>
|
||||
this.dbService.update(DbStores.Playlists, {
|
||||
...portal,
|
||||
favorites: portal.favorites.filter(
|
||||
(i) =>
|
||||
(i as Partial<XtreamItem>).stream_id !==
|
||||
favoriteId &&
|
||||
(i as Partial<XtreamSerieItem>).series_id !==
|
||||
favoriteId
|
||||
),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
updatePlaylistPositions(
|
||||
positionUpdates: {
|
||||
id: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ng-container *ngIf="items.length > 0; else noItems">
|
||||
<ng-container *ngIf="items?.length > 0; else noItems">
|
||||
<div class="grid">
|
||||
<mat-card
|
||||
*ngFor="
|
||||
@@ -25,6 +25,13 @@
|
||||
<div class="title">{{ item.name }}</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div
|
||||
class="no-content"
|
||||
*ngIf="!(items | filterBy : searchPhrase() : 'name')?.length"
|
||||
>
|
||||
<mat-icon class="icon">search</mat-icon>
|
||||
<div>Nothing found, try to change you search request</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #noItems>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
:host {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
gap: 30px;
|
||||
justify-content: center;
|
||||
|
||||
mat-card {
|
||||
@@ -25,13 +25,13 @@
|
||||
|
||||
.stream-icon-placeholder {
|
||||
width: 100%;
|
||||
background: #ccc;
|
||||
background: #999;
|
||||
height: 300px;
|
||||
|
||||
&::after {
|
||||
content: 'No cover';
|
||||
position: absolute;
|
||||
top: 130px;
|
||||
position: relative;
|
||||
top: 140px;
|
||||
left: 70px;
|
||||
}
|
||||
}
|
||||
@@ -52,3 +52,13 @@
|
||||
width: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-card {
|
||||
overflow: hidden;
|
||||
|
||||
transition: all 0.9s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
mat-card:hover {
|
||||
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
@@ -26,6 +26,16 @@ import { PortalStore } from '../portal.store';
|
||||
{{ item.category_name }}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div
|
||||
class="no-content"
|
||||
*ngIf="
|
||||
!(items | filterBy : searchText() : 'category_name')
|
||||
?.length
|
||||
"
|
||||
>
|
||||
<mat-icon class="icon">search</mat-icon>
|
||||
<div>Nothing found, try to change you search request</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #noItems>
|
||||
@@ -41,7 +51,7 @@ import { PortalStore } from '../portal.store';
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
margin: 10px;
|
||||
margin: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: 350px 450px;
|
||||
justify-content: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
@@ -19,6 +24,19 @@
|
||||
.image {
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
|
||||
.placeholder-cover {
|
||||
width: 300px;
|
||||
height: 500px;
|
||||
background-color: #999;
|
||||
|
||||
&::after {
|
||||
content: 'No cover';
|
||||
position: relative;
|
||||
top: 230px;
|
||||
left: 115px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image img {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div id="navigation">
|
||||
<button mat-icon-button routerLink="/">
|
||||
<mat-icon>home</mat-icon>
|
||||
<button mat-button routerLink="/">
|
||||
<mat-icon>home</mat-icon> Playlists
|
||||
</button>
|
||||
|
||||
<div>
|
||||
@@ -22,8 +22,8 @@
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
||||
<button mat-icon-button>
|
||||
<mat-icon>info</mat-icon>
|
||||
<button mat-button (click)="favoritesClicked.emit()">
|
||||
<mat-icon>star</mat-icon> Favorites
|
||||
</button>
|
||||
</div>
|
||||
<div id="sub-panel">
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
::ng-deep {
|
||||
.dark-theme {
|
||||
#navigation,
|
||||
#sub-panel {
|
||||
background: black;
|
||||
}
|
||||
|
||||
#sub-panel {
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -6,19 +19,21 @@
|
||||
|
||||
#navigation {
|
||||
height: 70px;
|
||||
background: black;
|
||||
background: white;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#sub-panel {
|
||||
background: black;
|
||||
border-bottom: 1px solid #333;
|
||||
background: white;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 0 10px 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
|
||||
.breadcrumbs {
|
||||
span {
|
||||
@@ -39,8 +54,8 @@
|
||||
text-align: center;
|
||||
|
||||
.search-input {
|
||||
padding: 8px 8px 8px 40px;
|
||||
width: 300px;
|
||||
padding: 6px 8px 6px 40px;
|
||||
width: 250px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #333;
|
||||
color: #000;
|
||||
|
||||
@@ -31,6 +31,7 @@ export class NavigationBarComponent {
|
||||
|
||||
@Output() contentTypeChanged = new EventEmitter<ContentType>();
|
||||
@Output() breadcrumbClicked = new EventEmitter<Breadcrumb>();
|
||||
@Output() favoritesClicked = new EventEmitter<void>();
|
||||
|
||||
ContentTypeEnum = ContentType;
|
||||
portalStore = inject(PortalStore);
|
||||
|
||||
@@ -40,6 +40,26 @@
|
||||
<div class="label">Episode run time:</div>
|
||||
{{ item.info.episode_run_time }}
|
||||
</label>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
*ngIf="!isFavorite; else removeFromFavoritesButton"
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star_outline</mat-icon> Add to favorites
|
||||
</button>
|
||||
|
||||
<ng-template #removeFromFavoritesButton>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star</mat-icon>Remove from favorites
|
||||
</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { JsonPipe, NgIf } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
XtreamSerieDetails,
|
||||
XtreamSerieEpisode,
|
||||
} from '../../../../shared/xtream-serie-details.interface';
|
||||
import { PlaylistsService } from '../../services/playlists.service';
|
||||
import { selectCurrentPlaylist } from '../../state/selectors';
|
||||
import { PlayerDialogComponent } from '../player-dialog/player-dialog.component';
|
||||
import { SeasonContainerComponent } from '../season-container/season-container.component';
|
||||
@@ -18,6 +21,7 @@ import { SeasonContainerComponent } from '../season-container/season-container.c
|
||||
imports: [
|
||||
JsonPipe,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
NgIf,
|
||||
SeasonContainerComponent,
|
||||
PlayerDialogComponent,
|
||||
@@ -25,9 +29,44 @@ import { SeasonContainerComponent } from '../season-container/season-container.c
|
||||
})
|
||||
export class SerialDetailsComponent {
|
||||
@Input({ required: true }) item: XtreamSerieDetails;
|
||||
@Input() seriesId: number;
|
||||
|
||||
@Output() addToFavoritesClicked = new EventEmitter<any>();
|
||||
@Output() playClicked = new EventEmitter<XtreamSerieEpisode>();
|
||||
@Output() removeFromFavoritesClicked = new EventEmitter<number>();
|
||||
|
||||
store = inject(Store);
|
||||
currentPlaylist = this.store.selectSignal(selectCurrentPlaylist);
|
||||
private playlistService = inject(PlaylistsService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private portalId = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
isFavorite = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.checkFavoriteStatus();
|
||||
}
|
||||
|
||||
checkFavoriteStatus() {
|
||||
this.playlistService
|
||||
.getPortalFavorites(this.portalId)
|
||||
.subscribe((favorites) => {
|
||||
this.isFavorite = favorites.some(
|
||||
(i) => (i as any).series_id === this.seriesId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
toggleFavorite() {
|
||||
if (this.isFavorite) {
|
||||
this.removeFromFavoritesClicked.emit(this.seriesId);
|
||||
} else {
|
||||
this.addToFavoritesClicked.emit({
|
||||
name: this.item.info.name,
|
||||
series_id: this.seriesId,
|
||||
cover: this.item.info.cover,
|
||||
});
|
||||
}
|
||||
this.isFavorite = !this.isFavorite;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,13 @@
|
||||
"
|
||||
>
|
||||
<div class="image">
|
||||
<img *ngIf="item.info.movie_image" [src]="item.info.movie_image" />
|
||||
<img
|
||||
*ngIf="item.info.movie_image; else placeholderCover"
|
||||
[src]="item.info.movie_image"
|
||||
/>
|
||||
<ng-template #placeholderCover>
|
||||
<div class="placeholder-cover"></div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="details">
|
||||
<h2>{{ item.info.name }}</h2>
|
||||
@@ -46,8 +52,26 @@
|
||||
color="accent"
|
||||
(click)="playClicked.emit(item)"
|
||||
>
|
||||
Play
|
||||
Play</button
|
||||
>
|
||||
<button
|
||||
*ngIf="!isFavorite; else removeFromFavoritesButton"
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star_outline</mat-icon> Add to favorites
|
||||
</button>
|
||||
|
||||
<ng-template #removeFromFavoritesButton>
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star</mat-icon>Remove from favorites
|
||||
</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,17 +1,66 @@
|
||||
import { JsonPipe, NgIf } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { XtreamVodDetails } from '../../../../shared/xtream-vod-details.interface';
|
||||
import { PlaylistsService } from '../../services/playlists.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vod-details',
|
||||
templateUrl: './vod-details.component.html',
|
||||
styleUrls: ['../detail-view.scss'],
|
||||
standalone: true,
|
||||
imports: [JsonPipe, MatButtonModule, NgIf],
|
||||
imports: [JsonPipe, MatButtonModule, NgIf, MatIconModule],
|
||||
})
|
||||
export class VodDetailsComponent {
|
||||
export class VodDetailsComponent implements OnInit {
|
||||
@Input({ required: true }) item: XtreamVodDetails;
|
||||
|
||||
@Output() addToFavoritesClicked = new EventEmitter<any>();
|
||||
@Output() playClicked = new EventEmitter<XtreamVodDetails>();
|
||||
@Output() removeFromFavoritesClicked = new EventEmitter<number>();
|
||||
|
||||
private playlistService = inject(PlaylistsService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private portalId = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
isFavorite = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.checkFavoriteStatus();
|
||||
}
|
||||
|
||||
checkFavoriteStatus() {
|
||||
this.playlistService
|
||||
.getPortalFavorites(this.portalId)
|
||||
.subscribe((favorites) => {
|
||||
this.isFavorite = favorites.some(
|
||||
(i) => i.stream_id === this.item.movie_data.stream_id
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
toggleFavorite() {
|
||||
if (this.isFavorite) {
|
||||
this.removeFromFavoritesClicked.emit(
|
||||
this.item.movie_data.stream_id
|
||||
);
|
||||
} else {
|
||||
this.addToFavoritesClicked.emit({
|
||||
name: this.item.movie_data.name,
|
||||
stream_id: this.item.movie_data.stream_id,
|
||||
container_extension: this.item.movie_data.container_extension,
|
||||
cover: this.item.info.movie_image,
|
||||
stream_type: 'movie',
|
||||
});
|
||||
}
|
||||
this.isFavorite = !this.isFavorite;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
(breadcrumbClicked)="breadcrumbClicked($event)"
|
||||
(contentTypeChanged)="changeContentType($event)"
|
||||
(searchTextChanged)="setSearchPhrase($event)"
|
||||
(favoritesClicked)="favoritesClicked()"
|
||||
[searchVisible]="
|
||||
currentLayout === 'category' || currentLayout === 'category_content'
|
||||
"
|
||||
@@ -30,12 +31,23 @@
|
||||
*ngIf="currentLayout === 'vod-details'"
|
||||
[item]="vodDetails"
|
||||
(playClicked)="playVod($event)"
|
||||
(addToFavoritesClicked)="addToFavorites($event)"
|
||||
(removeFromFavoritesClicked)="removeFromFavorites($event)"
|
||||
/>
|
||||
|
||||
<app-serial-details
|
||||
*ngIf="currentLayout === 'serie-details'"
|
||||
[item]="vodDetails"
|
||||
[seriesId]="contentId"
|
||||
(playClicked)="playEpisode($event)"
|
||||
(addToFavoritesClicked)="addToFavorites($event)"
|
||||
(removeFromFavoritesClicked)="removeFromFavorites($event)"
|
||||
/>
|
||||
|
||||
<app-category-content-view
|
||||
*ngIf="currentLayout === 'favorites'"
|
||||
[items]="favorites$ | async"
|
||||
(itemClicked)="itemClicked($event)"
|
||||
/>
|
||||
|
||||
<app-player-view *ngIf="currentLayout === 'player'" />
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { JsonPipe, KeyValuePipe, NgFor, NgIf, NgSwitch } from '@angular/common';
|
||||
import {
|
||||
AsyncPipe,
|
||||
JsonPipe,
|
||||
KeyValuePipe,
|
||||
NgFor,
|
||||
NgIf,
|
||||
NgSwitch,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
NgZone,
|
||||
@@ -39,10 +46,12 @@ import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
XtreamSerieDetails,
|
||||
XtreamSerieEpisode,
|
||||
} from '../../../shared/xtream-serie-details.interface';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
import { Settings, VideoPlayer } from '../settings/settings.interface';
|
||||
import { STORE_KEY } from '../shared/enums/store-keys.enum';
|
||||
import { Breadcrumb } from './breadcrumb.interface';
|
||||
@@ -75,7 +84,8 @@ type LayoutView =
|
||||
| 'category_content'
|
||||
| 'vod-details'
|
||||
| 'player'
|
||||
| 'serie-details';
|
||||
| 'serie-details'
|
||||
| 'favorites';
|
||||
|
||||
@Component({
|
||||
selector: 'app-xtream-main-container',
|
||||
@@ -100,18 +110,21 @@ type LayoutView =
|
||||
SerialDetailsComponent,
|
||||
PlayerDialogComponent,
|
||||
MatProgressSpinnerModule,
|
||||
AsyncPipe,
|
||||
],
|
||||
})
|
||||
export class XtreamMainContainerComponent implements OnInit {
|
||||
dataService = inject(DataService);
|
||||
dialog = inject(MatDialog);
|
||||
ngZone = inject(NgZone);
|
||||
playlistService = inject(PlaylistsService);
|
||||
portalStore = inject(PortalStore);
|
||||
snackBar = inject(MatSnackBar);
|
||||
storage = inject(StorageMap);
|
||||
store = inject(Store);
|
||||
currentPlaylist = this.store.selectSignal(selectCurrentPlaylist);
|
||||
|
||||
favorites$: Observable<any>;
|
||||
breadcrumbs: Breadcrumb[] = [];
|
||||
items = [];
|
||||
listeners = [];
|
||||
@@ -123,6 +136,7 @@ export class XtreamMainContainerComponent implements OnInit {
|
||||
) as Signal<Settings>;
|
||||
isLoading = true;
|
||||
searchPhrase = this.portalStore.searchPhrase();
|
||||
contentId: number;
|
||||
|
||||
commandsList = [
|
||||
new IpcCommand(XTREAM_RESPONSE, (response: XtreamResponse) =>
|
||||
@@ -143,6 +157,10 @@ export class XtreamMainContainerComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.setInitialBreadcrumb();
|
||||
console.log(this.currentPlaylist()._id);
|
||||
this.favorites$ = this.playlistService.getPortalFavorites(
|
||||
this.currentPlaylist()._id
|
||||
);
|
||||
|
||||
this.commandsList.forEach((command) => {
|
||||
if (this.dataService.isElectron) {
|
||||
@@ -229,6 +247,7 @@ export class XtreamMainContainerComponent implements OnInit {
|
||||
if (item.stream_type && item.stream_type === 'movie') {
|
||||
action = XtreamCodeActions.GetVodInfo;
|
||||
this.breadcrumbs.push({ title: item.name, action });
|
||||
this.contentId = item.stream_id;
|
||||
this.sendRequest({ action, vod_id: item.stream_id });
|
||||
} else if (item.stream_type && item.stream_type === 'live') {
|
||||
this.breadcrumbs.push({
|
||||
@@ -239,6 +258,7 @@ export class XtreamMainContainerComponent implements OnInit {
|
||||
} else if (item.series_id) {
|
||||
action = XtreamCodeActions.GetSeriesInfo;
|
||||
this.breadcrumbs.push({ title: item.name, action });
|
||||
this.contentId = item.series_id;
|
||||
this.sendRequest({ action, series_id: item.series_id });
|
||||
}
|
||||
}
|
||||
@@ -368,4 +388,29 @@ export class XtreamMainContainerComponent implements OnInit {
|
||||
console.log(searchPhrase);
|
||||
this.searchPhrase = searchPhrase;
|
||||
}
|
||||
|
||||
addToFavorites(item: any) {
|
||||
this.playlistService
|
||||
.addPortalFavorite(this.currentPlaylist()._id, item)
|
||||
.subscribe(() => {
|
||||
this.snackBar.open('Added to favorites', null, {
|
||||
duration: 1000,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeFromFavorites(favoriteId: number) {
|
||||
this.playlistService
|
||||
.removeFromPortalFavorites(this.currentPlaylist()._id, favoriteId)
|
||||
.subscribe(() => {
|
||||
this.snackBar.open('Removed from favorites', null, {
|
||||
duration: 1000,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
favoritesClicked() {
|
||||
this.currentLayout = 'favorites';
|
||||
this.setInitialBreadcrumb();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user