Fit Split to Screen (#769)

* Updated readme with new host information and new feature site.

* Implemented basic fit to screen splitting option for manga reader such that the reader will try to fit the whole cover on the screen via scaling it.

Updated a bunch of defaults in the preferences to give a better experience for first installs.

* Refactored the stat scheduling code slightly to clean it up and have better logging.

* Replaced @import with @use to lower css bundling.

* Changed up the defaults for the reading preferences to give a better experience. Fixed a duplicate render on automatic scaling due to emitting a valuechange with automatic scaling changing fit.

Implemented basic form of fit to screen. Still needs some tweaking and optimization.

* Update link to new feature server and update kavita homepage to use www.

* Updated the serverInfo to match backend. Tweaked some of the css for the changelog

* Added publish date for changelog

* First page works except for tablet

* I'm stumped, taking a break

* Hide the arrow for nav events

* Ensure specials in reading lists don't have their extensions visible

* Testing out removing no-connection

* Fixed a bug in infinite scroller where next chapter spacer when clicked would emit for prev chapter load. Fixed an issue where next/prev chapter loaders would execute when they shouldn't.

* Fit Split is working in all cases as of this code. New optimization is still needed.

* Fit to screen is now working well

* Updated the bookmark effect to look much better

* Updated new issue template to inform users to request features on our site.

* Removed an empty migration
This commit is contained in:
Joseph Milazzo 2021-11-18 08:55:52 -06:00 committed by GitHub
parent 199398df95
commit 3bfbd042a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 227 additions and 108 deletions

View File

@ -7,6 +7,10 @@ assignees: ''
--- ---
**If this is a feature request, request [here](https://feats.kavitareader.com/) instead. Feature requests will be deleted from Github.**
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.

View File

@ -1,4 +1,6 @@
namespace API.DTOs.Update using System;
namespace API.DTOs.Update
{ {
/// <summary> /// <summary>
/// Update Notification denoting a new release available for user to update to /// Update Notification denoting a new release available for user to update to
@ -34,5 +36,9 @@
/// Is this a pre-release /// Is this a pre-release
/// </summary> /// </summary>
public bool IsPrerelease { get; init; } public bool IsPrerelease { get; init; }
/// <summary>
/// Date of the publish
/// </summary>
public string PublishDate { get; init; }
} }
} }

View File

@ -16,7 +16,7 @@ namespace API.Entities
/// <summary> /// <summary>
/// Manga Reader Option: Which side of a split image should we show first /// Manga Reader Option: Which side of a split image should we show first
/// </summary> /// </summary>
public PageSplitOption PageSplitOption { get; set; } = PageSplitOption.SplitRightToLeft; public PageSplitOption PageSplitOption { get; set; } = PageSplitOption.FitSplit;
/// <summary> /// <summary>
/// Manga Reader Option: How the manga reader should perform paging or reading of the file /// Manga Reader Option: How the manga reader should perform paging or reading of the file
/// <example> /// <example>
@ -25,14 +25,15 @@ namespace API.Entities
/// </example> /// </example>
/// </summary> /// </summary>
public ReaderMode ReaderMode { get; set; } public ReaderMode ReaderMode { get; set; }
/// <summary> /// <summary>
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction /// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
/// </summary> /// </summary>
public bool AutoCloseMenu { get; set; } public bool AutoCloseMenu { get; set; } = true;
/// <summary> /// <summary>
/// Book Reader Option: Should the background color be dark /// Book Reader Option: Should the background color be dark
/// </summary> /// </summary>
public bool BookReaderDarkMode { get; set; } = false; public bool BookReaderDarkMode { get; set; } = true;
/// <summary> /// <summary>
/// Book Reader Option: Override extra Margin /// Book Reader Option: Override extra Margin
/// </summary> /// </summary>

View File

@ -4,6 +4,7 @@
{ {
SplitLeftToRight = 0, SplitLeftToRight = 0,
SplitRightToLeft = 1, SplitRightToLeft = 1,
NoSplit = 2 NoSplit = 2,
FitSplit = 3
} }
} }

View File

@ -17,6 +17,6 @@ namespace API.Interfaces
void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false); void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false);
void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false);
void CancelStatsTasks(); void CancelStatsTasks();
void RunStatCollection(); Task RunStatCollection();
} }
} }

View File

