Polish Round 1 (#2396)

This commit is contained in:
Joe Milazzo 2023-11-04 12:29:10 -05:00 committed by GitHub
parent cf2c43d390
commit 02b002d81a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
197 changed files with 1233 additions and 1751 deletions

View File

@ -76,6 +76,17 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" />
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
<PackageReference Include="ExCSS" Version="4.2.4" />
<PackageReference Include="Hangfire" Version="1.8.6" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.6" />
<PackageReference Include="Hangfire.InMemory" Version="0.6.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.54" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />

View File

@ -603,7 +603,7 @@ public class AccountController : BaseApiController
{
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser!.UserName));
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser.UserName));
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-invited"));
}
@ -684,7 +684,6 @@ public class AccountController : BaseApiController
{
var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email);
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
_logger.LogCritical("[Invite User]: Token {UserName}: {Token}", user.UserName, user.ConfirmationToken);
if (!_emailService.IsValidEmail(dto.Email))
{
@ -706,7 +705,6 @@ public class AccountController : BaseApiController
InvitingUser = adminUser.UserName!,
ServerConfirmationLink = emailLink
}));
}
return Ok(new InviteUserResponse
@ -834,13 +832,13 @@ public class AccountController : BaseApiController
public async Task<ActionResult<string>> ConfirmForgotPassword(ConfirmPasswordResetDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
if (user == null)
{
return BadRequest(await _localizationService.Get("en", "bad-credentials"));
}
try
{
if (user == null)
{
return BadRequest(await _localizationService.Get("en", "bad-credentials"));
}
var result = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider,
"ResetPassword", dto.Token);
if (!result)

View File

@ -18,7 +18,9 @@ public class MemberDto
public bool IsPending { get; init; }
public AgeRestrictionDto? AgeRestriction { get; init; }
public DateTime Created { get; init; }
public DateTime CreatedUtc { get; init; }
public DateTime LastActive { get; init; }
public DateTime LastActiveUtc { get; init; }
public IEnumerable<LibraryDto>? Libraries { get; init; }
public IEnumerable<string>? Roles { get; init; }
}

View File

@ -665,7 +665,9 @@ public class UserRepository : IUserRepository
Username = u.UserName,
Email = u.Email,
Created = u.Created,
CreatedUtc = u.CreatedUtc,
LastActive = u.LastActive,
LastActiveUtc = u.LastActiveUtc,
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
IsPending = !u.EmailConfirmed,
AgeRestriction = new AgeRestrictionDto()

View File

@ -120,6 +120,7 @@ public static class Seed
}
});
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
{
var roles = typeof(PolicyConstants)

View File

@ -78,10 +78,8 @@ public class StatisticService : IStatisticService
.CountAsync();
var lastActive = await _context.AppUserProgresses
.OrderByDescending(p => p.LastModified)
.Where(p => p.AppUserId == userId)
.Select(p => p.LastModified)
.FirstOrDefaultAsync();
.MaxAsync(p => p.LastModified);
// First get the total pages per library
@ -103,15 +101,28 @@ public class StatisticService : IStatisticService
})
.ToListAsync();
var averageReadingTimePerWeek = _context.AppUserProgresses
// New solution. Calculate total hours then divide by number of weeks from time account was created (or min reading event) till now
var averageReadingTimePerWeek = await _context.AppUserProgresses
.Where(p => p.AppUserId == userId)
.Join(_context.Chapter, p => p.ChapterId, c => c.Id,
(p, c) => new
{
AverageReadingHours = Math.Min((float) p.PagesRead / (float) c.Pages, 1.0) * ((float) c.AvgHoursToRead)
AverageReadingHours = Math.Min((float) p.PagesRead / (float) c.Pages, 1.0) *
((float) c.AvgHoursToRead)
})
.Select(x => x.AverageReadingHours)
.Average() * 7.0;
.SumAsync();
var earliestReadDate = await _context.AppUserProgresses
.Where(p => p.AppUserId == userId)
.MinAsync(p => p.Created);
var timeDifference = DateTime.Now - earliestReadDate;
var deltaWeeks = (int)Math.Ceiling(timeDifference.TotalDays / 7);
averageReadingTimePerWeek /= deltaWeeks;
return new UserReadStatistics()
{

View File

@ -47,11 +47,7 @@
"src/styles.scss",
"node_modules/@fortawesome/fontawesome-free/css/all.min.css"
],
"scripts": [
"node_modules/lazysizes/lazysizes.min.js",
"node_modules/lazysizes/plugins/rias/ls.rias.min.js",
"node_modules/lazysizes/plugins/attrchange/ls.attrchange.min.js"
],
"scripts": [],
"sourceMap": {
"hidden": false,
"scripts": true,

433
UI/Web/package-lock.json generated
View File

@ -8,23 +8,23 @@
"name": "kavita-webui",
"version": "0.4.2",
"dependencies": {
"@angular/animations": "^16.2.9",
"@angular/cdk": "^16.2.8",
"@angular/common": "^16.2.9",
"@angular/compiler": "^16.2.9",
"@angular/core": "^16.2.9",
"@angular/forms": "^16.2.9",
"@angular/localize": "^16.2.9",
"@angular/platform-browser": "^16.2.9",
"@angular/platform-browser-dynamic": "^16.2.9",
"@angular/router": "^16.2.9",
"@angular/animations": "^16.2.12",
"@angular/cdk": "^16.2.11",
"@angular/common": "^16.2.12",
"@angular/compiler": "^16.2.12",
"@angular/core": "^16.2.12",
"@angular/forms": "^16.2.12",
"@angular/localize": "^16.2.12",
"@angular/platform-browser": "^16.2.12",
"@angular/platform-browser-dynamic": "^16.2.12",
"@angular/router": "^16.2.12",
"@fortawesome/fontawesome-free": "^6.4.2",
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
"@iplab/ngx-file-upload": "^16.0.2",
"@lithiumjs/angular": "^7.3.0",
"@lithiumjs/ngx-virtual-scroll": "^0.3.0",
"@microsoft/signalr": "^7.0.12",
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
"@ng-bootstrap/ng-bootstrap": "^15.1.2",
"@ngneat/transloco": "^6.0.0",
"@ngneat/transloco-locale": "^5.1.1",
"@ngneat/transloco-persist-lang": "^5.0.0",
@ -33,16 +33,18 @@
"@popperjs/core": "^2.11.7",
"@swimlane/ngx-charts": "^20.1.2",
"@tweenjs/tween.js": "^21.0.0",
"@types/file-saver": "^2.0.5",
"@types/file-saver": "^2.0.6",
"angular-animations": "^0.11.0",
"bootstrap": "^5.3.1",
"charts.css": "^1.1.0",
"eventsource": "^2.0.2",
"file-saver": "^2.0.5",
"lazysizes": "^5.3.2",
"luxon": "^3.4.3",
"ng-circle-progress": "^1.7.1",
"ng-lazyload-image": "^9.1.3",
"ng-select2-component": "^13.0.9",
"ngx-color-picker": "^15.0.0",
"ngx-extended-pdf-viewer": "^18.0.2",
"ngx-extended-pdf-viewer": "^18.1.4",
"ngx-file-drop": "^16.0.0",
"ngx-slider-v2": "^16.0.2",
"ngx-stars": "^1.6.5",
@ -54,20 +56,20 @@
"zone.js": "^0.13.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.2.6",
"@angular-devkit/build-angular": "^16.2.9",
"@angular-eslint/builder": "^16.2.0",
"@angular-eslint/eslint-plugin": "^16.2.0",
"@angular-eslint/eslint-plugin-template": "^16.2.0",
"@angular-eslint/schematics": "^16.2.0",
"@angular-eslint/template-parser": "^16.2.0",
"@angular/cli": "^16.2.6",
"@angular/compiler-cli": "^16.2.9",
"@types/d3": "^7.4.1",
"@types/luxon": "^3.3.2",
"@types/node": "^20.8.6",
"@angular/cli": "^16.2.9",
"@angular/compiler-cli": "^16.2.12",
"@types/d3": "^7.4.2",
"@types/luxon": "^3.3.3",
"@types/node": "^20.8.10",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"eslint": "^8.51.0",
"eslint": "^8.52.0",
"jsonminify": "^0.4.2",
"karma-coverage": "~2.2.0",
"ts-node": "~10.9.1",
@ -97,12 +99,12 @@
}
},
"node_modules/@angular-devkit/architect": {
"version": "0.1602.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.6.tgz",
"integrity": "sha512-b1NNV3yNg6Rt86ms20bJIroWUI8ihaEwv5k+EoijEXLoMs4eNs5PhqL+QE8rTj+q9pa1gSrWf2blXor2JGwf1g==",
"version": "0.1602.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.9.tgz",
"integrity": "sha512-U3vfb/e2sFfg0D9FyyRBXRPP7g4FBFtGK8Q3JPmvAVsHHwi5AUFRNR7YBChB/T5TMNY077HcTyEirVh2FeUpdA==",
"dev": true,
"dependencies": {
"@angular-devkit/core": "16.2.6",
"@angular-devkit/core": "16.2.9",
"rxjs": "7.8.1"
},
"engines": {
@ -112,15 +114,15 @@
}
},
"node_modules/@angular-devkit/build-angular": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.6.tgz",
"integrity": "sha512-QdU/q77K1P8CPEEZGxw1QqLcnA9ofboDWS7vcLRBmFmk2zydtLTApbK0P8GNDRbnmROOKkoaLo+xUTDJz9gvPA==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.9.tgz",
"integrity": "sha512-S1C4UYxRVyNt3C0wCxbT2jZ1dN5i37kS0mol3PQjbR8gQ0GQzHmzhjTBl1oImo8aouET9yhrk9etk65oat4mBQ==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "2.2.1",
"@angular-devkit/architect": "0.1602.6",
"@angular-devkit/build-webpack": "0.1602.6",
"@angular-devkit/core": "16.2.6",
"@angular-devkit/architect": "0.1602.9",
"@angular-devkit/build-webpack": "0.1602.9",
"@angular-devkit/core": "16.2.9",
"@babel/core": "7.22.9",
"@babel/generator": "7.22.9",
"@babel/helper-annotate-as-pure": "7.22.5",
@ -132,7 +134,7 @@
"@babel/runtime": "7.22.6",
"@babel/template": "7.22.5",
"@discoveryjs/json-ext": "0.5.7",
"@ngtools/webpack": "16.2.6",
"@ngtools/webpack": "16.2.9",
"@vitejs/plugin-basic-ssl": "1.0.1",
"ansi-colors": "4.1.3",
"autoprefixer": "10.4.14",
@ -702,12 +704,12 @@
"dev": true
},
"node_modules/@angular-devkit/build-webpack": {
"version": "0.1602.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.6.tgz",
"integrity": "sha512-BJPR6xdq7gRJ6bVWnZ81xHyH75j7lyLbegCXbvUNaM8TWVBkwWsSdqr2NQ717dNLLn5umg58SFpU/pWMq6CxMQ==",
"version": "0.1602.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.9.tgz",
"integrity": "sha512-+3IxovfBPR2Vy730mGa0SVKkd5LQVom85gjXOs7WcnnnZmfc1q/BtFlqTgW1UWvTxP8IQdm7UYWVclQfL/WExw==",
"dev": true,
"dependencies": {
"@angular-devkit/architect": "0.1602.6",
"@angular-devkit/architect": "0.1602.9",
"rxjs": "7.8.1"
},
"engines": {
@ -721,9 +723,9 @@
}
},
"node_modules/@angular-devkit/core": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.6.tgz",
"integrity": "sha512-iez/8NYXQT6fqVQLlKmZUIRkFUEZ88ACKbTwD4lBmk0+hXW+bQBxI7JOnE3C4zkcM2YeuTXIYsC5SebTKYiR4Q==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.9.tgz",
"integrity": "sha512-dcHWjHBNGm3yCeNz19y8A1At4KgyC6XHNnbFL0y+nnZYiaESXjUoXJYKASedI6A+Bpl0HNq2URhH6bL6Af3+4w==",
"dev": true,
"dependencies": {
"ajv": "8.12.0",
@ -748,12 +750,12 @@
}
},
"node_modules/@angular-devkit/schematics": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.6.tgz",
"integrity": "sha512-PhpRYHCJ3WvZXmng6Qk8TXeQf83jeBMAf7AIzI8h0fgeBocOl97Xf7bZpLg6GymiU+rVn15igQ4Rz9rKAay8bQ==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.9.tgz",
"integrity": "sha512-lB51CGCILpcSI37CwKUAGDLxMqh7zmuRbiPo9s9mSkCM4ccqxFlaL+VFTq2/laneARD6aikpOHnkVm5myNzQPw==",
"dev": true,
"dependencies": {
"@angular-devkit/core": "16.2.6",
"@angular-devkit/core": "16.2.9",
"jsonc-parser": "3.2.0",
"magic-string": "0.30.1",
"ora": "5.4.1",
@ -864,9 +866,9 @@
}
},
"node_modules/@angular/animations": {
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.9.tgz",
"integrity": "sha512-J+nsc2x/ZQuh+YwwTzxXUrV+7SBpJq6DDStfTFkZls9PWGRj9fjqQeRCWrfNLllpxopAEjhFkoyK06oSjcwqAw==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.12.tgz",
"integrity": "sha512-MD0ElviEfAJY8qMOd6/jjSSvtqER2RDAi0lxe6EtUacC1DHCYkaPrKW4vLqY+tmZBg1yf+6n+uS77pXcHHcA3w==",
"dependencies": {
"tslib": "^2.3.0"
},
@ -874,13 +876,13 @@
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/core": "16.2.9"
"@angular/core": "16.2.12"
}
},
"node_modules/@angular/cdk": {
"version": "16.2.8",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.8.tgz",
"integrity": "sha512-DvqxH909mgSSxWbc5xM5xKLjDMPXY3pzzSVAllngvc9KGPFw240WCs3tSpPaVJI50Esbzdu5O0CyTBfu9jUy4g==",
"version": "16.2.11",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.11.tgz",
"integrity": "sha512-FcJ9xd9ptjULdScnBNg7YkVnY9NKePFfmvvs2zt841Hd489L8BUkTUdbvtCLhMJTTSN+k+D+RYFhevZuhPKVVg==",
"dependencies": {
"tslib": "^2.3.0"
},
@ -894,15 +896,15 @@
}
},
"node_modules/@angular/cli": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.6.tgz",
"integrity": "sha512-9poPvUEmlufOAW1Cjk+aA5e2x3mInLtbYYSL/EYviDN2ugmavsSIvxAE/WLnxq6cPWqhNDbHDaqvcmqkcFM3Cw==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.9.tgz",
"integrity": "sha512-wkpV/Ni26LUeDmhee2TPXXEq3feEdZMSG8+nkfUK9kqIcxm0IjI1GLPeiVOX7aQobuKNe2cCAFNwsrXWjj+2og==",
"dev": true,
"dependencies": {
"@angular-devkit/architect": "0.1602.6",
"@angular-devkit/core": "16.2.6",
"@angular-devkit/schematics": "16.2.6",
"@schematics/angular": "16.2.6",
"@angular-devkit/architect": "0.1602.9",
"@angular-devkit/core": "16.2.9",
"@angular-devkit/schematics": "16.2.9",
"@schematics/angular": "16.2.9",
"@yarnpkg/lockfile": "1.1.0",
"ansi-colors": "4.1.3",
"ini": "4.1.1",
@ -961,9 +963,9 @@
"dev": true
},
"node_modules/@angular/common": {
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.9.tgz",
"integrity": "sha512-5Lh5KsxCkaoBDeSAghKNF5lCi0083ug4X2X7wnafsSd6Z3xt/rDjH9hDOP5SF5IDLtCVjJgHfs3cCLSTjRuNwg==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.12.tgz",
"integrity": "sha512-B+WY/cT2VgEaz9HfJitBmgdk4I333XG/ybC98CMC4Wz8E49T8yzivmmxXB3OD6qvjcOB6ftuicl6WBqLbZNg2w==",
"dependencies": {
"tslib": "^2.3.0"
},
@ -971,14 +973,14 @@
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/core": "16.2.9",
"@angular/core": "16.2.12",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/compiler": {
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.9.tgz",
"integrity": "sha512-lh799pnbdvzTVShJHOY1JC6c1pwBsZC4UIgB3Itklo9dskGybQma/gP+lE6RhqM4FblNfaaBXGlCMUuY8HkmEQ==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.12.tgz",
"integrity": "sha512-6SMXUgSVekGM7R6l1Z9rCtUGtlg58GFmgbpMCsGf+VXxP468Njw8rjT2YZkf5aEPxEuRpSHhDYjqz7n14cwCXQ==",
"dependencies": {
"tslib": "^2.3.0"
},
@ -986,7 +988,7 @@
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/core": "16.2.9"
"@angular/core": "16.2.12"
},
"peerDependenciesMeta": {
"@angular/core": {
@ -995,12 +997,12 @@
}
},
"node_modules/@angular/compiler-cli": {
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.9.tgz",
"integrity": "sha512-ecH2oOlijJdDqioD9IfgdqJGoRRHI6hAx5rwBxIaYk01ywj13KzvXWPrXbCIupeWtV/XUZUlbwf47nlmL5gxZg==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.12.tgz",
"integrity": "sha512-pWSrr152562ujh6lsFZR8NfNc5Ljj+zSTQO44DsuB0tZjwEpnRcjJEgzuhGXr+CoiBf+jTSPZKemtSktDk5aaA==",
"dev": true,
"dependencies": {
"@babel/core": "7.22.5",
"@babel/core": "7.23.2",
"@jridgewell/sourcemap-codec": "^1.4.14",
"chokidar": "^3.0.0",
"convert-source-map": "^1.5.1",
@ -1018,14 +1020,14 @@
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/compiler": "16.2.9",
"@angular/compiler": "16.2.12",
"typescript": ">=4.9.3 <5.2"
}
},
"node_modules/@angular/core": {
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.9.tgz",
"integrity": "sha512-chvPX29ZBcMDuh7rLIgb0Cru6oJ/0FaqRzfOI3wT4W2F9W1HOlCtipovzmPYaUAmXBWfVP4EBO9TOWnpog0S0w==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.12.tgz",
"integrity": "sha512-GLLlDeke/NjroaLYOks0uyzFVo6HyLl7VOm0K1QpLXnYvW63W9Ql/T3yguRZa7tRkOAeFZ3jw+1wnBD4O8MoUA==",
"dependencies": {
"tslib": "^2.3.0"
},
@ -1038,9 +1040,9 @@
}
},
"node_modules/@angular/forms": {
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.9.tgz",
"integrity": "sha512-rxlg2iNJNBH/uc7b5YqybfYc8BkLzzPv1d/nMsQUlY0O2UV2zwNRpcIiWbWd7+ZaKjcyPynVe9FsXC8wgWIABw==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.12.tgz",
"integrity": "sha512-1Eao89hlBgLR3v8tU91vccn21BBKL06WWxl7zLpQmG6Hun+2jrThgOE4Pf3os4fkkbH4Apj0tWL2fNIWe/blbw==",
"dependencies": {
"tslib": "^2.3.0"
},
@ -1048,18 +1050,18 @@
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/common": "16.2.9",
"@angular/core": "16.2.9",
"@angular/platform-browser": "16.2.9",
"@angular/common": "16.2.12",
"@angular/core": "16.2.12",
"@angular/platform-browser": "16.2.12",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/localize": {
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.2.9.tgz",
"integrity": "sha512-t5002NgBj+wjd81IXwg+yc2ypaBk6OWLAka1GXmWua3x7hwGw1yMtPFmzOE1cCNdXgWlluLxWclFjCUrAbGEww==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.2.12.tgz",
"integrity": "sha512-sNIHDlZKENPQqx64qGF99g2sOCy9i9O4VOmjKD/FZbeE8O5qBbaQlkwOlFoQIt35/cnvtAtf7oQF6tqmiVtS2w==",
"dependencies": {
"@babel/core": "7.22.5",
"@babel/core": "7.23.2",
"fast-glob": "3.3.0",
"yargs": "^17.2.1"
},
@ -1072,8 +1074,8 @@
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/compiler": "16.2.9",
"@angular/compiler-cli": "16.2.9"
"@angular/compiler": "16.2.12",
"@angular/compiler-cli": "16.2.12"
}
},
"node_modules/@angular/localize/node_modules/fast-glob": {
@ -1092,9 +1094,9 @@
}
},
"node_modules/@angular/platform-browser": {
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.9.tgz",
"integrity": "sha512-9Je7+Jmx0AOyRzBBumraVJG3M0R6YbT4c9jTUbLGJCcPxwDI3/u2ZzvW3rBqpmrDaqLxN5f1LcZeTZx287QeqQ==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.12.tgz",
"integrity": "sha512-NnH7ju1iirmVEsUq432DTm0nZBGQsBrU40M3ZeVHMQ2subnGiyUs3QyzDz8+VWLL/T5xTxWLt9BkDn65vgzlIQ==",
"dependencies": {
"tslib": "^2.3.0"
},
@ -1102,9 +1104,9 @@
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/animations": "16.2.9",
"@angular/common": "16.2.9",
"@angular/core": "16.2.9"
"@angular/animations": "16.2.12",
"@angular/common": "16.2.12",
"@angular/core": "16.2.12"
},
"peerDependenciesMeta": {
"@angular/animations": {
@ -1113,9 +1115,9 @@
}
},
"node_modules/@angular/platform-browser-dynamic": {
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.9.tgz",
"integrity": "sha512-ztpo0939vTZ/5CWVSvo41Yl6YPoTZ0If+yTrs7dk1ce0vFgaZXMlc+y5ZwjJIiMM5CvHbhL48Uk+HJNIojP98A==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.12.tgz",
"integrity": "sha512-ya54jerNgreCVAR278wZavwjrUWImMr2F8yM5n9HBvsMBbFaAQ83anwbOEiHEF2BlR+gJiEBLfpuPRMw20pHqw==",
"dependencies": {
"tslib": "^2.3.0"
},
@ -1123,16 +1125,16 @@
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/common": "16.2.9",
"@angular/compiler": "16.2.9",
"@angular/core": "16.2.9",
"@angular/platform-browser": "16.2.9"
"@angular/common": "16.2.12",
"@angular/compiler": "16.2.12",
"@angular/core": "16.2.12",
"@angular/platform-browser": "16.2.12"
}
},
"node_modules/@angular/router": {
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.9.tgz",
"integrity": "sha512-5vrJNMblTDx3WC3dtaqLddWNtR0P9iwpqffeZL1uobBIwP4hbJx+8Dos3TwxGR4hnopFKahoDQ5nC0NOQslyog==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.12.tgz",
"integrity": "sha512-aU6QnYSza005V9P3W6PpkieL56O0IHps96DjqI1RS8yOJUl3THmokqYN4Fm5+HXy4f390FN9i6ftadYQDKeWmA==",
"dependencies": {
"tslib": "^2.3.0"
},
@ -1140,9 +1142,9 @@
"node": "^16.14.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/common": "16.2.9",
"@angular/core": "16.2.9",
"@angular/platform-browser": "16.2.9",
"@angular/common": "16.2.12",
"@angular/core": "16.2.12",
"@angular/platform-browser": "16.2.12",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
@ -1173,25 +1175,25 @@
}
},
"node_modules/@babel/core": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz",
"integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==",
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
"integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.22.5",
"@babel/generator": "^7.22.5",
"@babel/helper-compilation-targets": "^7.22.5",
"@babel/helper-module-transforms": "^7.22.5",
"@babel/helpers": "^7.22.5",
"@babel/parser": "^7.22.5",
"@babel/template": "^7.22.5",
"@babel/traverse": "^7.22.5",
"@babel/types": "^7.22.5",
"convert-source-map": "^1.7.0",
"@babel/code-frame": "^7.22.13",
"@babel/generator": "^7.23.0",
"@babel/helper-compilation-targets": "^7.22.15",
"@babel/helper-module-transforms": "^7.23.0",
"@babel/helpers": "^7.23.2",
"@babel/parser": "^7.23.0",
"@babel/template": "^7.22.15",
"@babel/traverse": "^7.23.2",
"@babel/types": "^7.23.0",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.2.2",
"semver": "^6.3.0"
"json5": "^2.2.3",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
@ -1201,6 +1203,38 @@
"url": "https://opencollective.com/babel"
}
},
"node_modules/@babel/core/node_modules/@babel/generator": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dependencies": {
"@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core/node_modules/@babel/template": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dependencies": {
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core/node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@ -1213,6 +1247,7 @@
"version": "7.22.9",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz",
"integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==",
"dev": true,
"dependencies": {
"@babel/types": "^7.22.5",
"@jridgewell/gen-mapping": "^0.3.2",
@ -1571,13 +1606,26 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz",
"integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==",
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz",
"integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==",
"dependencies": {
"@babel/template": "^7.22.5",
"@babel/traverse": "^7.22.6",
"@babel/types": "^7.22.5"
"@babel/template": "^7.22.15",
"@babel/traverse": "^7.23.2",
"@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers/node_modules/@babel/template": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dependencies": {
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
@ -2863,6 +2911,7 @@
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
"integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.22.5",
"@babel/parser": "^7.22.5",
@ -3417,9 +3466,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz",
"integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==",
"version": "8.52.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz",
"integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -3435,12 +3484,12 @@
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
"version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.1",
"@humanwhocodes/object-schema": "^2.0.1",
"debug": "^4.1.1",
"minimatch": "^3.0.5"
},
@ -3462,9 +3511,9 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"dev": true
},
"node_modules/@iharbeck/ngx-virtual-scroller": {
@ -3711,9 +3760,9 @@
}
},
"node_modules/@ng-bootstrap/ng-bootstrap": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-15.1.1.tgz",
"integrity": "sha512-nZlIMMggtI3IHkGs0XPrUIUdpeEzQvfGV9M4I9IvCqiS2n4RwWoUvWK1ICo4csZqFNBDlCQx956gO6ZZUSL2mw==",
"version": "15.1.2",
"resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-15.1.2.tgz",
"integrity": "sha512-mM2yiGnt9o7KZLIFp8K1vjfmVfu7HR3d8dhH5SszfArbgn9DvvQ4P5D5TDGygzyBSzeyZe18p7I8rX8vgA6DKw==",
"dependencies": {
"tslib": "^2.3.0"
},
@ -3817,9 +3866,9 @@
}
},
"node_modules/@ngtools/webpack": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.6.tgz",
"integrity": "sha512-d8ZlZL6dOtWmHdjG9PTGBkdiJMcsXD2tp6WeFRVvTEuvCI3XvKsUXBvJDE+mZOhzn5pUEYt+1TR5DHjDZbME3w==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.9.tgz",
"integrity": "sha512-rOclD7FfT4OSwVA0nDnULbJS6TORJ0+sQiuT2ebaNFErYr3LOm6Zut05tnmzFw8q1cePrILbG+xpnbggNr9Pyw==",
"dev": true,
"engines": {
"node": "^16.14.0 || >=18.10.0",
@ -4243,13 +4292,13 @@
}
},
"node_modules/@schematics/angular": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.6.tgz",
"integrity": "sha512-fM09WPqST+nhVGV5Q3fhG7WKo96kgSVMsbz3wGS0DmTn4zge7ZWnrW3VvbxnMapmGoKa9DFPqdqNln4ADcdIMQ==",
"version": "16.2.9",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.9.tgz",
"integrity": "sha512-uiU2YbZRVHgk1N1DDsek/5CKhfpZ8myJYNJk8eHV5LswnXOP3aqvH23VhneaAgOYwK5fISC7eMG0pLVKMvFfZQ==",
"dev": true,
"dependencies": {
"@angular-devkit/core": "16.2.6",
"@angular-devkit/schematics": "16.2.6",
"@angular-devkit/core": "16.2.9",
"@angular-devkit/schematics": "16.2.9",
"jsonc-parser": "3.2.0"
},
"engines": {
@ -4434,9 +4483,9 @@
}
},
"node_modules/@types/d3": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.1.tgz",
"integrity": "sha512-lBpYmbHTCtFKO1DB1R7E9dXp9/g1F3JXSGOF7iKPZ+wRmYg/Q6tCRHODGOc5Qk25fJRe2PI60EDRf2HLPUncMA==",
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.2.tgz",
"integrity": "sha512-Y4g2Yb30ZJmmtqAJTqMRaqXwRawfvpdpVmyEYEcyGNhrQI/Zvkq3k7yE1tdN07aFSmNBfvmegMQ9Fe2qy9ZMhw==",
"dev": true,
"dependencies": {
"@types/d3-array": "*",
@ -4737,9 +4786,9 @@
}
},
"node_modules/@types/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ=="
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.6.tgz",
"integrity": "sha512-Mw671DVqoMHbjw0w4v2iiOro01dlT/WhWp5uwecBa0Wg8c+bcZOjgF1ndBnlaxhtvFCgTRBtsGivSVhrK/vnag=="
},
"node_modules/@types/geojson": {
"version": "7946.0.10",
@ -4769,9 +4818,9 @@
"dev": true
},
"node_modules/@types/luxon": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz",
"integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.3.tgz",
"integrity": "sha512-/BJF3NT0pRMuxrenr42emRUF67sXwcZCd+S1ksG/Fcf9O7C3kKCY4uJSbKBE4KDUIYr3WMsvfmWD8hRjXExBJQ==",
"dev": true
},
"node_modules/@types/mime": {
@ -4781,12 +4830,12 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.8.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz",
"integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==",
"version": "20.8.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
"integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
"dev": true,
"dependencies": {
"undici-types": "~5.25.1"
"undici-types": "~5.26.4"
}
},
"node_modules/@types/qs": {
@ -5322,6 +5371,12 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
"dev": true
},
"node_modules/@vitejs/plugin-basic-ssl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz",
@ -5839,6 +5894,17 @@
"ajv": "^8.8.2"
}
},
"node_modules/angular-animations": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/angular-animations/-/angular-animations-0.11.0.tgz",
"integrity": "sha512-P2RuOe+T97bhgGDLtOYK9V45QA5y+kFUxoJfRAua8Ymo0bI5lWyw8oiVmBoEIZUU+nooYoJvQXgVKuZJA7/z3g==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/animations": ">=6.0.0"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@ -6458,6 +6524,11 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
"node_modules/charts.css": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/charts.css/-/charts.css-1.1.0.tgz",
"integrity": "sha512-K1Qyb8ZKsu5cDrVbZeHECk/xSq6iOl8IDTR35uaMdhr/Vyyxvg9nYQy3KNB3aidxJ2E251afX5q2725N0uL3Vw=="
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@ -6724,7 +6795,8 @@
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"dev": true
},
"node_modules/cookie": {
"version": "0.5.0",
@ -7852,18 +7924,19 @@
}
},
"node_modules/eslint": {
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz",
"integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==",
"version": "8.52.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz",
"integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.2",
"@eslint/js": "8.51.0",
"@humanwhocodes/config-array": "^0.11.11",
"@eslint/js": "8.52.0",
"@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -10285,11 +10358,6 @@
"shell-quote": "^1.8.1"
}
},
"node_modules/lazysizes": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/lazysizes/-/lazysizes-5.3.2.tgz",
"integrity": "sha512-22UzWP+Vedi/sMeOr8O7FWimRVtiNJV2HCa+V8+peZOw6QbswN9k58VUhd7i6iK5bw5QkYrF01LJbeJe0PV8jg=="
},
"node_modules/less": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz",
@ -11160,6 +11228,19 @@
"rxjs": ">=6.4.0"
}
},
"node_modules/ng-lazyload-image": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/ng-lazyload-image/-/ng-lazyload-image-9.1.3.tgz",
"integrity": "sha512-GlajmzbKhQCvg9pcrASq4fe/MNv9KoifGe6N+xRbseaBrNj2uwU4Vwic041NlmAQFEkpDM1H2EJCAjjmJeF7Hg==",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": ">=11.0.0",
"@angular/core": ">=11.0.0",
"rxjs": ">=6.0.0"
}
},
"node_modules/ng-select2-component": {
"version": "13.0.9",
"resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.9.tgz",
@ -11188,9 +11269,9 @@
}
},
"node_modules/ngx-extended-pdf-viewer": {
"version": "18.0.2",
"resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-18.0.2.tgz",
"integrity": "sha512-CgRR39Fb0ZLsuzvNbiFPXuJL72bAqnvJyhytVQjsOo66ZQMnQA9dkU/YbkDKr25Btk/+PtAJmSkbBpPgyNxj1Q==",
"version": "18.1.4",
"resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-18.1.4.tgz",
"integrity": "sha512-adKahqatF/AgUfDLkEIpT1SQoBjDKgr+/0Tzdsy13uWvPgECw/2hzg1f0m0JvWkR2zeAUY44/yK5BBrOBnVbbQ==",
"dependencies": {
"lodash.deburr": "^4.1.0",
"tslib": "^2.3.0"
@ -14481,9 +14562,9 @@
}
},
"node_modules/undici-types": {
"version": "5.25.3",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==",
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/unicode-canonical-property-names-ecmascript": {

View File

@ -13,23 +13,23 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^16.2.9",
"@angular/cdk": "^16.2.8",
"@angular/common": "^16.2.9",
"@angular/compiler": "^16.2.9",
"@angular/core": "^16.2.9",
"@angular/forms": "^16.2.9",
"@angular/localize": "^16.2.9",
"@angular/platform-browser": "^16.2.9",
"@angular/platform-browser-dynamic": "^16.2.9",
"@angular/router": "^16.2.9",
"@angular/animations": "^16.2.12",
"@angular/cdk": "^16.2.11",
"@angular/common": "^16.2.12",
"@angular/compiler": "^16.2.12",
"@angular/core": "^16.2.12",
"@angular/forms": "^16.2.12",
"@angular/localize": "^16.2.12",
"@angular/platform-browser": "^16.2.12",
"@angular/platform-browser-dynamic": "^16.2.12",
"@angular/router": "^16.2.12",
"@fortawesome/fontawesome-free": "^6.4.2",
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
"@iplab/ngx-file-upload": "^16.0.2",
"@lithiumjs/angular": "^7.3.0",
"@lithiumjs/ngx-virtual-scroll": "^0.3.0",
"@microsoft/signalr": "^7.0.12",
"@ng-bootstrap/ng-bootstrap": "^15.1.1",
"@ng-bootstrap/ng-bootstrap": "^15.1.2",
"@ngneat/transloco": "^6.0.0",
"@ngneat/transloco-locale": "^5.1.1",
"@ngneat/transloco-persist-lang": "^5.0.0",
@ -38,16 +38,18 @@
"@popperjs/core": "^2.11.7",
"@swimlane/ngx-charts": "^20.1.2",
"@tweenjs/tween.js": "^21.0.0",
"@types/file-saver": "^2.0.5",
"@types/file-saver": "^2.0.6",
"angular-animations": "^0.11.0",
"bootstrap": "^5.3.1",
"charts.css": "^1.1.0",
"eventsource": "^2.0.2",
"file-saver": "^2.0.5",
"lazysizes": "^5.3.2",
"luxon": "^3.4.3",
"ng-circle-progress": "^1.7.1",
"ng-lazyload-image": "^9.1.3",
"ng-select2-component": "^13.0.9",
"ngx-color-picker": "^15.0.0",
"ngx-extended-pdf-viewer": "^18.0.2",
"ngx-extended-pdf-viewer": "^18.1.4",
"ngx-file-drop": "^16.0.0",
"ngx-slider-v2": "^16.0.2",
"ngx-stars": "^1.6.5",
@ -59,20 +61,20 @@
"zone.js": "^0.13.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.2.6",
"@angular-devkit/build-angular": "^16.2.9",
"@angular-eslint/builder": "^16.2.0",
"@angular-eslint/eslint-plugin": "^16.2.0",
"@angular-eslint/eslint-plugin-template": "^16.2.0",
"@angular-eslint/schematics": "^16.2.0",
"@angular-eslint/template-parser": "^16.2.0",
"@angular/cli": "^16.2.6",
"@angular/compiler-cli": "^16.2.9",
"@types/d3": "^7.4.1",
"@types/luxon": "^3.3.2",
"@types/node": "^20.8.6",
"@angular/cli": "^16.2.9",
"@angular/compiler-cli": "^16.2.12",
"@types/d3": "^7.4.2",
"@types/luxon": "^3.3.3",
"@types/node": "^20.8.10",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"eslint": "^8.51.0",
"eslint": "^8.52.0",
"jsonminify": "^0.4.2",
"karma-coverage": "~2.2.0",
"ts-node": "~10.9.1",

View File

@ -22,7 +22,7 @@ export class AdminGuard implements CanActivate {
}
this.toastr.error(this.translocoService.translate('toasts.unauthorized-1'));
this.router.navigateByUrl('/libraries');
this.router.navigateByUrl('/home');
return false;
})
);

