mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-03 13:44:31 -04:00
Misc Changes (#2015)
* Updated ng-bootstrap * Fixed an issue where jumpbar would be disabled when it shouldn't have been. * When there are duplicate files that make up a volume, show the count on series detail. * Added basic ISBN searching which will return a chapter back.
This commit is contained in:
parent
c57244abd1
commit
a2f4bc712c
@ -348,11 +348,11 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
|
|
||||||
result.Series = _context.Series
|
result.Series = _context.Series
|
||||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||||
.Where(s => (EF.Functions.Like(s.Name, $"%{searchQuery}%")
|
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%")
|
||||||
|| (s.OriginalName != null && EF.Functions.Like(s.OriginalName, $"%{searchQuery}%"))
|
|| (s.OriginalName != null && EF.Functions.Like(s.OriginalName, $"%{searchQuery}%"))
|
||||||
|| (s.LocalizedName != null && EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%"))
|
|| (s.LocalizedName != null && EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%"))
|
||||||
|| (EF.Functions.Like(s.NormalizedName, $"%{searchQueryNormalized}%"))
|
|| (EF.Functions.Like(s.NormalizedName, $"%{searchQueryNormalized}%"))
|
||||||
|| (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison)))
|
|| (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison))
|
||||||
.RestrictAgainstAgeRestriction(userRating)
|
.RestrictAgainstAgeRestriction(userRating)
|
||||||
.Include(s => s.Library)
|
.Include(s => s.Library)
|
||||||
.OrderBy(s => s.SortName!.ToLower())
|
.OrderBy(s => s.SortName!.ToLower())
|
||||||
@ -430,7 +430,9 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
|
|
||||||
result.Chapters = await _context.Chapter
|
result.Chapters = await _context.Chapter
|
||||||
.Include(c => c.Files)
|
.Include(c => c.Files)
|
||||||
.Where(c => EF.Functions.Like(c.TitleName, $"%{searchQuery}%"))
|
.Where(c => EF.Functions.Like(c.TitleName, $"%{searchQuery}%")
|
||||||
|
|| EF.Functions.Like(c.ISBN, $"%{searchQuery}%")
|
||||||
|
)
|
||||||
.Where(c => c.Files.All(f => fileIds.Contains(f.Id)))
|
.Where(c => c.Files.All(f => fileIds.Contains(f.Id)))
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.Take(maxRecords)
|
.Take(maxRecords)
|
||||||
|
@ -144,14 +144,4 @@ public static class Seed
|
|||||||
}
|
}
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// Responsible to copy (not overwrite) a set of favicons that Kavita can't parse from websites.
|
|
||||||
// /// </summary>
|
|
||||||
// /// <param name="directoryService"></param>
|
|
||||||
// /// <returns></returns>
|
|
||||||
// public static Task SeedFavicons(IDirectoryService directoryService)
|
|
||||||
// {
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
16
UI/Web/package-lock.json
generated
16
UI/Web/package-lock.json
generated
@ -22,7 +22,7 @@
|
|||||||
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
||||||
"@iplab/ngx-file-upload": "^16.0.1",
|
"@iplab/ngx-file-upload": "^16.0.1",
|
||||||
"@microsoft/signalr": "^7.0.5",
|
"@microsoft/signalr": "^7.0.5",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^14.1.1",
|
"@ng-bootstrap/ng-bootstrap": "^15.0.0",
|
||||||
"@popperjs/core": "^2.11.7",
|
"@popperjs/core": "^2.11.7",
|
||||||
"@swimlane/ngx-charts": "^20.1.2",
|
"@swimlane/ngx-charts": "^20.1.2",
|
||||||
"@tweenjs/tween.js": "^20.0.3",
|
"@tweenjs/tween.js": "^20.0.3",
|
||||||
@ -3697,17 +3697,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ng-bootstrap/ng-bootstrap": {
|
"node_modules/@ng-bootstrap/ng-bootstrap": {
|
||||||
"version": "14.1.1",
|
"version": "15.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-14.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-15.0.0.tgz",
|
||||||
"integrity": "sha512-3EIc+lCmqUlr7sghzx0r28sjk771zvyqe3SXkrT7grYFzQCVbjtms6Wr9OPbdbmpqDNXG6a8llUoyVgtp1B2Tg==",
|
"integrity": "sha512-BA/SI7sURpKwIex6bj2ujL+xUh8oYMrc5POdGC8sVe+yX3NRS0xNbozI6oke6pL2PpYq+/zaJTmuEJsEMCplZA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/common": "^15.0.0",
|
"@angular/common": "^16.0.0",
|
||||||
"@angular/core": "^15.0.0",
|
"@angular/core": "^16.0.0",
|
||||||
"@angular/forms": "^15.0.0",
|
"@angular/forms": "^16.0.0",
|
||||||
"@angular/localize": "^15.0.0",
|
"@angular/localize": "^16.0.0",
|
||||||
"@popperjs/core": "^2.11.6",
|
"@popperjs/core": "^2.11.6",
|
||||||
"rxjs": "^6.5.3 || ^7.4.0"
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
||||||
"@iplab/ngx-file-upload": "^16.0.1",
|
"@iplab/ngx-file-upload": "^16.0.1",
|
||||||
"@microsoft/signalr": "^7.0.5",
|
"@microsoft/signalr": "^7.0.5",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^14.1.1",
|
"@ng-bootstrap/ng-bootstrap": "^15.0.0",
|
||||||
"@popperjs/core": "^2.11.7",
|
"@popperjs/core": "^2.11.7",
|
||||||
"@swimlane/ngx-charts": "^20.1.2",
|
"@swimlane/ngx-charts": "^20.1.2",
|
||||||
"@tweenjs/tween.js": "^20.0.3",
|
"@tweenjs/tween.js": "^20.0.3",
|
||||||
|
@ -47,12 +47,12 @@ export class BookmarksComponent implements OnInit, OnDestroy {
|
|||||||
refresh: EventEmitter<void> = new EventEmitter();
|
refresh: EventEmitter<void> = new EventEmitter();
|
||||||
|
|
||||||
private onDestroy: Subject<void> = new Subject<void>();
|
private onDestroy: Subject<void> = new Subject<void>();
|
||||||
|
|
||||||
constructor(private readerService: ReaderService, private seriesService: SeriesService,
|
constructor(private readerService: ReaderService, private seriesService: SeriesService,
|
||||||
private downloadService: DownloadService, private toastr: ToastrService,
|
private downloadService: DownloadService, private toastr: ToastrService,
|
||||||
private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService,
|
private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService,
|
||||||
public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
||||||
private router: Router, private readonly cdRef: ChangeDetectorRef,
|
private router: Router, private readonly cdRef: ChangeDetectorRef,
|
||||||
private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute,
|
private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute,
|
||||||
private jumpbarService: JumpbarService) {
|
private jumpbarService: JumpbarService) {
|
||||||
this.filterSettings.ageRatingDisabled = true;
|
this.filterSettings.ageRatingDisabled = true;
|
||||||
|
@ -1,7 +1,22 @@
|
|||||||
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, HostListener,
|
import {
|
||||||
Inject, Input, OnChanges, OnDestroy, OnInit, Output, TemplateRef, TrackByFunction, ViewChild } from '@angular/core';
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
ContentChild,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
HostListener,
|
||||||
|
Inject,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
TemplateRef,
|
||||||
|
TrackByFunction,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller';
|
import { VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@ -22,7 +37,7 @@ import { ScrollService } from 'src/app/_services/scroll.service';
|
|||||||
styleUrls: ['./card-detail-layout.component.scss'],
|
styleUrls: ['./card-detail-layout.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
@Input() header: string = '';
|
@Input() header: string = '';
|
||||||
@Input() isLoading: boolean = false;
|
@Input() isLoading: boolean = false;
|
||||||
@ -67,8 +82,6 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
updateApplied: number = 0;
|
updateApplied: number = 0;
|
||||||
hasResumedJumpKey: boolean = false;
|
hasResumedJumpKey: boolean = false;
|
||||||
|
|
||||||
private onDestory: Subject<void> = new Subject();
|
|
||||||
|
|
||||||
get Breakpoint() {
|
get Breakpoint() {
|
||||||
return Breakpoint;
|
return Breakpoint;
|
||||||
}
|
}
|
||||||
@ -99,6 +112,8 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
this.changeDetectionRef.markForCheck();
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('filterSettings: ', this.filterSettings)
|
||||||
|
|
||||||
if (this.pagination === undefined) {
|
if (this.pagination === undefined) {
|
||||||
this.pagination = {currentPage: 1, itemsPerPage: this.items.length, totalItems: this.items.length, totalPages: 1};
|
this.pagination = {currentPage: 1, itemsPerPage: this.items.length, totalItems: this.items.length, totalPages: 1};
|
||||||
this.changeDetectionRef.markForCheck();
|
this.changeDetectionRef.markForCheck();
|
||||||
@ -138,14 +153,8 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.onDestory.next();
|
|
||||||
this.onDestory.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
hasCustomSort() {
|
hasCustomSort() {
|
||||||
return this.filter.sortOptions !== null || this.filterSettings?.presets?.sortOptions !== null;
|
return this.filter.sortOptions !== null || this.filterSettings?.presets?.sortOptions !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
performAction(action: ActionItem<any>) {
|
performAction(action: ActionItem<any>) {
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<i class="fa fa-arrow-down" title="Descending"></i>
|
<i class="fa fa-arrow-down" title="Descending"></i>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="sortingOption">
|
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="sortingOption">
|
||||||
<option *ngFor="let opt of sortingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
<option *ngFor="let opt of sortingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
@ -40,10 +40,10 @@
|
|||||||
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
|
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
|
||||||
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.Cards" class="btn-check" id="layout-mode-default" autocomplete="off">
|
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.Cards" class="btn-check" id="layout-mode-default" autocomplete="off">
|
||||||
<label class="btn btn-outline-primary" for="layout-mode-default">Card</label>
|
<label class="btn btn-outline-primary" for="layout-mode-default">Card</label>
|
||||||
|
|
||||||
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.List" class="btn-check" id="layout-mode-col1" autocomplete="off">
|
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.List" class="btn-check" id="layout-mode-col1" autocomplete="off">
|
||||||
<label class="btn btn-outline-primary" for="layout-mode-col1">List</label>
|
<label class="btn btn-outline-primary" for="layout-mode-col1">List</label>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
</app-side-nav-companion-bar>
|
</app-side-nav-companion-bar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -63,7 +63,7 @@
|
|||||||
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed" fillStyle="filled">{{unreadCount}}</app-tag-badge>
|
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed" fillStyle="filled">{{unreadCount}}</app-tag-badge>
|
||||||
</div>
|
</div>
|
||||||
<app-image height="100%" maxHeight="400px" objectFit="contain" background="none" [imageUrl]="seriesImage"></app-image>
|
<app-image height="100%" maxHeight="400px" objectFit="contain" background="none" [imageUrl]="seriesImage"></app-image>
|
||||||
<div class="progress-banner" *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
|
<div class="progress-banner" *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
|
||||||
<ngb-progressbar type="primary" height="5px" [value]="series.pagesRead" [max]="series.pages"></ngb-progressbar>
|
<ngb-progressbar type="primary" height="5px" [value]="series.pagesRead" [max]="series.pages"></ngb-progressbar>
|
||||||
</div>
|
</div>
|
||||||
<div class="under-image" *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
|
<div class="under-image" *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
|
||||||
@ -95,7 +95,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto ms-2">
|
<div class="col-auto ms-2">
|
||||||
<button class="btn btn-secondary" (click)="toggleWantToRead()" title="{{isWantToRead ? 'Remove from' : 'Add to'}} Want to Read">
|
<button class="btn btn-secondary" (click)="toggleWantToRead()" title="{{isWantToRead ? 'Remove from' : 'Add to'}} Want to Read">
|
||||||
<span>
|
<span>
|
||||||
@ -159,15 +159,16 @@
|
|||||||
<ng-container *ngIf="!item.isChapter; else chapterCardItem">
|
<ng-container *ngIf="!item.isChapter; else chapterCardItem">
|
||||||
<app-card-item class="col-auto mt-2 mb-2" *ngIf="item.volume.number !== 0" [entity]="item.volume" [title]="item.volume.name" (click)="openVolume(item.volume)"
|
<app-card-item class="col-auto mt-2 mb-2" *ngIf="item.volume.number !== 0" [entity]="item.volume" [title]="item.volume.name" (click)="openVolume(item.volume)"
|
||||||
[imageUrl]="imageService.getVolumeCoverImage(item.volume.id)"
|
[imageUrl]="imageService.getVolumeCoverImage(item.volume.id)"
|
||||||
[read]="item.volume.pagesRead" [total]="item.volume.pages" [actions]="volumeActions"
|
[read]="item.volume.pagesRead" [total]="item.volume.pages" [actions]="volumeActions"
|
||||||
(selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, volumes.length, $event)"
|
[count]="item.volume.chapters[0].files.length"
|
||||||
|
(selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, volumes.length, $event)"
|
||||||
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
|
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #chapterCardItem>
|
<ng-template #chapterCardItem>
|
||||||
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.chapter.isSpecial" [entity]="item.chapter" [title]="item.chapter.title" (click)="openChapter(item.chapter)"
|
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.chapter.isSpecial" [entity]="item.chapter" [title]="item.chapter.title" (click)="openChapter(item.chapter)"
|
||||||
[imageUrl]="imageService.getChapterCoverImage(item.chapter.id)"
|
[imageUrl]="imageService.getChapterCoverImage(item.chapter.id)"
|
||||||
[read]="item.chapter.pagesRead" [total]="item.chapter.pages" [actions]="chapterActions"
|
[read]="item.chapter.pagesRead" [total]="item.chapter.pages" [actions]="chapterActions"
|
||||||
[count]="item.chapter.files.length"
|
[count]="item.volume.chapters[0].files.length"
|
||||||
(selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, storyChapters.length, $event)"
|
(selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, storyChapters.length, $event)"
|
||||||
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
|
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -223,7 +224,7 @@
|
|||||||
<ng-template #volumeListLayout>
|
<ng-template #volumeListLayout>
|
||||||
<ng-container *ngFor="let volume of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity">
|
<ng-container *ngFor="let volume of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity">
|
||||||
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(volume.id)"
|
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(volume.id)"
|
||||||
[seriesName]="series.name" [entity]="volume"
|
[seriesName]="series.name" [entity]="volume"
|
||||||
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||||
[pagesRead]="volume.pagesRead" [totalPages]="volume.pages" (read)="openVolume(volume)"
|
[pagesRead]="volume.pagesRead" [totalPages]="volume.pages" (read)="openVolume(volume)"
|
||||||
[blur]="user?.preferences?.blurUnreadSummaries || false">
|
[blur]="user?.preferences?.blurUnreadSummaries || false">
|
||||||
@ -310,7 +311,7 @@
|
|||||||
</virtual-scroller>
|
</virtual-scroller>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="TabID.Related" *ngIf="hasRelations">
|
<li [ngbNavItem]="TabID.Related" *ngIf="hasRelations">
|
||||||
<a ngbNavLink>Related</a>
|
<a ngbNavLink>Related</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.7.2.18"
|
"version": "0.7.2.20"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user