@ -29,7 +29,7 @@ namespace API.Services.HostedServices
// These methods will automatically check if stat collection is disabled to prevent sending any data regardless // These methods will automatically check if stat collection is disabled to prevent sending any data regardless
// of when setting was changed // of when setting was changed
await taskScheduler.ScheduleStatsTasks(); await taskScheduler.ScheduleStatsTasks();
taskScheduler.RunStatCollection(); await taskScheduler.RunStatCollection();
} }
catch (Exception) catch (Exception)
{ {

View File

@ -23,7 +23,6 @@ namespace API.Services
private readonly IStatsService _statsService; private readonly IStatsService _statsService;
private readonly IVersionUpdaterService _versionUpdaterService; private readonly IVersionUpdaterService _versionUpdaterService;
private const string SendDataTask = "finalize-stats";
public static BackgroundJobServer Client => new BackgroundJobServer(); public static BackgroundJobServer Client => new BackgroundJobServer();
private static readonly Random Rnd = new Random(); private static readonly Random Rnd = new Random();
@ -89,19 +88,27 @@ namespace API.Services
} }
_logger.LogDebug("Scheduling stat collection daily"); _logger.LogDebug("Scheduling stat collection daily");
RecurringJob.AddOrUpdate(SendDataTask, () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), TimeZoneInfo.Local); RecurringJob.AddOrUpdate("report-stats", () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), TimeZoneInfo.Local);
} }
public void CancelStatsTasks() public void CancelStatsTasks()
{ {
_logger.LogDebug("Cancelling/Removing StatsTasks"); _logger.LogDebug("Cancelling/Removing StatsTasks");
RecurringJob.RemoveIfExists(SendDataTask); RecurringJob.RemoveIfExists("report-stats");
} }
public void RunStatCollection() /// <summary>
/// First time run stat collection. Executes immediately on a background thread. Does not block.
/// </summary>
public async Task RunStatCollection()
{ {
_logger.LogInformation("Enqueuing stat collection"); var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).AllowStatCollection;
if (!allowStatCollection)
{
_logger.LogDebug("User has opted out of stat collection, not sending stats");
return;
}
BackgroundJob.Enqueue(() => _statsService.Send()); BackgroundJob.Enqueue(() => _statsService.Send());
} }

View File

@ -38,6 +38,11 @@ namespace API.Services.Tasks
/// </summary> /// </summary>
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public string Html_Url { get; init; } public string Html_Url { get; init; }
/// <summary>
/// Date Release was Published
/// </summary>
// ReSharper disable once InconsistentNaming
public string Published_At { get; init; }
} }
public class UntrustedCertClientFactory : DefaultHttpClientFactory public class UntrustedCertClientFactory : DefaultHttpClientFactory
@ -109,7 +114,8 @@ namespace API.Services.Tasks
UpdateBody = _markdown.Transform(update.Body.Trim()), UpdateBody = _markdown.Transform(update.Body.Trim()),
UpdateTitle = update.Name, UpdateTitle = update.Name,
UpdateUrl = update.Html_Url, UpdateUrl = update.Html_Url,
IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker,
PublishDate = update.Published_At
}; };
} }

View File