View File

@ -6,9 +6,11 @@ export interface Member {
username: string;
email: string;
lastActive: string; // datetime
lastActiveUtc: string; // datetime
created: string; // datetime
createdUtc: string; // datetime
roles: string[];
libraries: Library[];
ageRestriction: AgeRestriction;
isPending: boolean;
}
}

View File

@ -1,10 +1,10 @@
import {inject, Pipe, PipeTransform} from '@angular/core';
import {Pipe, PipeTransform} from '@angular/core';
import { DayOfWeek } from 'src/app/_services/statistics.service';
import {translate, TranslocoService} from "@ngneat/transloco";
import {translate} from "@ngneat/transloco";
@Pipe({
name: 'dayOfWeek',
standalone: true
name: 'dayOfWeek',
standalone: true
})
export class DayOfWeekPipe implements PipeTransform {

View File

@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core';
import { FITTING_OPTION } from '../_models/reader-enums';
import { FITTING_OPTION } from '../manga-reader/_models/reader-enums';
@Pipe({
name: 'fittingIcon',

View File

@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core';
import { LayoutMode } from '../_models/layout-mode';
import { LayoutMode } from '../manga-reader/_models/layout-mode';
@Pipe({
name: 'layoutModeIcon',

View File

@ -18,8 +18,6 @@ export class ProviderImagePipe implements PipeTransform {
case ScrobbleProvider.Kavita:
return 'assets/images/logo-32.png';
}
return '';
}
}

View File

@ -1,9 +1,8 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Routes } from '@angular/router';
import { AdminGuard } from '../_guards/admin.guard';
import { DashboardComponent } from './dashboard/dashboard.component';
import { DashboardComponent } from '../admin/dashboard/dashboard.component';
const routes: Routes = [
export const routes: Routes = [
{path: '**', component: DashboardComponent, pathMatch: 'full', canActivate: [AdminGuard]},
{
path: '',
@ -15,9 +14,3 @@ const routes: Routes = [
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminRoutingModule { }

View File

@ -0,0 +1,14 @@
import { Routes } from "@angular/router";
import { AuthGuard } from "../_guards/auth.guard";
import { AllSeriesComponent } from "../all-series/_components/all-series/all-series.component";
export const routes: Routes = [
{path: '**', component: AllSeriesComponent, pathMatch: 'full', canActivate: [AuthGuard]},
{
path: '',
component: AllSeriesComponent,
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard],
}
];

View File

@ -0,0 +1,16 @@
import { Routes } from "@angular/router";
import { AdminGuard } from "../_guards/admin.guard";
import { AuthGuard } from "../_guards/auth.guard";
import { AnnouncementsComponent } from "../announcements/_components/announcements/announcements.component";
export const routes: Routes = [
{path: '**', component: AnnouncementsComponent, pathMatch: 'full', canActivate: [AuthGuard, AdminGuard]},
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard, AdminGuard],
children: [
{path: 'announcements', component: AnnouncementsComponent},
]
}
];

View File

@ -0,0 +1,10 @@
import { Routes } from '@angular/router';
import { BookReaderComponent } from '../book-reader/_components/book-reader/book-reader.component';
export const routes: Routes = [
{
path: ':chapterId',
component: BookReaderComponent,
}
];

View File

@ -0,0 +1,15 @@
import { Routes } from "@angular/router";
import { AuthGuard } from "../_guards/auth.guard";
import { BookmarksComponent } from "../bookmark/_components/bookmarks/bookmarks.component";
export const routes: Routes = [
{path: '**', component: BookmarksComponent, pathMatch: 'full', canActivate: [AuthGuard]},
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard],
children: [
{path: 'bookmarks', component: BookmarksComponent},
]
}
];

View File

@ -0,0 +1,17 @@
import { Routes } from '@angular/router';
import { AuthGuard } from '../_guards/auth.guard';
import { AllCollectionsComponent } from '../collections/_components/all-collections/all-collections.component';
import { CollectionDetailComponent } from '../collections/_components/collection-detail/collection-detail.component';
export const routes: Routes = [
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard],
children: [
{path: '', component: AllCollectionsComponent, pathMatch: 'full'},
{path: ':id', component: CollectionDetailComponent},
]
}
];

