Stablize the Styles (#1128)

* Fixed a bug where adding multiple series to reading list would throw an error on UI, but it was successful.

* When a series has a reading list, we now show the connection on Series detail.

* Removed all baseurl code from UI and not-connected component since we no longer use it.

* Fixed tag badges not showing a border. Added last read time to the series detail page

* Fixed up error interceptor to remove no-connection code

* Changed implementation for series detail. Book libraries will never send chapters back. Volume 0 volumes will not be sent in volumes ever. Fixed up more renaming logic on books to send more accurate representations to the UI.

* Cleaned up the selected tab and tab display logic

* Fixed a bad where statement in reading lists for series

* Fixed up tab logic again

* Fixed a small margin on search backdrop

* Made badge expander button smaller to align with badges

* Fixed a few UIs due to .form-group and .form-row being removed

* Updated Theme component page to help with style testing

* Added more components to theme tester

* Cleaned up some styling

* Fixed opacity on search item hover
This commit is contained in:
Joseph Milazzo 2022-02-28 13:09:37 -06:00 committed by GitHub
parent cb9b54b8de
commit 864d693790
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 425 additions and 187 deletions

View File

@ -1152,6 +1152,56 @@ public class ReaderServiceTests
#region GetContinuePoint
[Fact]
public async Task GetContinuePoint_ShouldReturnFirstVolume_NoProgress()
{
_context.Series.Add(new Series()
{
Name = "Test",
Library = new Library() {
Name = "Test LIb",
Type = LibraryType.Manga,
},
Volumes = new List<Volume>()
{
EntityFactory.CreateVolume("0", new List<Chapter>()
{
EntityFactory.CreateChapter("95", false, new List<MangaFile>(), 1),
EntityFactory.CreateChapter("96", false, new List<MangaFile>(), 1),
}),
EntityFactory.CreateVolume("1", new List<Chapter>()
{
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
EntityFactory.CreateChapter("2", false, new List<MangaFile>(), 1),
}),
EntityFactory.CreateVolume("2", new List<Chapter>()
{
EntityFactory.CreateChapter("21", false, new List<MangaFile>(), 1),
EntityFactory.CreateChapter("22", false, new List<MangaFile>(), 1),
}),
EntityFactory.CreateVolume("3", new List<Chapter>()
{
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
EntityFactory.CreateChapter("32", false, new List<MangaFile>(), 1),
}),
}
});
_context.AppUser.Add(new AppUser()
{
UserName = "majora2007"
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
var nextChapter = await readerService.GetContinuePoint(1, 1);
Assert.Equal("1", nextChapter.Range);
}
[Fact]
public async Task GetContinuePoint_ShouldReturnFirstNonSpecial()
{

View File

@ -201,8 +201,8 @@ public class SeriesServiceTests
Assert.Equal(6, detail.Chapters.Count());
Assert.NotEmpty(detail.Volumes);
Assert.Equal(3, detail.Volumes.Count()); // This returns 3 because 0 volume will still come
Assert.All(detail.Volumes, dto => Assert.Contains(dto.Name, new[] {"0", "2", "3"}));
Assert.Equal(2, detail.Volumes.Count()); // Volume 0 shouldn't be sent in Volumes
Assert.All(detail.Volumes, dto => Assert.Contains(dto.Name, new[] {"2", "3"}));
}
[Fact]
@ -239,10 +239,11 @@ public class SeriesServiceTests
var detail = await _seriesService.GetSeriesDetail(1, 1);
Assert.NotEmpty(detail.Chapters);
Assert.Equal(3, detail.Chapters.Count()); // volume 2 has a 0 chapter aka a single chapter that is represented as a volume. We don't show in Chapters area
// volume 2 has a 0 chapter aka a single chapter that is represented as a volume. We don't show in Chapters area
Assert.Equal(3, detail.Chapters.Count());
Assert.NotEmpty(detail.Volumes);
Assert.Equal(3, detail.Volumes.Count());
Assert.Equal(2, detail.Volumes.Count());
}
[Fact]
@ -279,6 +280,46 @@ public class SeriesServiceTests
Assert.Equal(2, detail.Volumes.Count());
}
[Fact]
public async Task SeriesDetail_WhenBookLibrary_ShouldReturnVolumesAndSpecial()
{
await ResetDb();
_context.Series.Add(new Series()
{
Name = "Test",
Library = new Library() {
Name = "Test LIb",
Type = LibraryType.Book,
},
Volumes = new List<Volume>()
{
EntityFactory.CreateVolume("0", new List<Chapter>()
{
EntityFactory.CreateChapter("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", true, new List<MangaFile>()),
}),
EntityFactory.CreateVolume("2", new List<Chapter>()
{
EntityFactory.CreateChapter("Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub", false, new List<MangaFile>()),
}),
}
});
await _context.SaveChangesAsync();
var detail = await _seriesService.GetSeriesDetail(1, 1);
Assert.NotEmpty(detail.Volumes);
Assert.Equal("2 - Ano Orokamono ni mo Kyakkou wo! - Volume 2", detail.Volumes.ElementAt(0).Name);
Assert.NotEmpty(detail.Specials);
Assert.Equal("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", detail.Specials.ElementAt(0).Range);
// A book library where all books are Volumes, will show no "chapters" on the UI because it doesn't make sense
Assert.Empty(detail.Chapters);
Assert.Equal(1, detail.Volumes.Count());
}
[Fact]
public async Task SeriesDetail_ShouldSortVolumesByName()
{

View File

@ -49,6 +49,15 @@ namespace API.Controllers
return Ok(items);
}
[HttpGet("lists-for-series")]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForSeries(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(userId, seriesId, true);
return Ok(items);
}
/// <summary>
/// Fetches all reading list items for a given list including rich metadata around series, volume, chapters, and progress
/// </summary>

View File

@ -18,6 +18,10 @@ namespace API.DTOs
/// </summary>
public int PagesRead { get; set; }
/// <summary>
/// DateTime representing last time the series was Read. Calculated at API-time.
/// </summary>
public DateTime LatestReadDate { get; set; }
/// <summary>
/// Rating from logged in user. Calculated at API-time.
/// </summary>
public int UserRating { get; set; }

View File

@ -19,6 +19,9 @@ public interface IReadingListRepository
Task<IEnumerable<ReadingListItemDto>> AddReadingProgressModifiers(int userId, IList<ReadingListItemDto> items);
Task<ReadingListDto> GetReadingListDtoByTitleAsync(string title);
Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId);
Task<IEnumerable<ReadingListDto>> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId,
bool includePromoted);
void Remove(ReadingListItem item);
void BulkRemove(IEnumerable<ReadingListItem> items);
void Update(ReadingList list);
@ -62,6 +65,18 @@ public class ReadingListRepository : IReadingListRepository
return await PagedList<ReadingListDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
}
public async Task<IEnumerable<ReadingListDto>> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted)
{
var query = _context.ReadingList
.Where(l => l.AppUserId == userId || (includePromoted && l.Promoted ))
.Where(l => l.Items.Any(i => i.SeriesId == seriesId))
.OrderBy(l => l.LastModified)
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.AsNoTracking();
return await query.ToListAsync();
}
public async Task<ReadingList> GetReadingListByIdAsync(int readingListId)
{
return await _context.ReadingList

View File

@ -469,6 +469,7 @@ public class SeriesRepository : ISeriesRepository
if (rating == null) continue;
s.UserRating = rating.Rating;
s.UserReview = rating.Review;
s.LatestReadDate = userProgress.Max(p => p.LastModified);
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
@ -217,14 +218,34 @@ public class SeriesService : ISeriesService
var chapters = volumes.SelectMany(v => v.Chapters).ToList();
// For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number.
var processedVolumes = new List<VolumeDto>();
if (libraryType == LibraryType.Book)
{
foreach (var volume in volumes)
{
var firstChapter = volume.Chapters.First();
if (!string.IsNullOrEmpty(firstChapter.TitleName)) volume.Name += $" - {firstChapter.TitleName}";
// On Books, skip volumes that are specials, since these will be shown
if (firstChapter.IsSpecial) continue;
if (string.IsNullOrEmpty(firstChapter.TitleName))
{
if (!firstChapter.Range.Equals(Parser.Parser.DefaultVolume))
{
var title = Path.GetFileNameWithoutExtension(firstChapter.Range);
if (string.IsNullOrEmpty(title)) continue;
volume.Name += $" - {title}";
}
}
else
{
volume.Name += $" - {firstChapter.TitleName}";
}
processedVolumes.Add(volume);
}
}
else
{
processedVolumes = volumes.Where(v => v.Number > 0).ToList();
}
var specials = new List<ChapterDto>();
@ -233,14 +254,26 @@ public class SeriesService : ISeriesService
chapter.Title = Parser.Parser.CleanSpecialTitle(chapter.Title);
specials.Add(chapter);
}
// Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes)
IEnumerable<ChapterDto> retChapters;
if (libraryType == LibraryType.Book)
{
retChapters = Array.Empty<ChapterDto>();
} else
{
retChapters = chapters
.Where(ShouldIncludeChapter)
.OrderBy(c => float.Parse(c.Number), new ChapterSortComparer());
}
return new SeriesDetailDto()
{
Specials = specials,
// Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes)
Chapters = chapters
.Where(ShouldIncludeChapter)
.OrderBy(c => float.Parse(c.Number), new ChapterSortComparer()),
Volumes = volumes,
Chapters = retChapters,
Volumes = processedVolumes,
StorylineChapters = volumes
.Where(v => v.Number == 0)
.SelectMany(v => v.Chapters)

View File

@ -1,4 +1,4 @@
import { Injectable, OnDestroy } from '@angular/core';
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
@ -8,14 +8,12 @@ import {
import { Observable, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { catchError, take } from 'rxjs/operators';
import { catchError } from 'rxjs/operators';
import { AccountService } from '../_services/account.service';
import { environment } from 'src/environments/environment';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
public urlKey: string = 'kavita--no-connection-url';
constructor(private router: Router, private toastr: ToastrService, private accountService: AccountService) {}
@ -44,12 +42,6 @@ export class ErrorInterceptor implements HttpInterceptor {
if (this.toastr.previousToastMessage !== 'Something unexpected went wrong.') {
this.toastr.error('Something unexpected went wrong.');
}
// If we are not on no-connection, redirect there and save current url so when we refersh, we redirect back there
// if (this.router.url !== '/no-connection') {
// localStorage.setItem(this.urlKey, this.router.url);
// this.router.navigateByUrl('/no-connection');
// }
break;
}
return throwError(error);
@ -126,8 +118,7 @@ export class ErrorInterceptor implements HttpInterceptor {
private handleAuthError(error: any) {
// NOTE: Signin has error.error or error.statusText available.
// if statement is due to http/2 spec issue: https://github.com/angular/angular/issues/23334
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
this.accountService.logout();
});
this.accountService.logout();
this.router.navigateByUrl('/login');
}
}

View File

@ -1,10 +1,10 @@
/**
* This is for base url only. Not to be used my applicaiton, only loading and bootstrapping app
*/
export class ConfigData {
baseUrl: string = '/';
// export class ConfigData {
// baseUrl: string = '/';
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
}
// constructor(baseUrl: string) {
// this.baseUrl = baseUrl;
// }
// }

View File

@ -4,16 +4,41 @@ import { Volume } from './volume';
export interface Series {
id: number;
name: string;
originalName: string; // This is not shown to user
/**
* This is not shown to user
*/
originalName: string;
localizedName: string;
sortName: string;
coverImageLocked: boolean;
volumes: Volume[];
pages: number; // Total pages in series
pagesRead: number; // Total pages the logged in user has read
userRating: number; // User rating
userReview: string; // User review
/**
* Total pages in series
*/
pages: number;
/**
* Total pages the logged in user has read
*/
pagesRead: number;
/**
* User's rating (0-5)
*/
userRating: number;
/**
* The user's review
*/
userReview: string;
libraryId: number;
created: string; // DateTime when entity was created
/**
* DateTime the entity was created
*/
created: string;
/**
* Format of the Series
*/
format: MangaFormat;
/**
* DateTime that represents last time the logged in user read this series
*/
latestReadDate: string;
}

View File

@ -30,6 +30,10 @@ export class ReadingListService {
);
}
getReadingListsForSeries(seriesId: number) {
return this.httpClient.get<ReadingList[]>(this.baseUrl + 'readinglist/lists-for-series?seriesId=' + seriesId);
}
getListItems(readingListId: number) {
return this.httpClient.get<ReadingListItem[]>(this.baseUrl + 'readinglist/items?readingListId=' + readingListId);
}
@ -47,7 +51,7 @@ export class ReadingListService {
}
updateByMultipleSeries(readingListId: number, seriesIds: Array<number>) {
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-multiple-series', {readingListId, seriesIds});
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-multiple-series', {readingListId, seriesIds}, { responseType: 'text' as 'json' });
}
updateBySeries(readingListId: number, seriesId: number) {

View File

@ -20,22 +20,15 @@
</div>
</div>
<!-- <div class="mb-3">
<label for="settings-baseurl">Base Url</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="baseUrlTooltip" role="button" tabindex="0"></i>
<ng-template #baseUrlTooltip>Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita</ng-template>
<span class="visually-hidden" id="settings-baseurl-help">Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita</span>
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text">
</div> -->
<div class="row g-0 mb-2">
<div class="form-group col-md-6 col-sm-12 pe-2">
<div class="col-md-6 col-sm-12 pe-2">
<label for="settings-port" class="form-label">Port</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
<ng-template #portTooltip>Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</ng-template>
<span class="visually-hidden" id="settings-port-help">Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</span>
<input id="settings-port" aria-describedby="settings-port-help" class="form-control" formControlName="port" type="number" step="1" min="1" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
</div>
<div class="form-group col-md-6 col-sm-12">
<div class="col-md-6 col-sm-12">
<label for="logging-level-port" class="form-label">Logging Level</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="loggingLevelTooltip" role="button" tabindex="0"></i>
<ng-template #loggingLevelTooltip>Use debug to help identify issues. Debug can eat up a lot of disk space. Requires restart to take effect.</ng-template>
<span class="visually-hidden" id="logging-level-port-help">Port the server listens on. Requires restart to take effect.</span>

View File

@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LibraryDetailComponent } from './library-detail/library-detail.component';
import { NotConnectedComponent } from './not-connected/not-connected.component';
import { SeriesDetailComponent } from './series-detail/series-detail.component';
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
import { UserLoginComponent } from './user-login/user-login.component';
@ -16,7 +15,6 @@ import { ThemeTestComponent } from './theme-test/theme-test.component';
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
const routes: Routes = [
{path: '', component: UserLoginComponent},
{
path: 'admin',
canActivate: [AdminGuard],
@ -37,6 +35,10 @@ const routes: Routes = [
canActivate: [AuthGuard],
loadChildren: () => import('./reading-list/reading-list.module').then(m => m.ReadingListModule)
},
{
path: 'registration',
loadChildren: () => import('../app/registration/registration.module').then(m => m.RegistrationModule)
},
{
path: '',
runGuardsAndResolvers: 'always',
@ -66,13 +68,10 @@ const routes: Routes = [
]
},
{
path: 'registration',
loadChildren: () => import('../app/registration/registration.module').then(m => m.RegistrationModule)
},
{path: 'login', component: UserLoginComponent}, // TODO: move this to registration module
{path: 'no-connection', component: NotConnectedComponent},
{path: 'theme', component: ThemeTestComponent},
{path: '', component: UserLoginComponent},
{path: 'login', component: UserLoginComponent}, // TODO: move this to registration module
{path: '**', component: UserLoginComponent, pathMatch: 'full'}
];

View File

@ -7,7 +7,7 @@ import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbPopoverModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
import { NgbAccordionModule, NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbPopoverModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
import { NavHeaderComponent } from './nav-header/nav-header.component';
import { JwtInterceptor } from './_interceptors/jwt.interceptor';
import { UserLoginComponent } from './user-login/user-login.component';
@ -17,7 +17,6 @@ import { LibraryComponent } from './library/library.component';
import { SharedModule } from './shared/shared.module';
import { LibraryDetailComponent } from './library-detail/library-detail.component';
import { SeriesDetailComponent } from './series-detail/series-detail.component';
import { NotConnectedComponent } from './not-connected/not-connected.component';
import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component';
import { CarouselModule } from './carousel/carousel.module';
@ -29,7 +28,6 @@ import { CardsModule } from './cards/cards.module';
import { CollectionsModule } from './collections/collections.module';
import { ReadingListModule } from './reading-list/reading-list.module';
import { SAVER, getSaver } from './shared/_providers/saver.provider';
import { ConfigData } from './_models/config-data';
import { NavEventsToggleComponent } from './nav-events-toggle/nav-events-toggle.component';
import { PersonRolePipe } from './_pipes/person-role.pipe';
import { SeriesMetadataDetailComponent } from './series-metadata-detail/series-metadata-detail.component';
@ -48,7 +46,6 @@ import { ThemeTestComponent } from './theme-test/theme-test.component';
LibraryComponent,
LibraryDetailComponent,
SeriesDetailComponent,
NotConnectedComponent, // Move into ExtrasModule
ReviewSeriesModalComponent,
RecentlyAddedComponent,
OnDeckComponent,
@ -85,6 +82,8 @@ import { ThemeTestComponent } from './theme-test/theme-test.component';
ReadingListModule,
RegistrationModule,
NgbAccordionModule, // ThemeTest Component only
ToastrModule.forRoot({
positionClass: 'toast-bottom-right',
@ -99,7 +98,7 @@ import { ThemeTestComponent } from './theme-test/theme-test.component';
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
Title,
{provide: SAVER, useFactory: getSaver},
{ provide: APP_BASE_HREF, useFactory: (config: ConfigData) => config.baseUrl, deps: [ConfigData] },
// { provide: APP_BASE_HREF, useFactory: (config: ConfigData) => config.baseUrl, deps: [ConfigData] },
],
entryComponents: [],
bootstrap: [AppComponent]

View File

@ -28,7 +28,7 @@
</div>
<div class="modal-footer" style="justify-content: normal">
<div style="width: 100%;">
<div class="form-row">
<div class="d-flex">
<div class="col-9 col-lg-10">
<label class="visually-hidden" class="form-label" for="add-rlist">Collection</label>
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">

View File

@ -386,7 +386,7 @@
<ng-template ngbPaginationPages let-page let-pages="pages" *ngIf="pagination.totalItems / pagination.itemsPerPage > 20">
<li class="ngb-custom-pages-item" *ngIf="pagination.totalPages > 1">
<div class="form-group d-flex flex-nowrap px-2">
<div class="d-flex flex-nowrap px-2">
<label
id="paginationInputLabel-{{id}}"
for="paginationInput-{{id}}"

View File

@ -2,13 +2,6 @@ form {
max-height: 38px;
}
// input {
// width: 15px;
// opacity: 1;
// position: relative;
// left: 4px;
// border: none;
// }
.search-result img {
width: 100% !important;
@ -114,7 +107,7 @@ form {
overflow-y: auto;
display: flex;
flex-wrap: wrap;
margin-top: 10px;
margin-top: 9px;
}
.list-group {

View File

@ -72,16 +72,6 @@
margin-top: 5px;
}
.form-inline .form-group {
width: 100%;
}
@media (min-width: var(--grid-breakpoints-sm)) {
.form-inline .form-group {
width: 100%;
}
}
.scroll-to-top:hover {
animation: MoveUpDown 1s linear infinite;
}

View File

@ -1,3 +0,0 @@
<div class="text-center">
<h4 style="margin-top: 100px;">Oops! Looks like the server is not online.</h4>
</div>

View File

@ -1,27 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { MemberService } from '../_services/member.service';
@Component({
selector: 'app-not-connected',
templateUrl: './not-connected.component.html',
styleUrls: ['./not-connected.component.scss']
})
export class NotConnectedComponent implements OnInit {
constructor(private memberService: MemberService, private router: Router) { }
ngOnInit(): void {
// BUG: TODO: This causes an infinite reload loop on the UI when the API on backend doesn't exist
// We make a call to backend on refresh so that if it's up, we can redirect to /home
this.memberService.adminExists().subscribe((exists) => {
const pageResume = localStorage.getItem('kavita--no-connection-url');
if (pageResume && pageResume !== '/no-connection') {
localStorage.setItem('kavita--no-connection-url', '');
this.router.navigateByUrl(pageResume);
} else {
this.router.navigateByUrl('/home');
}
});
}
}

View File

@ -28,12 +28,12 @@
</div>
<div class="modal-footer" style="justify-content: normal">
<div style="width: 100%;">
<div class="form-row">
<div class="d-flex">
<div class="col-9 col-lg-10">
<label class="form-label visually-hidden" for="add-rlist">Reading List</label>
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
</div>
<div class="col-2">
<div class="col-2 ps-2">
<button type="submit" class="btn btn-primary" (click)="create()">Create</button>
</div>
</div>

View File

@ -52,7 +52,7 @@
<app-read-more class="user-review {{userReview ? 'mt-1' : ''}}" [text]="series?.userReview || ''" [maxLength]="250"></app-read-more>
</div>
<div *ngIf="seriesMetadata" class="mt-2">
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [series]="series"></app-series-metadata-detail>
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [readingLists]="readingLists" [series]="series"></app-series-metadata-detail>
</div>
@ -63,7 +63,7 @@
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs mb-2" [destroyOnHide]="false" (navChange)="onNavChange($event)">
<li [ngbNavItem]="TabID.Specials" *ngIf="hasSpecials">
<a ngbNavLink>{{libraryType === LibraryType.Book ? 'Books': 'Specials'}}</a>
<a ngbNavLink>Specials</a>
<ng-template ngbNavContent>
<div class="row g-0">
<ng-container *ngFor="let chapter of specials; let idx = index; trackBy: trackByChapterIdentity">
@ -74,7 +74,7 @@
</div>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Storyline" *ngIf="libraryType !== LibraryType.Book && (hasNonSpecialVolumeChapters || hasNonSpecialNonVolumeChapters)">
<li [ngbNavItem]="TabID.Storyline" *ngIf="libraryType !== LibraryType.Book && (volumes.length > 0 || chapters.length > 0)">
<a ngbNavLink>Storyline</a>
<ng-template ngbNavContent>
<div class="row g-0">
@ -91,7 +91,7 @@
</div>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Volumes" *ngIf="hasNonSpecialVolumeChapters">
<li [ngbNavItem]="TabID.Volumes" *ngIf="volumes.length > 0">
<a ngbNavLink>{{libraryType === LibraryType.Book ? 'Books': 'Volumes'}}</a>
<ng-template ngbNavContent>
<div class="row g-0">
@ -103,7 +103,7 @@
</div>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Chapters" *ngIf="hasNonSpecialNonVolumeChapters">
<li [ngbNavItem]="TabID.Chapters" *ngIf="chapters.length > 0">
<a ngbNavLink>{{utilityService.formatChapterName(libraryType) + 's'}}</a>
<ng-template ngbNavContent>
<div class="row g-0">

View File

@ -19,6 +19,7 @@ import { ScanSeriesEvent } from '../_models/events/scan-series-event';
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
import { LibraryType } from '../_models/library';
import { MangaFormat } from '../_models/manga-format';
import { ReadingList } from '../_models/reading-list';
import { Series } from '../_models/series';
import { SeriesMetadata } from '../_models/series-metadata';
import { Volume } from '../_models/volume';
@ -29,6 +30,7 @@ import { ImageService } from '../_services/image.service';
import { LibraryService } from '../_services/library.service';
import { EVENTS, MessageHubService } from '../_services/message-hub.service';
import { ReaderService } from '../_services/reader.service';
import { ReadingListService } from '../_services/reading-list.service';
import { SeriesService } from '../_services/series.service';
@ -72,12 +74,11 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
hasSpecials = false;
specials: Array<Chapter> = [];
activeTabId = TabID.Storyline;
hasNonSpecialVolumeChapters = false;
hasNonSpecialNonVolumeChapters = false;
userReview: string = '';
libraryType: LibraryType = LibraryType.Manga;
seriesMetadata: SeriesMetadata | null = null;
readingLists: Array<ReadingList> = [];
/**
* Poster image for the Series
*/
@ -171,6 +172,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
private confirmService: ConfirmService, private titleService: Title,
private downloadService: DownloadService, private actionService: ActionService,
public imageSerivce: ImageService, private messageHub: MessageHubService,
private readingListService: ReadingListService
) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
@ -332,6 +334,9 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
this.coverImageOffset = 0;
this.seriesService.getMetadata(seriesId).subscribe(metadata => this.seriesMetadata = metadata);
this.readingListService.getReadingListsForSeries(seriesId).subscribe(lists => {
this.readingLists = lists;
});
this.setContinuePoint();
forkJoin([
@ -352,7 +357,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this));
this.seriesService.getSeriesDetail(this.seriesId).subscribe(detail => {
this.hasSpecials = detail.specials.length > 0
this.hasSpecials = detail.specials.length > 0;
this.specials = detail.specials;
this.chapters = detail.chapters;
@ -373,30 +378,23 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
* This assumes loadPage() has already primed all the calculations and state variables. Do not call directly.
*/
updateSelectedTab() {
// This shows Chapters/Issues tab
// If this has chapters that are not specials
if (this.chapters.filter(c => !c.isSpecial).length > 0) {
this.hasNonSpecialNonVolumeChapters = true;
// Book libraries only have Volumes or Specials enabled
if (this.libraryType === LibraryType.Book) {
if (this.volumes.length === 0) {
this.activeTabId = TabID.Specials;
} else {
this.activeTabId = TabID.Volumes;
}
return;
}
// This shows Volumes tab
if (this.volumes.filter(v => v.number !== 0).length !== 0) {
this.hasNonSpecialVolumeChapters = true;
}
// If an update occured and we were on specials, re-activate Volumes/Chapters
if (!this.hasSpecials && !this.hasNonSpecialVolumeChapters && this.activeTabId != TabID.Storyline) {
this.activeTabId = TabID.Storyline;
}
if (this.libraryType == LibraryType.Book && !this.hasSpecials){
this.activeTabId = TabID.Volumes;
} else if (this.hasNonSpecialVolumeChapters || this.hasNonSpecialNonVolumeChapters) {
this.activeTabId = TabID.Storyline;
} else {
if (this.volumes.length === 0 && this.chapters.length === 0 && this.specials.length > 0) {
this.activeTabId = TabID.Specials;
} else {
this.activeTabId = TabID.Storyline;
}
}
createHTML() {

View File

@ -14,6 +14,9 @@
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo('format', series.format)" [selectionMode]="TagBadgeCursor.Clickable">
<app-series-format [format]="series.format">{{utilityService.mangaFormat(series.format)}}</app-series-format>
</app-tag-badge>
<app-tag-badge title="Last Read" class="col-auto" *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'" [selectionMode]="TagBadgeCursor.Selectable">
Last Read: {{series.latestReadDate | date:'shortDate'}}
</app-tag-badge>
</ng-container>
</div>
@ -43,6 +46,25 @@
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="readingLists && readingLists.length > 0">
<div class="col-md-4">
<h5>Reading Lists</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="readingLists">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-tag-badge a11y-click="13,32" class="col-auto" routerLink="/lists/{{item.id}}" [selectionMode]="TagBadgeCursor.Clickable">
<!-- TODO: Build a promoted badge code -->
<span *ngIf="item.promoted">
<i class="fa fa-angle-double-up" aria-hidden="true"></i>&nbsp;
<span class="visually-hidden">(promoted)</span>
</span>
{{item.title}}
</app-tag-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="seriesMetadata.writers && seriesMetadata.writers.length > 0">
<div class="col-md-4">
<h5>Writers/Authors</h5>

View File

@ -3,6 +3,7 @@ import { Router } from '@angular/router';
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
import { UtilityService } from '../shared/_services/utility.service';
import { MangaFormat } from '../_models/manga-format';
import { ReadingList } from '../_models/reading-list';
import { Series } from '../_models/series';
import { SeriesMetadata } from '../_models/series-metadata';
import { MetadataService } from '../_services/metadata.service';
@ -15,6 +16,10 @@ import { MetadataService } from '../_services/metadata.service';
export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
@Input() seriesMetadata!: SeriesMetadata;
/**
* Reading lists with a connection to the Series
*/
@Input() readingLists: Array<ReadingList> = [];
@Input() series!: Series;
isCollapsed: boolean = true;

View File

@ -1,7 +1,7 @@
<div class="badge-expander">
<div class="content">
<ng-container *ngFor="let item of visibleItems; index as i;" [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
<button type="button" *ngIf="!isCollapsed && itemsLeft !== 0" class="btn btn-outline-primary" (click)="toggleVisible()" [attr.aria-expanded]="!isCollapsed">
<button type="button" *ngIf="!isCollapsed && itemsLeft !== 0" class="btn btn-sm btn-outline-primary" (click)="toggleVisible()" [attr.aria-expanded]="!isCollapsed">
and {{itemsLeft}} more
</button>
</div>

View File

@ -5,8 +5,4 @@
.collapsed {
height: 35px;
overflow: hidden;
}
.badge-expander {
//display: inline-block;
}

View File

@ -8,6 +8,7 @@ import { Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/co
export class BadgeExpanderComponent implements OnInit {
@Input() items: Array<any> = [];
@Input() itemsTillExpander: number = 4;
@ContentChild('badgeExpanderItem') itemTemplate!: TemplateRef<any>;
@ -15,12 +16,12 @@ export class BadgeExpanderComponent implements OnInit {
isCollapsed: boolean = false;
get itemsLeft() {
return Math.max(this.items.length - 4, 0);
return Math.max(this.items.length - this.itemsTillExpander, 0);
}
constructor() { }
ngOnInit(): void {
this.visibleItems = this.items.slice(0, 4);
this.visibleItems = this.items.slice(0, this.itemsTillExpander);
}
toggleVisible() {

View File

@ -1,4 +1,4 @@
<div class="tagbadge cursor clickable">
<div class="tagbadge cursor clickable" *ngIf="person !== undefined">
<div class="d-flex">
<!-- <img src="..." class="align-self-center me-3" alt="..."> -->
<i class="fa fa-user-circle align-self-center me-2" aria-hidden="true"></i>

View File

@ -39,7 +39,7 @@
<div class="mb-3">
<label for="stat-collection" class="form-label" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
<div class="form-check">
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input" formControlName="allowStatCollection">
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input">
<label for="stat-collection" class="form-check-label">Normal Checkbox</label>
</div>
</div>
@ -47,7 +47,7 @@
<div class="mb-3">
<label for="stat-collection" class="form-label" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
<div class="form-check">
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input" formControlName="allowStatCollection" [disabled]="true">
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input" [disabled]="true">
<label for="stat-collection" class="form-check-label">Disabled Checkbox</label>
</div>
</div>
@ -56,20 +56,20 @@
<p>Labels should have form-check-label on them and inputs form-check-input</p>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" id="site-dark-mode" formControlName="siteDarkMode" [value]="true" aria-labelledby="site-dark-mode-label">
<input class="form-check-input" type="radio" id="site-dark-mode" [value]="true" aria-labelledby="site-dark-mode-label">
<label class="form-check-label" for="site-dark-mode">True</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="site-not-dark-mode" formControlName="siteDarkMode" [value]="false" aria-labelledby="site-dark-mode-label">
<label class="form-check-label" for="site-not-dark-mode">False</label>
<input class="form-check-input" type="radio" id="site-not-dark-mode2" [value]="false" aria-labelledby="site-dark-mode-label">
<label class="form-check-label" for="site-not-dark-mode2">False</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="site-not-dark-mode" formControlName="siteDarkMode" [disabled]="true" [value]="false" aria-labelledby="site-dark-mode-label">
<label class="form-check-label" for="site-not-dark-mode">Disabled</label>
<input class="form-check-input" type="radio" id="site-not-dark-mode3" [disabled]="true" [value]="false" aria-labelledby="site-dark-mode-label">
<label class="form-check-label" for="site-not-dark-mode3">Disabled</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="site-not-dark-mode" formControlName="siteDarkMode" readonly [value]="false" aria-labelledby="site-dark-mode-label">
<label class="form-check-label" for="site-not-dark-mode">Readonly</label>
<input class="form-check-input" type="radio" id="site-not-dark-mode4" readonly [value]="false" aria-labelledby="site-dark-mode-label">
<label class="form-check-label" for="site-not-dark-mode4">Readonly</label>
</div>
</div>
@ -116,11 +116,28 @@
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed">Non Allowed</app-tag-badge>
</div>
<h2>Badge</h2>
TODO
<h2>Person Badge with Expander</h2>
<div class="g-2">
<app-person-badge></app-person-badge>
<app-badge-expander [items]="people" [itemsTillExpander]="1">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
<h2>Switch</h2>
TODO: New component, will be used in place of 2 way radios
<form>
<div class="mb-3">
<label id="auto-close-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" id="auto-close" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
</div>
</div>
</div>
</form>
<h2>Dropdown/List Group</h2>
<div class="dropdown" >
@ -130,5 +147,42 @@ TODO: New component, will be used in place of 2 way radios
</ul>
</div>
<h2></h2>
<h2>Accordion</h2>
<ngb-accordion [closeOthers]="true" activeIds="reading-panel" #acc="ngbAccordion">
<ngb-panel id="reading-panel" title="Reading">
<ng-template ngbPanelHeader>
<h2 class="accordion-header">
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded('reading-panel')" aria-controls="collapseOne">
Reading
</button>
</h2>
</ng-template>
<ng-template ngbPanelContent>
<p>This is the body of the accordion...........This is the body of the accordion asdfasdf asThis is the body of the accordion asdfasdf asThis is the body of the accordion asdfasdf asThis is the body of the accordion asdfasdf asThis is the body of the accordion asdfasdf as</p>
</ng-template>
</ngb-panel>
<ngb-panel id="reading-panel2">
<ng-template ngbPanelHeader>
<h2 class="accordion-header">
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded('reading-panel')" aria-controls="collapseOne">
Header 2
</button>
</h2>
</ng-template>
<ng-template ngbPanelContent>
<p>This is the body of the accordion asdfasdf as
dfas
f asdfasdfasdf asdfasdfaaff asdf
as fd
asfasf asdfasdfafd
</p>
</ng-template>
</ngb-panel>
</ngb-accordion>
<h2>Cards</h2>
<app-card-item [entity]="seriesNotRead"></app-card-item>
<app-card-item [entity]="seriesNotRead" [count]="10"></app-card-item>
<app-card-item [entity]="seriesWithProgress"></app-card-item>

View File

@ -2,6 +2,9 @@ import { Component, OnInit } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
import { ThemeService } from '../theme.service';
import { MangaFormat } from '../_models/manga-format';
import { Person, PersonRole } from '../_models/person';
import { Series } from '../_models/series';
import { NavService } from '../_services/nav.service';
@Component({
@ -20,6 +23,47 @@ export class ThemeTestComponent implements OnInit {
];
active = this.tabs[0];
people: Array<Person> = [
{id: 1, name: 'Joe', role: PersonRole.Artist},
{id: 2, name: 'Joe 2', role: PersonRole.Artist},
];
seriesNotRead: Series = {
id: 1,
name: 'Test Series',
pages: 0,
pagesRead: 10,
format: MangaFormat.ARCHIVE,
libraryId: 1,
coverImageLocked: false,
created: '',
latestReadDate: '',
localizedName: '',
originalName: '',
sortName: '',
userRating: 0,
userReview: '',
volumes: []
}
seriesWithProgress: Series = {
id: 1,
name: 'Test Series',
pages: 5,
pagesRead: 10,
format: MangaFormat.ARCHIVE,
libraryId: 1,
coverImageLocked: false,
created: '',
latestReadDate: '',
localizedName: '',
originalName: '',
sortName: '',
userRating: 0,
userReview: '',
volumes: []
}
get TagBadgeCursor(): typeof TagBadgeCursor {
return TagBadgeCursor;
}

View File

@ -86,7 +86,7 @@ export class UserLoginComponent implements OnInit {
// Check if user came here from another url, else send to library route
const pageResume = localStorage.getItem('kavita--auth-intersection-url');
if (pageResume && pageResume !== '/no-connection' && pageResume !== '/login') {
if (pageResume && pageResume !== '/login') {
localStorage.setItem('kavita--auth-intersection-url', '');
this.router.navigateByUrl(pageResume);
} else {

View File

@ -59,7 +59,7 @@
<h3 id="manga-header">Image Reader</h3>
<div class="row g-0">
<div class="form-group col-md-6 col-sm-12 pe-2 mb-2">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-reading-direction" class="form-label">Reading Direction</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #readingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
<span class="visually-hidden" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
@ -68,7 +68,7 @@
</select>
</div>
<div class="form-group col-md-6 col-sm-12 pe-2 mb-2">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-scaling-option" class="form-label">Scaling Options</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
<ng-template #taskBackupTooltip>How to scale the image to your screen.</ng-template>
<span class="visually-hidden" id="settings-scaling-option-help">How to scale the image to your screen.</span>
@ -79,7 +79,7 @@
</div>
<div class="row g-0">
<div class="form-group col-md-6 col-sm-12 pe-2 mb-2">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-pagesplit-option" class="form-label">Page Splitting</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
<ng-template #pageSplitOptionTooltip>How to split a full width image (ie both left and right images are combined)</ng-template>
<span class="visually-hidden" id="settings-pagesplit-option-help">How to split a full width image (ie both left and right images are combined)</span>
@ -87,7 +87,7 @@
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="form-group col-md-6 col-sm-12 pe-2 mb-2">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-readingmode-option" class="form-label">Reading Mode</label>
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
<option *ngFor="let opt of readingModes" [value]="opt.value">{{opt.text | titlecase}}</option>
@ -97,7 +97,6 @@
<div class="mb-3">
<!-- TODO: Turn this into a checkbox -->
<label id="auto-close-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
@ -109,7 +108,7 @@
<hr>
<h3>Book Reader</h3>
<div class="row g-0">
<div class="form-group col-md-6 col-sm-12 pe-2 mb-3">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label id="dark-mode-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
@ -119,7 +118,7 @@
</div>
</div>
<div class="form-group col-md-6 col-sm-12 pe-2 mb-3">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label id="taptopaginate-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
@ -133,7 +132,7 @@
</div>
<div class="row g-0">
<div class="form-group col-md-6 col-sm-12 pe-2 mb-3">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-reading-direction" class="form-label">Book Reading Direction</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #bookReadingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
<span class="visually-hidden" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
@ -143,7 +142,7 @@
</div>
<div class="form-group col-md-6 col-sm-12 pe-2 mb-3">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-fontfamily-option" class="form-label">Font Family</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
<ng-template #fontFamilyOptionTooltip>Font familty to load up. Default will load the book's default font</ng-template>
<span class="visually-hidden" id="settings-fontfamily-option-help">Font familty to load up. Default will load the book's default font</span>
@ -156,18 +155,18 @@
<div class="row g-0">
<div class="form-group col-md-4 col-sm-12 pe-2 mb-3">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="font-size" class="form-label">Font Size</label>
<div class="custom-slider"><ngx-slider [options]="bookReaderFontSizeOptions" formControlName="bookReaderFontSize" aria-labelledby="font-size"></ngx-slider></div>
</div>
<div class="form-group col-md-4 col-sm-12 pe-2 mb-3">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label class="form-label">Line Height</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
<ng-template #bookLineHeightOptionTooltip>How much spacing between the lines of the book</ng-template>
<span class="visually-hidden" id="settings-booklineheight-option-help">How much spacing between the lines of the book</span>
<div class="custom-slider"><ngx-slider [options]="bookReaderLineSpacingOptions" formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help"></ngx-slider></div>
</div>
<div class="form-group col-md-4 col-sm-12 pe-2 mb-3">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label class="form-label">Margin</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
<ng-template #bookReaderMarginOptionTooltip>How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</ng-template>
<span class="visually-hidden" id="settings-bookmargin-option-help">How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</span>

View File

@ -1,22 +1,20 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { ConfigData } from './app/_models/config-data';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
function fetchConfig(): Promise<ConfigData> {
return fetch(environment.apiUrl + 'settings/base-url')
.then(response => response.text())
.then(response => new ConfigData(response));
}
// function fetchConfig(): Promise<ConfigData> {
// return fetch(environment.apiUrl + 'settings/base-url')
// .then(response => response.text())
// .then(response => new ConfigData(response));
// }
fetchConfig().then(config => {
platformBrowserDynamic([ { provide: ConfigData, useValue: config } ])
.bootstrapModule(AppModule)
.catch(err => console.error(err));
});
// fetchConfig().then(config => {
// });
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@ -57,6 +57,10 @@ label, select, .clickable {
cursor: pointer;
}
.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label {
cursor: default;
}
html, body { height: 100%; color-scheme: var(--color-scheme); }
body {
margin: 0;

View File

@ -1,11 +1,11 @@
.tagbadge {
border-color: var(--tagbadge-border-color);
border: 1px solid var(--tagbadge-border-color);
color: var(--tagbadge-text-color);
background-color: var(--tagbadge-bg-color);
}
.typeahead-input .tagbadge {
border-color: var(--tagbadge-typeahead-border-color);
border: 1px solid var(--tagbadge-typeahead-border-color);
color: var(--tagbadge-typeahead-text-color);
background-color: var(--tagbadge-typeahead-bg-color);
}

View File

@ -94,7 +94,7 @@
--list-group-item-bg-color: #343a40;
--list-group-item-border-color: rgba(239, 239, 239, 0.125);
--list-group-hover-text-color: white;
--list-group-hover-bg-color: var(--accordion-body-bg-color);
--list-group-hover-bg-color: rgb(22, 27, 34);
--list-group-active-border-color: none;
/* Popover */