@ -3,7 +3,7 @@
We're always looking for people to help make Kavita even better, there are a number of ways to contribute. We're always looking for people to help make Kavita even better, there are a number of ways to contribute.
## Documentation ## ## Documentation ##
Setup guides, FAQ, the more information we have on the [wiki](https://github.com/Kareadita/Kavita/wiki) the better. Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavitareader.com/) the better.
## Development ## ## Development ##

View File

@ -80,15 +80,15 @@ services:
**Note: Kavita is under heavy development and is being updated all the time, so the tag for current builds is `:nightly`. The `:latest` tag will be the latest stable release.** **Note: Kavita is under heavy development and is being updated all the time, so the tag for current builds is `:nightly`. The `:latest` tag will be the latest stable release.**
## Feature Requests ## Feature Requests
Got a great idea? Throw it up on the FeatHub or vote on another idea. Please check the [Project Board](https://github.com/Kareadita/Kavita/projects) first for a list of planned features. Got a great idea? Throw it up on our [Feature Request site](https://feats.kavitareader.com/) or vote on another idea. Please check the [Project Board](https://github.com/Kareadita/Kavita/projects) first for a list of planned features.
[![Feature Requests](https://feathub.com/Kareadita/Kavita?format=svg)](https://feathub.com/Kareadita/Kavita)
## Contributors ## Contributors
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md). This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
<a href="https://github.com/Kareadita/Kavita/graphs/contributors"><img src="https://opencollective.com/kavita/contributors.svg?width=890&button=false" /></a> <a href="https://github.com/Kareadita/Kavita/graphs/contributors">
<img src="https://opencollective.com/kavita/contributors.svg?width=890&button=false&avatarHeight=42" />
</a>
## Donate ## Donate
@ -99,7 +99,7 @@ expenses related to Kavita. Back us through [OpenCollective](https://opencollect
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Kavita#backer) Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Kavita#backer)
<img src="https://opencollective.com/Kavita/backers.svg?width=890"></a> <img src="https://opencollective.com/kavita/backers.svg?width=890&avatarHeight=42"></a>
## Sponsors ## Sponsors
@ -116,9 +116,6 @@ Thank you to [<img src="/Logo/jetbrains.svg" alt="" width="32"> JetBrains](http:
* [<img src="/Logo/rider.svg" alt="" width="32"> Rider](http://www.jetbrains.com/rider/) * [<img src="/Logo/rider.svg" alt="" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/dottrace.svg" alt="" width="32"> dotTrace](http://www.jetbrains.com/dottrace/) * [<img src="/Logo/dottrace.svg" alt="" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
## Sentry
Thank you to [<img src="/Logo/sentry.svg" alt="" width="64">](https://sentry.io/welcome/) for providing us with free license to their software.
### License ### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) * [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)

View File

@ -46,10 +46,10 @@ export class ErrorInterceptor implements HttpInterceptor {
} }
// If we are not on no-connection, redirect there and save current url so when we refersh, we redirect back there // 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') { // if (this.router.url !== '/no-connection') {
localStorage.setItem(this.urlKey, this.router.url); // localStorage.setItem(this.urlKey, this.router.url);
this.router.navigateByUrl('/no-connection'); // this.router.navigateByUrl('/no-connection');
} // }
break; break;
} }
return throwError(error); return throwError(error);

View File

@ -5,4 +5,5 @@ export interface UpdateVersionEvent {
updateTitle: string; updateTitle: string;
updateUrl: string; updateUrl: string;
isDocker: boolean; isDocker: boolean;
publishDate: string;
} }

View File

@ -1,5 +1,18 @@
export enum PageSplitOption { export enum PageSplitOption {
/**
* Renders the left side of the image then the right side
*/
SplitLeftToRight = 0, SplitLeftToRight = 0,
/**
* Renders the right side of the image then the left side
*/
SplitRightToLeft = 1, SplitRightToLeft = 1,
NoSplit = 2 /**
* Don't split and show the image in original size
*/
NoSplit = 2,
/**
* Don't split and scale the image to fit screen space
*/
FitSplit = 3
} }

View File

@ -26,5 +26,5 @@ export interface Preferences {
export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}]; export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}];
export const scalingOptions = [{text: 'Automatic', value: ScalingOption.Automatic}, {text: 'Fit to Height', value: ScalingOption.FitToHeight}, {text: 'Fit to Width', value: ScalingOption.FitToWidth}, {text: 'Original', value: ScalingOption.Original}]; export const scalingOptions = [{text: 'Automatic', value: ScalingOption.Automatic}, {text: 'Fit to Height', value: ScalingOption.FitToHeight}, {text: 'Fit to Width', value: ScalingOption.FitToWidth}, {text: 'Original', value: ScalingOption.Original}];
export const pageSplitOptions = [{text: 'Right to Left', value: PageSplitOption.SplitRightToLeft}, {text: 'Left to Right', value: PageSplitOption.SplitLeftToRight}, {text: 'No Split', value: PageSplitOption.NoSplit}]; export const pageSplitOptions = [{text: 'Fit to Screen', value: PageSplitOption.FitSplit}, {text: 'Right to Left', value: PageSplitOption.SplitRightToLeft}, {text: 'Left to Right', value: PageSplitOption.SplitLeftToRight}, {text: 'No Split', value: PageSplitOption.NoSplit}];
export const readingModes = [{text: 'Left to Right', value: READER_MODE.MANGA_LR}, {text: 'Up to Down', value: READER_MODE.MANGA_UD}, {text: 'Webtoon', value: READER_MODE.WEBTOON}]; export const readingModes = [{text: 'Left to Right', value: READER_MODE.MANGA_LR}, {text: 'Up to Down', value: READER_MODE.MANGA_UD}, {text: 'Webtoon', value: READER_MODE.WEBTOON}];

View File