View File

@ -0,0 +1,13 @@
import { Routes } from '@angular/router';
import { AuthGuard } from '../_guards/auth.guard';
import { DashboardComponent } from '../dashboard/_components/dashboard.component';
export const routes: Routes = [
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard],
component: DashboardComponent,
}
];

View File

@ -0,0 +1,20 @@
import { Routes } from '@angular/router';
import { AuthGuard } from '../_guards/auth.guard';
import { LibraryAccessGuard } from '../_guards/library-access.guard';
import { LibraryDetailComponent } from '../library-detail/library-detail.component';
export const routes: Routes = [
{
path: ':libraryId',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard, LibraryAccessGuard],
component: LibraryDetailComponent
},
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard, LibraryAccessGuard],
component: LibraryDetailComponent
}
];

View File

@ -0,0 +1,15 @@
import { Routes } from '@angular/router';
import { MangaReaderComponent } from '../manga-reader/_components/manga-reader/manga-reader.component';
export const routes: Routes = [
{
path: ':chapterId',
component: MangaReaderComponent
},
{
// This will allow the MangaReader to have a list to use for next/prev chapters rather than natural sort order
path: ':chapterId/list/:listId',
component: MangaReaderComponent
}
];

View File

@ -0,0 +1,9 @@
import { Routes } from '@angular/router';
import { PdfReaderComponent } from '../pdf-reader/_components/pdf-reader/pdf-reader.component';
export const routes: Routes = [
{
path: ':chapterId',
component: PdfReaderComponent,
}
];

