mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Feature/bookmark feedback (#508)
* ImageService had a stream reset before writting out to array. Added logging statment for updating series metadata. Removed ConcurencyCheck due to bad update issue for CollectionTag. * Added a new screen which lets you quickly see all your bookmarks for a given user. * Built user bookmark page in user settings. Moved user settings to it's own lazy loaded module. Removed unneded debouncing from downloader and just used throttleTime instead. * Removed a not-yet implemented tab from series modal * Fixed a bug in clear bookmarks and adjusted icons within anchors to have proper styling
This commit is contained in:
parent
623e555633
commit
68bb5ed5a8
@ -11,6 +11,7 @@ using API.Extensions;
|
|||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
@ -22,16 +23,18 @@ namespace API.Controllers
|
|||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly ILogger<ReaderController> _logger;
|
||||||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||||
private readonly NaturalSortComparer _naturalSortComparer = new NaturalSortComparer();
|
private readonly NaturalSortComparer _naturalSortComparer = new NaturalSortComparer();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ReaderController(IDirectoryService directoryService, ICacheService cacheService, IUnitOfWork unitOfWork)
|
public ReaderController(IDirectoryService directoryService, ICacheService cacheService, IUnitOfWork unitOfWork, ILogger<ReaderController> logger)
|
||||||
{
|
{
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -350,19 +353,31 @@ namespace API.Controllers
|
|||||||
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId));
|
return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a list of all bookmarked pages for a User
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("get-all-bookmarks")]
|
||||||
|
public async Task<ActionResult<IEnumerable<BookmarkDto>>> GetAllBookmarks()
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
if (user.Bookmarks == null) return Ok(Array.Empty<BookmarkDto>());
|
||||||
|
return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes all bookmarks for all chapters linked to a Series
|
/// Removes all bookmarks for all chapters linked to a Series
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost("remove-bookmarks")]
|
[HttpPost("remove-bookmarks")]
|
||||||
public async Task<ActionResult> RemoveBookmarks(int seriesId)
|
public async Task<ActionResult> RemoveBookmarks(RemoveBookmarkForSeriesDto dto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
if (user.Bookmarks == null) return Ok("Nothing to remove");
|
if (user.Bookmarks == null) return Ok("Nothing to remove");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId == seriesId).ToList();
|
user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId != dto.SeriesId).ToList();
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
@ -370,8 +385,9 @@ namespace API.Controllers
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(ex, "There was an exception when trying to clear bookmarks");
|
||||||
await _unitOfWork.RollbackAsync();
|
await _unitOfWork.RollbackAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ using API.Interfaces;
|
|||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
@ -154,8 +155,8 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
series.Name = updateSeries.Name.Trim();
|
series.Name = updateSeries.Name.Trim();
|
||||||
series.LocalizedName = updateSeries.LocalizedName.Trim();
|
series.LocalizedName = updateSeries.LocalizedName.Trim();
|
||||||
series.SortName = updateSeries.SortName.Trim();
|
series.SortName = updateSeries.SortName?.Trim();
|
||||||
series.Summary = updateSeries.Summary.Trim();
|
series.Summary = updateSeries.Summary?.Trim(); // BUG: There was an exceptionSystem.NullReferenceException: Object reference not set to an instance of an object.
|
||||||
|
|
||||||
var needsRefreshMetadata = false;
|
var needsRefreshMetadata = false;
|
||||||
if (!updateSeries.CoverImageLocked)
|
if (!updateSeries.CoverImageLocked)
|
||||||
@ -296,8 +297,9 @@ namespace API.Controllers
|
|||||||
return Ok("Successfully updated");
|
return Ok("Successfully updated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(ex, "There was an exception when updating metadata");
|
||||||
await _unitOfWork.RollbackAsync();
|
await _unitOfWork.RollbackAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,6 +329,19 @@ namespace API.Controllers
|
|||||||
return Ok(series);
|
return Ok(series);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches Series for a set of Ids. This will check User for permission access and filter out any Ids that don't exist or
|
||||||
|
/// the user does not have access to.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("series-by-ids")]
|
||||||
|
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
|
||||||
|
{
|
||||||
|
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
|
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, user.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
API/DTOs/RemoveBookmarkForSeriesDto.cs
Normal file
7
API/DTOs/RemoveBookmarkForSeriesDto.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
public class RemoveBookmarkForSeriesDto
|
||||||
|
{
|
||||||
|
public int SeriesId { get; init; }
|
||||||
|
}
|
||||||
|
}
|
9
API/DTOs/SeriesByIdsDto.cs
Normal file
9
API/DTOs/SeriesByIdsDto.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
public class SeriesByIdsDto
|
||||||
|
{
|
||||||
|
public int[] SeriesIds { get; init; }
|
||||||
|
}
|
||||||
|
}
|
@ -66,9 +66,9 @@ namespace API.Data
|
|||||||
return new CollectionTag()
|
return new CollectionTag()
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
NormalizedTitle = API.Parser.Parser.Normalize(title).ToUpper(),
|
NormalizedTitle = API.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
|
||||||
Title = title,
|
Title = title?.Trim(),
|
||||||
Summary = summary,
|
Summary = summary?.Trim(),
|
||||||
Promoted = promoted
|
Promoted = promoted
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -443,5 +443,21 @@ namespace API.Data
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<SeriesDto>> GetSeriesDtoForIdsAsync(IEnumerable<int> seriesIds, int userId)
|
||||||
|
{
|
||||||
|
var allowedLibraries = _context.Library
|
||||||
|
.Include(l => l.AppUsers)
|
||||||
|
.Where(library => library.AppUsers.Any(x => x.Id == userId))
|
||||||
|
.Select(l => l.Id);
|
||||||
|
|
||||||
|
return await _context.Series
|
||||||
|
.Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId))
|
||||||
|
.OrderBy(s => s.SortName)
|
||||||
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
|
.AsNoTracking()
|
||||||
|
.AsSplitQuery()
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,6 +106,17 @@ namespace API.Data
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId)
|
||||||
|
{
|
||||||
|
return await _context.AppUserBookmark
|
||||||
|
.Where(x => x.AppUserId == userId)
|
||||||
|
.OrderBy(x => x.Page)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
|
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
|
||||||
{
|
{
|
||||||
return await _context.Users
|
return await _context.Users
|
||||||
|
@ -9,7 +9,7 @@ namespace API.Entities
|
|||||||
/// Represents a user entered field that is used as a tagging and grouping mechanism
|
/// Represents a user entered field that is used as a tagging and grouping mechanism
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Index(nameof(Id), nameof(Promoted), IsUnique = true)]
|
[Index(nameof(Id), nameof(Promoted), IsUnique = true)]
|
||||||
public class CollectionTag : IHasConcurrencyToken
|
public class CollectionTag
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -42,12 +42,14 @@ namespace API.Entities
|
|||||||
|
|
||||||
public ICollection<SeriesMetadata> SeriesMetadatas { get; set; }
|
public ICollection<SeriesMetadata> SeriesMetadatas { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
/// <inheritdoc />
|
/// Not Used due to not using concurrency update
|
||||||
[ConcurrencyCheck]
|
/// </summary>
|
||||||
public uint RowVersion { get; private set; }
|
public uint RowVersion { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
|
/// Not Used due to not using concurrency update
|
||||||
|
/// </summary>
|
||||||
public void OnSavingChanges()
|
public void OnSavingChanges()
|
||||||
{
|
{
|
||||||
RowVersion++;
|
RowVersion++;
|
||||||
|
@ -63,5 +63,6 @@ namespace API.Interfaces
|
|||||||
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
||||||
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
||||||
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
|
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
|
||||||
|
Task<IEnumerable<SeriesDto>> GetSeriesDtoForIdsAsync(IEnumerable<int> seriesIds, int userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,5 +19,6 @@ namespace API.Interfaces
|
|||||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
|
||||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
|
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
|
||||||
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId);
|
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId);
|
||||||
|
Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@ namespace API.Services
|
|||||||
using var img = Image.NewFromFile(path);
|
using var img = Image.NewFromFile(path);
|
||||||
using var stream = new MemoryStream();
|
using var stream = new MemoryStream();
|
||||||
img.JpegsaveStream(stream);
|
img.JpegsaveStream(stream);
|
||||||
|
stream.Position = 0;
|
||||||
return stream.ToArray();
|
return stream.ToArray();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -28,6 +28,10 @@ export class ReaderService {
|
|||||||
return this.httpClient.post(this.baseUrl + 'reader/unbookmark', {seriesId, volumeId, chapterId, page});
|
return this.httpClient.post(this.baseUrl + 'reader/unbookmark', {seriesId, volumeId, chapterId, page});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllBookmarks() {
|
||||||
|
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/get-all-bookmarks');
|
||||||
|
}
|
||||||
|
|
||||||
getBookmarks(chapterId: number) {
|
getBookmarks(chapterId: number) {
|
||||||
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/get-bookmarks?chapterId=' + chapterId);
|
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/get-bookmarks?chapterId=' + chapterId);
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,10 @@ export class SeriesService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllSeriesByIds(seriesIds: Array<number>) {
|
||||||
|
return this.httpClient.post<Series[]>(this.baseUrl + 'series/series-by-ids', {seriesIds: seriesIds});
|
||||||
|
}
|
||||||
|
|
||||||
getSeries(seriesId: number) {
|
getSeries(seriesId: number) {
|
||||||
return this.httpClient.get<Series>(this.baseUrl + 'series/' + seriesId);
|
return this.httpClient.get<Series>(this.baseUrl + 'series/' + seriesId);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,6 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
@ -1,8 +1,7 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { asyncScheduler } from 'rxjs';
|
import { finalize, take, takeWhile } from 'rxjs/operators';
|
||||||
import { finalize, take, takeWhile, throttleTime } from 'rxjs/operators';
|
|
||||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||||
import { ServerService } from 'src/app/_services/server.service';
|
import { ServerService } from 'src/app/_services/server.service';
|
||||||
import { SettingsService } from '../settings.service';
|
import { SettingsService } from '../settings.service';
|
||||||
@ -92,7 +91,6 @@ export class ManageSystemComponent implements OnInit {
|
|||||||
downloadLogs() {
|
downloadLogs() {
|
||||||
this.downloadLogsInProgress = true;
|
this.downloadLogsInProgress = true;
|
||||||
this.downloadService.downloadLogs().pipe(
|
this.downloadService.downloadLogs().pipe(
|
||||||
throttleTime(100, asyncScheduler, { leading: true, trailing: true }),
|
|
||||||
takeWhile(val => {
|
takeWhile(val => {
|
||||||
return val.state != 'DONE';
|
return val.state != 'DONE';
|
||||||
}),
|
}),
|
||||||
|
@ -7,7 +7,6 @@ import { NotConnectedComponent } from './not-connected/not-connected.component';
|
|||||||
import { SeriesDetailComponent } from './series-detail/series-detail.component';
|
import { SeriesDetailComponent } from './series-detail/series-detail.component';
|
||||||
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
||||||
import { UserLoginComponent } from './user-login/user-login.component';
|
import { UserLoginComponent } from './user-login/user-login.component';
|
||||||
import { UserPreferencesComponent } from './user-preferences/user-preferences.component';
|
|
||||||
import { AuthGuard } from './_guards/auth.guard';
|
import { AuthGuard } from './_guards/auth.guard';
|
||||||
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
||||||
import { InProgressComponent } from './in-progress/in-progress.component';
|
import { InProgressComponent } from './in-progress/in-progress.component';
|
||||||
@ -24,6 +23,10 @@ const routes: Routes = [
|
|||||||
path: 'collections',
|
path: 'collections',
|
||||||
loadChildren: () => import('./collections/collections.module').then(m => m.CollectionsModule)
|
loadChildren: () => import('./collections/collections.module').then(m => m.CollectionsModule)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'preferences',
|
||||||
|
loadChildren: () => import('./user-settings/user-settings.module').then(m => m.UserSettingsModule)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
runGuardsAndResolvers: 'always',
|
runGuardsAndResolvers: 'always',
|
||||||
@ -49,7 +52,6 @@ const routes: Routes = [
|
|||||||
{path: 'library', component: LibraryComponent},
|
{path: 'library', component: LibraryComponent},
|
||||||
{path: 'recently-added', component: RecentlyAddedComponent},
|
{path: 'recently-added', component: RecentlyAddedComponent},
|
||||||
{path: 'in-progress', component: InProgressComponent},
|
{path: 'in-progress', component: InProgressComponent},
|
||||||
{path: 'preferences', component: UserPreferencesComponent},
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{path: 'login', component: UserLoginComponent},
|
{path: 'login', component: UserLoginComponent},
|
||||||
|
@ -18,7 +18,6 @@ import { SharedModule } from './shared/shared.module';
|
|||||||
import { LibraryDetailComponent } from './library-detail/library-detail.component';
|
import { LibraryDetailComponent } from './library-detail/library-detail.component';
|
||||||
import { SeriesDetailComponent } from './series-detail/series-detail.component';
|
import { SeriesDetailComponent } from './series-detail/series-detail.component';
|
||||||
import { NotConnectedComponent } from './not-connected/not-connected.component';
|
import { NotConnectedComponent } from './not-connected/not-connected.component';
|
||||||
import { UserPreferencesComponent } from './user-preferences/user-preferences.component';
|
|
||||||
import { AutocompleteLibModule } from 'angular-ng-autocomplete';
|
import { AutocompleteLibModule } from 'angular-ng-autocomplete';
|
||||||
import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component';
|
import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component';
|
||||||
import { CarouselModule } from './carousel/carousel.module';
|
import { CarouselModule } from './carousel/carousel.module';
|
||||||
@ -37,6 +36,8 @@ import { RecentlyAddedComponent } from './recently-added/recently-added.componen
|
|||||||
import { InProgressComponent } from './in-progress/in-progress.component';
|
import { InProgressComponent } from './in-progress/in-progress.component';
|
||||||
import { CardsModule } from './cards/cards.module';
|
import { CardsModule } from './cards/cards.module';
|
||||||
import { CollectionsModule } from './collections/collections.module';
|
import { CollectionsModule } from './collections/collections.module';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SAVER, getSaver } from './shared/_providers/saver.provider';
|
||||||
|
|
||||||
let sentryProviders: any[] = [];
|
let sentryProviders: any[] = [];
|
||||||
|
|
||||||
@ -93,7 +94,6 @@ if (environment.production) {
|
|||||||
LibraryDetailComponent,
|
LibraryDetailComponent,
|
||||||
SeriesDetailComponent,
|
SeriesDetailComponent,
|
||||||
NotConnectedComponent, // Move into ExtrasModule
|
NotConnectedComponent, // Move into ExtrasModule
|
||||||
UserPreferencesComponent, // Move into SettingsModule
|
|
||||||
ReviewSeriesModalComponent,
|
ReviewSeriesModalComponent,
|
||||||
PersonBadgeComponent,
|
PersonBadgeComponent,
|
||||||
RecentlyAddedComponent,
|
RecentlyAddedComponent,
|
||||||
@ -109,11 +109,11 @@ if (environment.production) {
|
|||||||
|
|
||||||
NgbDropdownModule, // Nav
|
NgbDropdownModule, // Nav
|
||||||
AutocompleteLibModule, // Nav
|
AutocompleteLibModule, // Nav
|
||||||
NgbTooltipModule, // Shared & SettingsModule
|
//NgbTooltipModule, // Shared & SettingsModule
|
||||||
NgbRatingModule, // Series Detail
|
NgbRatingModule, // Series Detail
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
NgbAccordionModule, // User Preferences
|
//NgbAccordionModule, // User Preferences
|
||||||
NgxSliderModule, // User Preference
|
//NgxSliderModule, // User Preference
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
|
|
||||||
|
|
||||||
@ -135,6 +135,7 @@ if (environment.production) {
|
|||||||
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
||||||
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
||||||
Title,
|
Title,
|
||||||
|
{provide: SAVER, useFactory: getSaver},
|
||||||
...sentryProviders,
|
...sentryProviders,
|
||||||
],
|
],
|
||||||
entryComponents: [],
|
entryComponents: [],
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { asyncScheduler } from 'rxjs';
|
import { finalize, take, takeWhile } from 'rxjs/operators';
|
||||||
import { finalize, take, takeWhile, throttleTime } from 'rxjs/operators';
|
|
||||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||||
import { PageBookmark } from 'src/app/_models/page-bookmark';
|
import { PageBookmark } from 'src/app/_models/page-bookmark';
|
||||||
import { Series } from 'src/app/_models/series';
|
import { Series } from 'src/app/_models/series';
|
||||||
@ -55,7 +54,6 @@ export class BookmarksModalComponent implements OnInit {
|
|||||||
downloadBookmarks() {
|
downloadBookmarks() {
|
||||||
this.isDownloading = true;
|
this.isDownloading = true;
|
||||||
this.downloadService.downloadBookmarks(this.bookmarks).pipe(
|
this.downloadService.downloadBookmarks(this.bookmarks).pipe(
|
||||||
throttleTime(100, asyncScheduler, { leading: true, trailing: true }),
|
|
||||||
takeWhile(val => {
|
takeWhile(val => {
|
||||||
return val.state != 'DONE';
|
return val.state != 'DONE';
|
||||||
}),
|
}),
|
||||||
|
@ -82,12 +82,6 @@
|
|||||||
</li>
|
</li>
|
||||||
<li [ngbNavItem]="tabs[1]">
|
<li [ngbNavItem]="tabs[1]">
|
||||||
<a ngbNavLink>{{tabs[1]}}</a>
|
<a ngbNavLink>{{tabs[1]}}</a>
|
||||||
<ng-template ngbNavContent>
|
|
||||||
<p>Not Yet implemented</p>
|
|
||||||
</ng-template>
|
|
||||||
</li>
|
|
||||||
<li [ngbNavItem]="tabs[2]">
|
|
||||||
<a ngbNavLink>{{tabs[2]}}</a>
|
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<p class="alert alert-primary" role="alert">
|
<p class="alert alert-primary" role="alert">
|
||||||
Upload and choose a new cover image. Press Save to upload and override the cover.
|
Upload and choose a new cover image. Press Save to upload and override the cover.
|
||||||
@ -95,8 +89,8 @@
|
|||||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="series.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="series.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
<li [ngbNavItem]="tabs[3]">
|
<li [ngbNavItem]="tabs[2]">
|
||||||
<a ngbNavLink>{{tabs[3]}}</a>
|
<a ngbNavLink>{{tabs[2]}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<h4>Information</h4>
|
<h4>Information</h4>
|
||||||
<div class="row no-gutters mb-2">
|
<div class="row no-gutters mb-2">
|
||||||
|
@ -28,7 +28,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
isCollapsed = true;
|
isCollapsed = true;
|
||||||
volumeCollapsed: any = {};
|
volumeCollapsed: any = {};
|
||||||
tabs = ['General', 'Fix Match', 'Cover Image', 'Info'];
|
tabs = ['General', 'Cover Image', 'Info'];
|
||||||
active = this.tabs[0];
|
active = this.tabs[0];
|
||||||
editSeriesForm!: FormGroup;
|
editSeriesForm!: FormGroup;
|
||||||
libraryName: string | undefined = undefined;
|
libraryName: string | undefined = undefined;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { asyncScheduler, Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
import { finalize, take, takeUntil, takeWhile, throttleTime } from 'rxjs/operators';
|
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||||
import { Download } from 'src/app/shared/_models/download';
|
import { Download } from 'src/app/shared/_models/download';
|
||||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||||
@ -96,7 +96,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
if (!wantToDownload) { return; }
|
if (!wantToDownload) { return; }
|
||||||
this.downloadInProgress = true;
|
this.downloadInProgress = true;
|
||||||
this.download$ = this.downloadService.downloadVolume(volume).pipe(
|
this.download$ = this.downloadService.downloadVolume(volume).pipe(
|
||||||
throttleTime(100, asyncScheduler, { leading: true, trailing: true }),
|
|
||||||
takeWhile(val => {
|
takeWhile(val => {
|
||||||
return val.state != 'DONE';
|
return val.state != 'DONE';
|
||||||
}),
|
}),
|
||||||
@ -112,7 +111,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
if (!wantToDownload) { return; }
|
if (!wantToDownload) { return; }
|
||||||
this.downloadInProgress = true;
|
this.downloadInProgress = true;
|
||||||
this.download$ = this.downloadService.downloadChapter(chapter).pipe(
|
this.download$ = this.downloadService.downloadChapter(chapter).pipe(
|
||||||
throttleTime(100, asyncScheduler, { leading: true, trailing: true }),
|
|
||||||
takeWhile(val => {
|
takeWhile(val => {
|
||||||
return val.state != 'DONE';
|
return val.state != 'DONE';
|
||||||
}),
|
}),
|
||||||
@ -128,7 +126,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
if (!wantToDownload) { return; }
|
if (!wantToDownload) { return; }
|
||||||
this.downloadInProgress = true;
|
this.downloadInProgress = true;
|
||||||
this.download$ = this.downloadService.downloadSeries(series).pipe(
|
this.download$ = this.downloadService.downloadSeries(series).pipe(
|
||||||
throttleTime(100, asyncScheduler, { leading: true, trailing: true }),
|
|
||||||
takeWhile(val => {
|
takeWhile(val => {
|
||||||
return val.state != 'DONE';
|
return val.state != 'DONE';
|
||||||
}),
|
}),
|
||||||
|
@ -54,24 +54,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<!-- <div class="back-to-top">
|
|
||||||
<button class="btn btn-icon" (click)="navService?.toggleDarkMode()">
|
|
||||||
<i class="fa {{(navService?.darkMode$ | async) ? 'fa-moon' : 'fa-sun'}}" style="color: white" aria-hidden="true"></i>
|
|
||||||
<span class="sr-only">Dark mode Toggle. Current value {{(navService?.darkMode$ | async) ? 'On' : 'Off'}}</span>
|
|
||||||
</button>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="back-to-top">
|
<div class="back-to-top">
|
||||||
<button class="btn btn-icon scroll-to-top" (click)="scrollToTop()" *ngIf="backToTopNeeded">
|
<button class="btn btn-icon scroll-to-top" (click)="scrollToTop()" *ngIf="backToTopNeeded">
|
||||||
<i class="fa fa-angle-double-up" style="color: white" aria-hidden="true"></i>
|
<i class="fa fa-angle-double-up" style="color: white" aria-hidden="true"></i>
|
||||||
<span class="sr-only">Scroll to Top</span>
|
<span class="sr-only">Scroll to Top</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO: Put SignalR notification button dropdown here. -->
|
<!-- TODO: Put SignalR notification button dropdown here. -->
|
||||||
<div ngbDropdown class="nav-item dropdown" display="dynamic" placement="bottom-right" *ngIf="(accountService.currentUser$ | async) as user" dropdown>
|
<div ngbDropdown class="nav-item dropdown" display="dynamic" placement="bottom-right" *ngIf="(accountService.currentUser$ | async) as user" dropdown>
|
||||||
<button class="btn btn-outline-secondary primary-text" ngbDropdownToggle>{{user.username | titlecase}}</button>
|
<button class="btn btn-outline-secondary primary-text" ngbDropdownToggle>{{user.username | titlecase}}</button>
|
||||||
<div ngbDropdownMenu >
|
<div ngbDropdownMenu >
|
||||||
<button ngbDropdownItem routerLink="/preferences">User Settings</button>
|
<button ngbDropdownItem routerLink="/preferences/">User Settings</button>
|
||||||
<button ngbDropdownItem routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')">Server Settings</button>
|
<button ngbDropdownItem routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')">Server Settings</button>
|
||||||
<button ngbDropdownItem (click)="logout()">Logout</button>
|
<button ngbDropdownItem (click)="logout()">Logout</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,8 +3,7 @@ import { Title } from '@angular/platform-browser';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { asyncScheduler } from 'rxjs';
|
import { finalize, take, takeWhile } from 'rxjs/operators';
|
||||||
import { finalize, take, takeWhile, throttleTime } from 'rxjs/operators';
|
|
||||||
import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component';
|
import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component';
|
||||||
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
|
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
|
||||||
import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config';
|
import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config';
|
||||||
@ -456,7 +455,6 @@ export class SeriesDetailComponent implements OnInit {
|
|||||||
if (!wantToDownload) { return; }
|
if (!wantToDownload) { return; }
|
||||||
this.downloadInProgress = true;
|
this.downloadInProgress = true;
|
||||||
this.downloadService.downloadSeries(this.series).pipe(
|
this.downloadService.downloadSeries(this.series).pipe(
|
||||||
throttleTime(100, asyncScheduler, { leading: true, trailing: true }),
|
|
||||||
takeWhile(val => {
|
takeWhile(val => {
|
||||||
return val.state != 'DONE';
|
return val.state != 'DONE';
|
||||||
}),
|
}),
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { Series } from 'src/app/_models/series';
|
import { Series } from 'src/app/_models/series';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { ConfirmService } from '../confirm.service';
|
import { ConfirmService } from '../confirm.service';
|
||||||
import { saveAs } from 'file-saver';
|
|
||||||
import { Chapter } from 'src/app/_models/chapter';
|
import { Chapter } from 'src/app/_models/chapter';
|
||||||
import { Volume } from 'src/app/_models/volume';
|
import { Volume } from 'src/app/_models/volume';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { Observable } from 'rxjs';
|
import { asyncScheduler, Observable } from 'rxjs';
|
||||||
import { SAVER, Saver } from '../_providers/saver.provider';
|
import { SAVER, Saver } from '../_providers/saver.provider';
|
||||||
import { download, Download } from '../_models/download';
|
import { download, Download } from '../_models/download';
|
||||||
import { PageBookmark } from 'src/app/_models/page-bookmark';
|
import { PageBookmark } from 'src/app/_models/page-bookmark';
|
||||||
import { debounceTime } from 'rxjs/operators';
|
import { throttleTime } from 'rxjs/operators';
|
||||||
|
|
||||||
|
const DEBOUNCE_TIME = 100;
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -42,7 +43,7 @@ export class DownloadService {
|
|||||||
downloadLogs() {
|
downloadLogs() {
|
||||||
return this.httpClient.get(this.baseUrl + 'server/logs',
|
return this.httpClient.get(this.baseUrl + 'server/logs',
|
||||||
{observe: 'events', responseType: 'blob', reportProgress: true}
|
{observe: 'events', responseType: 'blob', reportProgress: true}
|
||||||
).pipe(debounceTime(300), download((blob, filename) => {
|
).pipe(throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }), download((blob, filename) => {
|
||||||
this.save(blob, filename)
|
this.save(blob, filename)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ export class DownloadService {
|
|||||||
downloadSeries(series: Series) {
|
downloadSeries(series: Series) {
|
||||||
return this.httpClient.get(this.baseUrl + 'download/series?seriesId=' + series.id,
|
return this.httpClient.get(this.baseUrl + 'download/series?seriesId=' + series.id,
|
||||||
{observe: 'events', responseType: 'blob', reportProgress: true}
|
{observe: 'events', responseType: 'blob', reportProgress: true}
|
||||||
).pipe(debounceTime(300), download((blob, filename) => {
|
).pipe(throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }), download((blob, filename) => {
|
||||||
this.save(blob, filename)
|
this.save(blob, filename)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -59,7 +60,7 @@ export class DownloadService {
|
|||||||
downloadChapter(chapter: Chapter) {
|
downloadChapter(chapter: Chapter) {
|
||||||
return this.httpClient.get(this.baseUrl + 'download/chapter?chapterId=' + chapter.id,
|
return this.httpClient.get(this.baseUrl + 'download/chapter?chapterId=' + chapter.id,
|
||||||
{observe: 'events', responseType: 'blob', reportProgress: true}
|
{observe: 'events', responseType: 'blob', reportProgress: true}
|
||||||
).pipe(debounceTime(300), download((blob, filename) => { //NOTE: DO I need debounceTime since I have throttleTime()?
|
).pipe(throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }), download((blob, filename) => { //NOTE: DO I need debounceTime since I have throttleTime()?
|
||||||
this.save(blob, filename)
|
this.save(blob, filename)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -67,7 +68,7 @@ export class DownloadService {
|
|||||||
downloadVolume(volume: Volume): Observable<Download> {
|
downloadVolume(volume: Volume): Observable<Download> {
|
||||||
return this.httpClient.get(this.baseUrl + 'download/volume?volumeId=' + volume.id,
|
return this.httpClient.get(this.baseUrl + 'download/volume?volumeId=' + volume.id,
|
||||||
{observe: 'events', responseType: 'blob', reportProgress: true}
|
{observe: 'events', responseType: 'blob', reportProgress: true}
|
||||||
).pipe(debounceTime(300), download((blob, filename) => {
|
).pipe(throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }), download((blob, filename) => {
|
||||||
this.save(blob, filename)
|
this.save(blob, filename)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -79,7 +80,7 @@ export class DownloadService {
|
|||||||
downloadBookmarks(bookmarks: PageBookmark[]) {
|
downloadBookmarks(bookmarks: PageBookmark[]) {
|
||||||
return this.httpClient.post(this.baseUrl + 'download/bookmarks', {bookmarks},
|
return this.httpClient.post(this.baseUrl + 'download/bookmarks', {bookmarks},
|
||||||
{observe: 'events', responseType: 'blob', reportProgress: true}
|
{observe: 'events', responseType: 'blob', reportProgress: true}
|
||||||
).pipe(debounceTime(300), download((blob, filename) => {
|
).pipe(throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }), download((blob, filename) => {
|
||||||
this.save(blob, filename)
|
this.save(blob, filename)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -35,11 +35,7 @@ import { NgCircleProgressModule } from 'ng-circle-progress';
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
//NgbDropdownModule,
|
|
||||||
//NgbProgressbarModule,
|
|
||||||
//NgbTooltipModule,
|
|
||||||
NgbCollapseModule,
|
NgbCollapseModule,
|
||||||
//LazyLoadImageModule,
|
|
||||||
NgCircleProgressModule.forRoot(),
|
NgCircleProgressModule.forRoot(),
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
@ -55,6 +51,6 @@ import { NgCircleProgressModule } from 'ng-circle-progress';
|
|||||||
TagBadgeComponent,
|
TagBadgeComponent,
|
||||||
CircularLoaderComponent,
|
CircularLoaderComponent,
|
||||||
],
|
],
|
||||||
providers: [{provide: SAVER, useFactory: getSaver}]
|
//providers: [{provide: SAVER, useFactory: getSaver}]
|
||||||
})
|
})
|
||||||
export class SharedModule { }
|
export class SharedModule { }
|
||||||
|
@ -1,192 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<h2>User Preferences</h2>
|
|
||||||
<p>
|
|
||||||
These are global settings that are bound to your account.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ngb-accordion [closeOthers]="true" activeIds="site-panel">
|
|
||||||
<ngb-panel id="site-panel" title="Site">
|
|
||||||
<ng-template ngbPanelContent>
|
|
||||||
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
|
||||||
<div class="form-group">
|
|
||||||
<label id="site-dark-mode-label" aria-describedby="site-heading">Dark Mode</label>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="custom-control custom-radio custom-control-inline">
|
|
||||||
<input type="radio" id="site-dark-mode" formControlName="siteDarkMode" class="custom-control-input" [value]="true" aria-labelledby="site-dark-mode-label">
|
|
||||||
<label class="custom-control-label" for="site-dark-mode">True</label>
|
|
||||||
</div>
|
|
||||||
<div class="custom-control custom-radio custom-control-inline">
|
|
||||||
<input type="radio" id="site-not-dark-mode" formControlName="siteDarkMode" class="custom-control-input" [value]="false" aria-labelledby="site-dark-mode-label">
|
|
||||||
<label class="custom-control-label" for="site-not-dark-mode">False</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="float-right mb-3">
|
|
||||||
<button type="button" class="btn btn-secondary mr-2" (click)="resetForm()">Reset</button>
|
|
||||||
<button type="submit" class="btn btn-primary" (click)="save()" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ng-template>
|
|
||||||
</ngb-panel>
|
|
||||||
<ngb-panel id="reading-panel" title="Reading">
|
|
||||||
<ng-template ngbPanelContent>
|
|
||||||
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
|
||||||
<h3 id="manga-header">Manga</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="settings-reading-direction">Reading Direction</label> <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="sr-only" 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>
|
|
||||||
<select class="form-control" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
|
|
||||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="settings-scaling-option">Scaling Options</label> <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="sr-only" id="settings-scaling-option-help">How to scale the image to your screen.</span>
|
|
||||||
<select class="form-control" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
|
|
||||||
<option *ngFor="let opt of scalingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="settings-pagesplit-option">Page Splitting</label> <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="sr-only" id="settings-pagesplit-option-help">How to split a full width image (ie both left and right images are combined)</span>
|
|
||||||
<select class="form-control" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
|
|
||||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="settings-readingmode-option">Reading Mode</label>
|
|
||||||
<select class="form-control" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
|
|
||||||
<option *ngFor="let opt of readingModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label id="auto-close-label">Auto Close Menu</label>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="custom-control custom-radio custom-control-inline">
|
|
||||||
<input type="radio" id="auto-close" formControlName="autoCloseMenu" class="custom-control-input" [value]="true" aria-labelledby="auto-close-label">
|
|
||||||
<label class="custom-control-label" for="auto-close">True</label>
|
|
||||||
</div>
|
|
||||||
<div class="custom-control custom-radio custom-control-inline">
|
|
||||||
<input type="radio" id="not-auto-close" formControlName="autoCloseMenu" class="custom-control-input" [value]="false" aria-labelledby="auto-close-label">
|
|
||||||
<label class="custom-control-label" for="not-auto-close">False</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<h3>Books</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<label id="dark-mode-label">Dark Mode</label>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="custom-control custom-radio custom-control-inline">
|
|
||||||
<input type="radio" id="dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="true" aria-labelledby="dark-mode-label">
|
|
||||||
<label class="custom-control-label" for="dark-mode">True</label>
|
|
||||||
</div>
|
|
||||||
<div class="custom-control custom-radio custom-control-inline">
|
|
||||||
<input type="radio" id="not-dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="false" aria-labelledby="dark-mode-label">
|
|
||||||
<label class="custom-control-label" for="not-dark-mode">False</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="settings-book-reading-direction">Book Reading Direction</label> <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="sr-only" 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>
|
|
||||||
<select class="form-control" aria-describedby="settings-reading-direction-help" formControlName="bookReaderReadingDirection">
|
|
||||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label id="taptopaginate-label">Tap to Paginate</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
|
|
||||||
<ng-template #tapToPaginateOptionTooltip>Should the sides of the book reader screen allow tapping on it to move to prev/next page</ng-template>
|
|
||||||
<span class="sr-only" id="settings-taptopaginate-option-help">Should the sides of the book reader screen allow tapping on it to move to prev/next page</span>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="custom-control custom-radio custom-control-inline">
|
|
||||||
<input type="radio" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="custom-control-input" [value]="true" aria-labelledby="taptopaginate-label">
|
|
||||||
<label class="custom-control-label" for="taptopaginate">True</label>
|
|
||||||
</div>
|
|
||||||
<div class="custom-control custom-radio custom-control-inline">
|
|
||||||
<input type="radio" id="not-taptopaginate" formControlName="bookReaderTapToPaginate" class="custom-control-input" [value]="false" aria-labelledby="taptopaginate-label">
|
|
||||||
<label class="custom-control-label" for="not-taptopaginate">False</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="settings-fontfamily-option">Font Family</label> <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="sr-only" id="settings-fontfamily-option-help">Font familty to load up. Default will load the book's default font</span>
|
|
||||||
<select class="form-control" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
|
|
||||||
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label id="font-size">Font Size</label>
|
|
||||||
<ngx-slider [options]="bookReaderFontSizeOptions" formControlName="bookReaderFontSize" aria-labelledby="font-size"></ngx-slider>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Line Height</label> <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="sr-only" id="settings-booklineheight-option-help">How much spacing between the lines of the book</span>
|
|
||||||
<ngx-slider [options]="bookReaderLineSpacingOptions" formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help"></ngx-slider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Margin</label> <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="sr-only" 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>
|
|
||||||
<ngx-slider [options]="bookReaderMarginOptions" formControlName="bookReaderMargin" aria-describedby="bookmargin"></ngx-slider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="float-right mb-3">
|
|
||||||
<button type="button" class="btn btn-secondary mr-2" (click)="resetForm()">Reset</button>
|
|
||||||
<button type="submit" class="btn btn-primary" (click)="save()" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ng-template>
|
|
||||||
</ngb-panel>
|
|
||||||
|
|
||||||
|
|
||||||
<ngb-panel id="password-panel" title="Password">
|
|
||||||
<ng-template ngbPanelContent>
|
|
||||||
<p>Change your Password</p>
|
|
||||||
<div class="alert alert-danger" role="alert" *ngIf="resetPasswordErrors.length > 0">
|
|
||||||
<div *ngFor="let error of resetPasswordErrors">{{error}}</div>
|
|
||||||
</div>
|
|
||||||
<form [formGroup]="passwordChangeForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="new-password">New Password</label>
|
|
||||||
<input class="form-control" type="password" id="new-password" formControlName="password" required>
|
|
||||||
<div id="password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
|
||||||
<div *ngIf="password?.errors?.required">
|
|
||||||
This field is required
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="confirm-password">Confirm Password</label>
|
|
||||||
<input class="form-control" type="password" id="confirm-password" formControlName="confirmPassword" aria-describedby="password-validations" required>
|
|
||||||
<div id="password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
|
||||||
<div *ngIf="!passwordsMatch">
|
|
||||||
Passwords must match
|
|
||||||
</div>
|
|
||||||
<div *ngIf="confirmPassword?.errors?.required">
|
|
||||||
This field is required
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="float-right mb-3">
|
|
||||||
<button type="button" class="btn btn-secondary mr-2" (click)="resetPasswordForm()">Reset</button>
|
|
||||||
<button type="submit" class="btn btn-primary" (click)="savePasswordForm()" [disabled]="!passwordChangeForm.valid || !(passwordChangeForm.dirty || passwordChangeForm.touched)">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ng-template>
|
|
||||||
</ngb-panel>
|
|
||||||
</ngb-accordion>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
@ -0,0 +1,42 @@
|
|||||||
|
<p *ngIf="series.length === 0 && !loadingBookmarks">
|
||||||
|
There are no bookmarks. Try creating <a href="https://wiki.kavitareader.com/guides/webreader/bookmarks" target="_blank">one <i class="fa fa-external-link-alt" aria-hidden="true"></i></a>.
|
||||||
|
</p>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li *ngFor="let series of series" class="list-group-item">
|
||||||
|
<div>
|
||||||
|
<h4>
|
||||||
|
<a href="/library/{{series.libraryId}}/series/{{series.id}}" >{{series.name | titlecase}}</a>
|
||||||
|
<span class="badge badge-secondary badge-pill">{{getBookmarkPages(series.id)}}</span>
|
||||||
|
<div class="float-right">
|
||||||
|
<button class="btn btn-danger mr-2 btn-sm" (click)="clearBookmarks(series)" [disabled]="clearingSeries[series.id]" placement="top" ngbTooltip="Clear Bookmarks" attr.aria-label="Clear Bookmarks">
|
||||||
|
<ng-container *ngIf="clearingSeries[series.id]; else notClearing">
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #notClearing>
|
||||||
|
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||||
|
</ng-template>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary mr-2 btn-sm" (click)="downloadBookmarks(series)" [disabled]="downloadingSeries[series.id]" placement="top" ngbTooltip="Download Bookmarks" attr.aria-label="Download Bookmarks">
|
||||||
|
<ng-container *ngIf="downloadingSeries[series.id]; else notDownloading">
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
<span class="sr-only">Downloading...</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #notDownloading>
|
||||||
|
<i class="fa fa-arrow-alt-circle-down" aria-hidden="true"></i>
|
||||||
|
</ng-template>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary mr-2 btn-sm" (click)="viewBookmarks(series)" placement="top" ngbTooltip="View Bookmarks" attr.aria-label="View Bookmarks">
|
||||||
|
<i class="fa fa-pen" title="View Bookmarks"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li *ngIf="loadingBookmarks" class="list-group-item">
|
||||||
|
<div class="spinner-border text-secondary" role="status">
|
||||||
|
<span class="invisible">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
@ -0,0 +1,96 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
import { take, takeWhile, finalize } from 'rxjs/operators';
|
||||||
|
import { BookmarksModalComponent } from 'src/app/cards/_modals/bookmarks-modal/bookmarks-modal.component';
|
||||||
|
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||||
|
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||||
|
import { PageBookmark } from 'src/app/_models/page-bookmark';
|
||||||
|
import { Series } from 'src/app/_models/series';
|
||||||
|
import { ReaderService } from 'src/app/_services/reader.service';
|
||||||
|
import { SeriesService } from 'src/app/_services/series.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-series-bookmarks',
|
||||||
|
templateUrl: './series-bookmarks.component.html',
|
||||||
|
styleUrls: ['./series-bookmarks.component.scss']
|
||||||
|
})
|
||||||
|
export class SeriesBookmarksComponent implements OnInit {
|
||||||
|
|
||||||
|
bookmarks: Array<PageBookmark> = [];
|
||||||
|
series: Array<Series> = [];
|
||||||
|
loadingBookmarks: boolean = false;
|
||||||
|
seriesIds: {[id: number]: number} = {};
|
||||||
|
downloadingSeries: {[id: number]: boolean} = {};
|
||||||
|
clearingSeries: {[id: number]: boolean} = {};
|
||||||
|
|
||||||
|
constructor(private readerService: ReaderService, private seriesService: SeriesService,
|
||||||
|
private modalService: NgbModal, private downloadService: DownloadService, private toastr: ToastrService,
|
||||||
|
private confirmService: ConfirmService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadBookmarks();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBookmarks() {
|
||||||
|
this.loadingBookmarks = true;
|
||||||
|
this.readerService.getAllBookmarks().pipe(take(1)).subscribe(bookmarks => {
|
||||||
|
this.bookmarks = bookmarks;
|
||||||
|
this.seriesIds = {};
|
||||||
|
this.bookmarks.forEach(bmk => {
|
||||||
|
if (!this.seriesIds.hasOwnProperty(bmk.seriesId)) {
|
||||||
|
this.seriesIds[bmk.seriesId] = 1;
|
||||||
|
} else {
|
||||||
|
this.seriesIds[bmk.seriesId] += 1;
|
||||||
|
}
|
||||||
|
this.downloadingSeries[bmk.seriesId] = false;
|
||||||
|
this.clearingSeries[bmk.seriesId] = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ids = Object.keys(this.seriesIds).map(k => parseInt(k, 10));
|
||||||
|
this.seriesService.getAllSeriesByIds(ids).subscribe(series => {
|
||||||
|
this.series = series;
|
||||||
|
this.loadingBookmarks = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
viewBookmarks(series: Series) {
|
||||||
|
const bookmarkModalRef = this.modalService.open(BookmarksModalComponent, { scrollable: true, size: 'lg' });
|
||||||
|
bookmarkModalRef.componentInstance.series = series;
|
||||||
|
bookmarkModalRef.closed.pipe(take(1)).subscribe(() => {
|
||||||
|
this.loadBookmarks();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearBookmarks(series: Series) {
|
||||||
|
if (!await this.confirmService.confirm('Are you sure you want to clear all bookmarks for ' + series.name + '? This cannot be undone.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearingSeries[series.id] = true;
|
||||||
|
this.readerService.clearBookmarks(series.id).subscribe(() => {
|
||||||
|
const index = this.series.indexOf(series);
|
||||||
|
if (index > -1) {
|
||||||
|
this.series.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.clearingSeries[series.id] = false;
|
||||||
|
this.toastr.success(series.name + '\'s bookmarks have been removed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getBookmarkPages(seriesId: number) {
|
||||||
|
return this.seriesIds[seriesId];
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadBookmarks(series: Series) {
|
||||||
|
this.downloadingSeries[series.id] = true;
|
||||||
|
this.downloadService.downloadBookmarks(this.bookmarks.filter(bmk => bmk.seriesId === series.id)).pipe(
|
||||||
|
takeWhile(val => {
|
||||||
|
return val.state != 'DONE';
|
||||||
|
}),
|
||||||
|
finalize(() => {
|
||||||
|
this.downloadingSeries[series.id] = false;
|
||||||
|
})).subscribe(() => {/* No Operation */});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,203 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h2>User Dashboard</h2>
|
||||||
|
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs">
|
||||||
|
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||||
|
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | titlecase }}</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<ng-container *ngIf="tab.fragment === ''">
|
||||||
|
<p>
|
||||||
|
These are global settings that are bound to your account.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ngb-accordion [closeOthers]="true" activeIds="site-panel">
|
||||||
|
<ngb-panel id="site-panel" title="Site">
|
||||||
|
<ng-template ngbPanelContent>
|
||||||
|
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
||||||
|
<div class="form-group">
|
||||||
|
<label id="site-dark-mode-label" aria-describedby="site-heading">Dark Mode</label>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-radio custom-control-inline">
|
||||||
|
<input type="radio" id="site-dark-mode" formControlName="siteDarkMode" class="custom-control-input" [value]="true" aria-labelledby="site-dark-mode-label">
|
||||||
|
<label class="custom-control-label" for="site-dark-mode">True</label>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-radio custom-control-inline">
|
||||||
|
<input type="radio" id="site-not-dark-mode" formControlName="siteDarkMode" class="custom-control-input" [value]="false" aria-labelledby="site-dark-mode-label">
|
||||||
|
<label class="custom-control-label" for="site-not-dark-mode">False</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="float-right mb-3">
|
||||||
|
<button type="button" class="btn btn-secondary mr-2" (click)="resetForm()">Reset</button>
|
||||||
|
<button type="submit" class="btn btn-primary" (click)="save()" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-panel>
|
||||||
|
<ngb-panel id="reading-panel" title="Reading">
|
||||||
|
<ng-template ngbPanelContent>
|
||||||
|
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
||||||
|
<h3 id="manga-header">Manga</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="settings-reading-direction">Reading Direction</label> <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="sr-only" 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>
|
||||||
|
<select class="form-control" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
|
||||||
|
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="settings-scaling-option">Scaling Options</label> <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="sr-only" id="settings-scaling-option-help">How to scale the image to your screen.</span>
|
||||||
|
<select class="form-control" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
|
||||||
|
<option *ngFor="let opt of scalingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="settings-pagesplit-option">Page Splitting</label> <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="sr-only" id="settings-pagesplit-option-help">How to split a full width image (ie both left and right images are combined)</span>
|
||||||
|
<select class="form-control" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
|
||||||
|
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="settings-readingmode-option">Reading Mode</label>
|
||||||
|
<select class="form-control" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
|
||||||
|
<option *ngFor="let opt of readingModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label id="auto-close-label">Auto Close Menu</label>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-radio custom-control-inline">
|
||||||
|
<input type="radio" id="auto-close" formControlName="autoCloseMenu" class="custom-control-input" [value]="true" aria-labelledby="auto-close-label">
|
||||||
|
<label class="custom-control-label" for="auto-close">True</label>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-radio custom-control-inline">
|
||||||
|
<input type="radio" id="not-auto-close" formControlName="autoCloseMenu" class="custom-control-input" [value]="false" aria-labelledby="auto-close-label">
|
||||||
|
<label class="custom-control-label" for="not-auto-close">False</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<h3>Books</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label id="dark-mode-label">Dark Mode</label>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-radio custom-control-inline">
|
||||||
|
<input type="radio" id="dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="true" aria-labelledby="dark-mode-label">
|
||||||
|
<label class="custom-control-label" for="dark-mode">True</label>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-radio custom-control-inline">
|
||||||
|
<input type="radio" id="not-dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="false" aria-labelledby="dark-mode-label">
|
||||||
|
<label class="custom-control-label" for="not-dark-mode">False</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="settings-book-reading-direction">Book Reading Direction</label> <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="sr-only" 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>
|
||||||
|
<select class="form-control" aria-describedby="settings-reading-direction-help" formControlName="bookReaderReadingDirection">
|
||||||
|
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label id="taptopaginate-label">Tap to Paginate</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #tapToPaginateOptionTooltip>Should the sides of the book reader screen allow tapping on it to move to prev/next page</ng-template>
|
||||||
|
<span class="sr-only" id="settings-taptopaginate-option-help">Should the sides of the book reader screen allow tapping on it to move to prev/next page</span>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-radio custom-control-inline">
|
||||||
|
<input type="radio" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="custom-control-input" [value]="true" aria-labelledby="taptopaginate-label">
|
||||||
|
<label class="custom-control-label" for="taptopaginate">True</label>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-radio custom-control-inline">
|
||||||
|
<input type="radio" id="not-taptopaginate" formControlName="bookReaderTapToPaginate" class="custom-control-input" [value]="false" aria-labelledby="taptopaginate-label">
|
||||||
|
<label class="custom-control-label" for="not-taptopaginate">False</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="settings-fontfamily-option">Font Family</label> <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="sr-only" id="settings-fontfamily-option-help">Font familty to load up. Default will load the book's default font</span>
|
||||||
|
<select class="form-control" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
|
||||||
|
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label id="font-size">Font Size</label>
|
||||||
|
<ngx-slider [options]="bookReaderFontSizeOptions" formControlName="bookReaderFontSize" aria-labelledby="font-size"></ngx-slider>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Line Height</label> <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="sr-only" id="settings-booklineheight-option-help">How much spacing between the lines of the book</span>
|
||||||
|
<ngx-slider [options]="bookReaderLineSpacingOptions" formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help"></ngx-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Margin</label> <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="sr-only" 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>
|
||||||
|
<ngx-slider [options]="bookReaderMarginOptions" formControlName="bookReaderMargin" aria-describedby="bookmargin"></ngx-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="float-right mb-3">
|
||||||
|
<button type="button" class="btn btn-secondary mr-2" (click)="resetForm()">Reset</button>
|
||||||
|
<button type="submit" class="btn btn-primary" (click)="save()" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-panel>
|
||||||
|
|
||||||
|
|
||||||
|
<ngb-panel id="password-panel" title="Password">
|
||||||
|
<ng-template ngbPanelContent>
|
||||||
|
<p>Change your Password</p>
|
||||||
|
<div class="alert alert-danger" role="alert" *ngIf="resetPasswordErrors.length > 0">
|
||||||
|
<div *ngFor="let error of resetPasswordErrors">{{error}}</div>
|
||||||
|
</div>
|
||||||
|
<form [formGroup]="passwordChangeForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new-password">New Password</label>
|
||||||
|
<input class="form-control" type="password" id="new-password" formControlName="password" required>
|
||||||
|
<div id="password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
||||||
|
<div *ngIf="password?.errors?.required">
|
||||||
|
This field is required
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm-password">Confirm Password</label>
|
||||||
|
<input class="form-control" type="password" id="confirm-password" formControlName="confirmPassword" aria-describedby="password-validations" required>
|
||||||
|
<div id="password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
||||||
|
<div *ngIf="!passwordsMatch">
|
||||||
|
Passwords must match
|
||||||
|
</div>
|
||||||
|
<div *ngIf="confirmPassword?.errors?.required">
|
||||||
|
This field is required
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="float-right mb-3">
|
||||||
|
<button type="button" class="btn btn-secondary mr-2" (click)="resetPasswordForm()">Reset</button>
|
||||||
|
<button type="submit" class="btn btn-primary" (click)="savePasswordForm()" [disabled]="!passwordChangeForm.valid || !(passwordChangeForm.dirty || passwordChangeForm.touched)">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-panel>
|
||||||
|
</ngb-accordion>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="tab.fragment === 'bookmarks'">
|
||||||
|
<app-series-bookmarks></app-series-bookmarks>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||||
|
</div>
|
@ -2,13 +2,14 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
|||||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { pageSplitOptions, Preferences, readingDirections, scalingOptions, readingModes } from '../_models/preferences/preferences';
|
|
||||||
import { User } from '../_models/user';
|
|
||||||
import { AccountService } from '../_services/account.service';
|
|
||||||
import { Options } from '@angular-slider/ngx-slider';
|
import { Options } from '@angular-slider/ngx-slider';
|
||||||
import { BookService } from '../book-reader/book.service';
|
|
||||||
import { NavService } from '../_services/nav.service';
|
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { BookService } from 'src/app/book-reader/book.service';
|
||||||
|
import { readingDirections, scalingOptions, pageSplitOptions, readingModes, Preferences } from 'src/app/_models/preferences/preferences';
|
||||||
|
import { User } from 'src/app/_models/user';
|
||||||
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
|
import { NavService } from 'src/app/_services/nav.service';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-preferences',
|
selector: 'app-user-preferences',
|
||||||
@ -48,8 +49,25 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
fontFamilies: Array<string> = [];
|
fontFamilies: Array<string> = [];
|
||||||
|
|
||||||
constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService, private navService: NavService, private titleService: Title) {
|
//tabs = ['Preferences', 'Bookmarks'];
|
||||||
|
tabs: Array<{title: string, fragment: string}> = [
|
||||||
|
{title: 'Preferences', fragment: ''},
|
||||||
|
{title: 'Bookmarks', fragment: 'bookmarks'},
|
||||||
|
];
|
||||||
|
active = this.tabs[0];
|
||||||
|
|
||||||
|
constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService,
|
||||||
|
private navService: NavService, private titleService: Title, private route: ActivatedRoute) {
|
||||||
this.fontFamilies = this.bookService.getFontFamilies();
|
this.fontFamilies = this.bookService.getFontFamilies();
|
||||||
|
|
||||||
|
this.route.fragment.subscribe(frag => {
|
||||||
|
const tab = this.tabs.filter(item => item.fragment === frag);
|
||||||
|
if (tab.length > 0) {
|
||||||
|
this.active = tab[0];
|
||||||
|
} else {
|
||||||
|
this.active = this.tabs[0]; // Default to first tab
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -159,5 +177,4 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
this.resetPasswordErrors = err;
|
this.resetPasswordErrors = err;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
22
UI/Web/src/app/user-settings/user-settings-routing.module.ts
Normal file
22
UI/Web/src/app/user-settings/user-settings-routing.module.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
import { AuthGuard } from '../_guards/auth.guard';
|
||||||
|
import { UserPreferencesComponent } from './user-preferences/user-preferences.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{path: '**', component: UserPreferencesComponent, pathMatch: 'full'},
|
||||||
|
{
|
||||||
|
runGuardsAndResolvers: 'always',
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
children: [
|
||||||
|
{path: '', component: UserPreferencesComponent, pathMatch: 'full'},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes), ],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class UserSettingsRoutingModule { }
|
27
UI/Web/src/app/user-settings/user-settings.module.ts
Normal file
27
UI/Web/src/app/user-settings/user-settings.module.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SeriesBookmarksComponent } from './series-bookmarks/series-bookmarks.component';
|
||||||
|
import { UserPreferencesComponent } from './user-preferences/user-preferences.component';
|
||||||
|
import { NgbAccordionModule, NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { NgxSliderModule } from '@angular-slider/ngx-slider';
|
||||||
|
import { UserSettingsRoutingModule } from './user-settings-routing.module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
SeriesBookmarksComponent,
|
||||||
|
UserPreferencesComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgbAccordionModule,
|
||||||
|
NgbNavModule,
|
||||||
|
NgbTooltipModule,
|
||||||
|
NgxSliderModule,
|
||||||
|
UserSettingsRoutingModule,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class UserSettingsModule { }
|
@ -20,7 +20,10 @@ $dark-item-accent-bg: #292d32;
|
|||||||
}
|
}
|
||||||
|
|
||||||
a, .btn-link {
|
a, .btn-link {
|
||||||
color: #4ac694;
|
color: $dark-hover-color;
|
||||||
|
> i {
|
||||||
|
color: $dark-hover-color !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.btn {
|
a.btn {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user