@ -1,8 +1,9 @@
export interface ServerInfo { export interface ServerInfo {
os: string; os: string;
dotNetVersion: string; dotnetVersion: string;
runTimeVersion: string; runTimeVersion: string;
kavitaVersion: string; kavitaVersion: string;
buildBranch: string; NumOfCores: number;
culture: string; installId: string;
isDocker: boolean;
} }

View File

@ -1,15 +1,19 @@
<ng-container *ngFor="let update of updates; let indx = index;"> <div class="changelog">
<ng-container *ngFor="let update of updates; let indx = index;">
<div class="card w-100 mb-2" style="width: 18rem;"> <div class="card w-100 mb-2" style="width: 18rem;">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{{update.updateTitle}}&nbsp; <h4 class="card-title">{{update.updateTitle}}&nbsp;
<span class="badge badge-secondary" *ngIf="update.updateVersion === update.currentVersion">Installed</span> <span class="badge badge-secondary" *ngIf="update.updateVersion === update.currentVersion">Installed</span>
<span class="badge badge-secondary" *ngIf="update.updateVersion > update.currentVersion">Available</span> <span class="badge badge-secondary" *ngIf="update.updateVersion > update.currentVersion">Available</span>
</h5> </h4>
<h6 class="card-subtitle mb-2 text-muted">Published: {{update.publishDate | date: 'short'}}</h6>
<pre class="card-text update-body" [innerHtml]="update.updateBody | safeHtml"></pre> <pre class="card-text update-body" [innerHtml]="update.updateBody | safeHtml"></pre>
<a *ngIf="!update.isDocker" href="{{update.updateUrl}}" class="btn btn-{{indx === 0 ? 'primary' : 'secondary'}} float-right" target="_blank">Download</a> <a *ngIf="!update.isDocker" href="{{update.updateUrl}}" class="btn btn-{{indx === 0 ? 'primary' : 'secondary'}} float-right" target="_blank">Download</a>
</div> </div>
</div> </div>
</ng-container> </ng-container>
</div>
<div class="spinner-border text-secondary" *ngIf="isLoading" role="status"> <div class="spinner-border text-secondary" *ngIf="isLoading" role="status">

View File

@ -3,3 +3,19 @@
word-wrap: break-word; word-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
} }
::ng-deep .changelog {
h1 {
font-size: 26px;
}
p, ul {
margin-bottom: 0px;
}
}

View File

@ -33,8 +33,8 @@
<dt>Version</dt> <dt>Version</dt>
<dd>{{serverInfo.kavitaVersion}}</dd> <dd>{{serverInfo.kavitaVersion}}</dd>
<dt>.NET Version</dt> <dt>Install ID</dt>
<dd>{{serverInfo.dotNetVersion}}</dd> <dd>{{serverInfo.installId}}</dd>
</dl> </dl>
</div> </div>
@ -43,7 +43,7 @@
<div> <div>
<div class="row"> <div class="row">
<div class="col-4">Home page:</div> <div class="col-4">Home page:</div>
<div class="col"><a href="https://kavitareader.com" target="_blank">kavitareader.com</a></div> <div class="col"><a href="https://www.kavitareader.com" target="_blank">kavitareader.com</a></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-4">Wiki:</div> <div class="col-4">Wiki:</div>
@ -63,7 +63,6 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-4">Feature Requests:</div> <div class="col-4">Feature Requests:</div>
<div class="col"><a href="https://feathub.com/Kareadita/Kavita" target="_blank">Feathub</a><br/> <div class="col"><a href="https://feats.kavitareader.com" target="_blank">https://feats.kavitareader.com</a><br/>
<a href="https://github.com/Kareadita/Kavita/issues" target="_blank">Github issues</a></div>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
@import "../../../theme/colors"; @use "../../../theme/colors";
.bulk-select { .bulk-select {
background-color: $dark-form-background-no-opacity; background-color: colors.$dark-form-background-no-opacity;
border-bottom: 2px solid $primary-color; border-bottom: 2px solid colors.$primary-color;
color: white; color: white;
} }
@ -11,5 +11,5 @@
} }
.highlight { .highlight {
color: $primary-color !important; color: colors.$primary-color !important;
} }

View File