View File

@ -0,0 +1,18 @@
import { Routes } from "@angular/router";
import { AuthGuard } from "../_guards/auth.guard";
import { ReadingListDetailComponent } from "../reading-list/_components/reading-list-detail/reading-list-detail.component";
import { ReadingListsComponent } from "../reading-list/_components/reading-lists/reading-lists.component";
export const routes: Routes = [
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard],
children: [
{path: '', component: ReadingListsComponent, pathMatch: 'full'},
{path: ':id', component: ReadingListDetailComponent, pathMatch: 'full'},
]
},
{path: '**', component: ReadingListsComponent, pathMatch: 'full', canActivate: [AuthGuard]},
];

View File

@ -0,0 +1,43 @@
import { Routes } from '@angular/router';
import { UserLoginComponent } from '../registration/user-login/user-login.component';
import { ConfirmEmailChangeComponent } from '../registration/_components/confirm-email-change/confirm-email-change.component';
import { ConfirmEmailComponent } from '../registration/_components/confirm-email/confirm-email.component';
import { ConfirmMigrationEmailComponent } from '../registration/_components/confirm-migration-email/confirm-migration-email.component';
import { ConfirmResetPasswordComponent } from '../registration/_components/confirm-reset-password/confirm-reset-password.component';
import { RegisterComponent } from '../registration/_components/register/register.component';
import { ResetPasswordComponent } from '../registration/_components/reset-password/reset-password.component';
export const routes: Routes = [
{
path: '',
component: UserLoginComponent
},
{
path: 'login',
component: UserLoginComponent
},
{
path: 'confirm-email',
component: ConfirmEmailComponent,
},
{
path: 'confirm-migration-email',
component: ConfirmMigrationEmailComponent,
},
{
path: 'confirm-email-update',
component: ConfirmEmailChangeComponent,
},
{
path: 'register',
component: RegisterComponent,
},
{
path: 'reset-password',
component: ResetPasswordComponent
},
{
path: 'confirm-reset-password',
component: ConfirmResetPasswordComponent
}
];

