mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Backend Bugfixes and Enhanced Selections (#754)
* Updated some signatures to avoid a ToArray() within a loop. * Use UpdateSeries directly when adding new series, rather than a modified version for new series only. * Refactored some messages for scanner loop to reduce duplicate code and write messages more clear. Hooked in a RefreshMetadataProgress event (no UI changes). * Fixed a bug on docker where backup service was using different logic than non-docker, which isn't needed after config change last release. * Allow user to make more than 1 backup per day * Implemented a select all checkbox for library access modal
This commit is contained in:
parent
0aff08c9cd
commit
f6bfabde4c
@ -26,8 +26,6 @@ namespace API.Data
|
||||
"temp"
|
||||
};
|
||||
|
||||
private static readonly string ConfigDirectory = Path.Join(Directory.GetCurrentDirectory(), "config");
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// In v0.4.8 we moved all config files to config/ to match with how docker was setup. This will move all config files from current directory
|
||||
@ -66,8 +64,8 @@ namespace API.Data
|
||||
Console.WriteLine(
|
||||
"Migrating files from pre-v0.4.8. All Kavita config files are now located in config/");
|
||||
|
||||
Console.WriteLine($"Creating {ConfigDirectory}");
|
||||
DirectoryService.ExistOrCreate(ConfigDirectory);
|
||||
Console.WriteLine($"Creating {DirectoryService.ConfigDirectory}");
|
||||
DirectoryService.ExistOrCreate(DirectoryService.ConfigDirectory);
|
||||
|
||||
try
|
||||
{
|
||||
@ -116,13 +114,13 @@ namespace API.Data
|
||||
|
||||
foreach (var folderToMove in AppFolders)
|
||||
{
|
||||
if (new DirectoryInfo(Path.Join(ConfigDirectory, folderToMove)).Exists) continue;
|
||||
if (new DirectoryInfo(Path.Join(DirectoryService.ConfigDirectory, folderToMove)).Exists) continue;
|
||||
|
||||
try
|
||||
{
|
||||
DirectoryService.CopyDirectoryToDirectory(
|
||||
Path.Join(Directory.GetCurrentDirectory(), folderToMove),
|
||||
Path.Join(ConfigDirectory, folderToMove));
|
||||
Path.Join(DirectoryService.ConfigDirectory, folderToMove));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@ -144,7 +142,7 @@ namespace API.Data
|
||||
{
|
||||
try
|
||||
{
|
||||
fileInfo.CopyTo(Path.Join(ConfigDirectory, fileInfo.Name));
|
||||
fileInfo.CopyTo(Path.Join(DirectoryService.ConfigDirectory, fileInfo.Name));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
@ -266,7 +266,7 @@ namespace API.Services
|
||||
/// <param name="directoryPath"></param>
|
||||
/// <param name="prepend">An optional string to prepend to the target file's name</param>
|
||||
/// <returns></returns>
|
||||
public bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "")
|
||||
public static bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "", ILogger logger = null)
|
||||
{
|
||||
ExistOrCreate(directoryPath);
|
||||
string currentFile = null;
|
||||
@ -282,19 +282,24 @@ namespace API.Services
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Tried to copy {File} but it doesn't exist", file);
|
||||
logger?.LogWarning("Tried to copy {File} but it doesn't exist", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath);
|
||||
logger?.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "")
|
||||
{
|
||||
return CopyFilesToDirectory(filePaths, directoryPath, prepend, _logger);
|
||||
}
|
||||
|
||||
public IEnumerable<string> ListDirectory(string rootPath)
|
||||
{
|
||||
if (!Directory.Exists(rootPath)) return ImmutableList<string>.Empty;
|
||||
|
@ -218,14 +218,18 @@ namespace API.Services
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var totalTime = 0L;
|
||||
_logger.LogInformation("[MetadataService] Refreshing Library {LibraryName}. Total Items: {TotalSize}. Total Chunks: {TotalChunks} with {ChunkSize} size", library.Name, chunkInfo.TotalSize, chunkInfo.TotalChunks, chunkInfo.ChunkSize);
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress,
|
||||
MessageFactory.RefreshMetadataProgressEvent(library.Id, 0F));
|
||||
|
||||
for (var chunk = 1; chunk <= chunkInfo.TotalChunks; chunk++)
|
||||
var i = 0;
|
||||
for (var chunk = 1; chunk <= chunkInfo.TotalChunks; chunk++, i++)
|
||||
{
|
||||
if (chunkInfo.TotalChunks == 0) continue;
|
||||
totalTime += stopwatch.ElapsedMilliseconds;
|
||||
stopwatch.Restart();
|
||||
_logger.LogInformation("[MetadataService] Processing chunk {ChunkNumber} / {TotalChunks} with size {ChunkSize}. Series ({SeriesStart} - {SeriesEnd}",
|
||||
chunk, chunkInfo.TotalChunks, chunkInfo.ChunkSize, chunk * chunkInfo.ChunkSize, (chunk + 1) * chunkInfo.ChunkSize);
|
||||
|
||||
var nonLibrarySeries = await _unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id,
|
||||
new UserParams()
|
||||
{
|
||||
@ -233,6 +237,7 @@ namespace API.Services
|
||||
PageSize = chunkInfo.ChunkSize
|
||||
});
|
||||
_logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count);
|
||||
|
||||
Parallel.ForEach(nonLibrarySeries, series =>
|
||||
{
|
||||
try
|
||||
@ -275,8 +280,14 @@ namespace API.Services
|
||||
"[MetadataService] Processed {SeriesStart} - {SeriesEnd} out of {TotalSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}",
|
||||
chunk * chunkInfo.ChunkSize, (chunk * chunkInfo.ChunkSize) + nonLibrarySeries.Count, chunkInfo.TotalSize, stopwatch.ElapsedMilliseconds, library.Name);
|
||||
}
|
||||
var progress = Math.Max(0F, Math.Min(100F, i * 1F / chunkInfo.TotalChunks));
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress,
|
||||
MessageFactory.RefreshMetadataProgressEvent(library.Id, progress));
|
||||
}
|
||||
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress,
|
||||
MessageFactory.RefreshMetadataProgressEvent(library.Id, 100F));
|
||||
|
||||
_logger.LogInformation("[MetadataService] Updated metadata for {SeriesNumber} series in library {LibraryName} in {ElapsedMilliseconds} milliseconds total", chunkInfo.TotalSize, library.Name, totalTime);
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,6 @@ using API.Extensions;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
using Hangfire;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -20,8 +19,6 @@ namespace API.Services.Tasks
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<BackupService> _logger;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly string _tempDirectory = DirectoryService.TempDirectory;
|
||||
private readonly string _logDirectory = DirectoryService.LogDirectory;
|
||||
|
||||
private readonly IList<string> _backupFiles;
|
||||
|
||||
@ -35,30 +32,16 @@ namespace API.Services.Tasks
|
||||
var loggingSection = config.GetLoggingFileName();
|
||||
var files = LogFiles(maxRollingFiles, loggingSection);
|
||||
|
||||
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
|
||||
|
||||
_backupFiles = new List<string>()
|
||||
{
|
||||
_backupFiles = new List<string>()
|
||||
{
|
||||
"data/appsettings.json",
|
||||
"data/Hangfire.db",
|
||||
"data/Hangfire-log.db",
|
||||
"data/kavita.db",
|
||||
"data/kavita.db-shm", // This wont always be there
|
||||
"data/kavita.db-wal" // This wont always be there
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_backupFiles = new List<string>()
|
||||
{
|
||||
"appsettings.json",
|
||||
"Hangfire.db",
|
||||
"Hangfire-log.db",
|
||||
"kavita.db",
|
||||
"kavita.db-shm", // This wont always be there
|
||||
"kavita.db-wal" // This wont always be there
|
||||
};
|
||||
}
|
||||
"appsettings.json",
|
||||
"Hangfire.db", // This is not used atm
|
||||
"Hangfire-log.db", // This is not used atm
|
||||
"kavita.db",
|
||||
"kavita.db-shm", // This wont always be there
|
||||
"kavita.db-wal" // This wont always be there
|
||||
};
|
||||
|
||||
foreach (var file in files.Select(f => (new FileInfo(f)).Name).ToList())
|
||||
{
|
||||
@ -72,7 +55,7 @@ namespace API.Services.Tasks
|
||||
var fi = new FileInfo(logFileName);
|
||||
|
||||
var files = maxRollingFiles > 0
|
||||
? DirectoryService.GetFiles(_logDirectory, $@"{Path.GetFileNameWithoutExtension(fi.Name)}{multipleFileRegex}\.log")
|
||||
? DirectoryService.GetFiles(DirectoryService.LogDirectory, $@"{Path.GetFileNameWithoutExtension(fi.Name)}{multipleFileRegex}\.log")
|
||||
: new[] {"kavita.log"};
|
||||
return files;
|
||||
}
|
||||
@ -93,7 +76,7 @@ namespace API.Services.Tasks
|
||||
return;
|
||||
}
|
||||
|
||||
var dateString = DateTime.Now.ToShortDateString().Replace("/", "_");
|
||||
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
|
||||
var zipPath = Path.Join(backupDirectory, $"kavita_backup_{dateString}.zip");
|
||||
|
||||
if (File.Exists(zipPath))
|
||||
@ -102,7 +85,7 @@ namespace API.Services.Tasks
|
||||
return;
|
||||
}
|
||||
|
||||
var tempDirectory = Path.Join(_tempDirectory, dateString);
|
||||
var tempDirectory = Path.Join(DirectoryService.TempDirectory, dateString);
|
||||
DirectoryService.ExistOrCreate(tempDirectory);
|
||||
DirectoryService.ClearDirectory(tempDirectory);
|
||||
|
||||
|
@ -360,14 +360,13 @@ namespace API.Services.Tasks
|
||||
Series existingSeries;
|
||||
try
|
||||
{
|
||||
existingSeries = allSeries.SingleOrDefault(s =>
|
||||
(s.NormalizedName.Equals(key.NormalizedName) || Parser.Parser.Normalize(s.OriginalName).Equals(key.NormalizedName))
|
||||
&& (s.Format == key.Format || s.Format == MangaFormat.Unknown));
|
||||
existingSeries = allSeries.SingleOrDefault(s => FindSeries(s, key));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// NOTE: If I ever want to put Duplicates table, this is where it can go
|
||||
_logger.LogCritical(e, "[ScannerService] There are multiple series that map to normalized key {Key}. You can manually delete the entity via UI and rescan to fix it. This will be skipped", key.NormalizedName);
|
||||
var duplicateSeries = allSeries.Where(s => s.NormalizedName == key.NormalizedName || Parser.Parser.Normalize(s.OriginalName) == key.NormalizedName).ToList();
|
||||
var duplicateSeries = allSeries.Where(s => FindSeries(s, key));
|
||||
foreach (var series in duplicateSeries)
|
||||
{
|
||||
_logger.LogCritical("[ScannerService] Duplicate Series Found: {Key} maps with {Series}", key.Name, series.OriginalName);
|
||||
@ -378,21 +377,20 @@ namespace API.Services.Tasks
|
||||
|
||||
if (existingSeries != null) continue;
|
||||
|
||||
existingSeries = DbFactory.Series(infos[0].Series);
|
||||
existingSeries.Format = key.Format;
|
||||
newSeries.Add(existingSeries);
|
||||
var s = DbFactory.Series(infos[0].Series);
|
||||
s.Format = key.Format;
|
||||
s.LibraryId = library.Id; // We have to manually set this since we aren't adding the series to the Library's series.
|
||||
newSeries.Add(s);
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
foreach(var series in newSeries)
|
||||
{
|
||||
_logger.LogDebug("[ScannerService] Processing series {SeriesName}", series.OriginalName);
|
||||
UpdateSeries(series, parsedSeries);
|
||||
_unitOfWork.SeriesRepository.Attach(series);
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("[ScannerService] Processing series {SeriesName}", series.OriginalName);
|
||||
UpdateVolumes(series, ParseScannedFiles.GetInfosByName(parsedSeries, series).ToArray());
|
||||
series.Pages = series.Volumes.Sum(v => v.Pages);
|
||||
series.LibraryId = library.Id; // We have to manually set this since we aren't adding the series to the Library's series.
|
||||
_unitOfWork.SeriesRepository.Attach(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
_logger.LogInformation(
|
||||
"[ScannerService] Added {NewSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}",
|
||||
@ -403,7 +401,7 @@ namespace API.Services.Tasks
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical(ex, "[ScannerService] There was a critical exception adding new series entry for {SeriesName} with a duplicate index key: {IndexKey}",
|
||||
_logger.LogCritical(ex, "[ScannerService] There was a critical exception adding new series entry for {SeriesName} with a duplicate index key: {IndexKey} ",
|
||||
series.Name, $"{series.Name}_{series.NormalizedName}_{series.LocalizedName}_{series.LibraryId}_{series.Format}");
|
||||
}
|
||||
|
||||
@ -418,13 +416,19 @@ namespace API.Services.Tasks
|
||||
newSeries.Count, stopwatch.ElapsedMilliseconds, library.Name);
|
||||
}
|
||||
|
||||
private static bool FindSeries(Series series, ParsedSeries parsedInfoKey)
|
||||
{
|
||||
return (series.NormalizedName.Equals(parsedInfoKey.NormalizedName) || Parser.Parser.Normalize(series.OriginalName).Equals(parsedInfoKey.NormalizedName))
|
||||
&& (series.Format == parsedInfoKey.Format || series.Format == MangaFormat.Unknown);
|
||||
}
|
||||
|
||||
private void UpdateSeries(Series series, Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("[ScannerService] Processing series {SeriesName}", series.OriginalName);
|
||||
|
||||
var parsedInfos = ParseScannedFiles.GetInfosByName(parsedSeries, series).ToArray();
|
||||
var parsedInfos = ParseScannedFiles.GetInfosByName(parsedSeries, series);
|
||||
UpdateVolumes(series, parsedInfos);
|
||||
series.Pages = series.Volumes.Sum(v => v.Pages);
|
||||
|
||||
@ -491,7 +495,7 @@ namespace API.Services.Tasks
|
||||
/// <param name="missingSeries">Series not found on disk or can't be parsed</param>
|
||||
/// <param name="removeCount"></param>
|
||||
/// <returns>the updated existingSeries</returns>
|
||||
public static IList<Series> RemoveMissingSeries(IList<Series> existingSeries, IEnumerable<Series> missingSeries, out int removeCount)
|
||||
public static IEnumerable<Series> RemoveMissingSeries(IList<Series> existingSeries, IEnumerable<Series> missingSeries, out int removeCount)
|
||||
{
|
||||
var existingCount = existingSeries.Count;
|
||||
var missingList = missingSeries.ToList();
|
||||
@ -505,7 +509,7 @@ namespace API.Services.Tasks
|
||||
return existingSeries;
|
||||
}
|
||||
|
||||
private void UpdateVolumes(Series series, ParserInfo[] parsedInfos)
|
||||
private void UpdateVolumes(Series series, IList<ParserInfo> parsedInfos)
|
||||
{
|
||||
var startingVolumeCount = series.Volumes.Count;
|
||||
// Add new volumes and update chapters per volume
|
||||
@ -559,7 +563,7 @@ namespace API.Services.Tasks
|
||||
/// </summary>
|
||||
/// <param name="volume"></param>
|
||||
/// <param name="parsedInfos"></param>
|
||||
private void UpdateChapters(Volume volume, ParserInfo[] parsedInfos)
|
||||
private void UpdateChapters(Volume volume, IList<ParserInfo> parsedInfos)
|
||||
{
|
||||
// Add new chapters
|
||||
foreach (var info in parsedInfos)
|
||||
|
@ -60,6 +60,20 @@ namespace API.SignalR
|
||||
};
|
||||
}
|
||||
|
||||
public static SignalRMessage RefreshMetadataProgressEvent(int libraryId, float progress)
|
||||
{
|
||||
return new SignalRMessage()
|
||||
{
|
||||
Name = SignalREvents.RefreshMetadataProgress,
|
||||
Body = new
|
||||
{
|
||||
LibraryId = libraryId,
|
||||
Progress = progress,
|
||||
EventTime = DateTime.Now
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static SignalRMessage RefreshMetadataEvent(int libraryId, int seriesId)
|
||||
|
@ -4,7 +4,14 @@
|
||||
{
|
||||
public const string UpdateVersion = "UpdateVersion";
|
||||
public const string ScanSeries = "ScanSeries";
|
||||
/// <summary>
|
||||
/// Event during Refresh Metadata for cover image change
|
||||
/// </summary>
|
||||
public const string RefreshMetadata = "RefreshMetadata";
|
||||
/// <summary>
|
||||
/// Event sent out during Refresh Metadata for progress tracking
|
||||
/// </summary>
|
||||
public const string RefreshMetadataProgress = "RefreshMetadataProgress";
|
||||
public const string ScanLibrary = "ScanLibrary";
|
||||
public const string SeriesAdded = "SeriesAdded";
|
||||
public const string SeriesRemoved = "SeriesRemoved";
|
||||
|
@ -16,6 +16,7 @@ export enum EVENTS {
|
||||
UpdateAvailable = 'UpdateAvailable',
|
||||
ScanSeries = 'ScanSeries',
|
||||
RefreshMetadata = 'RefreshMetadata',
|
||||
RefreshMetadataProgress = 'RefreshMetadataProgress',
|
||||
SeriesAdded = 'SeriesAdded',
|
||||
SeriesRemoved = 'SeriesRemoved',
|
||||
ScanLibraryProgress = 'ScanLibraryProgress',
|
||||
@ -89,6 +90,13 @@ export class MessageHubService {
|
||||
this.scanLibrary.emit(resp.body);
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.RefreshMetadataProgress, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.RefreshMetadataProgress,
|
||||
payload: resp.body
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.SeriesAddedToCollection, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.SeriesAddedToCollection,
|
||||
|
@ -6,17 +6,24 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="list-group">
|
||||
<li class="list-group-item" *ngFor="let library of selectedLibraries; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="library-{{i}}" type="checkbox" attr.aria-label="Library {{library.data.name}}" class="form-check-input"
|
||||
[(ngModel)]="library.selected" name="library">
|
||||
<label attr.for="library-{{i}}" class="form-check-label">{{library.data.name}}</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="selectedLibraries.length === 0">
|
||||
There are no libraries setup yet.
|
||||
</li>
|
||||
<div class="list-group" *ngIf="!isLoading">
|
||||
<div class="form-check">
|
||||
<input id="selectall" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
<label for="selectall" class="form-check-label">{{selectAll ? 'Deselect' : 'Select'}} All</label>
|
||||
</div>
|
||||
<ul>
|
||||
<li class="list-group-item" *ngFor="let library of allLibraries; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="library-{{i}}" type="checkbox" class="form-check-input" attr.aria-label="Library {{library.name}}"
|
||||
[ngModel]="selections.isSelected(library)" (change)="handleSelection(library)">
|
||||
<label attr.for="library-{{i}}" class="form-check-label">{{library.name}}</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="allLibraries.length === 0">
|
||||
There are no libraries setup yet.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SelectionModel } from 'src/app/typeahead/typeahead.component';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { Member } from 'src/app/_models/member';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
@ -15,24 +16,21 @@ export class LibraryAccessModalComponent implements OnInit {
|
||||
@Input() member: Member | undefined;
|
||||
allLibraries: Library[] = [];
|
||||
selectedLibraries: Array<{selected: boolean, data: Library}> = [];
|
||||
selections!: SelectionModel<Library>;
|
||||
selectAll: boolean = false;
|
||||
isLoading: boolean = false;
|
||||
|
||||
get hasSomeSelected() {
|
||||
console.log(this.selections != null && this.selections.hasSomeSelected());
|
||||
return this.selections != null && this.selections.hasSomeSelected();
|
||||
}
|
||||
|
||||
constructor(public modal: NgbActiveModal, private libraryService: LibraryService, private fb: FormBuilder) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.libraryService.getLibraries().subscribe(libs => {
|
||||
this.allLibraries = libs;
|
||||
this.selectedLibraries = libs.map(item => {
|
||||
return {selected: false, data: item};
|
||||
});
|
||||
|
||||
if (this.member !== undefined) {
|
||||
this.member.libraries.forEach(lib => {
|
||||
const foundLibrary = this.selectedLibraries.filter(item => item.data.name === lib.name);
|
||||
if (foundLibrary.length > 0) {
|
||||
foundLibrary[0].selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.setupSelections();
|
||||
});
|
||||
}
|
||||
|
||||
@ -45,25 +43,41 @@ export class LibraryAccessModalComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedLibraries = this.selectedLibraries.filter(item => item.selected).map(item => item.data);
|
||||
const selectedLibraries = this.selections.selected();
|
||||
this.libraryService.updateLibrariesForMember(this.member?.username, selectedLibraries).subscribe(() => {
|
||||
this.modal.close(true);
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.selectedLibraries = this.allLibraries.map(item => {
|
||||
return {selected: false, data: item};
|
||||
});
|
||||
|
||||
|
||||
setupSelections() {
|
||||
this.selections = new SelectionModel<Library>(false, this.allLibraries);
|
||||
this.isLoading = false;
|
||||
|
||||
// If a member is passed in, then auto-select their libraries
|
||||
if (this.member !== undefined) {
|
||||
this.member.libraries.forEach(lib => {
|
||||
const foundLibrary = this.selectedLibraries.filter(item => item.data.name === lib.name);
|
||||
if (foundLibrary.length > 0) {
|
||||
foundLibrary[0].selected = true;
|
||||
}
|
||||
this.selections.toggle(lib, true, (a, b) => a.name === b.name);
|
||||
});
|
||||
this.selectAll = this.selections.selected().length === this.allLibraries.length;
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setupSelections();
|
||||
}
|
||||
|
||||
toggleAll() {
|
||||
this.selectAll = !this.selectAll;
|
||||
this.allLibraries.forEach(s => this.selections.toggle(s, this.selectAll));
|
||||
}
|
||||
|
||||
handleSelection(item: Library) {
|
||||
this.selections.toggle(item);
|
||||
const numberOfSelected = this.selections.selected().length;
|
||||
if (numberOfSelected == 0) {
|
||||
this.selectAll = false;
|
||||
} else if (numberOfSelected == this.selectedLibraries.length) {
|
||||
this.selectAll = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
|
||||
// when a progress event comes in, show it on the UI next to library
|
||||
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => {
|
||||
if (event.event != EVENTS.ScanLibraryProgress) return;
|
||||
if (event.event !== EVENTS.ScanLibraryProgress) return;
|
||||
|
||||
const scanEvent = event.payload as ScanLibraryProgressEvent;
|
||||
this.scanInProgress[scanEvent.libraryId] = {progress: scanEvent.progress !== 100};
|
||||
@ -55,6 +55,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
<h6>Applies to Series</h6>
|
||||
<div class="form-check">
|
||||
<input id="selectall" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="someSelected">
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
<label for="selectall" class="form-check-label">{{selectAll ? 'Deselect' : 'Select'}} All</label>
|
||||
</div>
|
||||
<ul>
|
||||
|
@ -35,6 +35,11 @@ export class EditCollectionTagsComponent implements OnInit {
|
||||
imageUrls: Array<string> = [];
|
||||
selectedCover: string = '';
|
||||
|
||||
get hasSomeSelected() {
|
||||
return this.selections != null && this.selections.hasSomeSelected();
|
||||
}
|
||||
|
||||
|
||||
constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
|
||||
private collectionService: CollectionTagService, private toastr: ToastrService,
|
||||
private confirmSerivce: ConfirmService, private libraryService: LibraryService,
|
||||
@ -133,11 +138,6 @@ export class EditCollectionTagsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
get someSelected() {
|
||||
const selected = this.selections.selected();
|
||||
return (selected.length !== this.series.length && selected.length !== 0);
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
this.collectionTagForm.patchValue({
|
||||
coverImageIndex: index
|
||||
|
@ -5,6 +5,8 @@ import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap
|
||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
||||
import { TypeaheadSettings } from './typeahead-settings';
|
||||
|
||||
export type SelectionCompareFn<T> = (a: T, b: T) => boolean;
|
||||
|
||||
/**
|
||||
* SelectionModel<T> is used for keeping track of multiple selections. Simple interface with ability to toggle.
|
||||
* @param selectedState Optional state to set selectedOptions to. If not passed, defaults to false.
|
||||
@ -30,10 +32,16 @@ export class SelectionModel<T> {
|
||||
/**
|
||||
* Will toggle if the data item is selected or not. If data option is not tracked, will add it and set state to true.
|
||||
* @param data Item to toggle
|
||||
* @param selectedState Force the state
|
||||
* @param compareFn An optional function to use for the lookup, else will use shallowEqual implementation
|
||||
*/
|
||||
toggle(data: T, selectedState?: boolean) {
|
||||
//const dataItem = this._data.filter(d => d.value == data);
|
||||
const dataItem = this._data.filter(d => this.shallowEqual(d.value, data));
|
||||
toggle(data: T, selectedState?: boolean, compareFn?: SelectionCompareFn<T>) {
|
||||
let lookupMethod = this.shallowEqual;
|
||||
if (compareFn != undefined || compareFn != null) {
|
||||
lookupMethod = compareFn;
|
||||
}
|
||||
|
||||
const dataItem = this._data.filter(d => lookupMethod(d.value, data));
|
||||
if (dataItem.length > 0) {
|
||||
if (selectedState != undefined) {
|
||||
dataItem[0].selected = selectedState;
|
||||
@ -45,6 +53,7 @@ export class SelectionModel<T> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is the passed item selected
|
||||
* @param data item to check against
|
||||
@ -65,6 +74,15 @@ export class SelectionModel<T> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns If some of the items are selected, but not all
|
||||
*/
|
||||
hasSomeSelected(): boolean {
|
||||
const selectedCount = this._data.filter(d => d.selected).length;
|
||||
return (selectedCount !== this._data.length && selectedCount !== 0)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns All Selected items
|
||||
|
Loading…
x
Reference in New Issue
Block a user