@ -1,4 +1,4 @@
@import '../../../theme/colors'; @use '../../../theme/colors';
$image-height: 230px; $image-height: 230px;
$image-width: 160px; $image-width: 160px;
@ -14,7 +14,7 @@ $image-width: 160px;
} }
.selected { .selected {
outline: 5px solid $primary-color; outline: 5px solid colors.$primary-color;
outline-width: medium; outline-width: medium;
outline-offset: -1px; outline-offset: -1px;
} }
@ -22,7 +22,7 @@ $image-width: 160px;
ngx-file-drop ::ng-deep > div { ngx-file-drop ::ng-deep > div {
// styling for the outer drop box // styling for the outer drop box
width: 100%; width: 100%;
border: 2px solid $primary-color; border: 2px solid colors.$primary-color;
border-radius: 5px; border-radius: 5px;
height: 100px; height: 100px;
margin: auto; margin: auto;

View File

@ -28,7 +28,7 @@
<ng-container *ngFor="let item of webtoonImages | async; let index = index;"> <ng-container *ngFor="let item of webtoonImages | async; let index = index;">
<img src="{{item.src}}" style="display: block" class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}}" *ngIf="pageNum >= pageNum - bufferPages && pageNum <= pageNum + bufferPages" rel="nofollow" alt="image" (load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;"> <img src="{{item.src}}" style="display: block" class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}}" *ngIf="pageNum >= pageNum - bufferPages && pageNum <= pageNum + bufferPages" rel="nofollow" alt="image" (load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;">
</ng-container> </ng-container>
<div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadPrevChapter.emit()"> <div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadNextChapter.emit()">
<div> <div>
<button class="btn btn-icon mx-auto"> <button class="btn btn-icon mx-auto">
<i class="fa fa-angle-double-down animate" aria-hidden="true"></i> <i class="fa fa-angle-double-down animate" aria-hidden="true"></i>

View File

