mirror of
https://github.com/Kareadita/Kavita.git
synced 2026-05-27 01:52:36 -04:00
Epub Text Bleeding Finally Fixed! (#4086)
Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com> Co-authored-by: Gazy Mahomar <gmahomarf@users.noreply.github.com> Co-authored-by: Stefans.A <104719225+privatestefans@users.noreply.github.com>
This commit is contained in:
@@ -13,8 +13,11 @@
|
||||
}
|
||||
|
||||
.tag-card:hover {
|
||||
background-color: #3a3a3a;
|
||||
background-color: var(--card-hover-bg-color);
|
||||
//transform: translateY(-3px); // Cool effect but has a weird background issue. ROBBIE: Fix this
|
||||
& .tag-name, & .tag-meta {
|
||||
color: var(--card-hover-text-color)
|
||||
}
|
||||
}
|
||||
|
||||
.tag-name {
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface Preferences {
|
||||
locale: string;
|
||||
bookReaderHighlightSlots: HighlightSlot[];
|
||||
colorScapeEnabled: boolean;
|
||||
dataSaver: boolean;
|
||||
|
||||
// Kavita+
|
||||
aniListScrobblingEnabled: boolean;
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface ReadingListItem {
|
||||
pagesRead: number;
|
||||
pagesTotal: number;
|
||||
seriesName: string;
|
||||
seriesSortName: string;
|
||||
seriesFormat: MangaFormat;
|
||||
seriesId: number;
|
||||
chapterId: number;
|
||||
|
||||
@@ -38,7 +38,7 @@ export class FontService {
|
||||
|
||||
getFontFace(font: EpubFont): FontFace {
|
||||
if (font.provider === FontProvider.System) {
|
||||
return new FontFace(font.name, `url('/assets/fonts/${font.name}/${font.fileName}')`);
|
||||
return new FontFace(font.name, `url('assets/fonts/${font.name}/${font.fileName}')`);
|
||||
}
|
||||
|
||||
return new FontFace(font.name, `url(${this.baseUrl}font?fontId=${font.id}&apiKey=${this.encodedKey})`);
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
color: var(--dropdown-item-text-color);
|
||||
background-color: var(--dropdown-item-bg-color);
|
||||
&:hover {
|
||||
color: var(--dropdown-item-text-color);
|
||||
color: var(--dropdown-item-hover-text-color);
|
||||
background-color: var(--dropdown-item-hover-bg-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
&:focus-visible {
|
||||
color: var(--dropdown-item-text-color);
|
||||
color: var(--dropdown-item-hover-text-color);
|
||||
background-color: var(--dropdown-item-hover-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--card-overlay-text-color);
|
||||
}
|
||||
|
||||
.overlay-information--centered {
|
||||
position: absolute;
|
||||
border-radius: 15px;
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-md-6 pe-4">
|
||||
<app-setting-multi-check-box
|
||||
id="libraries"
|
||||
[title]="t('libraries-label')"
|
||||
[options]="libraryOptions()"
|
||||
formControlName="libraries"
|
||||
@@ -102,6 +103,7 @@
|
||||
|
||||
<div class="col-md-6">
|
||||
<app-setting-multi-check-box
|
||||
id="roles"
|
||||
[title]="t('roles-label')"
|
||||
[options]="roleOptions"
|
||||
formControlName="roles"
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 pe-4">
|
||||
<app-setting-multi-check-box
|
||||
id="libraries"
|
||||
[title]="t('libraries-label')"
|
||||
[options]="libraryOptions()"
|
||||
formControlName="libraries"
|
||||
@@ -39,6 +40,7 @@
|
||||
|
||||
<div class="col-md-6">
|
||||
<app-setting-multi-check-box
|
||||
id="roles"
|
||||
[title]="t('roles-label')"
|
||||
[options]="roleOptions"
|
||||
formControlName="roles"
|
||||
|
||||
@@ -205,19 +205,21 @@
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-md-6 pe-4">
|
||||
<app-setting-multi-check-box
|
||||
[title]="t('default-roles-label')"
|
||||
[options]="roleOptions"
|
||||
id="libraries"
|
||||
[title]="t('default-libraries-label')"
|
||||
[options]="libraryOptions()"
|
||||
[loading]="loading()"
|
||||
formControlName="defaultRoles"
|
||||
formControlName="defaultLibraries"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<app-setting-multi-check-box
|
||||
[title]="t('default-libraries-label')"
|
||||
[options]="libraryOptions()"
|
||||
id="roles"
|
||||
[title]="t('default-roles-label')"
|
||||
[options]="roleOptions"
|
||||
[loading]="loading()"
|
||||
formControlName="defaultLibraries"
|
||||
formControlName="defaultRoles"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,26 +19,26 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (clickToPaginate() && !hidePagination()) {
|
||||
<div class="tap-to-paginate">
|
||||
<div class="left {{clickOverlayClass('left')}} no-observe"
|
||||
(click)="movePage(isLeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD)"
|
||||
[ngClass]="{'immersive' : immersiveMode()}"
|
||||
tabindex="-1"></div>
|
||||
<div class="{{scrollbarNeeded() ? 'right-with-scrollbar' : 'right'}} {{clickOverlayClass('right')}} no-observe"
|
||||
(click)="movePage(isLeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)"
|
||||
[ngClass]="{'immersive' : immersiveMode()}"
|
||||
tabindex="-1"></div>
|
||||
</div>
|
||||
}
|
||||
<div #readingSection class="reading-section {{layoutMode() | columnLayoutClass}} {{writingStyle() | writingStyleClass}}"
|
||||
[ngStyle]="{'width': pageWidthForPagination()}"
|
||||
[ngClass]="{'immersive' : immersiveMode() || !actionBarVisible()}" [@isLoading]="isLoading()" (click)="handleReaderClick($event)">
|
||||
|
||||
@if (clickToPaginate() && !hidePagination()) {
|
||||
<div class="left {{clickOverlayClass('left')}} no-observe"
|
||||
(click)="movePage(isLeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD)"
|
||||
[ngClass]="{'immersive' : immersiveMode()}"
|
||||
tabindex="-1"
|
||||
[ngStyle]="{height: PageHeightForPagination}"></div>
|
||||
<div class="{{scrollbarNeeded() ? 'right-with-scrollbar' : 'right'}} {{clickOverlayClass('right')}} no-observe"
|
||||
(click)="movePage(isLeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)"
|
||||
[ngClass]="{'immersive' : immersiveMode()}"
|
||||
tabindex="-1"
|
||||
[ngStyle]="{height: PageHeightForPagination}"></div>
|
||||
}
|
||||
|
||||
<div #bookContainer class="book-container {{writingStyle() | writingStyleClass}}"
|
||||
[ngClass]="{'immersive' : immersiveMode()}"
|
||||
(mousedown)="mouseDown($event)" >
|
||||
[ngClass]="{'immersive' : immersiveMode()}"
|
||||
(mousedown)="mouseDown($event)" >
|
||||
|
||||
<div #readingHtml class="book-content {{layoutMode() | columnLayoutClass}} {{writingStyle() | writingStyleClass}}"
|
||||
[ngStyle]="{'max-height': columnHeight(), 'max-width': verticalBookContentWidth(), 'width': verticalBookContentWidth(), 'column-width': columnWidth()}"
|
||||
@@ -63,7 +63,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center d-none d-md-block center-group">
|
||||
<div class="text-center center-group">
|
||||
@if (isLoading()) {
|
||||
<div class="spinner-border spinner-border-sm text-primary" style="border-radius: 50%;" role="status">
|
||||
<span class="visually-hidden">{{ t('loading-book') }}</span>
|
||||
|
||||
@@ -84,6 +84,7 @@ $action-bar-height: 38px;
|
||||
}
|
||||
|
||||
.center-group {
|
||||
display: block;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
@@ -98,6 +99,10 @@ $action-bar-height: 38px;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.center-group {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-group {
|
||||
justify-self: end;
|
||||
}
|
||||
@@ -234,6 +239,7 @@ $action-bar-height: 38px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
writing-mode: horizontal-tb;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
|
||||
@@ -306,14 +312,21 @@ $pagination-opacity: 0;
|
||||
//$pagination-opacity: 0.7;
|
||||
|
||||
|
||||
|
||||
.tap-to-paginate {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.right {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: $action-bar-height;
|
||||
width: 20vw;
|
||||
z-index: 3;
|
||||
height: calc(100vh - $action-bar-height*2);
|
||||
background: $pagination-color;
|
||||
border-color: transparent;
|
||||
border: none !important;
|
||||
@@ -323,6 +336,7 @@ $pagination-opacity: 0;
|
||||
|
||||
&.immersive {
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
&.no-pointer-events {
|
||||
@@ -335,8 +349,8 @@ $pagination-opacity: 0;
|
||||
position: absolute;
|
||||
right: 17px;
|
||||
top: $action-bar-height;
|
||||
width: 18%;
|
||||
z-index: 3;
|
||||
width: 18vw;
|
||||
height: calc(100vh - $action-bar-height*2);
|
||||
background: $pagination-color;
|
||||
opacity: $pagination-opacity;
|
||||
border-color: transparent;
|
||||
@@ -346,6 +360,7 @@ $pagination-opacity: 0;
|
||||
|
||||
&.immersive {
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,17 +369,17 @@ $pagination-opacity: 0;
|
||||
left: 0px;
|
||||
top: $action-bar-height;
|
||||
width: 20vw;
|
||||
height: calc(100vh - $action-bar-height*2);
|
||||
background: $pagination-color;
|
||||
opacity: $pagination-opacity;
|
||||
border-color: transparent;
|
||||
border: none !important;
|
||||
z-index: 3;
|
||||
outline: none;
|
||||
height: 100vw;
|
||||
cursor: pointer;
|
||||
|
||||
&.immersive {
|
||||
top: 0px;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,7 +425,7 @@ $pagination-opacity: 0;
|
||||
|
||||
i {
|
||||
background-color: unset;
|
||||
color: var(--br-actionbar-button-text-color);
|
||||
color: var(--br-actionbar-button-text-color) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
|
||||
@@ -359,6 +359,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
*/
|
||||
debugMode = model<boolean>(!environment.production && true);
|
||||
|
||||
/**
|
||||
* Will be set to true if this.scroll(...) is called but the actual scroll is still delayed
|
||||
* This can also be used to debug glitches or race conditions related to page scrolling
|
||||
* For instance, when we invoke a scroll action, but another scroll is scheduled to be triggered afterward
|
||||
*/
|
||||
hasDelayedScroll: boolean = false;
|
||||
|
||||
|
||||
@ViewChild('bookContainer', {static: false}) bookContainerElemRef!: ElementRef<HTMLDivElement>;
|
||||
@@ -506,23 +512,26 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
|
||||
get PageHeightForPagination() {
|
||||
pageHeightForPagination = computed(() => {
|
||||
const layoutMode = this.layoutMode();
|
||||
const immersiveMode = this.immersiveMode();
|
||||
const widthHeight = this.windowHeight();
|
||||
|
||||
if (layoutMode=== BookPageLayoutMode.Default) {
|
||||
if (layoutMode === BookPageLayoutMode.Default) {
|
||||
// Ensure Angular updates this pageHeightForPagination when these signal have an update
|
||||
if (this.isLoading()) return;
|
||||
this.windowHeight();
|
||||
this.writingStyle();
|
||||
|
||||
// if the book content is less than the height of the container, override and return height of container for pagination area
|
||||
if (this.bookContainerElemRef?.nativeElement?.clientHeight > this.bookContentElemRef?.nativeElement?.clientHeight) {
|
||||
return (this.bookContainerElemRef?.nativeElement?.clientHeight || 0) + 'px';
|
||||
}
|
||||
|
||||
return (this.bookContentElemRef?.nativeElement?.scrollHeight || 0) - ((this.topOffset * (immersiveMode ? 0 : 1)) * 2) + 'px';
|
||||
return (this.bookContentElemRef?.nativeElement?.scrollHeight || 0) - ((this.topOffset * (immersiveMode ? 0 : 1)) * 2) + 'px';
|
||||
}
|
||||
|
||||
if (immersiveMode) return widthHeight + 'px';
|
||||
return (widthHeight) - (this.topOffset * 2) + 'px';
|
||||
}
|
||||
return '100%';
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.navService.hideNavBar();
|
||||
@@ -534,9 +543,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const layoutMode = this.layoutMode();
|
||||
const writingStyle = this.writingStyle();
|
||||
|
||||
const windowWidth = this.windowWidth();
|
||||
const marginLeft = this.pageStyles()['margin-left'];
|
||||
const margin = (this.convertVwToPx(parseInt(marginLeft, 10)) * 2);
|
||||
// const windowWidth = this.windowWidth();
|
||||
// const marginLeft = this.pageStyles()['margin-left'];
|
||||
// const margin = (this.convertVwToPx(parseInt(marginLeft, 10)) * 2);
|
||||
const base = writingStyle === WritingStyle.Vertical ? this.pageHeight() : this.pageWidth();
|
||||
|
||||
|
||||
@@ -950,27 +959,33 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.updateWidthAndHeightCalcs();
|
||||
this.updateImageSizes();
|
||||
|
||||
// Refresh page styles to handle margin changes on window resize
|
||||
this.applyPageStyles(this.pageStyles());
|
||||
|
||||
// Attempt to restore the reading position
|
||||
this.snapScrollOnResize();
|
||||
|
||||
afterFrame(() => {
|
||||
this.injectImageBookmarkIndicators(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Only applies to non BookPageLayoutMode. Default and WritingStyle Horizontal
|
||||
* Only applies to non BookPageLayoutMode.Default and WritingStyle Horizontal
|
||||
* @private
|
||||
*/
|
||||
private snapScrollOnResize() {
|
||||
const layoutMode = this.layoutMode();
|
||||
if (layoutMode === BookPageLayoutMode.Default) return;
|
||||
|
||||
const resumeElement = this.lastSeenScrollPartPath || (this.getFirstVisibleElementXPath() ?? '');
|
||||
if (resumeElement) {
|
||||
|
||||
const resumeElement = this.getFirstVisibleElementXPath() ?? null;
|
||||
if (resumeElement !== null) {
|
||||
|
||||
const element = this.getElementFromXPath(resumeElement);
|
||||
//console.log('Attempting to snap to element: ', element);
|
||||
if (this.debugMode()) {
|
||||
const element = this.getElementFromXPath(resumeElement);
|
||||
//console.log('Attempting to snap to element: ', element);
|
||||
this.logSelectedElement('yellow');
|
||||
}
|
||||
|
||||
this.scrollTo(resumeElement, 30); // This works pretty well, but not perfect
|
||||
}
|
||||
@@ -1360,7 +1375,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
this.document.documentElement.style.setProperty('--book-reader-content-max-height', maxHeight);
|
||||
this.document.documentElement.style.setProperty('--book-reader-content-max-width', maxWidth);
|
||||
|
||||
}
|
||||
|
||||
updateSingleImagePageStyles() {
|
||||
@@ -1401,7 +1415,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
// Virtual Paging stuff
|
||||
this.updateWidthAndHeightCalcs();
|
||||
this.applyLayoutMode(this.layoutMode());
|
||||
this.addEmptyPageIfRequired();
|
||||
// this.addEmptyPageIfRequired(); // Already called in this.applyPageStyles()
|
||||
|
||||
// Find all the part ids and their top offset
|
||||
this.setupPageAnchors();
|
||||
@@ -1415,7 +1429,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
// we need to click the document before arrow keys will scroll down.
|
||||
this.reader.nativeElement.focus();
|
||||
this.scroll(() => this.handleScrollEvent()); // Will set lastSeenXPath and save progress
|
||||
afterFrame(() => this.handleScrollEvent()); // Will set lastSeenXPath and save progress
|
||||
this.isLoading.set(false);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
@@ -1425,8 +1439,15 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
private scroll(lambda: () => void) {
|
||||
if (this.hasDelayedScroll) console.warn("Another scroll operation is still pending while this scroll function is being called again");
|
||||
this.hasDelayedScroll = true;
|
||||
|
||||
// `afterFrame() + setTimeout()` can likely be replaced with `requestAnimationFrame()` instead
|
||||
afterFrame(() => {
|
||||
setTimeout(lambda, SCROLL_DELAY)
|
||||
setTimeout(() => {
|
||||
this.hasDelayedScroll = false;
|
||||
lambda();
|
||||
}, SCROLL_DELAY)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1485,26 +1506,34 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
private addEmptyPageIfRequired(): void {
|
||||
const bookContentElem = this.bookContentElemRef.nativeElement;
|
||||
const oldEmptyGap = bookContentElem.querySelector('.kavita-empty-gap');
|
||||
|
||||
if (this.layoutMode() !== BookPageLayoutMode.Column2 || this.isSingleImagePage) {
|
||||
oldEmptyGap?.remove(); // We don't need empty gap for this condition
|
||||
return;
|
||||
}
|
||||
|
||||
const pageSize = this.pageSize();
|
||||
const [_, totalScroll] = this.getScrollOffsetAndTotalScroll();
|
||||
let [_, totalScroll] = this.getScrollOffsetAndTotalScroll();
|
||||
|
||||
if (oldEmptyGap) totalScroll -= pageSize/2;
|
||||
const lastPageSize = totalScroll % pageSize;
|
||||
|
||||
if (lastPageSize >= pageSize / 2 || lastPageSize === 0) {
|
||||
// The last page needs more than one column, no pages will be duplicated
|
||||
oldEmptyGap?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Need to adjust height with the column gap to ensure we don't have too much extra page
|
||||
const columnHeight = this.pageHeight() - COLUMN_GAP;
|
||||
const emptyPage = this.renderer.createElement('div');
|
||||
const columnHeight = this.pageHeight() - (COLUMN_GAP * 2);
|
||||
const emptyPage = oldEmptyGap ?? this.renderer.createElement('div');
|
||||
emptyPage.classList.add('kavita-empty-gap');
|
||||
|
||||
this.renderer.setStyle(emptyPage, 'height', columnHeight + 'px');
|
||||
this.renderer.setStyle(emptyPage, 'width', this.columnWidth());
|
||||
this.renderer.appendChild(this.bookContentElemRef.nativeElement, emptyPage);
|
||||
this.renderer.appendChild(bookContentElem, emptyPage);
|
||||
}
|
||||
|
||||
goBack() {
|
||||
@@ -1548,7 +1577,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
if (currentVirtualPage > 1) {
|
||||
// Calculate the target scroll position for the previous page
|
||||
const targetScroll = (currentVirtualPage - 2) * pageSize - (this.layoutMode() === BookPageLayoutMode.Column2 ? 3 : 0)
|
||||
const targetScroll = (currentVirtualPage - 2) * pageSize;
|
||||
|
||||
const isVertical = this.writingStyle() === WritingStyle.Vertical;
|
||||
|
||||
@@ -1601,7 +1630,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (currentVirtualPage < totalVirtualPages) {
|
||||
|
||||
// Calculate the target scroll position for the next page
|
||||
const targetScroll = (currentVirtualPage * pageSize) + (this.layoutMode() === BookPageLayoutMode.Column2 ? 1 : 0);
|
||||
const targetScroll = (currentVirtualPage * pageSize);
|
||||
const isVertical = this.writingStyle() === WritingStyle.Vertical;
|
||||
|
||||
// +0 apparently goes forward 1 virtual page...
|
||||
@@ -1649,9 +1678,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const columnGapModifier = this.columnGapModifier();
|
||||
if (this.readingSectionElemRef == null) return 0;
|
||||
|
||||
// Give an additional pixels for buffer
|
||||
return this.readingSectionElemRef.nativeElement.clientWidth - margin
|
||||
+ (COLUMN_GAP * columnGapModifier);
|
||||
return this.reader.nativeElement.offsetWidth - margin + (COLUMN_GAP * columnGapModifier);
|
||||
});
|
||||
|
||||
columnGapModifier = computed(() => {
|
||||
@@ -1674,13 +1701,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
|
||||
|
||||
getVerticalPageWidth() {
|
||||
getVerticalPageWidth = computed(() => {
|
||||
if (!(this.pageStyles() || {}).hasOwnProperty('margin-left')) return 0; // TODO: Test this, added for safety during refactor
|
||||
|
||||
const margin = (window.innerWidth * (parseInt(this.pageStyles()['margin-left'], 10) / 100)) * 2;
|
||||
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
||||
const margin = (this.windowWidth() * (parseInt(this.pageStyles()['margin-left'], 10) / 100)) * 2;
|
||||
const windowWidth = this.windowWidth() || document.documentElement.clientWidth;
|
||||
return windowWidth - margin;
|
||||
}
|
||||
});
|
||||
|
||||
convertVwToPx(vwValue: number) {
|
||||
const viewportWidth = Math.max(this.readingSectionElemRef?.nativeElement?.clientWidth ?? 0, window.innerWidth || 0);
|
||||
@@ -1741,21 +1768,40 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
getFirstVisibleElementXPath() {
|
||||
let resumeElement: string | null = null;
|
||||
if (!this.bookContentElemRef || !this.bookContentElemRef.nativeElement) return null;
|
||||
const bookContentElement = this.bookContentElemRef?.nativeElement;
|
||||
if (!bookContentElement) return null;
|
||||
|
||||
//const container = this.getViewportBoundingRect();
|
||||
|
||||
const intersectingEntries = Array.from(this.bookContentElemRef.nativeElement.querySelectorAll('div,o,p,ul,li,a,img,h1,h2,h3,h4,h5,h6,span'))
|
||||
const intersectingEntries = Array.from(bookContentElement.querySelectorAll('div,o,p,ul,li,a,img,h1,h2,h3,h4,h5,h6,span,figure'))
|
||||
.filter(element => !element.classList.contains('no-observe'))
|
||||
.filter(entry => {
|
||||
//return this.isPartiallyContainedIn(container, entry);
|
||||
return this.utilityService.isInViewport(entry, this.topOffset);
|
||||
.filter(element => {
|
||||
//return this.isPartiallyContainedIn(container, element);
|
||||
return this.utilityService.isInViewport(element, this.topOffset)
|
||||
|
||||
/* Remove main container element
|
||||
<div class="book-content"> <-- bookContentElement
|
||||
<div class="body"> <--- we don't need this
|
||||
<style></style>
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
*/
|
||||
&& element.parentElement !== bookContentElement;
|
||||
})
|
||||
.filter((element, i, entries) => {
|
||||
// Remove any children element contained in another element that exist on this entries
|
||||
return !entries.some(item => element !== item && item.contains(element))
|
||||
|
||||
// Remove element that don't have any content
|
||||
&& (element.textContent?.trim().length || element.querySelectorAll('img, svg').length !== 0 || /^(img|svg)$/im.test(element.tagName));
|
||||
});
|
||||
|
||||
intersectingEntries.sort((a, b) => this.sortElementsForLayout(a, b));
|
||||
|
||||
if (intersectingEntries.length > 0) {
|
||||
let path = this.readerService.getXPathTo(intersectingEntries[0]);
|
||||
const element = this.findTopLevelElement(intersectingEntries[0], intersectingEntries[1], bookContentElement);
|
||||
let path = this.readerService.getXPathTo(element);
|
||||
if (path === '') return;
|
||||
|
||||
resumeElement = path;
|
||||
@@ -1763,6 +1809,63 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return resumeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the top level element that has the same parent.
|
||||
* Illustrated with example below:
|
||||
*
|
||||
* <section>
|
||||
* <p> <-- We want to get this element instead
|
||||
* <span> ... target ... </span>
|
||||
* </p>
|
||||
* <p>
|
||||
* <span> ... nextSibling ... </span>
|
||||
* </p>
|
||||
* <section>
|
||||
*/
|
||||
private findTopLevelElement(target: Element, nextSibling: Element, root: Element): Element | null {
|
||||
|
||||
// If no sibling provided, then lets transverse to parent element where the element display is not inline
|
||||
if (nextSibling == null) {
|
||||
let current: Element | null = target;
|
||||
while (current && current !== root) {
|
||||
const displayStyle = window.getComputedStyle(current).getPropertyValue('display');
|
||||
|
||||
if (!displayStyle.includes('inline')) return current;
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
// Immediately return if it's already sibling
|
||||
if (target.parentElement === nextSibling.parentElement) return target;
|
||||
|
||||
const ancestors: Element[] = [];
|
||||
let current: Element | null = null
|
||||
|
||||
// Collect all parent element from the next sibling
|
||||
current = nextSibling.parentElement;
|
||||
while (current && current !== root) {
|
||||
ancestors.push(current);
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
// Traverse up from target to find the similar parent with nextSibling
|
||||
current = target;
|
||||
while (current && current !== root) {
|
||||
let parent: Element | null = current.parentElement;
|
||||
|
||||
if (parent && ancestors.includes(parent)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
current = parent;
|
||||
}
|
||||
|
||||
console.warn("Unable to find similar parent element from the next sibling", target, nextSibling);
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort elements based on layout mode for better scroll position tracking
|
||||
*/
|
||||
@@ -1881,7 +1984,14 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.updateImageSizes(); // Re-call this as we will change window width/height again
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.scrollTo(resumeElement);
|
||||
this.addEmptyPageIfRequired(); // Try add after layout updated on next frame
|
||||
|
||||
// When the user switches pages, there may be a pending scroll that moves to the start or end of the page
|
||||
// For example, `this.scrollWithinPage(...)` might be triggered when the user presses the prev/next page button
|
||||
// So, we don't need to do another page scroll here
|
||||
if (!this.hasDelayedScroll) {
|
||||
this.scrollTo(resumeElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1961,7 +2071,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
scrollTo(partSelector: string, timeout: number = 0) {
|
||||
const element = this.getElementFromXPath(partSelector);
|
||||
const element = this.getElementFromXPath(partSelector) as HTMLElement;
|
||||
|
||||
if (element === null) {
|
||||
if (!environment.production) {
|
||||
@@ -1975,7 +2085,14 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const writingStyle = this.writingStyle();
|
||||
|
||||
if (layout !== BookPageLayoutMode.Default) {
|
||||
afterFrame(() => this.scrollService.scrollIntoView(element as HTMLElement, {timeout, scrollIntoViewOptions: {'block': 'start', 'inline': 'start'}}));
|
||||
afterFrame(() => {
|
||||
// scrollIntoView method will only scroll to the visible area of the element (not including margin)
|
||||
// so we need to apply scroll-margin to that element to correctly scroll into it
|
||||
let margin = window.getComputedStyle(element).margin;
|
||||
if(margin !== '0px') element.style.scrollMargin = margin;
|
||||
|
||||
this.scrollService.scrollIntoView(element, {timeout, scrollIntoViewOptions: {'block': 'start', 'inline': 'start'}})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2063,6 +2180,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.updateImageSizeTimeout = setTimeout( () => {
|
||||
this.updateImageSizes();
|
||||
this.injectImageBookmarkIndicators(true);
|
||||
|
||||
// This needs to be checked after the bookmark indicator has been injected or removed
|
||||
// When switching layout, these indicators may affect the page's total scrollWidth
|
||||
this.addEmptyPageIfRequired();
|
||||
}, 200);
|
||||
|
||||
this.updateSingleImagePageStyles();
|
||||
@@ -2332,7 +2453,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
redRect.style.top = `${viewport.top}px`;
|
||||
redRect.style.width = `${viewport.width}px`;
|
||||
redRect.style.height = `${viewport.height}px`;
|
||||
redRect.style.border = '1px solid red';
|
||||
redRect.style.outline = '1px solid red';
|
||||
redRect.style.pointerEvents = 'none';
|
||||
redRect.style.zIndex = '1000';
|
||||
redRect.title = `Width: ${viewport.width}px`;
|
||||
@@ -2357,7 +2478,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
greenRect.style.top = `${viewport.top}px`;
|
||||
greenRect.style.width = `${margin}px`;
|
||||
greenRect.style.height = `${viewport.height}px`;
|
||||
greenRect.style.border = '1px solid green';
|
||||
greenRect.style.outline = '1px solid green';
|
||||
greenRect.style.pointerEvents = 'none';
|
||||
greenRect.style.zIndex = '1000';
|
||||
greenRect.title = `Width: ${margin}px`;
|
||||
@@ -2376,7 +2497,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
greenRect.style.top = `${viewport.top}px`;
|
||||
greenRect.style.width = `${margin}px`;
|
||||
greenRect.style.height = `${viewport.height}px`;
|
||||
greenRect.style.border = '1px solid green';
|
||||
greenRect.style.outline = '1px solid green';
|
||||
greenRect.style.pointerEvents = 'none';
|
||||
greenRect.style.zIndex = '1000';
|
||||
greenRect.title = `Width: ${margin}px`;
|
||||
@@ -2437,13 +2558,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
logSelectedElement() {
|
||||
const element = this.getElementFromXPath(this.lastSeenScrollPartPath);
|
||||
logSelectedElement(color='red') {
|
||||
const element = this.getElementFromXPath(this.lastSeenScrollPartPath) as HTMLElement | null;
|
||||
if (element) {
|
||||
console.log(element);
|
||||
(element as HTMLElement).style.border = '1px solid red';
|
||||
element.style.outline = '1px solid ' + color;
|
||||
setTimeout(() => {
|
||||
(element as HTMLElement).style.border = '';
|
||||
element.style.outline = '';
|
||||
}, 1_000);
|
||||
}
|
||||
}
|
||||
@@ -2451,8 +2572,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly environment = environment;
|
||||
protected readonly BookPageLayoutMode = BookPageLayoutMode;
|
||||
protected readonly WritingStyle = WritingStyle;
|
||||
protected readonly ReadingDirection = ReadingDirection;
|
||||
protected readonly PAGING_DIRECTION = PAGING_DIRECTION;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
.clickable:hover, .clickable:focus {
|
||||
background-color: var(--list-group-hover-bg-color, --primary-color);
|
||||
background-color: var(--list-group-hover-bg-color, var(--primary-color));
|
||||
}
|
||||
|
||||
.collection {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
.clickable:hover, .clickable:focus {
|
||||
background-color: var(--list-group-hover-bg-color, --primary-color);
|
||||
background-color: var(--list-group-hover-bg-color, var(--primary-color));
|
||||
}
|
||||
|
||||
.pill {
|
||||
|
||||
@@ -267,6 +267,8 @@ export class CardDetailLayoutComponent<TFilter extends number, TSort extends num
|
||||
let name = '';
|
||||
if (item.hasOwnProperty('sortName')) {
|
||||
name = item.sortName;
|
||||
} else if (item.hasOwnProperty('seriesSortName')) { // Reading List Item
|
||||
name = item.seriesSortName;
|
||||
} else if (item.hasOwnProperty('seriesName')) {
|
||||
name = item.seriesName;
|
||||
} else if (item.hasOwnProperty('name')) {
|
||||
|
||||
@@ -59,10 +59,18 @@ export class CarouselReelComponent {
|
||||
|
||||
swiper: Swiper | undefined;
|
||||
|
||||
get progressChange() {
|
||||
const totalItems = this.items.length;
|
||||
const itemsToMove = Math.min(5, totalItems);
|
||||
const progressPerItem = 1 / totalItems;
|
||||
return Math.min(0.25, progressPerItem * itemsToMove);
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
if (this.swiper) {
|
||||
if (this.swiper.isEnd) return;
|
||||
this.swiper.setProgress(this.swiper.progress + 0.25, 600);
|
||||
|
||||
this.swiper.setProgress(this.swiper.progress + this.progressChange, 600);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
@@ -70,7 +78,7 @@ export class CarouselReelComponent {
|
||||
prevPage() {
|
||||
if (this.swiper) {
|
||||
if (this.swiper.isBeginning) return;
|
||||
this.swiper.setProgress(this.swiper.progress - 0.25, 600);
|
||||
this.swiper.setProgress(this.swiper.progress - this.progressChange, 600);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
.text-accent {
|
||||
font-size: small;
|
||||
color: var(---accent-text-color);
|
||||
color: var(--accent-text-color);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@if (accountService.currentUser$ | async; as user) {
|
||||
<div class="{{theme}}" #container>
|
||||
|
||||
@if (isLoading) {
|
||||
@if (isLoading && !disableLoadingIndicator()) {
|
||||
<div class="loading-message-container">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
{{t('loading-message')}}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
HostListener,
|
||||
inject,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
OnInit, signal,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
@@ -110,6 +110,10 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
backgroundColor: string = this.themeMap[this.theme].background;
|
||||
fontColor: string = this.themeMap[this.theme].font;
|
||||
|
||||
/**
|
||||
* True if Preferences.DataSaver is true
|
||||
*/
|
||||
disableLoadingIndicator = signal(false);
|
||||
isLoading: boolean = true;
|
||||
/**
|
||||
* How much of the current document is loaded
|
||||
@@ -260,6 +264,9 @@ export class PdfReaderComponent implements OnInit, OnDestroy {
|
||||
this.backgroundColor = this.themeMap[this.theme].background;
|
||||
this.fontColor = this.themeMap[this.theme].font; // TODO: Move this to an observable or something
|
||||
|
||||
this.disableLoadingIndicator.set(this.user.preferences.dataSaver);
|
||||
pdfDefaultOptions.disableAutoFetch = this.user.preferences.dataSaver;
|
||||
|
||||
this.calcScrollbarNeeded();
|
||||
|
||||
this.bookService.getBookInfo(this.chapterId).subscribe(info => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {JumpKey} from 'src/app/_models/jumpbar/jump-key';
|
||||
@@ -43,7 +42,6 @@ export class ReadingListsComponent implements OnInit {
|
||||
private router = inject(Router);
|
||||
private jumpbarService = inject(JumpbarService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private ngbModal = inject(NgbModal);
|
||||
private titleService = inject(Title);
|
||||
|
||||
protected readonly WikiLink = WikiLink;
|
||||
|
||||
+2
-2
@@ -24,11 +24,11 @@
|
||||
@for (opt of options(); track opt.value; let index = $index) {
|
||||
<li class="list-group-item">
|
||||
<div class="form-check">
|
||||
<input id="option--{{index}}" type="checkbox" class="form-check-input"
|
||||
<input id="{{id()}}--option--{{index}}" type="checkbox" class="form-check-input"
|
||||
[checked]="isChecked(opt)" (change)="onCheckboxChange(opt, $event)"
|
||||
[disabled]="isDisabled(opt)"
|
||||
>
|
||||
<label class="form-check-label" for="option--{{index}}">{{opt.label}}</label>
|
||||
<label class="form-check-label" for="{{id()}}--option--{{index}}">{{opt.label}}</label>
|
||||
@if (opt.colour) {
|
||||
@let c = opt.colour;
|
||||
<i class="fas fa-circle" [ngStyle]="{ 'color': `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})` }"></i>
|
||||
|
||||
+4
@@ -64,6 +64,10 @@ export interface MultiCheckBoxItem<T> {
|
||||
})
|
||||
export class SettingMultiCheckBox<T> implements ControlValueAccessor {
|
||||
|
||||
/**
|
||||
* Id to prepend to input id to ensure uniqueness
|
||||
*/
|
||||
id = input.required<string>();
|
||||
/**
|
||||
* Title to display above the checkboxes
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use '../../../../theme/variables' as theme;
|
||||
|
||||
h2 {
|
||||
color: white;
|
||||
color: var(--side-nav-header-text-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
}
|
||||
|
||||
.side-nav-header {
|
||||
color: #d5d5d5;
|
||||
color: var(--pref-side-nav-header-text-color);
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
+12
@@ -84,6 +84,18 @@
|
||||
</app-setting-switch>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-switch [title]="t('data-saver-label')" [subtitle]="t('data-saver-tooltip')">
|
||||
<ng-template #switch>
|
||||
<div class="form-check form-switch float-end">
|
||||
<input type="checkbox" role="switch" id="data-saver"
|
||||
formControlName="dataSaver" class="form-check-input"
|
||||
aria-labelledby="auto-close-label">
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-setting-switch>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-4">
|
||||
<app-setting-item [canEdit]="false" [showEdit]="false" [allowClickEvents]="true"
|
||||
[title]="t('highlight-bar-label')" [subtitle]="t('highlight-bar-tooltip')">
|
||||
|
||||
+2
-3
@@ -47,6 +47,7 @@ type UserPreferencesForm = FormGroup<{
|
||||
locale: FormControl<string>,
|
||||
bookReaderHighlightSlots: FormArray<FormControl<HighlightSlot>>,
|
||||
colorScapeEnabled: FormControl<boolean>,
|
||||
dataSaver: FormControl<boolean>,
|
||||
|
||||
aniListScrobblingEnabled: FormControl<boolean>,
|
||||
wantToReadSync: FormControl<boolean>,
|
||||
@@ -96,9 +97,6 @@ export class ManageUserPreferencesComponent implements OnInit {
|
||||
loading = signal(true);
|
||||
ageRatings = signal<AgeRatingDto[]>([]);
|
||||
libraries = signal<Library[]>([]);
|
||||
libraryOptions = computed(() => this.libraries().map(l => {
|
||||
return { label: l.name, value: l.id };
|
||||
}));
|
||||
|
||||
locales: Array<KavitaLocale> = [];
|
||||
|
||||
@@ -165,6 +163,7 @@ export class ManageUserPreferencesComponent implements OnInit {
|
||||
locale: this.fb.control<string>(pref.locale || 'en'),
|
||||
bookReaderHighlightSlots: this.fb.array(pref.bookReaderHighlightSlots.map(s => this.fb.control(s))),
|
||||
colorScapeEnabled: this.fb.control<boolean>(pref.colorScapeEnabled),
|
||||
dataSaver: this.fb.control<boolean>(pref.dataSaver),
|
||||
|
||||
aniListScrobblingEnabled: this.fb.control<boolean>(pref.aniListScrobblingEnabled),
|
||||
wantToReadSync: this.fb.control<boolean>(pref.wantToReadSync),
|
||||
|
||||
@@ -210,6 +210,8 @@
|
||||
"highlight-bar-tooltip": "These colors are shared between all books",
|
||||
"colorscape-label": "Use ColorScape",
|
||||
"colorscape-tooltip": "Global toggle to enable/disable the dynamic gradient feature. Will override theme settings",
|
||||
"data-saver-label": "Data saver",
|
||||
"data-saver-tooltip": "Minimizes data usage by preventing automatic prefetching (e.g., PDF reader)",
|
||||
|
||||
"kavitaplus-settings-title": "Kavita+",
|
||||
"anilist-scrobbling-label": "AniList Scrobbling",
|
||||
@@ -782,7 +784,7 @@
|
||||
|
||||
"license": {
|
||||
"title": "Kavita+ License",
|
||||
"kavita+-warning": "Kavita+ is separate from Kavita. If you uninstall Kavita without unsubscribing, you will be charged.",
|
||||
"kavita+-warning": "Kavita+ is separate from Kavita. Uninstalling without first cancelling your subscription will continue the billing cycle.",
|
||||
"manage": "Manage",
|
||||
"invalid-license-tooltip": "If your subscription has ended, you must email support to get a new subscription created",
|
||||
"check": "Check",
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
const IP = 'localhost';
|
||||
|
||||
// All requests to the backend are proxies through the Angular server, we let the browser pick the host
|
||||
// This comes with the advantage that you don't need to change anything to test on a different device on the
|
||||
// network.
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiUrl: 'http://' + IP + ':4200/api/',
|
||||
hubUrl: 'http://'+ IP + ':4200/hubs/',
|
||||
apiUrl: '/api/',
|
||||
hubUrl: '/hubs/',
|
||||
buyLink: 'https://buy.stripe.com/test_9AQ5mi058h1PcIo3cf?prefilled_promo_code=FREETRIAL',
|
||||
manageLink: 'https://billing.stripe.com/p/login/test_14kfZocuh6Tz5ag7ss'
|
||||
};
|
||||
|
||||
@@ -14,10 +14,18 @@
|
||||
--bs-btn-active-bg: var(--primary-color-dark-shade);
|
||||
--bs-btn-active-border-color: var(--primary-color-dark-shade);
|
||||
|
||||
&:hover {
|
||||
i {
|
||||
color: var(--btn-primary-text-color);
|
||||
}
|
||||
|
||||
&:hover, &:focus-visible {
|
||||
color: var(--btn-primary-hover-text-color);
|
||||
background-color: var(--btn-primary-hover-bg-color);
|
||||
border-color: var(--btn-primary-hover-border-color);
|
||||
|
||||
i {
|
||||
color: var(--btn-primary-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +38,18 @@
|
||||
background-color: var(--btn-outline-primary-bg-color);
|
||||
border-color: var(--btn-outline-primary-border-color);
|
||||
|
||||
&:hover {
|
||||
i {
|
||||
color: var(--btn-outline-primary-text-color);
|
||||
}
|
||||
|
||||
&:hover, &:focus-visible {
|
||||
color: var(--btn-outline-primary-hover-text-color) !important;
|
||||
background-color: var(--btn-outline-primary-hover-bg-color) !important;
|
||||
border-color: var(--btn-outline-primary-hover-border-color) !important;
|
||||
|
||||
i {
|
||||
color: var(--btn-outline-primary-hover-text-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +98,11 @@
|
||||
background-color: var(--btn-secondary-outline-bg-color);
|
||||
border-color: var(--btn-secondary-outline-border-color);
|
||||
|
||||
&:hover {
|
||||
i {
|
||||
color: var(--btn-secondary-outline-text-color);
|
||||
}
|
||||
|
||||
&:hover, &:focus-visible {
|
||||
--bs-btn-color: var(--btn-secondary-outline-hover-text-color);
|
||||
--bs-btn-hover-bg: var(-btn-secondary-outline-hover-bg-color);
|
||||
--bs-btn-hover-border-color: var(--btn-secondary-outline-hover-border-color);
|
||||
@@ -94,6 +114,10 @@
|
||||
box-shadow: inset 0px -2px 0px 0px var(--btn-secondary-outline-text-color);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
i {
|
||||
color: var(--btn-secondary-outline-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,10 +127,18 @@
|
||||
background-color: var(--btn-danger-outline-bg-color);
|
||||
border-color: var(--btn-danger-outline-border-color);
|
||||
|
||||
&:hover {
|
||||
i {
|
||||
color: var(--btn-danger-outline-text-color);
|
||||
}
|
||||
|
||||
&:hover, &:focus-visible {
|
||||
color: var(--btn-danger-outline-hover-text-color);
|
||||
background-color: var(--btn-danger-outline-hover-bg-color);
|
||||
border-color: var(--btn-danger-outline-hover-border-color);
|
||||
|
||||
i {
|
||||
color: var(--btn-danger-outline-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +164,9 @@
|
||||
background-color: var(--btn-disabled-bg-color);
|
||||
color: var(--btn-disabled-text-color);
|
||||
border-color: var(--btn-disabled-border-color);
|
||||
i {
|
||||
color: var(--btn-disabled-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
button:disabled, .form-control:disabled, .form-control[readonly], .disabled, :disabled {
|
||||
@@ -144,14 +179,14 @@ button:disabled, .form-control:disabled, .form-control[readonly], .disabled, :di
|
||||
color: var(--body-text-color) !important;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
&:hover, &:focus, &:focus-visible {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.btn:focus, .btn:active, .btn:active:focus {
|
||||
box-shadow: 0 0 0 0 var(---btn-focus-boxshadow-color) !important;
|
||||
box-shadow: 0 0 0 0 var(--btn-focus-boxshadow-color) !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -160,11 +195,15 @@ button:disabled, .form-control:disabled, .form-control[readonly], .disabled, :di
|
||||
color: var(--body-text-color);
|
||||
border: none;
|
||||
|
||||
i {
|
||||
color: var(--body-text-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
--bs-btn-disabled-bg: transparent;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
&:hover, &:focus, &:focus-visible {
|
||||
color: var(--body-text-color);
|
||||
border: none;
|
||||
}
|
||||
@@ -178,14 +217,26 @@ button:disabled, .form-control:disabled, .form-control[readonly], .disabled, :di
|
||||
|
||||
.btn-primary-text {
|
||||
color: var(--btn-primary-text-text-color);
|
||||
|
||||
i {
|
||||
color: var(--btn-primary-text-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary-text {
|
||||
color: var(--btn-secondary-text-text-color);
|
||||
|
||||
i {
|
||||
color: var(--btn-secondary-text-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-danger-text {
|
||||
color: var(--btn-danger-text-text-color);
|
||||
|
||||
i {
|
||||
color: var(--btn-danger-text-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,6 +245,10 @@ button:disabled, .form-control:disabled, .form-control[readonly], .disabled, :di
|
||||
color: var(--btn-secondary-text-color);
|
||||
background-color: var(--btn-secondary-bg-color);
|
||||
border-color: var(--btn-secondary-border-color);
|
||||
|
||||
i {
|
||||
color: var(--btn-secondary-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.btn.btn-secondary.alt {
|
||||
@@ -205,15 +260,17 @@ button:disabled, .form-control:disabled, .form-control[readonly], .disabled, :di
|
||||
font-weight: var(--btn-secondary-font-weight);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
&:focus, &:focus-visible {
|
||||
background-color: var(--btn-alt-focus-bg-color);
|
||||
box-shadow: 0 0 0 0.05rem var(--btn-alt-focus-boxshadow-color);
|
||||
font-weight: var(--btn-secondary-font-weight);
|
||||
}
|
||||
}
|
||||
|
||||
button i.fa {
|
||||
color: var(--btn-fa-icon-color);
|
||||
button {
|
||||
i.fa, i.fa-regular {
|
||||
color: var(--btn-fa-icon-color);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-check:focus + .btn, .btn:focus {
|
||||
@@ -230,7 +287,6 @@ button i.fa {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
@@ -239,10 +295,3 @@ button i.fa {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
//
|
||||
//.btn-primary .btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show {
|
||||
// --bs-btn-active-bg: var(--primary-color-dark-shade);
|
||||
// --bs-btn-active-border-color: var(--primary-color-dark-shade);
|
||||
//}
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ $image-height: 232.91px;
|
||||
$image-filter-height: 160px;
|
||||
$image-width: 160px;
|
||||
|
||||
.card-title {
|
||||
--bs-card-title-color: var(--card-title-text-color);
|
||||
}
|
||||
|
||||
.card-item-container {
|
||||
.card {
|
||||
max-width: $image-width;
|
||||
@@ -76,12 +80,19 @@ $image-width: 160px;
|
||||
height: $image-filter-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .card-title {
|
||||
color: var(--card-overlay-text-color);
|
||||
}
|
||||
|
||||
& + .card-body {
|
||||
color: var(--card-overlay-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0 5px !important;
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
background-color: var(--card-body-bg-color);
|
||||
border-width: var(--card-border-width);
|
||||
border-style: var(--card-border-style);
|
||||
border-color: var(--card-border-color);
|
||||
|
||||
@@ -3,14 +3,16 @@ input:not([type="range"]), .form-control {
|
||||
color: var(--input-text-color);
|
||||
border-color: var(--input-border-color);
|
||||
|
||||
&:focus {
|
||||
&:focus:not(:checked) {
|
||||
border-color: var(--input-focused-border-color);
|
||||
background-color: var(--input-bg-color);
|
||||
color: var(--input-text-color);
|
||||
box-shadow: 0 0 0 .25rem var(--input-focus-boxshadow-color);
|
||||
}
|
||||
|
||||
&:read-only {
|
||||
// Checkboxes are selected by the :read-only pseudo-class, even when they're editable
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/:read-only
|
||||
&:read-only:not([type="checkbox"]) {
|
||||
background-color: var(--input-bg-readonly-color);
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
@@ -14,11 +14,6 @@
|
||||
box-shadow: 0 0 0 0.25rem var(--input-focus-boxshadow-color);
|
||||
}
|
||||
|
||||
&:read-only {
|
||||
background-color: var(--input-bg-readonly-color);
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
}
|
||||
|
||||
.active-highlight {
|
||||
background-color: #2f2f2f;
|
||||
background-color: rgb(255 255 255 / 9%);
|
||||
background-color: var(--side-nav-item-color);
|
||||
width: 0.25rem;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
@@ -203,7 +202,7 @@
|
||||
padding-left: 1.125rem;
|
||||
|
||||
.side-nav-header {
|
||||
color: #ffffff;
|
||||
color: var(--side-nav-header-text-color);
|
||||
font-size: 1rem;
|
||||
margin-left: unset;
|
||||
|
||||
@@ -227,7 +226,7 @@
|
||||
text-align: unset;
|
||||
margin-left: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
color: #999999;
|
||||
color: var(--side-nav-text-color);
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
@@ -238,6 +237,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.side-nav-text {
|
||||
color: var(--side-nav-hover-text-color);
|
||||
}
|
||||
}
|
||||
.card-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -244,7 +244,8 @@
|
||||
--side-nav-mobile-box-shadow: 3px 0em 5px 10em rgb(0 0 0 / 50%);
|
||||
--side-nav-hover-text-color: white;
|
||||
--side-nav-hover-bg-color: black;
|
||||
--side-nav-text-color: hsla(0,0%,100%,.85);
|
||||
--side-nav-text-color: hsla(0, 0%, 100%, .85);
|
||||
--side-nav-header-text-color: white;
|
||||
--side-nav-border-radius: 3px;
|
||||
--side-nav-border: none;
|
||||
--side-nav-border-closed: none;
|
||||
@@ -256,8 +257,10 @@
|
||||
--side-nav-item-active-text-color: #fff;
|
||||
--side-nav-active-bg-color: transparent;
|
||||
--side-nav-overlay-color: var(--elevation-layer11-dark);
|
||||
--side-nav-item-color: rgb(255 255 255 / 9%);
|
||||
--side-nav-item-closed-color: var(--elevation-layer10);
|
||||
--side-nav-item-closed-hover-color: white;
|
||||
--pref-side-nav-header-text-color: #d5d5d5;
|
||||
|
||||
/* List items */
|
||||
--list-group-item-text-color: var(--body-text-color);
|
||||
@@ -353,6 +356,11 @@
|
||||
--card-overlay-bg-color: rgba(0, 0, 0, 0);
|
||||
--card-overlay-hover-bg-color: rgba(30,30,30,.6);
|
||||
--card-progress-triangle-size: 28px;
|
||||
--card-body-bg-color: rgba(0,0,0,0.7);
|
||||
--card-title-text-color: var(--card-text-color);
|
||||
--card-overlay-text-color: var(--card-text-color);
|
||||
--card-hover-text-color: var(--card-text-color);
|
||||
--card-hover-bg-color: #3a3a3a;
|
||||
|
||||
/* Slider */
|
||||
--slider-text-color: white;
|
||||
|
||||
Reference in New Issue
Block a user