mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
A collection of bug fixes (#3820)
Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
parent
6288d89651
commit
193e9b1da9
@ -310,7 +310,7 @@ public class SeriesController : BaseApiController
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="filterDto"></param>
|
/// <param name="filterDto"></param>
|
||||||
/// <param name="userParams"></param>
|
/// <param name="userParams"></param>
|
||||||
/// <param name="libraryId"></param>
|
/// <param name="libraryId">This is not in use</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("all-v2")]
|
[HttpPost("all-v2")]
|
||||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams,
|
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams,
|
||||||
@ -321,8 +321,6 @@ public class SeriesController : BaseApiController
|
|||||||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto, context);
|
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto, context);
|
||||||
|
|
||||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
|
|
||||||
|
|
||||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||||
|
|
||||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||||
|
@ -128,6 +128,7 @@ public class UsersController : BaseApiController
|
|||||||
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
||||||
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
|
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
|
||||||
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
|
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
|
||||||
|
existingPreferences.AllowAutomaticWebtoonReaderDetection = preferencesDto.AllowAutomaticWebtoonReaderDetection;
|
||||||
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
|
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
|
||||||
existingPreferences.ShareReviews = preferencesDto.ShareReviews;
|
existingPreferences.ShareReviews = preferencesDto.ShareReviews;
|
||||||
|
|
||||||
|
@ -102,11 +102,22 @@ export class AccountService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the user has any role in the restricted roles array or is an Admin
|
||||||
|
* @param user
|
||||||
|
* @param roles
|
||||||
|
* @param restrictedRoles
|
||||||
|
*/
|
||||||
hasAnyRole(user: User, roles: Array<Role>, restrictedRoles: Array<Role> = []) {
|
hasAnyRole(user: User, roles: Array<Role>, restrictedRoles: Array<Role> = []) {
|
||||||
if (!user || !user.roles) {
|
if (!user || !user.roles) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user is an admin, they have the role
|
||||||
|
if (this.hasAdminRole(user)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// If restricted roles are provided and the user has any of them, deny access
|
// If restricted roles are provided and the user has any of them, deny access
|
||||||
if (restrictedRoles.length > 0 && restrictedRoles.some(role => user.roles.includes(role))) {
|
if (restrictedRoles.length > 0 && restrictedRoles.some(role => user.roles.includes(role))) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
@for (action of currentItems; track action.title) {
|
@for (action of currentItems; track action.title) {
|
||||||
@if (willRenderAction(action)) {
|
@if (willRenderAction(action, user!)) {
|
||||||
<button class="btn btn-outline-primary text-start d-flex justify-content-between align-items-center w-100"
|
<button class="btn btn-outline-primary text-start d-flex justify-content-between align-items-center w-100"
|
||||||
(click)="handleItemClick(action)">
|
(click)="handleItemClick(action)">
|
||||||
{{action.title}}
|
{{action.title}}
|
||||||
|
@ -38,7 +38,7 @@ export class ActionableModalComponent implements OnInit {
|
|||||||
|
|
||||||
@Input() entity: ActionableEntity = null;
|
@Input() entity: ActionableEntity = null;
|
||||||
@Input() actions: ActionItem<any>[] = [];
|
@Input() actions: ActionItem<any>[] = [];
|
||||||
@Input() willRenderAction!: (action: ActionItem<any>) => boolean;
|
@Input() willRenderAction!: (action: ActionItem<any>, user: User) => boolean;
|
||||||
@Input() shouldRenderSubMenu!: (action: ActionItem<any>, dynamicList: null | Array<any>) => boolean;
|
@Input() shouldRenderSubMenu!: (action: ActionItem<any>, dynamicList: null | Array<any>) => boolean;
|
||||||
@Output() actionPerformed = new EventEmitter<ActionItem<any>>();
|
@Output() actionPerformed = new EventEmitter<ActionItem<any>>();
|
||||||
|
|
||||||
|
@ -20,25 +20,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<div subheader>
|
<div subheader>
|
||||||
<div class="pagination-cont">
|
<div class="pagination-cont">
|
||||||
<ng-container *ngIf="layoutMode !== BookPageLayoutMode.Default">
|
<!-- Column mode needs virtual pages -->
|
||||||
|
@if (layoutMode !== BookPageLayoutMode.Default) {
|
||||||
|
@let vp = getVirtualPage();
|
||||||
<div class="virt-pagination-cont">
|
<div class="virt-pagination-cont">
|
||||||
<div class="g-0 text-center">
|
<div class="g-0 text-center">
|
||||||
{{t('page-label')}}
|
{{t('page-label')}}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center justify-content-between text-center row g-0" *ngIf="getVirtualPage() as vp" >
|
<div class="d-flex align-items-center justify-content-between text-center row g-0">
|
||||||
<button class="btn btn-small btn-icon col-1" (click)="prevPage()" [title]="t('prev-page')">
|
<button class="btn btn-small btn-icon col-1" (click)="prevPage()" [title]="t('prev-page')" [disabled]="vp[0] === 1">
|
||||||
<i class="fa-solid fa-caret-left" aria-hidden="true"></i>
|
<i class="fa-solid fa-caret-left" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="col-1">{{vp[0]}}</div>
|
<div class="col-1">{{vp[0]}}</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<ngb-progressbar [title]="t('virtual-pages')" type="primary" height="5px" (click)="loadPage()" [value]="vp[0]" [max]="vp[1]"></ngb-progressbar>
|
<ngb-progressbar type="primary" height="5px" [value]="vp[0]" [max]="vp[1]"></ngb-progressbar>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-1 btn-icon" (click)="loadPage()" [title]="t('go-to-last-page')">{{vp[1]}}</div>
|
<div class="col-1 btn-icon">{{vp[1]}}</div>
|
||||||
<button class="btn btn-small btn-icon col-1" (click)="nextPage()" [title]="t('next-page')"><i class="fa-solid fa-caret-right" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-1" (click)="nextPage()" [title]="t('next-page')"><i class="fa-solid fa-caret-right" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
}
|
||||||
|
|
||||||
<div class="g-0 text-center">
|
<div class="g-0 text-center">
|
||||||
{{t('pagination-header')}}
|
{{t('pagination-header')}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -277,9 +277,9 @@ $action-bar-height: 38px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.virt-pagination-cont {
|
.virt-pagination-cont {
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
box-shadow: var(--drawer-pagination-horizontal-rule);
|
box-shadow: var(--drawer-pagination-horizontal-rule);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-bar {
|
.bottom-bar {
|
||||||
|
@ -63,6 +63,7 @@ import {
|
|||||||
PersonalToCEvent
|
PersonalToCEvent
|
||||||
} from "../personal-table-of-contents/personal-table-of-contents.component";
|
} from "../personal-table-of-contents/personal-table-of-contents.component";
|
||||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||||
|
import {ConfirmService} from "../../../shared/confirm.service";
|
||||||
|
|
||||||
|
|
||||||
enum TabID {
|
enum TabID {
|
||||||
@ -133,6 +134,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
private readonly utilityService = inject(UtilityService);
|
private readonly utilityService = inject(UtilityService);
|
||||||
private readonly libraryService = inject(LibraryService);
|
private readonly libraryService = inject(LibraryService);
|
||||||
private readonly themeService = inject(ThemeService);
|
private readonly themeService = inject(ThemeService);
|
||||||
|
private readonly confirmService = inject(ConfirmService);
|
||||||
private readonly cdRef = inject(ChangeDetectorRef);
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
protected readonly BookPageLayoutMode = BookPageLayoutMode;
|
protected readonly BookPageLayoutMode = BookPageLayoutMode;
|
||||||
@ -730,7 +732,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('window:keydown', ['$event'])
|
@HostListener('window:keydown', ['$event'])
|
||||||
handleKeyPress(event: KeyboardEvent) {
|
async handleKeyPress(event: KeyboardEvent) {
|
||||||
const activeElement = document.activeElement as HTMLElement;
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
const isInputFocused = activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA';
|
const isInputFocused = activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA';
|
||||||
if (isInputFocused) return;
|
if (isInputFocused) return;
|
||||||
@ -748,7 +750,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === KEY_CODES.G) {
|
} else if (event.key === KEY_CODES.G) {
|
||||||
this.goToPage();
|
await this.goToPage();
|
||||||
} else if (event.key === KEY_CODES.F) {
|
} else if (event.key === KEY_CODES.F) {
|
||||||
this.toggleFullscreen()
|
this.toggleFullscreen()
|
||||||
}
|
}
|
||||||
@ -905,33 +907,35 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
promptForPage() {
|
async promptForPage() {
|
||||||
const question = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages - 1});
|
const promptConfig = {...this.confirmService.defaultPrompt};
|
||||||
const goToPageNum = window.prompt(question, '');
|
// Pages are called sections in the UI, manga reader uses the go-to-page string so we use a different one here
|
||||||
|
promptConfig.header = translate('book-reader.go-to-section');
|
||||||
|
promptConfig.content = translate('book-reader.go-to-section-prompt', {totalSections: this.maxPages - 1});
|
||||||
|
|
||||||
|
const goToPageNum = await this.confirmService.prompt(undefined, promptConfig);
|
||||||
|
|
||||||
if (goToPageNum === null || goToPageNum.trim().length === 0) { return null; }
|
if (goToPageNum === null || goToPageNum.trim().length === 0) { return null; }
|
||||||
return goToPageNum;
|
return goToPageNum;
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPage(pageNum?: number) {
|
async goToPage(pageNum?: number) {
|
||||||
let page = pageNum;
|
let page = pageNum;
|
||||||
if (pageNum === null || pageNum === undefined) {
|
if (pageNum === null || pageNum === undefined) {
|
||||||
const goToPageNum = this.promptForPage();
|
const goToPageNum = await this.promptForPage();
|
||||||
if (goToPageNum === null) { return; }
|
if (goToPageNum === null) { return; }
|
||||||
|
|
||||||
page = parseInt(goToPageNum.trim(), 10);
|
page = parseInt(goToPageNum.trim(), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page === undefined || this.pageNum === page) { return; }
|
if (page === undefined || this.pageNum === page) { return; }
|
||||||
|
|
||||||
if (page > this.maxPages) {
|
if (page > this.maxPages - 1) {
|
||||||
page = this.maxPages;
|
page = this.maxPages - 1;
|
||||||
} else if (page < 0) {
|
} else if (page < 0) {
|
||||||
page = 0;
|
page = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(page === 0 || page === this.maxPages - 1)) {
|
|
||||||
page -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pageNum = page;
|
this.pageNum = page;
|
||||||
this.loadPage();
|
this.loadPage();
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,12 @@
|
|||||||
} @else {
|
} @else {
|
||||||
@for (chapterGroup of chapters; track chapterGroup.title + chapterGroup.children.length) {
|
@for (chapterGroup of chapters; track chapterGroup.title + chapterGroup.children.length) {
|
||||||
<ul class="chapter-title">
|
<ul class="chapter-title">
|
||||||
<li class="{{chapterGroup.page === pageNum ? 'active': ''}}" (click)="loadChapterPage(chapterGroup.page, '')">
|
<li class="{{isChapterSelected(chapterGroup) ? 'active': ''}}" (click)="loadChapterPage(chapterGroup.page, '')">
|
||||||
{{chapterGroup.title}}
|
{{chapterGroup.title}}
|
||||||
</li>
|
</li>
|
||||||
@for(chapter of chapterGroup.children; track chapter.title + chapter.part) {
|
@for(chapter of chapterGroup.children; track chapter.title + chapter.part) {
|
||||||
<ul>
|
<ul>
|
||||||
<li class="{{cleanIdSelector(chapter.part) === currentPageAnchor ? 'active' : ''}}">
|
<li class="{{isAnchorSelected(chapter) ? 'active' : ''}}">
|
||||||
<a href="javascript:void(0);" (click)="loadChapterPage(chapter.page, chapter.part)">{{chapter.title}}</a>
|
<a href="javascript:void(0);" (click)="loadChapterPage(chapter.page, chapter.part)">{{chapter.title}}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -3,9 +3,10 @@
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-title {
|
.chapter-title {
|
||||||
padding-inline-start: 1rem;
|
padding-inline-start: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -31,9 +31,8 @@ export class TableOfContentsComponent implements OnChanges {
|
|||||||
@Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter();
|
@Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter();
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges) {
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
console.log('Current Page: ', this.pageNum, this.currentPageAnchor);
|
//console.log('Current Page: ', this.pageNum, this.currentPageAnchor);
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanIdSelector(id: string) {
|
cleanIdSelector(id: string) {
|
||||||
@ -47,4 +46,30 @@ export class TableOfContentsComponent implements OnChanges {
|
|||||||
loadChapterPage(pageNum: number, part: string) {
|
loadChapterPage(pageNum: number, part: string) {
|
||||||
this.loadChapter.emit({pageNum, part});
|
this.loadChapter.emit({pageNum, part});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isChapterSelected(chapterGroup: BookChapterItem) {
|
||||||
|
if (chapterGroup.page === this.pageNum) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = this.chapters.indexOf(chapterGroup);
|
||||||
|
if (idx < 0) {
|
||||||
|
return false; // should never happen
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextIdx = idx + 1;
|
||||||
|
// Last chapter
|
||||||
|
if (nextIdx >= this.chapters.length) {
|
||||||
|
return chapterGroup.page < this.pageNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passed chapter, and next chapter has not been reached
|
||||||
|
const next = this.chapters[nextIdx];
|
||||||
|
return chapterGroup.page < this.pageNum && next.page > this.pageNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAnchorSelected(chapter: BookChapterItem) {
|
||||||
|
return this.cleanIdSelector(chapter.part) === this.currentPageAnchor
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
<span class="card-actions">
|
<span class="card-actions">
|
||||||
@if (actions && actions.length > 0) {
|
@if (actions && actions.length > 0) {
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [inputActions]="actions" [labelBy]="title"></app-card-actionables>
|
<app-card-actionables [entity]="actionEntity" (actionHandler)="performAction($event)" [inputActions]="actions" [labelBy]="title"></app-card-actionables>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,7 +26,7 @@ import {Series} from 'src/app/_models/series';
|
|||||||
import {User} from 'src/app/_models/user';
|
import {User} from 'src/app/_models/user';
|
||||||
import {Volume} from 'src/app/_models/volume';
|
import {Volume} from 'src/app/_models/volume';
|
||||||
import {AccountService} from 'src/app/_services/account.service';
|
import {AccountService} from 'src/app/_services/account.service';
|
||||||
import {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service';
|
import {Action, ActionableEntity, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service';
|
||||||
import {ImageService} from 'src/app/_services/image.service';
|
import {ImageService} from 'src/app/_services/image.service';
|
||||||
import {LibraryService} from 'src/app/_services/library.service';
|
import {LibraryService} from 'src/app/_services/library.service';
|
||||||
import {EVENTS, MessageHubService} from 'src/app/_services/message-hub.service';
|
import {EVENTS, MessageHubService} from 'src/app/_services/message-hub.service';
|
||||||
@ -118,6 +118,10 @@ export class CardItemComponent implements OnInit {
|
|||||||
* This is the entity we are representing. It will be returned if an action is executed.
|
* This is the entity we are representing. It will be returned if an action is executed.
|
||||||
*/
|
*/
|
||||||
@Input({required: true}) entity!: CardEntity;
|
@Input({required: true}) entity!: CardEntity;
|
||||||
|
/**
|
||||||
|
* An optional entity to be used in the action callback
|
||||||
|
*/
|
||||||
|
@Input() actionEntity: ActionableEntity | null = null;
|
||||||
/**
|
/**
|
||||||
* If the entity is selected or not.
|
* If the entity is selected or not.
|
||||||
*/
|
*/
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
[trackByIdentity]="trackByIdentity"
|
[trackByIdentity]="trackByIdentity"
|
||||||
>
|
>
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions"
|
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions" [actionEntity]="item"
|
||||||
[imageUrl]="imageService.getCollectionCoverImage(item.id)"
|
[imageUrl]="imageService.getCollectionCoverImage(item.id)"
|
||||||
[linkUrl]="'/collections/' + item.id"
|
[linkUrl]="'/collections/' + item.id"
|
||||||
[count]="item.itemCount"
|
[count]="item.itemCount"
|
||||||
|
@ -92,12 +92,24 @@ export class AllCollectionsComponent implements OnInit {
|
|||||||
|
|
||||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this))
|
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(
|
||||||
|
this.handleCollectionActionCallback.bind(this), this.shouldRenderCollection.bind(this))
|
||||||
.filter(action => this.collectionService.actionListFilter(action, user));
|
.filter(action => this.collectionService.actionListFilter(action, user));
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldRenderCollection(action: ActionItem<UserCollection>, entity: UserCollection, user: User) {
|
||||||
|
switch (action.action) {
|
||||||
|
case Action.Promote:
|
||||||
|
return !entity.promoted;
|
||||||
|
case Action.UnPromote:
|
||||||
|
return entity.promoted;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadCollection(item: UserCollection) {
|
loadCollection(item: UserCollection) {
|
||||||
this.router.navigate(['collections', item.id]);
|
this.router.navigate(['collections', item.id]);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<app-card-actionables [entity]="collectionTag" [disabled]="actionInProgress" [inputActions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
|
<app-card-actionables [entity]="collectionTag" [disabled]="actionInProgress" [inputActions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||||
</h4>
|
</h4>
|
||||||
}
|
}
|
||||||
<h5 subtitle class="subtitle-with-actionables">{{t('item-count', {num: series.length})}}</h5>
|
<h5 subtitle class="ms-2 subtitle-with-actionables">{{t('item-count', {num: series.length})}}</h5>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</app-side-nav-companion-bar>
|
</app-side-nav-companion-bar>
|
||||||
}
|
}
|
||||||
|
@ -207,7 +207,8 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
|||||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this))
|
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(
|
||||||
|
this.handleCollectionActionCallback.bind(this), this.shouldRenderCollection.bind(this))
|
||||||
.filter(action => this.collectionService.actionListFilter(action, user));
|
.filter(action => this.collectionService.actionListFilter(action, user));
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
@ -225,6 +226,17 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldRenderCollection(action: ActionItem<UserCollection>, entity: UserCollection, user: User) {
|
||||||
|
switch (action.action) {
|
||||||
|
case Action.Promote:
|
||||||
|
return !entity.promoted;
|
||||||
|
case Action.UnPromote:
|
||||||
|
return entity.promoted;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterContentChecked(): void {
|
ngAfterContentChecked(): void {
|
||||||
this.scrollService.setScrollContainer(this.scrollingBlock);
|
this.scrollService.setScrollContainer(this.scrollingBlock);
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,7 @@ import {LoadingComponent} from '../../../shared/loading/loading.component';
|
|||||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||||
import {shareReplay} from "rxjs/operators";
|
import {shareReplay} from "rxjs/operators";
|
||||||
import {DblClickDirective} from "../../../_directives/dbl-click.directive";
|
import {DblClickDirective} from "../../../_directives/dbl-click.directive";
|
||||||
|
import {ConfirmService} from "../../../shared/confirm.service";
|
||||||
|
|
||||||
|
|
||||||
const PREFETCH_PAGES = 10;
|
const PREFETCH_PAGES = 10;
|
||||||
@ -150,9 +151,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
private readonly modalService = inject(NgbModal);
|
private readonly modalService = inject(NgbModal);
|
||||||
private readonly cdRef = inject(ChangeDetectorRef);
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
private readonly toastr = inject(ToastrService);
|
private readonly toastr = inject(ToastrService);
|
||||||
public readonly readerService = inject(ReaderService);
|
private readonly confirmService = inject(ConfirmService);
|
||||||
public readonly utilityService = inject(UtilityService);
|
protected readonly readerService = inject(ReaderService);
|
||||||
public readonly mangaReaderService = inject(MangaReaderService);
|
protected readonly utilityService = inject(UtilityService);
|
||||||
|
protected readonly mangaReaderService = inject(MangaReaderService);
|
||||||
|
|
||||||
|
|
||||||
protected readonly KeyDirection = KeyDirection;
|
protected readonly KeyDirection = KeyDirection;
|
||||||
protected readonly ReaderMode = ReaderMode;
|
protected readonly ReaderMode = ReaderMode;
|
||||||
@ -647,7 +650,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('window:keyup', ['$event'])
|
@HostListener('window:keyup', ['$event'])
|
||||||
handleKeyPress(event: KeyboardEvent) {
|
async handleKeyPress(event: KeyboardEvent) {
|
||||||
switch (this.readerMode) {
|
switch (this.readerMode) {
|
||||||
case ReaderMode.LeftRight:
|
case ReaderMode.LeftRight:
|
||||||
if (event.key === KEY_CODES.RIGHT_ARROW) {
|
if (event.key === KEY_CODES.RIGHT_ARROW) {
|
||||||
@ -682,7 +685,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
} else if (event.key === KEY_CODES.SPACE) {
|
} else if (event.key === KEY_CODES.SPACE) {
|
||||||
this.toggleMenu();
|
this.toggleMenu();
|
||||||
} else if (event.key === KEY_CODES.G) {
|
} else if (event.key === KEY_CODES.G) {
|
||||||
const goToPageNum = this.promptForPage();
|
const goToPageNum = await this.promptForPage();
|
||||||
if (goToPageNum === null) { return; }
|
if (goToPageNum === null) { return; }
|
||||||
this.goToPage(parseInt(goToPageNum.trim(), 10));
|
this.goToPage(parseInt(goToPageNum.trim(), 10));
|
||||||
} else if (event.key === KEY_CODES.B) {
|
} else if (event.key === KEY_CODES.B) {
|
||||||
@ -1593,9 +1596,16 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This is menu only code
|
// This is menu only code
|
||||||
promptForPage() {
|
async promptForPage() {
|
||||||
const question = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages});
|
// const question = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages});
|
||||||
const goToPageNum = window.prompt(question, '');
|
// const goToPageNum = window.prompt(question, '');
|
||||||
|
|
||||||
|
const promptConfig = {...this.confirmService.defaultPrompt};
|
||||||
|
promptConfig.header = translate('book-reader.go-to-page');
|
||||||
|
promptConfig.content = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages});
|
||||||
|
|
||||||
|
const goToPageNum = await this.confirmService.prompt(undefined, promptConfig);
|
||||||
|
|
||||||
if (goToPageNum === null || goToPageNum.trim().length === 0) { return null; }
|
if (goToPageNum === null || goToPageNum.trim().length === 0) { return null; }
|
||||||
return goToPageNum;
|
return goToPageNum;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="resetField()"></button>
|
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="resetField()"></button>
|
||||||
}
|
}
|
||||||
} @else {
|
} @else {
|
||||||
<div class="input-hint">
|
<div class="input-hint d-none d-lg-block">
|
||||||
Ctrl+K
|
Ctrl+K
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<div class="col-auto ms-2 d-none d-md-block">
|
<div class="col-auto ms-2">
|
||||||
<div class="card-actions btn-actions" [ngbTooltip]="t('more-alt')">
|
<div class="card-actions btn-actions" [ngbTooltip]="t('more-alt')">
|
||||||
<app-card-actionables [entity]="readingList" [inputActions]="actions" [labelBy]="readingList.title" iconClass="fa-ellipsis-h" btnClass="btn"></app-card-actionables>
|
<app-card-actionables [entity]="readingList" [inputActions]="actions" [labelBy]="readingList.title" iconClass="fa-ellipsis-h" btnClass="btn"></app-card-actionables>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,6 +58,7 @@ import {DefaultValuePipe} from "../../../_pipes/default-value.pipe";
|
|||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
import {DetailsTabComponent} from "../../../_single-module/details-tab/details-tab.component";
|
import {DetailsTabComponent} from "../../../_single-module/details-tab/details-tab.component";
|
||||||
import {IHasCast} from "../../../_models/common/i-has-cast";
|
import {IHasCast} from "../../../_models/common/i-has-cast";
|
||||||
|
import {User} from "../../../_models/user";
|
||||||
|
|
||||||
enum TabID {
|
enum TabID {
|
||||||
Storyline = 'storyline-tab',
|
Storyline = 'storyline-tab',
|
||||||
@ -251,7 +252,8 @@ export class ReadingListDetailComponent implements OnInit {
|
|||||||
if (user) {
|
if (user) {
|
||||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||||
|
|
||||||
this.actions = this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this))
|
this.actions = this.actionFactoryService
|
||||||
|
.getReadingListActions(this.handleReadingListActionCallback.bind(this), this.shouldRenderReadingListAction.bind(this))
|
||||||
.filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin));
|
.filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin));
|
||||||
this.isOwnedReadingList = this.actions.filter(a => a.action === Action.Edit).length > 0;
|
this.isOwnedReadingList = this.actions.filter(a => a.action === Action.Edit).length > 0;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
@ -307,6 +309,17 @@ export class ReadingListDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldRenderReadingListAction(action: ActionItem<ReadingList>, entity: ReadingList, user: User) {
|
||||||
|
switch (action.action) {
|
||||||
|
case Action.Promote:
|
||||||
|
return !entity.promoted;
|
||||||
|
case Action.UnPromote:
|
||||||
|
return entity.promoted;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
editReadingList(readingList: ReadingList) {
|
editReadingList(readingList: ReadingList) {
|
||||||
this.actionService.editReadingList(readingList, (readingList: ReadingList) => {
|
this.actionService.editReadingList(readingList, (readingList: ReadingList) => {
|
||||||
// Reload information around list
|
// Reload information around list
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
[trackByIdentity]="trackByIdentity"
|
[trackByIdentity]="trackByIdentity"
|
||||||
>
|
>
|
||||||
<ng-template #cardItem let-item let-position="idx" >
|
<ng-template #cardItem let-item let-position="idx" >
|
||||||
<app-card-item [title]="item.title" [entity]="item" [actions]="actions[item.id]"
|
<app-card-item [title]="item.title" [entity]="item" [actions]="actions[item.id]" [actionEntity]="item"
|
||||||
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
|
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
|
||||||
[linkUrl]="'/lists/' + item.id"
|
[linkUrl]="'/lists/' + item.id"
|
||||||
[count]="item.itemCount"
|
[count]="item.itemCount"
|
||||||
|
@ -24,6 +24,7 @@ import {Title} from "@angular/platform-browser";
|
|||||||
import {WikiLink} from "../../../_models/wiki";
|
import {WikiLink} from "../../../_models/wiki";
|
||||||
import {BulkSelectionService} from "../../../cards/bulk-selection.service";
|
import {BulkSelectionService} from "../../../cards/bulk-selection.service";
|
||||||
import {BulkOperationsComponent} from "../../../cards/bulk-operations/bulk-operations.component";
|
import {BulkOperationsComponent} from "../../../cards/bulk-operations/bulk-operations.component";
|
||||||
|
import {User} from "../../../_models/user";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-reading-lists',
|
selector: 'app-reading-lists',
|
||||||
@ -37,7 +38,7 @@ export class ReadingListsComponent implements OnInit {
|
|||||||
|
|
||||||
protected readonly bulkSelectionService = inject(BulkSelectionService);
|
protected readonly bulkSelectionService = inject(BulkSelectionService);
|
||||||
protected readonly actionService = inject(ActionService);
|
protected readonly actionService = inject(ActionService);
|
||||||
|
|
||||||
|
|
||||||
lists: ReadingList[] = [];
|
lists: ReadingList[] = [];
|
||||||
loadingLists = false;
|
loadingLists = false;
|
||||||
@ -69,10 +70,8 @@ export class ReadingListsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getActions(readingList: ReadingList) {
|
getActions(readingList: ReadingList) {
|
||||||
const d = this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this))
|
return this.actionFactoryService
|
||||||
.filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin || this.hasPromote));
|
.getReadingListActions(this.handleReadingListActionCallback.bind(this), this.shouldRenderReadingListAction.bind(this))
|
||||||
|
|
||||||
return this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this))
|
|
||||||
.filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin || this.hasPromote));
|
.filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin || this.hasPromote));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,4 +171,15 @@ export class ReadingListsComponent implements OnInit {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldRenderReadingListAction(action: ActionItem<ReadingList>, entity: ReadingList, user: User) {
|
||||||
|
switch (action.action) {
|
||||||
|
case Action.Promote:
|
||||||
|
return !entity.promoted;
|
||||||
|
case Action.UnPromote:
|
||||||
|
return entity.promoted;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -551,7 +551,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||||||
this.location.replaceState(newUrl)
|
this.location.replaceState(newUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSeriesActionCallback(action: ActionItem<Series>, series: Series) {
|
async handleSeriesActionCallback(action: ActionItem<Series>, series: Series) {
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
switch(action.action) {
|
switch(action.action) {
|
||||||
case(Action.MarkAsRead):
|
case(Action.MarkAsRead):
|
||||||
@ -565,16 +565,16 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case(Action.Scan):
|
case(Action.Scan):
|
||||||
this.actionService.scanSeries(series);
|
await this.actionService.scanSeries(series);
|
||||||
break;
|
break;
|
||||||
case(Action.RefreshMetadata):
|
case(Action.RefreshMetadata):
|
||||||
this.actionService.refreshSeriesMetadata(series, undefined, true, false);
|
await this.actionService.refreshSeriesMetadata(series, undefined, true, false);
|
||||||
break;
|
break;
|
||||||
case(Action.GenerateColorScape):
|
case(Action.GenerateColorScape):
|
||||||
this.actionService.refreshSeriesMetadata(series, undefined, false, true);
|
await this.actionService.refreshSeriesMetadata(series, undefined, false, true);
|
||||||
break;
|
break;
|
||||||
case(Action.Delete):
|
case(Action.Delete):
|
||||||
this.deleteSeries(series);
|
await this.deleteSeries(series);
|
||||||
break;
|
break;
|
||||||
case(Action.AddToReadingList):
|
case(Action.AddToReadingList):
|
||||||
this.actionService.addSeriesToReadingList(series);
|
this.actionService.addSeriesToReadingList(series);
|
||||||
@ -645,6 +645,9 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||||||
this.actionService.sendToDevice(volume.chapters.map(c => c.id), device);
|
this.actionService.sendToDevice(volume.chapters.map(c => c.id), device);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case (Action.Download):
|
||||||
|
this.downloadService.download('volume', volume);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -679,6 +682,9 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case (Action.Download):
|
||||||
|
this.downloadService.download('chapter', chapter);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ConfirmButton } from './confirm-button';
|
import {ConfirmButton} from './confirm-button';
|
||||||
|
|
||||||
export class ConfirmConfig {
|
export class ConfirmConfig {
|
||||||
_type: 'confirm' | 'alert' | 'info' = 'confirm';
|
_type: 'confirm' | 'alert' | 'info' | 'prompt' = 'confirm';
|
||||||
header: string = 'Confirm';
|
header: string = 'Confirm';
|
||||||
content: string = '';
|
content: string = '';
|
||||||
buttons: Array<ConfirmButton> = [];
|
buttons: Array<ConfirmButton> = [];
|
||||||
|
@ -5,8 +5,18 @@
|
|||||||
<button type="button" class="btn-close" [attr.aria-label]="t('common.close')" (click)="close()"></button>
|
<button type="button" class="btn-close" [attr.aria-label]="t('common.close')" (click)="close()"></button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" style="overflow-x: auto" [innerHtml]="(config.content | confirmTranslate)! | safeHtml">
|
|
||||||
</div>
|
@if (config._type === 'prompt') {
|
||||||
|
<div class="modal-body" style="overflow-x: auto">
|
||||||
|
<form [formGroup]="formGroup">
|
||||||
|
<div [innerHtml]="(config.content | confirmTranslate)! | safeHtml"></div>
|
||||||
|
<input type="text" class="form-control" aria-labelledby="modal-basic-title" formControlName="prompt" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="modal-body" style="overflow-x: auto" [innerHtml]="(config.content | confirmTranslate)! | safeHtml"></div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@for(btn of config.buttons; track btn) {
|
@for(btn of config.buttons; track btn) {
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import {Component, inject, OnInit} from '@angular/core';
|
import {Component, inject, OnInit} from '@angular/core';
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ConfirmButton } from './_models/confirm-button';
|
import {ConfirmButton} from './_models/confirm-button';
|
||||||
import { ConfirmConfig } from './_models/confirm-config';
|
import {ConfirmConfig} from './_models/confirm-config';
|
||||||
import {CommonModule} from "@angular/common";
|
|
||||||
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||||
import {TranslocoDirective} from "@jsverse/transloco";
|
import {TranslocoDirective} from "@jsverse/transloco";
|
||||||
import {ConfirmTranslatePipe} from "../../_pipes/confirm-translate.pipe";
|
import {ConfirmTranslatePipe} from "../../_pipes/confirm-translate.pipe";
|
||||||
|
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-confirm-dialog',
|
selector: 'app-confirm-dialog',
|
||||||
imports: [SafeHtmlPipe, TranslocoDirective, ConfirmTranslatePipe],
|
imports: [SafeHtmlPipe, TranslocoDirective, ConfirmTranslatePipe, ReactiveFormsModule],
|
||||||
templateUrl: './confirm-dialog.component.html',
|
templateUrl: './confirm-dialog.component.html',
|
||||||
styleUrls: ['./confirm-dialog.component.scss']
|
styleUrls: ['./confirm-dialog.component.scss']
|
||||||
})
|
})
|
||||||
@ -18,6 +18,9 @@ export class ConfirmDialogComponent implements OnInit {
|
|||||||
protected readonly modal = inject(NgbActiveModal);
|
protected readonly modal = inject(NgbActiveModal);
|
||||||
|
|
||||||
config!: ConfirmConfig;
|
config!: ConfirmConfig;
|
||||||
|
formGroup = new FormGroup({
|
||||||
|
'prompt': new FormControl('', []),
|
||||||
|
})
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.config) {
|
if (this.config) {
|
||||||
@ -37,6 +40,10 @@ export class ConfirmDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clickButton(button: ConfirmButton) {
|
clickButton(button: ConfirmButton) {
|
||||||
|
if (this.config._type === 'prompt') {
|
||||||
|
this.modal.close(button.type === 'primary' ? this.formGroup.get('prompt')?.value : '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.modal.close(button.type === 'primary');
|
this.modal.close(button.type === 'primary');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { take } from 'rxjs/operators';
|
import {take} from 'rxjs/operators';
|
||||||
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
|
import {ConfirmDialogComponent} from './confirm-dialog/confirm-dialog.component';
|
||||||
import { ConfirmConfig } from './confirm-dialog/_models/confirm-config';
|
import {ConfirmConfig} from './confirm-dialog/_models/confirm-config';
|
||||||
import {translate} from "@jsverse/transloco";
|
|
||||||
import {ConfirmButton} from "./confirm-dialog/_models/confirm-button";
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -15,6 +13,7 @@ export class ConfirmService {
|
|||||||
defaultConfirm = new ConfirmConfig();
|
defaultConfirm = new ConfirmConfig();
|
||||||
defaultAlert = new ConfirmConfig();
|
defaultAlert = new ConfirmConfig();
|
||||||
defaultInfo = new ConfirmConfig();
|
defaultInfo = new ConfirmConfig();
|
||||||
|
defaultPrompt = new ConfirmConfig();
|
||||||
|
|
||||||
constructor(private modalService: NgbModal) {
|
constructor(private modalService: NgbModal) {
|
||||||
this.defaultConfirm.buttons = [
|
this.defaultConfirm.buttons = [
|
||||||
@ -33,6 +32,13 @@ export class ConfirmService {
|
|||||||
];
|
];
|
||||||
this.defaultInfo.header = 'confirm.info';
|
this.defaultInfo.header = 'confirm.info';
|
||||||
this.defaultInfo._type = 'info';
|
this.defaultInfo._type = 'info';
|
||||||
|
|
||||||
|
this.defaultPrompt.buttons = [
|
||||||
|
{text: 'confirm.cancel', type: 'secondary'},
|
||||||
|
{text: 'confirm.ok', type: 'primary'}
|
||||||
|
];
|
||||||
|
this.defaultPrompt.header = 'confirm.prompt';
|
||||||
|
this.defaultPrompt._type = 'prompt';
|
||||||
}
|
}
|
||||||
|
|
||||||
public async confirm(content?: string, config?: ConfirmConfig): Promise<boolean> {
|
public async confirm(content?: string, config?: ConfirmConfig): Promise<boolean> {
|
||||||
@ -114,4 +120,32 @@ export class ConfirmService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async prompt(title: string | undefined = undefined, config: ConfirmConfig | undefined = undefined): Promise<string> {
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (title === undefined && config === undefined) {
|
||||||
|
console.error('Confirm must have either text or a config object passed');
|
||||||
|
return reject(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title !== undefined && config === undefined) {
|
||||||
|
config = this.defaultPrompt;
|
||||||
|
config.header = title;
|
||||||
|
}
|
||||||
|
if (title !== undefined && title !== '' && config!.header === '') {
|
||||||
|
config!.header = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalRef = this.modalService.open(ConfirmDialogComponent);
|
||||||
|
modalRef.componentInstance.config = config;
|
||||||
|
modalRef.closed.pipe(take(1)).subscribe(result => {
|
||||||
|
return resolve(result);
|
||||||
|
});
|
||||||
|
modalRef.dismissed.pipe(take(1)).subscribe(() => {
|
||||||
|
return resolve('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
<ng-container *transloco="let t; read:'change-password'">
|
<ng-container *transloco="let t; read:'change-password'">
|
||||||
<app-setting-item [title]="t('password-label')" [canEdit]="canEdit" [isEditMode]="isEditMode" (editMode)="updateEditMode($event)">
|
<app-setting-item [title]="t('password-label')"
|
||||||
|
[subtitle]="(hasChangePasswordAbility | async) ? '' : t('permission-error')"
|
||||||
|
[canEdit]="(hasChangePasswordAbility | async) || false"
|
||||||
|
[isEditMode]="isEditMode" (editMode)="updateEditMode($event)"
|
||||||
|
>
|
||||||
<ng-template #view>
|
<ng-template #view>
|
||||||
<span class="col-12">***************</span>
|
<span class="col-12">***************</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -15,13 +15,14 @@ import {AccountService} from 'src/app/_services/account.service';
|
|||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||||
|
import {AsyncPipe} from "@angular/common";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-change-password',
|
selector: 'app-change-password',
|
||||||
templateUrl: './change-password.component.html',
|
templateUrl: './change-password.component.html',
|
||||||
styleUrls: ['./change-password.component.scss'],
|
styleUrls: ['./change-password.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [ReactiveFormsModule, TranslocoDirective, SettingItemComponent]
|
imports: [ReactiveFormsModule, TranslocoDirective, SettingItemComponent, AsyncPipe]
|
||||||
})
|
})
|
||||||
export class ChangePasswordComponent implements OnInit, OnDestroy {
|
export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@ -110,7 +110,12 @@ export class ManageUserPreferencesComponent implements OnInit {
|
|||||||
get Locale() {
|
get Locale() {
|
||||||
if (!this.settingsForm.get('locale')) return 'English';
|
if (!this.settingsForm.get('locale')) return 'English';
|
||||||
|
|
||||||
return (this.locales || []).filter(l => l.fileName === this.settingsForm.get('locale')!.value)[0].renderName;
|
const locale = (this.locales || []).find(l => l.fileName === this.settingsForm.get('locale')!.value);
|
||||||
|
if (!locale) {
|
||||||
|
return 'English';
|
||||||
|
}
|
||||||
|
|
||||||
|
return locale.renderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -861,7 +861,7 @@
|
|||||||
"page-label": "Page",
|
"page-label": "Page",
|
||||||
|
|
||||||
"pagination-header": "Section",
|
"pagination-header": "Section",
|
||||||
"go-to-page": "Go to page",
|
"go-to-first-page": "Go to first page",
|
||||||
"go-to-last-page": "Go to last page",
|
"go-to-last-page": "Go to last page",
|
||||||
"prev-page": "Prev Page",
|
"prev-page": "Prev Page",
|
||||||
"next-page": "Next Page",
|
"next-page": "Next Page",
|
||||||
@ -882,7 +882,12 @@
|
|||||||
"incognito-mode-label": "Incognito Mode",
|
"incognito-mode-label": "Incognito Mode",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"go-to-page-prompt": "There are {{totalPages}} pages. What page do you want to go to?"
|
|
||||||
|
"go-to-page": "Go to page",
|
||||||
|
"go-to-page-prompt": "There are {{totalPages}} pages. What page do you want to go to?",
|
||||||
|
|
||||||
|
"go-to-section": "Go to section",
|
||||||
|
"go-to-section-prompt": "There are {{totalSections}} sections. What section do you want to go to?"
|
||||||
},
|
},
|
||||||
|
|
||||||
"personal-table-of-contents": {
|
"personal-table-of-contents": {
|
||||||
@ -2229,8 +2234,8 @@
|
|||||||
"tasks-tab": "{{tabs.tasks-tab}}",
|
"tasks-tab": "{{tabs.tasks-tab}}",
|
||||||
"info-tab": "{{tabs.info-tab}}",
|
"info-tab": "{{tabs.info-tab}}",
|
||||||
|
|
||||||
"pages-label": "{{edit-chapter-modal.pages-count}}",
|
"pages-label": "{{edit-chapter-modal.pages-label}}",
|
||||||
"words-label": "{{edit-chapter-modal.length-title}}",
|
"words-label": "{{edit-chapter-modal.words-label}}",
|
||||||
"pages-count": "{{edit-chapter-modal.pages-count}}",
|
"pages-count": "{{edit-chapter-modal.pages-count}}",
|
||||||
"words-count": "{{edit-chapter-modal.words-count}}",
|
"words-count": "{{edit-chapter-modal.words-count}}",
|
||||||
"reading-time-label": "{{edit-chapter-modal.reading-time-label}}",
|
"reading-time-label": "{{edit-chapter-modal.reading-time-label}}",
|
||||||
@ -2579,7 +2584,8 @@
|
|||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"cancel": "{{common.cancel}}",
|
"cancel": "{{common.cancel}}",
|
||||||
"ok": "Ok"
|
"ok": "Ok",
|
||||||
|
"prompt": "Question"
|
||||||
},
|
},
|
||||||
|
|
||||||
"toasts": {
|
"toasts": {
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
@use './theme/components/table';
|
@use './theme/components/table';
|
||||||
@use './theme/components/alerts';
|
@use './theme/components/alerts';
|
||||||
@use './theme/components/typeahead';
|
@use './theme/components/typeahead';
|
||||||
|
@use './theme/components/tooltip';
|
||||||
|
|
||||||
@use './theme/utilities/headings';
|
@use './theme/utilities/headings';
|
||||||
@use './theme/utilities/utilities';
|
@use './theme/utilities/utilities';
|
||||||
|
@ -31,9 +31,9 @@
|
|||||||
border-color: var(--btn-outline-primary-border-color);
|
border-color: var(--btn-outline-primary-border-color);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--btn-outline-primary-hover-text-color);
|
color: var(--btn-outline-primary-hover-text-color) !important;
|
||||||
background-color: var(--btn-outline-primary-hover-bg-color);
|
background-color: var(--btn-outline-primary-hover-bg-color) !important;
|
||||||
border-color: var(--btn-outline-primary-hover-border-color);
|
border-color: var(--btn-outline-primary-hover-border-color) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
UI/Web/src/theme/components/_tooltip.scss
Normal file
4
UI/Web/src/theme/components/_tooltip.scss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.tooltip {
|
||||||
|
--bs-tooltip-opacity: 1;
|
||||||
|
//--bs-tooltip-color:
|
||||||
|
}
|
@ -59,7 +59,6 @@
|
|||||||
--select2-option-highlighted-background: var(--dropdown-item-hover-bg-color);
|
--select2-option-highlighted-background: var(--dropdown-item-hover-bg-color);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Theming colors that performs a gradient for background. Can be disabled else automatically applied based on cover image colors.
|
/* Theming colors that performs a gradient for background. Can be disabled else automatically applied based on cover image colors.
|
||||||
* --colorscape-primary-color and the alpha variants will be updated in real time. the default variant is fixed and represents the default state and should
|
* --colorscape-primary-color and the alpha variants will be updated in real time. the default variant is fixed and represents the default state and should
|
||||||
* match the non-default/alpha on launch.
|
* match the non-default/alpha on launch.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user