@ -102,9 +102,7 @@
<div class="col-6"> <div class="col-6">
<div class="form-group"> <div class="form-group">
<select class="form-control" id="page-splitting" formControlName="pageSplitOption"> <select class="form-control" id="page-splitting" formControlName="pageSplitOption">
<option [value]="1">Right to Left</option> <option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text}}</option>
<option [value]="0">Left to Right</option>
<option [value]="2">None</option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
@import '../../theme/colors'; @use '../../theme/colors';
$center-width: 50%; $center-width: 50%;
$side-width: 25%; $side-width: 25%;
@ -178,7 +178,7 @@ canvas {
height: 2px; height: 2px;
} }
.custom-slider .ngx-slider .ngx-slider-selection { .custom-slider .ngx-slider .ngx-slider-selection {
background: $primary-color; background: colors.$primary-color;
} }
.custom-slider .ngx-slider .ngx-slider-pointer { .custom-slider .ngx-slider .ngx-slider-pointer {
@ -186,7 +186,7 @@ canvas {
height: 16px; height: 16px;
top: auto; /* to remove the default positioning */ top: auto; /* to remove the default positioning */
bottom: 0; bottom: 0;
background-color: $primary-color; // #333; background-color: colors.$primary-color; // #333;
border-top-left-radius: 3px; border-top-left-radius: 3px;
border-top-right-radius: 3px; border-top-right-radius: 3px;
} }
@ -217,7 +217,7 @@ canvas {
} }
.custom-slider .ngx-slider .ngx-slider-tick.ngx-slider-selected { .custom-slider .ngx-slider .ngx-slider-tick.ngx-slider-selected {
background: $primary-color; background: colors.$primary-color;
} }
} }
@ -237,19 +237,14 @@ canvas {
.bookmark-effect { .bookmark-effect {
animation: bookmark 1s cubic-bezier(0.165, 0.84, 0.44, 1); animation: bookmark 0.7s cubic-bezier(0.165, 0.84, 0.44, 1);
} }
@keyframes bookmark { @keyframes bookmark {
0%, 100% { 0%, 100% {
filter: opacity(1); border: 0px;
} }
50% { 50% {
filter: opacity(0.25); border: 5px solid colors.$primary-color;
} }
} }
// DEBUG
.active-image {
border: 5px solid red;
}

View File

@ -12,7 +12,7 @@ import { ScalingOption } from '../_models/preferences/scaling-option';
import { PageSplitOption } from '../_models/preferences/page-split-option'; import { PageSplitOption } from '../_models/preferences/page-split-option';
import { forkJoin, ReplaySubject, Subject } from 'rxjs'; import { forkJoin, ReplaySubject, Subject } from 'rxjs';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service'; import { KEY_CODES, UtilityService, Breakpoint } from '../shared/_services/utility.service';
import { CircularArray } from '../shared/data-structures/circular-array'; import { CircularArray } from '../shared/data-structures/circular-array';
import { MemberService } from '../_services/member.service'; import { MemberService } from '../_services/member.service';
import { Stack } from '../shared/data-structures/stack'; import { Stack } from '../shared/data-structures/stack';
@ -20,7 +20,7 @@ import { ChangeContext, LabelType, Options } from '@angular-slider/ngx-slider';
import { trigger, state, style, transition, animate } from '@angular/animations'; import { trigger, state, style, transition, animate } from '@angular/animations';
import { ChapterInfo } from './_models/chapter-info'; import { ChapterInfo } from './_models/chapter-info';
import { COLOR_FILTER, FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums'; import { COLOR_FILTER, FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums';
import { scalingOptions } from '../_models/preferences/preferences'; import { pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
import { READER_MODE } from '../_models/preferences/reader-mode'; import { READER_MODE } from '../_models/preferences/reader-mode';
import { MangaFormat } from '../_models/manga-format'; import { MangaFormat } from '../_models/manga-format';
import { LibraryService } from '../_services/library.service'; import { LibraryService } from '../_services/library.service';
@ -96,13 +96,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
scalingOptions = scalingOptions; scalingOptions = scalingOptions;
readingDirection = ReadingDirection.LeftToRight; readingDirection = ReadingDirection.LeftToRight;
scalingOption = ScalingOption.FitToHeight; scalingOption = ScalingOption.FitToHeight;
pageSplitOption = PageSplitOption.SplitRightToLeft; pageSplitOption = PageSplitOption.FitSplit;
currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT; currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT;
pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD; pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;
colorMode: COLOR_FILTER = COLOR_FILTER.NONE; colorMode: COLOR_FILTER = COLOR_FILTER.NONE;
autoCloseMenu: boolean = true; autoCloseMenu: boolean = true;
readerMode: READER_MODE = READER_MODE.MANGA_LR; readerMode: READER_MODE = READER_MODE.MANGA_LR;
pageSplitOptions = pageSplitOptions;
isLoading = true; isLoading = true;
@ViewChild('content') canvas: ElementRef | undefined; @ViewChild('content') canvas: ElementRef | undefined;
@ -266,6 +268,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return ReadingDirection; return ReadingDirection;
} }
get PageSplitOption(): typeof PageSplitOption {
return PageSplitOption;
}
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
public readerService: ReaderService, private location: Location, public readerService: ReaderService, private location: Location,
private formBuilder: FormBuilder, private navService: NavService, private formBuilder: FormBuilder, private navService: NavService,
@ -313,7 +319,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.generalSettingsForm = this.formBuilder.group({ this.generalSettingsForm = this.formBuilder.group({
autoCloseMenu: this.autoCloseMenu, autoCloseMenu: this.autoCloseMenu,
pageSplitOption: this.pageSplitOption + '', pageSplitOption: this.pageSplitOption,
fittingOption: this.translateScalingOption(this.scalingOption) fittingOption: this.translateScalingOption(this.scalingOption)
}); });
@ -321,8 +327,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => { this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => {
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value; this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
// On change of splitting, re-render the page if the page is already split const needsSplitting = this.isCoverImage();
const needsSplitting = this.canvasImage.width > this.canvasImage.height; // If we need to split on a menu change, then we need to re-render.
if (needsSplitting) { if (needsSplitting) {
this.loadPage(); this.loadPage();
} }
@ -619,15 +625,20 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
isSplitLeftToRight() { isSplitLeftToRight() {
return (this.generalSettingsForm?.get('pageSplitOption')?.value + '') === (PageSplitOption.SplitLeftToRight + ''); return parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) === PageSplitOption.SplitLeftToRight;
} }
/**
*
* @returns If the current model reflects no split of fit split
*/
isNoSplit() { isNoSplit() {
return (this.generalSettingsForm?.get('pageSplitOption')?.value + '') === (PageSplitOption.NoSplit + ''); const splitValue = parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10);
return splitValue === PageSplitOption.NoSplit || splitValue === PageSplitOption.FitSplit;
} }
updateSplitPage() { updateSplitPage() {
const needsSplitting = this.canvasImage.width > this.canvasImage.height; const needsSplitting = this.isCoverImage();
if (!needsSplitting || this.isNoSplit()) { if (!needsSplitting || this.isNoSplit()) {
this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT; this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT;
return; return;
@ -739,6 +750,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
loadNextChapter() { loadNextChapter() {
if (this.nextPageDisabled) { return; } if (this.nextPageDisabled) { return; }
if (this.nextChapterDisabled) { return; }
this.isLoading = true; this.isLoading = true;
if (this.nextChapterId === CHAPTER_ID_NOT_FETCHED || this.nextChapterId === this.chapterId) { if (this.nextChapterId === CHAPTER_ID_NOT_FETCHED || this.nextChapterId === this.chapterId) {
this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => { this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
@ -752,6 +764,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
loadPrevChapter() { loadPrevChapter() {
if (this.prevPageDisabled) { return; } if (this.prevPageDisabled) { return; }
if (this.prevChapterDisabled) { return; }
this.isLoading = true; this.isLoading = true;
this.continuousChaptersStack.pop(); this.continuousChaptersStack.pop();
const prevChapter = this.continuousChaptersStack.peek(); const prevChapter = this.continuousChaptersStack.peek();
@ -819,21 +832,23 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (needsScaling) { if (needsScaling) {
this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384; this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384;
this.canvas.nativeElement.height = isSafari ? 4_096 : 16_384; this.canvas.nativeElement.height = isSafari ? 4_096 : 16_384;
} else if (this.isCoverImage()) {
//this.canvas.nativeElement.width = this.canvasImage.width / 2;
//this.canvas.nativeElement.height = this.canvasImage.height;
} else { } else {
this.canvas.nativeElement.width = this.canvasImage.width; this.canvas.nativeElement.width = this.canvasImage.width;
this.canvas.nativeElement.height = this.canvasImage.height; this.canvas.nativeElement.height = this.canvasImage.height;
} }
} }
return true;
} }
renderPage() { renderPage() {
if (this.ctx && this.canvas) { if (this.ctx && this.canvas) {
this.canvasImage.onload = null; this.canvasImage.onload = null;
if (!this.setCanvasSize()) return; this.setCanvasSize();
const needsSplitting = this.canvasImage.width > this.canvasImage.height; const needsSplitting = this.isCoverImage();
this.updateSplitPage(); this.updateSplitPage();
if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) { if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) {
@ -844,8 +859,49 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height); this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height);
} else { } else {
if (!this.firstPageRendered && this.scalingOption === ScalingOption.Automatic) { if (!this.firstPageRendered && this.scalingOption === ScalingOption.Automatic) {
this.updateScalingForFirstPageRender();
}
let newScale = this.generalSettingsForm.get('fittingOption')?.value; // Fit Split on a page that needs splitting
if (this.shouldRenderAsFitSplit()) {
const windowWidth = window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
const windowHeight = window.innerHeight
|| document.documentElement.clientHeight
|| document.body.clientHeight;
// If the user's screen is wider than the image, just pretend this is no split, as it will render nicer
this.canvas.nativeElement.width = windowWidth;
this.canvas.nativeElement.height = windowHeight;
const ratio = this.canvasImage.width / this.canvasImage.height;
let newWidth = windowWidth;
let newHeight = newWidth / ratio;
if (newHeight > windowHeight) {
newHeight = windowHeight;
newWidth = newHeight * ratio;
}
// Optimization: When the screen is larger than newWidth, allow no split rendering to occur for a better fit
if (windowWidth > newWidth) {
this.ctx.drawImage(this.canvasImage, 0, 0);
} else {
this.ctx.drawImage(this.canvasImage, 0, 0, newWidth, newHeight);
}
} else {
this.ctx.drawImage(this.canvasImage, 0, 0);
}
}
// Reset scroll on non HEIGHT Fits
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
window.scrollTo(0, 0);
}
}
this.isLoading = false;
}
updateScalingForFirstPageRender() {
const windowWidth = window.innerWidth const windowWidth = window.innerWidth
|| document.documentElement.clientWidth || document.documentElement.clientWidth
|| document.body.clientWidth; || document.body.clientWidth;
@ -853,8 +909,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|| document.documentElement.clientHeight || document.documentElement.clientHeight
|| document.body.clientHeight; || document.body.clientHeight;
const widthRatio = windowWidth / this.canvasImage.width; const needsSplitting = this.isCoverImage();
const heightRatio = windowHeight / this.canvasImage.height; let newScale = this.generalSettingsForm.get('fittingOption')?.value;
const widthRatio = windowWidth / (this.canvasImage.width / (needsSplitting ? 2 : 1));
const heightRatio = windowHeight / (this.canvasImage.height);
// Given that we now have image dimensions, assuming this isn't a split image, // Given that we now have image dimensions, assuming this isn't a split image,
// Try to reset one time based on who's dimension (width/height) is smaller // Try to reset one time based on who's dimension (width/height) is smaller
@ -864,18 +922,18 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
newScale = FITTING_OPTION.HEIGHT; newScale = FITTING_OPTION.HEIGHT;
} }
this.generalSettingsForm.get('fittingOption')?.setValue(newScale);
this.firstPageRendered = true; this.firstPageRendered = true;
} this.generalSettingsForm.get('fittingOption')?.setValue(newScale, {emitEvent: false});
this.ctx.drawImage(this.canvasImage, 0, 0);
}
// Reset scroll on non HEIGHT Fits
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
window.scrollTo(0, 0);
} }
isCoverImage() {
return this.canvasImage.width > this.canvasImage.height;
} }
this.isLoading = false;
shouldRenderAsFitSplit() {
if (!this.isCoverImage() || parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) !== PageSplitOption.FitSplit) return false;
return true;
} }