View File

@ -0,0 +1,15 @@
import { Routes } from '@angular/router';
import { AuthGuard } from '../_guards/auth.guard';
import { UserPreferencesComponent } from '../user-settings/user-preferences/user-preferences.component';
export const routes: Routes = [
{path: '**', component: UserPreferencesComponent, pathMatch: 'full'},
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard],
children: [
{path: '', component: UserPreferencesComponent, pathMatch: 'full'},
]
}
];

View File

@ -0,0 +1,15 @@
import { Routes } from '@angular/router';
import { AuthGuard } from '../_guards/auth.guard';
import { WantToReadComponent } from '../want-to-read/_components/want-to-read/want-to-read.component';
export const routes: Routes = [
{path: '**', component: WantToReadComponent, pathMatch: 'full'},
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard],
children: [
{path: '', component: WantToReadComponent, pathMatch: 'full'},
]
}
];

View File

@ -95,10 +95,6 @@ export class ImageService {
return `${this.baseUrl}image/cover-upload?filename=${encodeURIComponent(filename)}&apiKey=${this.encodedKey}`;
}
updateErroredImage(event: any) {
event.target.src = this.placeholderImage;
}
updateErroredWebLinkImage(event: any) {
event.target.src = this.errorWebLinkImage;
}

View File

@ -2,9 +2,9 @@ import { HttpClient } from '@angular/common/http';
import {inject, Injectable} from '@angular/core';
import { environment } from 'src/environments/environment';
import { UserReadStatistics } from '../statistics/_models/user-read-statistics';
import { PublicationStatusPipe } from '../pipe/publication-status.pipe';
import { PublicationStatusPipe } from '../_pipes/publication-status.pipe';
import { map } from 'rxjs';
import { MangaFormatPipe } from '../pipe/manga-format.pipe';
import { MangaFormatPipe } from '../_pipes/manga-format.pipe';
import { FileExtensionBreakdown } from '../statistics/_models/file-breakdown';
import { TopUserRead } from '../statistics/_models/top-reads';
import { ReadHistoryEvent } from '../statistics/_models/read-history-event';

View File

