diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/API.Tests/Parsing/MangaParsingTests.cs index 7efc34254..a975cc7ee 100644 --- a/API.Tests/Parsing/MangaParsingTests.cs +++ b/API.Tests/Parsing/MangaParsingTests.cs @@ -211,6 +211,7 @@ public class MangaParsingTests [InlineData("หนึ่งความคิด นิจนิรันดร์ เล่ม 2", "หนึ่งความคิด นิจนิรันดร์")] [InlineData("不安の種\uff0b - 01", "不安の種\uff0b")] [InlineData("Giant Ojou-sama - Ch. 33.5 - Volume 04 Bonus Chapter", "Giant Ojou-sama")] + [InlineData("[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE", "")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Manga)); diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 58b1309fa..5fd8db860 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -130,6 +130,25 @@ public class ScannerServiceTests : AbstractDbTest Assert.NotNull(postLib.Series.First().Volumes.FirstOrDefault(v => v.Chapters.FirstOrDefault(c => c.IsSpecial) != null)); } + + [Fact] + public async Task ScanLibrary_SeriesWithUnbalancedParenthesis() + { + const string testcase = "Scan Library Parses as ( - Manga.json"; + + var library = await GenerateScannerData(testcase); + var scanner = CreateServices(); + await scanner.ScanLibrary(library.Id); + var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + + Assert.NotNull(postLib); + Assert.Single(postLib.Series); + + var series = postLib.Series.First(); + + Assert.Equal("Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE", series.Name); + } + /// /// This is testing that if the first file is named A and has a localized name of B if all other files are named B, it should still group and name the series A /// diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Scan Library Parses as ( - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Scan Library Parses as ( - Manga.json new file mode 100644 index 000000000..803f92586 --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Scan Library Parses as ( - Manga.json @@ -0,0 +1,4 @@ +[ + "[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE/[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE.zip", + "[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE/[218565]-(C92) [BRIO (Puyocha)] Something Else (THE IDOLM@STE.zip" +] \ No newline at end of file diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 8c1eeb0c0..dcb35298e 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -612,6 +612,7 @@ public class LibraryController : BaseApiController library.AllowScrobbling = false; } + _unitOfWork.LibraryRepository.Update(library); } diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 3466b5587..e122ae9f9 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -946,9 +946,7 @@ public class OpdsController : BaseApiController var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId, VolumeIncludes.Chapters); - // var chapters = - // (await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)) - // .OrderBy(x => x.MinNumber, _chapterSortComparerDefaultLast); + var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ", $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix); SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{_seriesService.FormatChapterName(userId, libraryType)}s"); @@ -1094,15 +1092,6 @@ public class OpdsController : BaseApiController }; } - private static FeedAuthor CreateAuthor(PersonDto person) - { - return new FeedAuthor() - { - Name = person.Name, - Uri = "http://opds-spec.org/author/" + person.Id - }; - } - private static FeedEntry CreateSeries(SearchResultDto searchResultDto, string apiKey, string prefix, string baseUrl) { return new FeedEntry() @@ -1122,6 +1111,15 @@ public class OpdsController : BaseApiController }; } + private static FeedAuthor CreateAuthor(PersonDto person) + { + return new FeedAuthor() + { + Name = person.Name, + Uri = "http://opds-spec.org/author/" + person.Id + }; + } + private static FeedEntry CreateChapter(string apiKey, string title, string? summary, int chapterId, int volumeId, int seriesId, string prefix, string baseUrl) { return new FeedEntry() diff --git a/API/Data/ManualMigrations/ManualMigrateRemovePeople.cs b/API/Data/ManualMigrations/ManualMigrateRemovePeople.cs index bf7d5725a..6966c0264 100644 --- a/API/Data/ManualMigrations/ManualMigrateRemovePeople.cs +++ b/API/Data/ManualMigrations/ManualMigrateRemovePeople.cs @@ -28,7 +28,7 @@ public static class ManualMigrateRemovePeople logger.LogCritical("Running ManualMigrateRemovePeople migration - Please be patient, this may take some time. This is not an error"); - context.Person.RemoveRange(context.Person); + context.Person.RemoveRange(context.Person); if (context.ChangeTracker.HasChanges()) { diff --git a/API/Data/ManualMigrations/ManualMigrateUnscrobbleBookLibraries.cs b/API/Data/ManualMigrations/ManualMigrateUnscrobbleBookLibraries.cs new file mode 100644 index 000000000..02c4886cb --- /dev/null +++ b/API/Data/ManualMigrations/ManualMigrateUnscrobbleBookLibraries.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using API.Entities; +using API.Entities.Enums; +using Kavita.Common.EnvironmentInfo; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// When I removed Scrobble support for Book libraries, I forgot to turn the setting off for said libraries. +/// +public static class ManualMigrateUnscrobbleBookLibraries +{ + public static async Task Migrate(DataContext context, ILogger logger) + { + if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateUnscrobbleBookLibraries")) + { + return; + } + + logger.LogCritical("Running ManualMigrateUnscrobbleBookLibraries migration - Please be patient, this may take some time. This is not an error"); + + var libs = await context.Library.Where(l => l.Type == LibraryType.Book).ToListAsync(); + foreach (var lib in libs) + { + lib.AllowScrobbling = false; + context.Entry(lib).State = EntityState.Modified; + } + + if (context.ChangeTracker.HasChanges()) + { + await context.SaveChangesAsync(); + } + + await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory() + { + Name = "ManualMigrateUnscrobbleBookLibraries", + ProductVersion = BuildInfo.Version.ToString(), + RanAt = DateTime.UtcNow + }); + await context.SaveChangesAsync(); + + logger.LogCritical("Running ManualMigrateUnscrobbleBookLibraries migration - Completed. This is not an error"); + } +} diff --git a/API/Helpers/Builders/LibraryBuilder.cs b/API/Helpers/Builders/LibraryBuilder.cs index 1cfd529a1..5550cfd51 100644 --- a/API/Helpers/Builders/LibraryBuilder.cs +++ b/API/Helpers/Builders/LibraryBuilder.cs @@ -20,8 +20,26 @@ public class LibraryBuilder : IEntityBuilder Series = new List(), Folders = new List(), AppUsers = new List(), - AllowScrobbling = type is LibraryType.LightNovel or LibraryType.Manga + AllowScrobbling = type is LibraryType.LightNovel or LibraryType.Manga, + LibraryFileTypes = new List() }; + + _library.LibraryFileTypes.Add(new LibraryFileTypeGroup() + { + FileTypeGroup = FileTypeGroup.Archive + }); + _library.LibraryFileTypes.Add(new LibraryFileTypeGroup() + { + FileTypeGroup = FileTypeGroup.Epub + }); + _library.LibraryFileTypes.Add(new LibraryFileTypeGroup() + { + FileTypeGroup = FileTypeGroup.Images + }); + _library.LibraryFileTypes.Add(new LibraryFileTypeGroup() + { + FileTypeGroup = FileTypeGroup.Pdf + }); } public LibraryBuilder(Library library) diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 1c56c65fc..696418c77 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -746,7 +746,6 @@ public class DirectoryService : IDirectoryService public IList ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null, SearchOption searchOption = SearchOption.AllDirectories) { - _logger.LogTrace("[ScanFiles] called on {Path}", folderPath); var files = new List(); if (!Exists(folderPath)) return files; diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 9102eb6d5..994daee80 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -126,6 +126,13 @@ public class ParseScannedFiles IDictionary> seriesPaths, Library library, bool forceCheck = false) { var fileExtensions = string.Join("|", library.LibraryFileTypes.Select(l => l.FileTypeGroup.GetRegex())); + + // If there are no library file types, skip scanning entirely + if (string.IsNullOrWhiteSpace(fileExtensions)) + { + return ArraySegment.Empty; + } + var matcher = BuildMatcher(library); var result = new List(); @@ -459,7 +466,7 @@ public class ParseScannedFiles await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Starting", library.Name, ProgressEventType.Started)); - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 1.A: Process {FolderCount} folders", library.Name, folders.Count()); + _logger.LogDebug("[ScannerService] Library {LibraryName} Step 1.A: Process {FolderCount} folders", library.Name, folders.Count); var processedScannedSeries = new ConcurrentBag(); foreach (var folder in folders) diff --git a/API/Services/Tasks/Scanner/Parser/BasicParser.cs b/API/Services/Tasks/Scanner/Parser/BasicParser.cs index 7687b3040..039e3acd6 100644 --- a/API/Services/Tasks/Scanner/Parser/BasicParser.cs +++ b/API/Services/Tasks/Scanner/Parser/BasicParser.cs @@ -29,24 +29,12 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag Format = Parser.ParseFormat(filePath), Title = Parser.RemoveExtensionIfSupported(fileName)!, FullFilePath = Parser.NormalizePath(filePath), - Series = string.Empty, - ComicInfo = comicInfo + Series = Parser.ParseSeries(fileName, type), + ComicInfo = comicInfo, + Chapters = Parser.ParseChapter(fileName, type), + Volumes = Parser.ParseVolume(fileName, type), }; - // This will be called if the epub is already parsed once then we call and merge the information, if the - if (Parser.IsEpub(filePath)) - { - ret.Chapters = Parser.ParseChapter(fileName, type); - ret.Series = Parser.ParseSeries(fileName, type); - ret.Volumes = Parser.ParseVolume(fileName, type); - } - else - { - ret.Chapters = Parser.ParseChapter(fileName, type); - ret.Series = type == LibraryType.Comic ? Parser.ParseComicSeries(fileName) : Parser.ParseSeries(fileName, type); - ret.Volumes = type == LibraryType.Comic ? Parser.ParseComicVolume(fileName) : Parser.ParseVolume(fileName, type); - } - if (ret.Series == string.Empty || Parser.IsImage(filePath)) { // Try to parse information out of each folder all the way to rootPath diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 1ce3bf9bf..1d24283d7 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -271,7 +271,7 @@ public static class Parser RegexTimeout), //Knights of Sidonia c000 (S2 LE BD Omake - BLAME!) [Habanero Scans] new Regex( - @"(?.*)(\bc\d+\b)", + @"(?.*?)(? @if (bulkSelectionService.selections$ | async; as selectionCount) { @if (selectionCount > 0) { -
-
+
+
diff --git a/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.scss b/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.scss index 4427c8dba..608b891e9 100644 --- a/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.scss +++ b/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.scss @@ -1,6 +1,7 @@ .bulk-select-container { - position: absolute; - width: 100%; + z-index: 1; + top: 0; + position: sticky; .bulk-select { background-color: var(--bulk-selection-bg-color); diff --git a/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts b/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts index f7b138d7a..9886cbadf 100644 --- a/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts +++ b/UI/Web/src/app/cards/bulk-operations/bulk-operations.component.ts @@ -37,7 +37,14 @@ export class BulkOperationsComponent implements OnInit { * Modal mode means don't fix to the top */ @Input() modalMode = false; - @Input() topOffset: number = 75; + /** + * On Series Detail this should be 12 + */ + @Input() marginLeft: number = 0; + /** + * On Series Detail this should be 12 + */ + @Input() marginRight: number = 8; hasMarkAsRead: boolean = false; hasMarkAsUnread: boolean = false; actions: Array> = []; diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.html b/UI/Web/src/app/chapter-detail/chapter-detail.component.html index 1b5b9d0ef..38baf9a61 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.html +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.html @@ -65,7 +65,7 @@ @if (accountService.isAdmin$ | async) {
-
@@ -73,7 +73,7 @@
- +
diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.scss b/UI/Web/src/app/chapter-detail/chapter-detail.component.scss index ca6c6d57c..9e56fa4cb 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.scss +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.scss @@ -1 +1,5 @@ @use '../../series-detail-common'; + +:host ::ng-deep .card-actions .btn.btn-actions { + padding: 0.375rem 0.75rem; +} diff --git a/UI/Web/src/app/library-detail/library-detail.component.ts b/UI/Web/src/app/library-detail/library-detail.component.ts index ed0419173..12f71e2f1 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -265,6 +265,14 @@ export class LibraryDetailComponent implements OnInit { case(Action.GenerateColorScape): await this.actionService.refreshLibraryMetadata(library, undefined, false); break; + case (Action.Delete): + await this.actionService.deleteLibrary(library, () => { + this.loadPageSource.next(true); + }); + break; + case (Action.AnalyzeFiles): + await this.actionService.analyzeFiles(library); + break; case(Action.Edit): this.actionService.editLibrary(library); break; diff --git a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.scss b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.scss index 8ff9695e9..003cd4bd4 100644 --- a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.scss +++ b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.scss @@ -20,7 +20,7 @@ .widget-button--indicator { position: absolute; - top: 30px; + top: 24px; color: var(--event-widget-activity-bg-color); &.error { @@ -79,6 +79,7 @@ .dark-menu-item { &.update-available { cursor: pointer; + opacity: 1; i.fa { color: var(--primary-color) !important; diff --git a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts index 4235dc407..38bf8cd9c 100644 --- a/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts +++ b/UI/Web/src/app/nav/_components/events-widget/events-widget.component.ts @@ -72,8 +72,6 @@ export class EventsWidgetComponent implements OnInit, OnDestroy { */ updateAvailable: boolean = false; - debugMode: boolean = false; - protected readonly EVENTS = EVENTS; diff --git a/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.scss b/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.scss index 14e068895..e0a4eb61d 100644 --- a/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.scss +++ b/UI/Web/src/app/nav/_components/grouped-typeahead/grouped-typeahead.component.scss @@ -1,8 +1,3 @@ -form { - max-height: 38px; -} - - .search-result img { width: 100% !important; } @@ -16,75 +11,89 @@ form { .typeahead-input { - border: 1px solid transparent; - border-radius: 4px; - padding: 0px 6px; - display: inline-block; - overflow: hidden; + border: 1px solid transparent; + border-radius: 4px; + overflow: hidden; + position: relative; + z-index: 1; + box-sizing: border-box; + box-shadow: none; + cursor: text; + background-color: var(--searchbar-bg-color); + color: var(--body-text-color); + min-height: 32px; + transition-property: all; + transition-duration: 0.3s; + display: block; + width: 50%; + + .search { + display: flex; position: relative; - z-index: 1; - box-sizing: border-box; - box-shadow: none; - cursor: text; - background-color: var(--input-bg-color); - color: var(--body-text-color); - min-height: 38px; + min-height: 32px; + + .btn-close { + position: unset; + margin-top: unset; + align-self: center; + margin: 0 8px; + } + } + + + input { + width: 100%; + padding: 0px 6px; + outline: 0 !important; + border-radius: .28571429rem; + margin: 0px !important; + text-indent: 0 !important; + line-height: inherit !important; + box-shadow: none !important; transition-property: all; transition-duration: 0.3s; display: block; + opacity: 1; + position: relative; + left: 4px; + border: none; + background-color: transparent; - .search { - display: flex; - position: relative; + &:focus-visible { + width: 100%; } + } + &.focused { + width: 100%; + border-color: var(--primary-color); input { - outline: 0 !important; - border-radius: .28571429rem; - padding: 0px !important; - min-height: 0px !important; - max-width: 100% !important; - margin: 0px !important; - text-indent: 0 !important; - line-height: inherit !important; - box-shadow: none !important; - width: 300px; - transition-property: all; - transition-duration: 0.3s; - display: block; - opacity: 1; - position: relative; - left: 4px; - border: none; - - &:focus-visible { - width: calc(100vw - 180px); - } - - &:empty { - padding-top: 6px !important; - } + color: #fff; } - - &.focused { - width: 98.5%; - border-color: var(--input-focused-border-color); - } - + } } +@media only screen and (max-width: 980px) { + .typeahead-input { + width: 65%; + } +} + /* small devices (phones, 650px and down) */ @media only screen and (max-width:650px) { - input { - width: 100% + .typeahead-input { + width: 75%; + margin: 0 auto; } +} - input:focus-visible { - width: 100% !important; - } +@media only screen and (max-width: 500px) { + .typeahead-input { + width: 100%; + } } @@ -160,10 +169,10 @@ ul ul { } .overlay { - position: absolute; + position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.4); - width: 100%; - height: 100dvh; -} \ No newline at end of file + width: 100vw; + height: 100vh; +} diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html index 958213225..7c49b2934 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html @@ -9,12 +9,14 @@ } - - + +