View File

@ -38,6 +38,7 @@ export class NavEventsToggleComponent implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.onDestroy.next(); this.onDestroy.next();
this.onDestroy.complete(); this.onDestroy.complete();
this.progressEventsSource.complete();
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -1,4 +1,4 @@
@import '~bootstrap/scss/mixins/_breakpoints.scss'; @import '~bootstrap/scss/mixins/_breakpoints.scss'; // TODO: Use @forwards for this?
$primary-color: white; $primary-color: white;
$bg-color: rgb(22, 27, 34); $bg-color: rgb(22, 27, 34);

View File

@ -136,7 +136,12 @@ export class ReadingListDetailComponent implements OnInit {
return 'Volume ' + this.utilityService.cleanSpecialTitle(item.chapterNumber); return 'Volume ' + this.utilityService.cleanSpecialTitle(item.chapterNumber);
} }
return this.utilityService.formatChapterName(this.libraryTypes[item.libraryId], true, true) + item.chapterNumber; let chapterNum = item.chapterNumber;
if (!item.chapterNumber.match(/^\d+$/)) {
chapterNum = this.utilityService.cleanSpecialTitle(item.chapterNumber);
}
return this.utilityService.formatChapterName(this.libraryTypes[item.libraryId], true, true) + chapterNum;
} }
orderUpdated(event: IndexUpdateEvent) { orderUpdated(event: IndexUpdateEvent) {

View File

@ -1,5 +1,3 @@
@import '../../../theme/colors';
$bg-color: #c9c9c9; $bg-color: #c9c9c9;
$bdr-color: #f2f2f2; $bdr-color: #f2f2f2;

View File

@ -1,4 +1,4 @@
@import "../../theme/_colors.scss"; @use "../../theme/colors";
.login { .login {
display: flex; display: flex;
@ -36,7 +36,7 @@
} }
.card { .card {
background-color: $primary-color; background-color: colors.$primary-color;
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
min-width: 300px; min-width: 300px;

View File

@ -1,6 +1,6 @@
// All dark style overrides should live here // All dark style overrides should live here
@import "../../theme/colors"; @use "../../theme/colors";
.bg-dark { .bg-dark {
color: $dark-text-color; color: $dark-text-color;
@ -32,6 +32,10 @@
box-shadow: inset 0px 0px 8px 1px $dark-form-background !important; box-shadow: inset 0px 0px 8px 1px $dark-form-background !important;
} }
.text-muted {
color: #d7d7d7 !important;
}
.breadcrumb { .breadcrumb {
background-color: $dark-item-accent-bg; background-color: $dark-item-accent-bg;
@ -181,6 +185,10 @@
background-color: $dark-form-background-no-opacity; background-color: $dark-form-background-no-opacity;
} }
.bs-popover-bottom > .arrow::after, .bs-popover-bottom > .arrow::before {
border-bottom-color: transparent;
}
} }

View File

@ -1,6 +1,6 @@
// Import colors for overrides of bootstrap theme // Import colors for overrides of bootstrap theme
@import './theme/_colors.scss'; @import './theme/colors';
@import './theme/_toastr.scss'; @import './theme/toastr';
// Bootstrap must be after _colors since we define the colors there // Bootstrap must be after _colors since we define the colors there
@import '~bootstrap/scss/bootstrap'; @import '~bootstrap/scss/bootstrap';