@ -12,7 +12,7 @@ import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
import {ReactiveFormsModule} from "@angular/forms";
import {UserReview} from "../review-card/user-review";
import {SpoilerComponent} from "../spoiler/spoiler.component";
import {SafeHtmlPipe} from "../../pipe/safe-html.pipe";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {TranslocoDirective} from "@ngneat/transloco";
@Component({

View File

@ -6,9 +6,9 @@ import {ReviewCardModalComponent} from "../review-card-modal/review-card-modal.c
import {AccountService} from "../../_services/account.service";
import {ReviewSeriesModalComponent} from "../review-series-modal/review-series-modal.component";
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
import {DefaultValuePipe} from "../../pipe/default-value.pipe";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {ImageComponent} from "../../shared/image/image.component";
import {ProviderImagePipe} from "../../pipe/provider-image.pipe";
import {ProviderImagePipe} from "../../_pipes/provider-image.pipe";
import {TranslocoDirective} from "@ngneat/transloco";
@Component({

View File

@ -6,13 +6,13 @@ import {ExternalSeriesDetail, SeriesStaff} from "../../_models/series-detail/ext
import {SeriesService} from "../../_services/series.service";
import {ImageComponent} from "../../shared/image/image.component";
import {LoadingComponent} from "../../shared/loading/loading.component";
import {SafeHtmlPipe} from "../../pipe/safe-html.pipe";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {A11yClickDirective} from "../../shared/a11y-click.directive";
import {MetadataDetailComponent} from "../../series-detail/_components/metadata-detail/metadata-detail.component";
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";
import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component";
import {ImageService} from "../../_services/image.service";
import {PublicationStatusPipe} from "../../pipe/publication-status.pipe";
import {PublicationStatusPipe} from "../../_pipes/publication-status.pipe";
import {SeriesMetadata} from "../../_models/metadata/series-metadata";
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
import {ActionService} from "../../_services/action.service";

View File

@ -8,7 +8,7 @@ import {
ViewEncapsulation
} from '@angular/core';
import {CommonModule} from '@angular/common';
import {SafeHtmlPipe} from "../../pipe/safe-html.pipe";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {TranslocoDirective} from "@ngneat/transloco";
@Component({

View File

@ -12,9 +12,9 @@ import {PaginatedResult, Pagination} from "../../_models/pagination";
import {SortableHeader, SortEvent} from "../table/_directives/sortable-header.directive";
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
import {TranslocoModule} from "@ngneat/transloco";
import {DefaultValuePipe} from "../../pipe/default-value.pipe";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
import {UtcToLocalTimePipe} from "../../pipe/utc-to-local-time.pipe";
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
@Component({
selector: 'app-user-scrobble-history',

View File

@ -3,7 +3,7 @@ import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angula
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Member } from 'src/app/_models/auth/member';
import { AccountService } from 'src/app/_services/account.service';
import { SentenceCasePipe } from '../../../pipe/sentence-case.pipe';
import { SentenceCasePipe } from '../../../_pipes/sentence-case.pipe';
import { NgIf } from '@angular/common';
import {TranslocoDirective} from "@ngneat/transloco";

View File

@ -1,95 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AdminRoutingModule } from './admin-routing.module';
import { DashboardComponent } from './dashboard/dashboard.component';
import {
NgbAccordionModule,
NgbCollapse,
NgbDropdownModule,
NgbNavModule,
NgbTooltipModule,
NgbTypeaheadModule
} from '@ng-bootstrap/ng-bootstrap';
import { ManageLibraryComponent } from './manage-library/manage-library.component';
import { ManageUsersComponent } from './manage-users/manage-users.component';
import { LibraryAccessModalComponent } from './_modals/library-access-modal/library-access-modal.component';
import { DirectoryPickerComponent } from './_modals/directory-picker/directory-picker.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ResetPasswordModalComponent } from './_modals/reset-password-modal/reset-password-modal.component';
import { ManageSettingsComponent } from './manage-settings/manage-settings.component';
import { ManageSystemComponent } from './manage-system/manage-system.component';
import { InviteUserComponent } from './invite-user/invite-user.component';
import { RoleSelectorComponent } from './role-selector/role-selector.component';
import { LibrarySelectorComponent } from './library-selector/library-selector.component';
import { EditUserComponent } from './edit-user/edit-user.component';
import { UserSettingsModule } from '../user-settings/user-settings.module';
import { ManageMediaSettingsComponent } from './manage-media-settings/manage-media-settings.component';
import { ManageEmailSettingsComponent } from './manage-email-settings/manage-email-settings.component';
import { ManageTasksSettingsComponent } from './manage-tasks-settings/manage-tasks-settings.component';
import { ManageLogsComponent } from './manage-logs/manage-logs.component';
import { VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
import { ManageAlertsComponent } from './manage-alerts/manage-alerts.component';
import {ManageScrobbleErrorsComponent} from "./manage-scrobble-errors/manage-scrobble-errors.component";
import {DefaultValuePipe} from "../pipe/default-value.pipe";
import {LibraryTypePipe} from "../pipe/library-type.pipe";
import {TimeAgoPipe} from "../pipe/time-ago.pipe";
import {SentenceCasePipe} from "../pipe/sentence-case.pipe";
import {FilterPipe} from "../pipe/filter.pipe";
import {TagBadgeComponent} from "../shared/tag-badge/tag-badge.component";
import {LoadingComponent} from "../shared/loading/loading.component";
import {
SideNavCompanionBarComponent
} from "../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
import {RouterModule} from "@angular/router";
import {LicenseComponent} from "./license/license.component";
@NgModule({
imports: [
CommonModule,
AdminRoutingModule,
ReactiveFormsModule,
RouterModule,
FormsModule,
NgbNavModule,
NgbTooltipModule,
NgbTypeaheadModule,
NgbDropdownModule,
NgbAccordionModule,
UserSettingsModule,
VirtualScrollerModule,
ManageScrobbleErrorsComponent,
DefaultValuePipe,
LibraryTypePipe,
TimeAgoPipe,
SentenceCasePipe,
FilterPipe,
TagBadgeComponent,
LoadingComponent,
SideNavCompanionBarComponent,
NgbCollapse,
ManageUsersComponent,
DashboardComponent,
ManageLibraryComponent,
LibraryAccessModalComponent,
DirectoryPickerComponent,
ResetPasswordModalComponent,
ManageSettingsComponent,
ManageSystemComponent,
InviteUserComponent,
RoleSelectorComponent,
LibrarySelectorComponent,
EditUserComponent,
ManageMediaSettingsComponent,
ManageEmailSettingsComponent,
ManageTasksSettingsComponent,
ManageLogsComponent,
ManageAlertsComponent,
LicenseComponent
],
providers: []
})
export class AdminModule { }

View File

@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} f
import {ActivatedRoute, RouterLink} from '@angular/router';
import {Title} from '@angular/platform-browser';
import {NavService} from '../../_services/nav.service';
import {SentenceCasePipe} from '../../pipe/sentence-case.pipe';
import {SentenceCasePipe} from '../../_pipes/sentence-case.pipe';
import {LicenseComponent} from '../license/license.component';
import {ManageTasksSettingsComponent} from '../manage-tasks-settings/manage-tasks-settings.component';
import {ServerStatsComponent} from '../../statistics/_components/server-stats/server-stats.component';

View File

@ -5,7 +5,7 @@ import { AgeRestriction } from 'src/app/_models/metadata/age-restriction';
import { Library } from 'src/app/_models/library';
import { Member } from 'src/app/_models/auth/member';
import { AccountService } from 'src/app/_services/account.service';
import { SentenceCasePipe } from '../../pipe/sentence-case.pipe';
import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe';
import { RestrictionSelectorComponent } from '../../user-settings/restriction-selector/restriction-selector.component';
import { LibrarySelectorComponent } from '../library-selector/library-selector.component';
import { RoleSelectorComponent } from '../role-selector/role-selector.component';

View File

@ -47,10 +47,10 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="close()">
{{t('cancel')}}
<button type="button" class="btn btn-{{invited ? 'primary' : 'secondary'}}" (click)="close()">
{{invited ? t('cancel') : t('close')}}
</button>
<button type="button" class="btn btn-primary" (click)="invite()" [disabled]="isSending || !inviteForm.valid || emailLink !== ''">
<button *ngIf="!invited" type="button" class="btn btn-primary" (click)="invite()" [disabled]="isSending || !inviteForm.valid || emailLink !== ''">
<span *ngIf="isSending" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>{{isSending ? t('inviting') : t('invite')}}</span>
</button>

View File

@ -13,7 +13,7 @@ import { LibrarySelectorComponent } from '../library-selector/library-selector.c
import { RoleSelectorComponent } from '../role-selector/role-selector.component';
import { NgIf } from '@angular/common';
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {SafeHtmlPipe} from "../../pipe/safe-html.pipe";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
@Component({
selector: 'app-invite-user',

View File

@ -17,7 +17,7 @@ import { ServerService } from 'src/app/_services/server.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { FilterPipe } from '../../pipe/filter.pipe';
import { FilterPipe } from '../../_pipes/filter.pipe';
import { LoadingComponent } from '../../shared/loading/loading.component';
import { NgIf, NgFor } from '@angular/common';
import {TranslocoDirective} from "@ngneat/transloco";

View File

@ -7,7 +7,7 @@ import {ServerSettings} from '../_models/server-settings';
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {NgIf, NgTemplateOutlet} from '@angular/common';
import {translate, TranslocoModule, TranslocoService} from "@ngneat/transloco";
import {SafeHtmlPipe} from "../../pipe/safe-html.pipe";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {ServerService} from "../../_services/server.service";
@Component({

View File

@ -17,13 +17,13 @@ import { Library } from 'src/app/_models/library';
import { LibraryService } from 'src/app/_services/library.service';
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { SentenceCasePipe } from '../../pipe/sentence-case.pipe';
import { TimeAgoPipe } from '../../pipe/time-ago.pipe';
import { LibraryTypePipe } from '../../pipe/library-type.pipe';
import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe';
import { TimeAgoPipe } from '../../_pipes/time-ago.pipe';
import { LibraryTypePipe } from '../../_pipes/library-type.pipe';
import { RouterLink } from '@angular/router';
import { NgFor, NgIf } from '@angular/common';
import {translate, TranslocoModule} from "@ngneat/transloco";
import {DefaultDatePipe} from "../../pipe/default-date.pipe";
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
@Component({
selector: 'app-manage-library',

View File

@ -23,13 +23,13 @@ import {ScrobbleError} from "../../_models/scrobbling/scrobble-error";
import {SeriesService} from "../../_services/series.service";
import {EditSeriesModalComponent} from "../../cards/_modals/edit-series-modal/edit-series-modal.component";
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
import {FilterPipe} from "../../pipe/filter.pipe";
import {FilterPipe} from "../../_pipes/filter.pipe";
import {LoadingComponent} from "../../shared/loading/loading.component";
import {TranslocoModule} from "@ngneat/transloco";
import {DefaultDatePipe} from "../../pipe/default-date.pipe";
import {DefaultValuePipe} from "../../pipe/default-value.pipe";
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
import {UtcToLocalTimePipe} from "../../pipe/utc-to-local-time.pipe";
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
@Component({
selector: 'app-manage-scrobble-errors',

View File

@ -10,11 +10,11 @@ import {Job} from 'src/app/_models/job/job';
import {UpdateNotificationModalComponent} from 'src/app/shared/update-notification/update-notification-modal.component';
import {NgbModal, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {DownloadService} from 'src/app/shared/_services/download.service';
import {DefaultValuePipe} from '../../pipe/default-value.pipe';
import {DefaultValuePipe} from '../../_pipes/default-value.pipe';
import {AsyncPipe, DatePipe, NgFor, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
import {UtcToLocalTimePipe} from "../../pipe/utc-to-local-time.pipe";
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
interface AdhocTask {
name: string;

View File

@ -11,12 +11,7 @@
<li *ngFor="let member of members; let idx = index;" class="list-group-item no-hover">
<div>
<h4>
<span id="member-name--{{idx}}">{{member.username | titlecase}} </span>
<span *ngIf="member.username === loggedInUsername">
<i class="fas fa-star" aria-hidden="true"></i>
<span class="visually-hidden">{{t('you-alt')}}</span>
</span>
<span class="badge bg-secondary text-dark" *ngIf="member.isPending">{{t('pending-title')}}</span>
<span id="member-name--{{idx}}" [ngClass]="{'highlight': member.username === loggedInUsername}">{{member.username | titlecase}}</span>
<div class="float-end" *ngIf="canEditMember(member)">
<button class="btn btn-danger btn-sm me-2" (click)="deleteUser(member)"
placement="top" [ngbTooltip]="t('delete-user-tooltip')" [attr.aria-label]="t('delete-user-alt', {user: member.username | titlecase})">
@ -27,24 +22,37 @@
<i class="fa fa-pen" aria-hidden="true"></i>
</button>
<button *ngIf="member.isPending" class="btn btn-secondary btn-sm me-2" (click)="resendEmail(member)"
placement="top" [ngbTooltip]="t('resend-invite-tooltip')" [attr.aria-label]="t('resend-invite-alt', {user: member.username | titlecase})">{{t('resend')}}</button>
<button *ngIf="member.isPending" class="btn btn-secondary btn-sm me-2" (click)="setup(member)"
placement="top" [ngbTooltip]="t('setup-user-tooltip')" [attr.aria-label]="t('setup-user-alt', {user: member.username | titlecase})">Setup</button>
<ng-container *ngIf="member.isPending">
<button class="btn btn-secondary btn-sm me-2" (click)="resendEmail(member)"
placement="top" [ngbTooltip]="t('resend-invite-tooltip')" [attr.aria-label]="t('resend-invite-alt', {user: member.username | titlecase})"><i class="fa-solid fa-share-from-square" aria-hidden="true"></i></button>
<button class="btn btn-secondary btn-sm" (click)="setup(member)"
placement="top" [ngbTooltip]="t('setup-user-tooltip')" [attr.aria-label]="t('setup-user-alt', {user: member.username | titlecase})"><i class="fa-solid fa-sliders" aria-hidden="true"></i></button>
</ng-container>
<button *ngIf="!member.isPending" class="btn btn-secondary btn-sm" (click)="updatePassword(member)"
placement="top" [ngbTooltip]="t('change-password-tooltip')" [attr.aria-label]="t('change-password-alt', {user: member.username | titlecase})"><i class="fa fa-key" aria-hidden="true"></i></button>
</div>
</h4>
<div class="user-info">
<div>{{t('last-active-title')}}
<span>{{member.lastActive | date: 'short' | defaultDate}} <i class="presence fa fa-circle ms-1" [title]="t('online-now-tooltip')" aria-hidden="true" *ngIf="(messageHub.onlineUsers$ | async)?.includes(member.username)"></i></span>
<span class="badge bg-secondary text-dark ms-1 pending-badge" *ngIf="member.isPending; else activeTime">{{t('pending-title')}}</span>
<ng-template #activeTime>
<span>{{member.lastActiveUtc | utcToLocalTime | defaultDate}} <i class="presence fa fa-circle ms-1" [title]="t('online-now-tooltip')" aria-hidden="true" *ngIf="(messageHub.onlineUsers$ | async)?.includes(member.username)"></i></span>
</ng-template>
</div>
<div *ngIf="!hasAdminRole(member) && member.libraries.length > 0">
{{t('sharing-title')}}
<app-tag-badge *ngFor="let lib of member.libraries" class="col-auto">{{lib.name}}</app-tag-badge>
</div>
<div *ngIf="!hasAdminRole(member)">{{t('sharing-title')}} {{formatLibraries(member)}}</div>
<div class="row g-0">
<div>
{{t('roles-title')}} <span *ngIf="getRoles(member).length === 0; else showRoles">{{t('none')}}</span>
<div *ngIf="getRoles(member) as roles">
{{t('roles-title')}} <span *ngIf="roles.length === 0; else showRoles">{{null | defaultValue}}</span>
<ng-template #showRoles>
<app-tag-badge *ngFor="let role of getRoles(member)" class="col-auto">{{role}}</app-tag-badge>
<ng-container *ngIf="hasAdminRole(member); else allRoles">
<app-tag-badge class="col-auto">Admin</app-tag-badge>
</ng-container>
<ng-template #allRoles>
<app-tag-badge *ngFor="let role of roles" class="col-auto">{{role}}</app-tag-badge>
</ng-template>
</ng-template>
</div>
</div>

View File

@ -6,3 +6,11 @@
.user-info > div {
margin-top: 3px;
}
.highlight {
color: var(--primary-color);
}
.pending-badge {
font-size: 15px;
}

View File

@ -13,9 +13,12 @@ import {EditUserComponent} from '../edit-user/edit-user.component';
import {ServerService} from 'src/app/_services/server.service';
import {Router} from '@angular/router';
import {TagBadgeComponent} from '../../shared/tag-badge/tag-badge.component';
import {AsyncPipe, DatePipe, NgFor, NgIf, TitleCasePipe} from '@angular/common';
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
import {DefaultDatePipe} from "../../pipe/default-date.pipe";
import {AsyncPipe, DatePipe, NgClass, NgFor, NgIf, TitleCasePipe} from '@angular/common';
import {translate, TranslocoModule, TranslocoService} from "@ngneat/transloco";
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
@Component({
selector: 'app-manage-users',
@ -23,7 +26,7 @@ import {DefaultDatePipe} from "../../pipe/default-date.pipe";
styleUrls: ['./manage-users.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgFor, NgIf, NgbTooltip, TagBadgeComponent, AsyncPipe, TitleCasePipe, DatePipe, TranslocoModule, DefaultDatePipe]
imports: [NgFor, NgIf, NgbTooltip, TagBadgeComponent, AsyncPipe, TitleCasePipe, DatePipe, TranslocoModule, DefaultDatePipe, NgClass, DefaultValuePipe, ReadMoreComponent, UtcToLocalTimePipe]
})
export class ManageUsersComponent implements OnInit {
@ -31,24 +34,24 @@ export class ManageUsersComponent implements OnInit {
loggedInUsername = '';
loadingMembers = false;
translocoService = inject(TranslocoService);
cdRef = inject(ChangeDetectorRef);
private readonly translocoService = inject(TranslocoService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly memberService = inject(MemberService);
private readonly accountService = inject(AccountService);
private readonly modalService = inject(NgbModal);
private readonly toastr = inject(ToastrService);
private readonly confirmService = inject(ConfirmService);
public readonly messageHub = inject(MessageHubService);
private readonly serverService = inject(ServerService);
private readonly router = inject(Router);
constructor(private memberService: MemberService,
private accountService: AccountService,
private modalService: NgbModal,
private toastr: ToastrService,
private confirmService: ConfirmService,
public messageHub: MessageHubService,
private serverService: ServerService,
private router: Router) {
constructor() {
this.accountService.currentUser$.pipe(take(1)).subscribe((user) => {
if (user) {
this.loggedInUsername = user.username;
this.cdRef.markForCheck();
}
});
}
ngOnInit(): void {
@ -136,7 +139,7 @@ export class ManageUsersComponent implements OnInit {
formatLibraries(member: Member) {
if (member.libraries.length === 0) {
return this.translocoService.translate('manage-users.none');
return translate('manage-users.none');
}
return member.libraries.map(item => item.name).join(', ');

View File

@ -35,11 +35,11 @@ import {SeriesFilterV2} from "../../../_models/metadata/v2/series-filter-v2";
@Component({
selector: 'app-all-series',
templateUrl: './all-series.component.html',
styleUrls: ['./all-series.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
selector: 'app-all-series',
templateUrl: './all-series.component.html',
styleUrls: ['./all-series.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [SideNavCompanionBarComponent, NgIf, BulkOperationsComponent, CardDetailLayoutComponent, SeriesCardComponent, DecimalPipe, TranslocoDirective]
})
export class AllSeriesComponent implements OnInit {

View File

@ -1,22 +0,0 @@
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { AuthGuard } from "../_guards/auth.guard";
import { AllSeriesComponent } from "./_components/all-series/all-series.component";
const routes: Routes = [
{path: '**', component: AllSeriesComponent, pathMatch: 'full', canActivate: [AuthGuard]},
{
path: '',
component: AllSeriesComponent,
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard],
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AllSeriesRoutingModule { }

View File

@ -1,25 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AllSeriesRoutingModule } from './all-series-routing.module';
import { AllSeriesComponent } from './_components/all-series/all-series.component';
import {SeriesCardComponent} from "../cards/series-card/series-card.component";
import {BulkOperationsComponent} from "../cards/bulk-operations/bulk-operations.component";
import {CardDetailLayoutComponent} from "../cards/card-detail-layout/card-detail-layout.component";
import {
SideNavCompanionBarComponent
} from "../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
@NgModule({
imports: [
CommonModule,
AllSeriesRoutingModule,
SeriesCardComponent,
BulkOperationsComponent,
CardDetailLayoutComponent,
SideNavCompanionBarComponent,
AllSeriesComponent,
]
})
export class AllSeriesModule { }

View File

@ -1,24 +0,0 @@
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { AdminGuard } from "../_guards/admin.guard";
import { AuthGuard } from "../_guards/auth.guard";
import { AnnouncementsComponent } from "./_components/announcements/announcements.component";
const routes: Routes = [
{path: '**', component: AnnouncementsComponent, pathMatch: 'full', canActivate: [AuthGuard, AdminGuard]},
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard, AdminGuard],
children: [
{path: 'announcments', component: AnnouncementsComponent},
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AnnouncementsRoutingModule { }

View File

@ -1,25 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AnnouncementsComponent } from './_components/announcements/announcements.component';
import { ChangelogComponent } from './_components/changelog/changelog.component';
import { AnnouncementsRoutingModule } from './announcements-routing.module';
import {ReadMoreComponent} from "../shared/read-more/read-more.component";
import {LoadingComponent} from "../shared/loading/loading.component";
import {
SideNavCompanionBarComponent
} from "../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
@NgModule({
imports: [
CommonModule,
AnnouncementsRoutingModule,
ReadMoreComponent,
LoadingComponent,
SideNavCompanionBarComponent,
AnnouncementsComponent,
ChangelogComponent
]
})
export class AnnouncementsModule { }

View File

@ -8,50 +8,48 @@ const routes: Routes = [
{
path: 'admin',
canActivate: [AdminGuard],
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
},
{
path: 'collections',
canActivate: [AuthGuard],
loadChildren: () => import('./collections/collections.module').then(m => m.CollectionsModule)
loadChildren: () => import('./_routes/admin-routing.module').then(m => m.routes)
},
{
path: 'preferences',
canActivate: [AuthGuard],
loadChildren: () => import('./user-settings/user-settings.module').then(m => m.UserSettingsModule)
loadChildren: () => import('./_routes/user-settings-routing.module').then(m => m.routes)
},
{
path: 'collections',
loadChildren: () => import('./_routes/collections-routing.module').then(m => m.routes)
},
{
path: 'lists',
canActivate: [AuthGuard],
loadChildren: () => import('./reading-list/reading-list.module').then(m => m.ReadingListModule)
loadChildren: () => import('./_routes/reading-list-routing.module').then(m => m.routes)
},
{
path: 'registration',
loadChildren: () => import('../app/registration/registration.module').then(m => m.RegistrationModule)
loadChildren: () => import('./_routes/registration.router.module').then(m => m.routes)
},
{
path: 'login',
loadChildren: () => import('./_routes/registration.router.module').then(m => m.routes) // TODO: Refactor so we just use /registration/login going forward
},
{
path: 'announcements',
loadChildren: () => import('../app/announcements/announcements.module').then(m => m.AnnouncementsModule)
loadChildren: () => import('./_routes/announcements-routing.module').then(m => m.routes)
},
{
path: 'bookmarks',
loadChildren: () => import('../app/bookmark/bookmark.module').then(m => m.BookmarkModule)
loadChildren: () => import('./_routes/bookmark-routing.module').then(m => m.routes)
},
{
path: 'all-series',
loadChildren: () => import('../app/all-series/all-series.module').then(m => m.AllSeriesModule)
},
{
path: 'libraries',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule)
},
{
path: 'libraries',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule)
loadChildren: () => import('./_routes/all-series-routing.module').then(m => m.routes)
},
{
path: 'want-to-read',
loadChildren: () => import('../app/want-to-read/want-to-read.module').then(m => m.WantToReadModule)
loadChildren: () => import('./_routes/want-to-read-routing.module').then(m => m.routes)
},
{
path: 'home',
loadChildren: () => import('./_routes/dashboard-routing.module').then(m => m.routes)
},
{
path: 'library',
@ -61,30 +59,30 @@ const routes: Routes = [
{
path: ':libraryId',
pathMatch: 'full',
loadChildren: () => import('../app/library-detail/library-detail.module').then(m => m.LibraryDetailModule)
loadChildren: () => import('./_routes/library-detail-routing.module').then(m => m.routes)
},
{
path: ':libraryId/series/:seriesId',
pathMatch: 'full',
loadChildren: () => import('../app/series-detail/series-detail.module').then(m => m.SeriesDetailModule)
loadComponent: () => import('../app/series-detail/_components/series-detail/series-detail.component').then(c => c.SeriesDetailComponent)
},
{
path: ':libraryId/series/:seriesId/manga',
loadChildren: () => import('../app/manga-reader/manga-reader.module').then(m => m.MangaReaderModule)
loadChildren: () => import('./_routes/manga-reader.router.module').then(m => m.routes)
},
{
path: ':libraryId/series/:seriesId/book',
loadChildren: () => import('../app/book-reader/book-reader.module').then(m => m.BookReaderModule)
loadChildren: () => import('./_routes/book-reader.router.module').then(m => m.routes)
},
{
path: ':libraryId/series/:seriesId/pdf',
loadChildren: () => import('../app/pdf-reader/pdf-reader.module').then(m => m.PdfReaderModule)
loadChildren: () => import('./_routes/pdf-reader.router.module').then(m => m.routes)
},
]
},
{path: 'login', loadChildren: () => import('../app/registration/registration.module').then(m => m.RegistrationModule)},
{path: '**', pathMatch: 'full', redirectTo: 'libraries'},
{path: '**', pathMatch: 'prefix', redirectTo: 'libraries'},
{path: '**', pathMatch: 'full', redirectTo: 'home'},
{path: 'libraries', pathMatch: 'full', redirectTo: 'home'},
{path: '**', pathMatch: 'prefix', redirectTo: 'home'},
];
@NgModule({

View File

@ -2,7 +2,7 @@ import {
ChangeDetectionStrategy, ChangeDetectorRef,
Component,
DestroyRef,
ElementRef, EventEmitter,
ElementRef, EventEmitter, HostListener,
inject,
Input,
OnInit, Output,
@ -15,6 +15,7 @@ import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/
import {ReaderService} from "../../../_services/reader.service";
import {ToastrService} from "ngx-toastr";
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {KEY_CODES} from "../../../shared/_services/utility.service";
enum BookLineOverlayMode {
None = 0,
@ -52,6 +53,17 @@ export class BookLineOverlayComponent implements OnInit {
get BookLineOverlayMode() { return BookLineOverlayMode; }
constructor(private elementRef: ElementRef, private toastr: ToastrService) {}
@HostListener('window:keydown', ['$event'])
handleKeyPress(event: KeyboardEvent) {
if (event.key === KEY_CODES.ESC_KEY) {
this.reset();
this.cdRef.markForCheck();
event.stopPropagation();
event.preventDefault();
return;
}
}
ngOnInit() {
if (this.parent) {

View File

@ -1,4 +1,5 @@
<div class="container-flex {{darkMode ? 'dark-mode' : ''}} reader-container {{ColumnLayout}} {{WritingStyleClass}}" tabindex="0" #reader>
<div class="container-flex {{darkMode ? 'dark-mode' : ''}} reader-container {{ColumnLayout}} {{WritingStyleClass}}"
tabindex="0" #reader (click)="handleContainerClick($event)" [ngClass]="{'pointer' : cursorIsPointer}">
<ng-container *transloco="let t; read: 'book-reader'">
<div class="fixed-top" #stickyTop>
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">{{t('skip-header')}}</a>
@ -98,7 +99,8 @@
</app-drawer>
</div>
<div #readingSection class="reading-section {{ColumnLayout}} {{WritingStyleClass}}" [ngStyle]="{'width': PageWidthForPagination}" [ngClass]="{'immersive' : immersiveMode || !actionBarVisible}" [@isLoading]="isLoading">
<div #readingSection class="reading-section {{ColumnLayout}} {{WritingStyleClass}}" [ngStyle]="{'width': PageWidthForPagination}"
[ngClass]="{'immersive' : immersiveMode || !actionBarVisible}" [@isLoading]="isLoading" (click)="handleReaderClick($event)">
<ng-container *ngIf="clickToPaginate">
<div class="left {{clickOverlayClass('left')}} no-pointer-events no-observe"
@ -111,9 +113,8 @@
[ngStyle]="{height: PageHeightForPagination}"></div>
</ng-container>
<div #bookContainer class="book-container {{WritingStyleClass}}"
[ngClass]="{'immersive' : immersiveMode, 'pointer' : cursorIsPointer}"
(click)="handleReaderClick($event)"
(mousedown)="mouseDown($event)">
[ngClass]="{'immersive' : immersiveMode}"
(mousedown)="mouseDown($event)" >
<div #readingHtml class="book-content {{ColumnLayout}} {{WritingStyleClass}}"
[ngStyle]="{'max-height': ColumnHeight, 'max-width': VerticalBookContentWidth, 'width': VerticalBookContentWidth, 'column-width': ColumnWidth}"

View File

@ -184,7 +184,7 @@ $action-bar-height: 38px;
position: relative;
height: 100%;
// background-color: purple !important;
//background-color: purple !important;
&.column-layout-1 {
height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
@ -198,10 +198,10 @@ $action-bar-height: 38px;
// Fixes an issue where chrome will cut of margins, doesn't seem to affect other browsers
overflow: auto;
}
}
&.pointer {
cursor: pointer;
}
.pointer {
cursor: pointer;
}
.book-content {
@ -325,6 +325,10 @@ $action-bar-height: 38px;
}
$pagination-color: transparent;
$pagination-opacity: 0;
//$pagination-color: red;
//$pagination-opacity: 0.7;
.right {
@ -356,9 +360,9 @@ $pagination-color: transparent;
width: 18%;
z-index: 3;
background: $pagination-color;
opacity: $pagination-opacity;
border-color: transparent;
border: none !important;
opacity: 0;
outline: none;
&.immersive {
@ -376,11 +380,12 @@ $pagination-color: transparent;
top: $action-bar-height;
width: 20vw;
background: $pagination-color;
opacity: $pagination-opacity;
border-color: transparent;
border: none !important;
z-index: 3;
opacity: 0;
outline: none;
height: 100vw;
&.immersive {
top: 0px;

View File

@ -523,7 +523,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
handleScrollEvent() {
// Highlight the current chapter we are on
if (Object.keys(this.pageAnchors).length !== 0) {
// get the height of the document so we can capture markers that are halfway on the document viewport
// get the height of the document, so we can capture markers that are halfway on the document viewport
const verticalOffset = this.reader.nativeElement?.scrollTop || (this.scrollService.scrollPosition + (this.document.body.offsetHeight / 2));
const alreadyReached = Object.values(this.pageAnchors).filter((i: number) => i <= verticalOffset);
@ -576,7 +576,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
const chapterId = this.route.snapshot.paramMap.get('chapterId');
if (libraryId === null || seriesId === null || chapterId === null) {
this.router.navigateByUrl('/libraries');
this.router.navigateByUrl('/home');
return;
}
@ -618,7 +618,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.cdRef.markForCheck();
this.bookService.getBookInfo(this.chapterId).subscribe(info => {
if (this.readingListMode && info.seriesFormat !== MangaFormat.EPUB) {
// Redirect to the manga reader.
@ -711,6 +710,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} else if (event.key === KEY_CODES.LEFT_ARROW) {
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD);
} else if (event.key === KEY_CODES.ESC_KEY) {
const isHighlighting = window.getSelection()?.toString() != '';
if (isHighlighting) return;
this.closeReader();
} else if (event.key === KEY_CODES.SPACE) {
this.toggleDrawer();
@ -1590,12 +1591,14 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private isCursorOverLeftPaginationArea(event: MouseEvent): boolean {
const leftPaginationAreaEnd = window.innerWidth * 0.2;
//console.log('user clicked on ', event.clientX, ' and left pagination ends on ', leftPaginationAreaEnd);
return event.clientX <= leftPaginationAreaEnd;
}
private isCursorOverRightPaginationArea(event: MouseEvent): boolean {
const rightPaginationAreaStart = event.clientX >= window.innerWidth * 0.8;
return rightPaginationAreaStart;
const rightPaginationAreaStart = window.innerWidth * 0.8;
//console.log('user clicked on ', event.clientX, ' and right pagination starts at ', rightPaginationAreaStart);
return event.clientX >= rightPaginationAreaStart;
}
private isCursorOverPaginationArea(event: MouseEvent): boolean {
@ -1611,25 +1614,39 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.cdRef.markForCheck();
}
// Responsibile for handling pagination only
handleContainerClick(event: MouseEvent) {
//if (event.target)
console.log('target: ', event.target);
if (['action-bar'].some(className => (event.target as Element).classList.contains(className))) {
console.log('exiting early')
return;
}
if (this.isCursorOverLeftPaginationArea(event)) {
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD);
} else if (this.isCursorOverRightPaginationArea(event)) {
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)
} else {
this.toggleMenu(event);
}
}
handleReaderClick(event: MouseEvent) {
if (!this.clickToPaginate) {
event.preventDefault();
event.stopPropagation();
this.toggleMenu(event);
return;
}
const isHighlighting = window.getSelection()?.toString() != '';
if (isHighlighting) {
event.preventDefault();
event.stopPropagation();
return;
}
if (this.isCursorOverLeftPaginationArea(event)) {
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD);
} else if (this.isCursorOverRightPaginationArea(event)) {
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)
} else {
this.toggleMenu(event);
}
}
toggleMenu(event: MouseEvent) {

View File

@ -1,16 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
@Pipe({
name: 'safeStyle',
standalone: true
})
export class SafeStylePipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
transform(value: string): unknown {
return this.sanitizer.bypassSecurityTrustStyle(value);
}
}

View File

@ -1,30 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookReaderComponent } from './_components/book-reader/book-reader.component';
import { BookReaderRoutingModule } from './book-reader.router.module';
import { SafeStylePipe } from './_pipes/safe-style.pipe';
import { ReactiveFormsModule } from '@angular/forms';
import { NgbAccordionModule, NgbNavModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { ReaderSettingsComponent } from './_components/reader-settings/reader-settings.component';
import { TableOfContentsComponent } from './_components/table-of-contents/table-of-contents.component';
import {DrawerComponent} from "../shared/drawer/drawer.component";
@NgModule({
imports: [
CommonModule,
BookReaderRoutingModule,
ReactiveFormsModule,
NgbProgressbarModule,
NgbTooltipModule,
NgbTooltipModule,
NgbAccordionModule,
NgbNavModule,
DrawerComponent,
BookReaderComponent, SafeStylePipe, TableOfContentsComponent, ReaderSettingsComponent,
], exports: [
BookReaderComponent,
SafeStylePipe
]
})
export class BookReaderModule { }

View File

@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { BookReaderComponent } from './_components/book-reader/book-reader.component';
const routes: Routes = [
{
path: ':chapterId',
component: BookReaderComponent,
}
];
@NgModule({
imports: [RouterModule.forChild(routes), ],
exports: [RouterModule]
})
export class BookReaderRoutingModule { }

View File

@ -66,7 +66,7 @@ export class BookmarksComponent implements OnInit {
private readonly translocoService = inject(TranslocoService);
constructor(private readerService: ReaderService, private seriesService: SeriesService,
constructor(private readerService: ReaderService,
private downloadService: DownloadService, private toastr: ToastrService,
private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService,
public imageService: ImageService, private actionFactoryService: ActionFactoryService,

View File

@ -1,23 +0,0 @@
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { AuthGuard } from "../_guards/auth.guard";
import { BookmarksComponent } from "./_components/bookmarks/bookmarks.component";
const routes: Routes = [
{path: '**', component: BookmarksComponent, pathMatch: 'full', canActivate: [AuthGuard]},
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard],
children: [
{path: 'bookmarks', component: BookmarksComponent},
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class BookmarkRoutingModule { }

View File

@ -1,25 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookmarkRoutingModule } from './bookmark-routing.module';
import { BookmarksComponent } from './_components/bookmarks/bookmarks.component';
import {BulkOperationsComponent} from "../cards/bulk-operations/bulk-operations.component";
import {CardDetailLayoutComponent} from "../cards/card-detail-layout/card-detail-layout.component";
import {
SideNavCompanionBarComponent
} from "../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
import {CardItemComponent} from "../cards/card-item/card-item.component";
@NgModule({
imports: [
CommonModule,
BookmarkRoutingModule,
BulkOperationsComponent,
CardDetailLayoutComponent,
SideNavCompanionBarComponent,
CardItemComponent,
BookmarksComponent
]
})
export class BookmarkModule { }

View File

@ -17,7 +17,7 @@ import { CollectionTag } from 'src/app/_models/collection-tag';
import { ReadingList } from 'src/app/_models/reading-list';
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
import {CommonModule} from "@angular/common";
import {FilterPipe} from "../../../pipe/filter.pipe";
import {FilterPipe} from "../../../_pipes/filter.pipe";
import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
@Component({

View File

@ -42,18 +42,18 @@ import {CommonModule} from "@angular/common";
import {TypeaheadComponent} from "../../../typeahead/_components/typeahead.component";
import {CoverImageChooserComponent} from "../../cover-image-chooser/cover-image-chooser.component";
import {EditSeriesRelationComponent} from "../../edit-series-relation/edit-series-relation.component";
import {SentenceCasePipe} from "../../../pipe/sentence-case.pipe";
import {MangaFormatPipe} from "../../../pipe/manga-format.pipe";
import {DefaultDatePipe} from "../../../pipe/default-date.pipe";
import {TimeAgoPipe} from "../../../pipe/time-ago.pipe";
import {SentenceCasePipe} from "../../../_pipes/sentence-case.pipe";
import {MangaFormatPipe} from "../../../_pipes/manga-format.pipe";
import {DefaultDatePipe} from "../../../_pipes/default-date.pipe";
import {TimeAgoPipe} from "../../../_pipes/time-ago.pipe";
import {TagBadgeComponent} from "../../../shared/tag-badge/tag-badge.component";
import {PublicationStatusPipe} from "../../../pipe/publication-status.pipe";
import {BytesPipe} from "../../../pipe/bytes.pipe";
import {PublicationStatusPipe} from "../../../_pipes/publication-status.pipe";
import {BytesPipe} from "../../../_pipes/bytes.pipe";
import {ImageComponent} from "../../../shared/image/image.component";
import {DefaultValuePipe} from "../../../pipe/default-value.pipe";
import {DefaultValuePipe} from "../../../_pipes/default-value.pipe";
import {TranslocoModule} from "@ngneat/transloco";
import {TranslocoDatePipe} from "@ngneat/transloco-locale";
import {UtcToLocalTimePipe} from "../../../pipe/utc-to-local-time.pipe";
import {UtcToLocalTimePipe} from "../../../_pipes/utc-to-local-time.pipe";
enum TabID {
General = 0,

View File

@ -43,8 +43,8 @@ import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
import {EntityInfoCardsComponent} from "../entity-info-cards/entity-info-cards.component";
import {CoverImageChooserComponent} from "../cover-image-chooser/cover-image-chooser.component";
import {ChapterMetadataDetailComponent} from "../chapter-metadata-detail/chapter-metadata-detail.component";
import {DefaultDatePipe} from "../../pipe/default-date.pipe";
import {BytesPipe} from "../../pipe/bytes.pipe";
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
import {BytesPipe} from "../../_pipes/bytes.pipe";
import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component";
import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component";
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";

View File

@ -8,11 +8,14 @@
</span>
<span *ngIf="header !== undefined && header.length > 0">
{{header}}&nbsp;
<span class="badge bg-primary rounded-pill" [attr.aria-label]="t('total-items', {count: pagination.totalItems})" *ngIf="pagination !== undefined">{{pagination.totalItems}}</span>
<span class="badge bg-primary rounded-pill"
[attr.aria-label]="t('total-items', {count: pagination.totalItems})"
*ngIf="pagination !== undefined">{{pagination.totalItems}}</span>
</span>
</h2>
</div>
</div>
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter>
<div class="viewport-container" [ngClass]="{'empty': items.length === 0 && !isLoading}">
<div class="content-container">
@ -20,9 +23,13 @@
<p *ngIf="items.length === 0 && !isLoading">
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
</p>
<virtual-scroller [ngClass]="{'empty': items.length === 0 && !isLoading}" #scroll [items]="items" [bufferAmount]="1" [parentScroll]="parentScroll">
<virtual-scroller [ngClass]="{'empty': items.length === 0 && !isLoading}" #scroll [items]="items" [bufferAmount]="bufferAmount" [parentScroll]="parentScroll" >
<div class="grid row g-0" #container>
<div class="card col-auto mt-2 mb-2" (click)="tryToSaveJumpKey(item)" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
<div class="card col-auto mt-2 mb-2"
(click)="tryToSaveJumpKey(item)"
*ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}"
[attr.jumpbar-index]="i">
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: scroll.viewPortInfo.startIndexWithBuffer + i }"></ng-container>
</div>
</div>
@ -32,8 +39,9 @@
<ng-container *ngIf="jumpBarKeysToRender.length >= 4 && items.length > 0 && scroll.viewPortInfo.maxScrollPosition > 0" [ngTemplateOutlet]="jumpBar" [ngTemplateOutletContext]="{ id: 'jumpbar' }"></ng-container>
</div>
<ng-template #cardTemplate>
<virtual-scroller #scroll [items]="items" [bufferAmount]="1">
<virtual-scroller #scroll [items]="items" [bufferAmount]="bufferAmount">
<div class="grid row g-0" #container>
<div class="card col-auto mt-2 mb-2" (click)="tryToSaveJumpKey(item)" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>

Some files were not shown because too many files have changed in this diff Show More