mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Custom Theme Support (#1077)
* Started the migration to bootstrap 5. Introduced a breakpoint system that bootstrap reflects for our screens. * sr only migrated * mr/ml -> me/ms * pl/pr -> ps/pe * btn-block * removed input-group-append * Added form-label to all labels * Added some style overrides for inputs * Replaced form-group with mb-3 * Ignore journal files * Update media to d-flex/flex-grow-1 * Fixed reading list detail page * For develop builds, don't inline critical styles * Fixed some downstream security issues * Fixed a layout issue in series detail * Fixed issue with btn-light not having background color. Updated layout for series detail metadata * Cleaned up nav search * Laid out the organization for custom theme components. Update _inputs.scss with variable overrides and depending on theme, it will just work. * Lots of theming work * Added inputs to the theme page * Login and input placeholder changes - Fixed login screen centering issue on all devices - Changed the format of the login screen - Change the input placeholder color * Added checkbox styles * Refactored tagbadges and removed some ngdeep selectors * Added nav bar component and refactored some styles into event widget * Cleaned nav events again and made dedicated popover body * Finished pagination component * Fixed up some styles with buttons * refactored dropdown component * Update accordion component * Refactored breadcrumbs and rating star. Fixed a missing style for cards * Fixed some styling issues on person badge, added modal component, and some global styles * Finished moving everything within dark to component files * Fixed up filter buttons, move card styles into a component theme, fixed slider style * Refactored library card and grouped typeahead * Updated normal typeahead component and reduced amount of ngdeep selector * Refactored grid breakpoints to be available by css variable, but it's hardcoded into the app * Ensure breakpoints are defined per theme * Fixed up some styling overrides and customization for nav links and alt button * Removed some deep styles, moved css out of splash container and brough back labels for login page * Finished css variable refactor * Refactored all the theme variable definitions into files for each theme. * Added back bootstrap overrides * Added a note about bootstrap theme colors being not-possible to swap out at runtime * Cleaned up some dead code * Implemented the ability to set a custom theme on the site. Cleaned up misc code throughout. * Additional changes - Fixed nav where "kavita" was not hiding correctly on small viewports - Fixed search bar to make the behavior more consistent - Fixed accordion buttons - Changed accordion buttons to be more responsive - Added radio button colors - Fixed radios on theme test page - Changed login and reset password card layouts to be more consistent. - Added primary color shade for when darker shading is needed. * Built a basic site, allow the user to apply different themes, refactored nav service code out. * Implemented the ability update a user's theme * Added unit tests for Scan and Get Content in SiteThemeService. * Fixed a bug in the login code and Pref code which wasn't joining on SiteTheme table. Wrote Unit tests and the UI component to manage current theme. * Implemented scan so that it manages custom themes with unit tests * Component updates - Repositioning style ordering - Adding indicator override - Adding select styles * SignlaR integration, some fixes when creating custom entities, one single migration. Just login functionality left. * More ui updated - Added .no-hover to prevent hover on elements where not needed - Changed all selects I could find to appropriate class - Changed up nav tabs to work more like bootstrap tabs than pills - Added padding to top of some containers to make styles consistent - Added ability to change navbar fontawesome icon colors - removed some unecessary inline styling - Changed radio button to appropriate class - Toned down primate color, a bit too bright for dark theme. - Added ability to change button fontawesome icon color * nav-tab fix for series-detail * Added themes folder to gitignore * Adding card overlay * Fixing up light theme * Everything is done. Only bug is that color-scheme isn't being set properly from css variable. * Checkboxes have pointer by default. Confirm/Confirm email use default (dark) theme by default * Fixed an error where color-scheme wasn't reflecting correctly on themes on first load * Fixed user preferences not available on login * Changing dual radios to switches and color tweaks * disabled primary APCA fix * button APCA fixes * Fixed some timing issues with first load and image service * Fixed swiper issues from upgrade * Changed themes to be scss files again and adjusted Seed code * Migrated carousel to css variables. Fixed a broken animation for search. * Cleaned up some backend smells * Fixed white border outline on nav tabs, added some variables for header * Nav bar has been css variable-ified * Added some basic eink stuff to make the app useable Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
c776ca3b72
commit
568ea9fd3a
2
.gitignore
vendored
2
.gitignore
vendored
@ -510,11 +510,13 @@ UI/Web/dist/
|
|||||||
/API/config/backups/
|
/API/config/backups/
|
||||||
/API/config/cache/
|
/API/config/cache/
|
||||||
/API/config/temp/
|
/API/config/temp/
|
||||||
|
/API/config/themes/
|
||||||
/API/config/stats/
|
/API/config/stats/
|
||||||
/API/config/bookmarks/
|
/API/config/bookmarks/
|
||||||
/API/config/kavita.db
|
/API/config/kavita.db
|
||||||
/API/config/kavita.db-shm
|
/API/config/kavita.db-shm
|
||||||
/API/config/kavita.db-wal
|
/API/config/kavita.db-wal
|
||||||
|
/API/config/kavita.db-journal
|
||||||
/API/config/Hangfire.db
|
/API/config/Hangfire.db
|
||||||
/API/config/Hangfire-log.db
|
/API/config/Hangfire-log.db
|
||||||
API/config/covers/
|
API/config/covers/
|
||||||
|
264
API.Tests/Services/SiteThemeServiceTests.cs
Normal file
264
API.Tests/Services/SiteThemeServiceTests.cs
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Enums.Theme;
|
||||||
|
using API.Helpers;
|
||||||
|
using API.Services;
|
||||||
|
using API.Services.Tasks;
|
||||||
|
using API.SignalR;
|
||||||
|
using AutoMapper;
|
||||||
|
using Kavita.Common;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
|
public class SiteThemeServiceTests
|
||||||
|
{
|
||||||
|
private readonly ILogger<SiteThemeService> _logger = Substitute.For<ILogger<SiteThemeService>>();
|
||||||
|
private readonly IHubContext<MessageHub> _messageHub = Substitute.For<IHubContext<MessageHub>>();
|
||||||
|
|
||||||
|
private readonly DbConnection _connection;
|
||||||
|
private readonly DataContext _context;
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
||||||
|
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||||
|
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||||
|
private const string BackupDirectory = "C:/kavita/config/backups/";
|
||||||
|
private const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
||||||
|
private const string SiteThemeDirectory = "C:/kavita/config/themes/";
|
||||||
|
|
||||||
|
public SiteThemeServiceTests()
|
||||||
|
{
|
||||||
|
var contextOptions = new DbContextOptionsBuilder()
|
||||||
|
.UseSqlite(CreateInMemoryDatabase())
|
||||||
|
.Options;
|
||||||
|
_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
|
||||||
|
|
||||||
|
_context = new DataContext(contextOptions);
|
||||||
|
Task.Run(SeedDb).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
||||||
|
var mapper = config.CreateMapper();
|
||||||
|
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Setup
|
||||||
|
|
||||||
|
private static DbConnection CreateInMemoryDatabase()
|
||||||
|
{
|
||||||
|
var connection = new SqliteConnection("Filename=:memory:");
|
||||||
|
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> SeedDb()
|
||||||
|
{
|
||||||
|
await _context.Database.MigrateAsync();
|
||||||
|
var filesystem = CreateFileSystem();
|
||||||
|
|
||||||
|
await Seed.SeedSettings(_context, new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem));
|
||||||
|
|
||||||
|
var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();
|
||||||
|
setting.Value = CacheDirectory;
|
||||||
|
|
||||||
|
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync();
|
||||||
|
setting.Value = BackupDirectory;
|
||||||
|
|
||||||
|
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync();
|
||||||
|
setting.Value = BookmarkDirectory;
|
||||||
|
|
||||||
|
_context.ServerSetting.Update(setting);
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "Joe",
|
||||||
|
UserPreferences = new AppUserPreferences
|
||||||
|
{
|
||||||
|
Theme = Seed.DefaultThemes[1]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.Library.Add(new Library()
|
||||||
|
{
|
||||||
|
Name = "Manga",
|
||||||
|
Folders = new List<FolderPath>()
|
||||||
|
{
|
||||||
|
new FolderPath()
|
||||||
|
{
|
||||||
|
Path = "C:/data/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return await _context.SaveChangesAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MockFileSystem CreateFileSystem()
|
||||||
|
{
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
|
||||||
|
fileSystem.AddDirectory("C:/kavita/config/");
|
||||||
|
fileSystem.AddDirectory(CacheDirectory);
|
||||||
|
fileSystem.AddDirectory(CoverImageDirectory);
|
||||||
|
fileSystem.AddDirectory(BackupDirectory);
|
||||||
|
fileSystem.AddDirectory(BookmarkDirectory);
|
||||||
|
fileSystem.AddDirectory(SiteThemeDirectory);
|
||||||
|
fileSystem.AddDirectory("C:/data/");
|
||||||
|
|
||||||
|
return fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ResetDb()
|
||||||
|
{
|
||||||
|
_context.SiteTheme.RemoveRange(_context.SiteTheme);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Scan_ShouldFindCustomFile()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
var filesystem = CreateFileSystem();
|
||||||
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
|
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||||
|
await siteThemeService.Scan();
|
||||||
|
|
||||||
|
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Scan_ShouldOnlyInsertOnceOnSecondScan()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
var filesystem = CreateFileSystem();
|
||||||
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
|
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||||
|
await siteThemeService.Scan();
|
||||||
|
|
||||||
|
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
||||||
|
|
||||||
|
await siteThemeService.Scan();
|
||||||
|
|
||||||
|
var customThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()).Where(t =>
|
||||||
|
API.Parser.Parser.Normalize(t.Name).Equals(API.Parser.Parser.Normalize("custom")));
|
||||||
|
Assert.Single(customThemes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Scan_ShouldDeleteWhenFileDoesntExistOnSecondScan()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
var filesystem = CreateFileSystem();
|
||||||
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
|
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||||
|
await siteThemeService.Scan();
|
||||||
|
|
||||||
|
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
|
||||||
|
|
||||||
|
filesystem.RemoveFile($"{SiteThemeDirectory}custom.css");
|
||||||
|
await siteThemeService.Scan();
|
||||||
|
|
||||||
|
var customThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()).Where(t =>
|
||||||
|
API.Parser.Parser.Normalize(t.Name).Equals(API.Parser.Parser.Normalize("custom")));
|
||||||
|
|
||||||
|
Assert.Empty(customThemes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetContent_ShouldReturnContent()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
var filesystem = CreateFileSystem();
|
||||||
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
|
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||||
|
|
||||||
|
_context.SiteTheme.Add(new SiteTheme()
|
||||||
|
{
|
||||||
|
Name = "Custom",
|
||||||
|
NormalizedName = API.Parser.Parser.Normalize("Custom"),
|
||||||
|
Provider = ThemeProvider.User,
|
||||||
|
FileName = "custom.css",
|
||||||
|
IsDefault = false
|
||||||
|
});
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var content = await siteThemeService.GetContent((await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom")).Id);
|
||||||
|
Assert.NotNull(content);
|
||||||
|
Assert.NotEmpty(content);
|
||||||
|
Assert.Equal("123", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateDefault_ShouldHaveOneDefault()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
var filesystem = CreateFileSystem();
|
||||||
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
|
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||||
|
|
||||||
|
_context.SiteTheme.Add(new SiteTheme()
|
||||||
|
{
|
||||||
|
Name = "Custom",
|
||||||
|
NormalizedName = API.Parser.Parser.Normalize("Custom"),
|
||||||
|
Provider = ThemeProvider.User,
|
||||||
|
FileName = "custom.css",
|
||||||
|
IsDefault = false
|
||||||
|
});
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var customTheme = (await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom"));
|
||||||
|
|
||||||
|
await siteThemeService.UpdateDefault(customTheme.Id);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Assert.Equal(customTheme.Id, (await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateDefault_ShouldThrowOnInvalidId()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
var filesystem = CreateFileSystem();
|
||||||
|
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||||
|
var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
|
||||||
|
|
||||||
|
_context.SiteTheme.Add(new SiteTheme()
|
||||||
|
{
|
||||||
|
Name = "Custom",
|
||||||
|
NormalizedName = API.Parser.Parser.Normalize("Custom"),
|
||||||
|
Provider = ThemeProvider.User,
|
||||||
|
FileName = "custom.css",
|
||||||
|
IsDefault = false
|
||||||
|
});
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<KavitaException>(async () => await siteThemeService.UpdateDefault(10));
|
||||||
|
Assert.Equal("Theme file missing or invalid", ex.Message);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -310,4 +310,8 @@
|
|||||||
<Reference Include="System.Drawing.Common" />
|
<Reference Include="System.Drawing.Common" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="config\themes" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -106,7 +106,10 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
UserName = registerDto.Username,
|
UserName = registerDto.Username,
|
||||||
Email = registerDto.Email,
|
Email = registerDto.Email,
|
||||||
UserPreferences = new AppUserPreferences(),
|
UserPreferences = new AppUserPreferences
|
||||||
|
{
|
||||||
|
Theme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme()
|
||||||
|
},
|
||||||
ApiKey = HashUtil.ApiKey()
|
ApiKey = HashUtil.ApiKey()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -179,22 +182,23 @@ namespace API.Controllers
|
|||||||
|
|
||||||
// Update LastActive on account
|
// Update LastActive on account
|
||||||
user.LastActive = DateTime.Now;
|
user.LastActive = DateTime.Now;
|
||||||
user.UserPreferences ??= new AppUserPreferences();
|
user.UserPreferences ??= new AppUserPreferences
|
||||||
|
{
|
||||||
|
Theme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme()
|
||||||
|
};
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
_unitOfWork.UserRepository.Update(user);
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
_logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive);
|
_logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive);
|
||||||
|
|
||||||
return new UserDto
|
var dto = _mapper.Map<UserDto>(user);
|
||||||
{
|
dto.Token = await _tokenService.CreateToken(user);
|
||||||
Username = user.UserName,
|
dto.RefreshToken = await _tokenService.CreateRefreshToken(user);
|
||||||
Email = user.Email,
|
var pref = await _unitOfWork.UserRepository.GetPreferencesAsync(user.UserName);
|
||||||
Token = await _tokenService.CreateToken(user),
|
pref.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||||
RefreshToken = await _tokenService.CreateRefreshToken(user),
|
dto.Preferences = _mapper.Map<UserPreferencesDto>(pref);
|
||||||
ApiKey = user.ApiKey,
|
return dto;
|
||||||
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("refresh-token")]
|
[HttpPost("refresh-token")]
|
||||||
@ -358,7 +362,10 @@ namespace API.Controllers
|
|||||||
UserName = dto.Email,
|
UserName = dto.Email,
|
||||||
Email = dto.Email,
|
Email = dto.Email,
|
||||||
ApiKey = HashUtil.ApiKey(),
|
ApiKey = HashUtil.ApiKey(),
|
||||||
UserPreferences = new AppUserPreferences()
|
UserPreferences = new AppUserPreferences
|
||||||
|
{
|
||||||
|
Theme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
|
64
API/Controllers/ThemeController.cs
Normal file
64
API/Controllers/ThemeController.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.DTOs.Theme;
|
||||||
|
using API.Services;
|
||||||
|
using API.Services.Tasks;
|
||||||
|
using Kavita.Common;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
public class ThemeController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly ISiteThemeService _siteThemeService;
|
||||||
|
private readonly ITaskScheduler _taskScheduler;
|
||||||
|
|
||||||
|
public ThemeController(IUnitOfWork unitOfWork, ISiteThemeService siteThemeService, ITaskScheduler taskScheduler)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_siteThemeService = siteThemeService;
|
||||||
|
_taskScheduler = taskScheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<IEnumerable<SiteThemeDto>>> GetThemes()
|
||||||
|
{
|
||||||
|
return Ok(await _unitOfWork.SiteThemeRepository.GetThemeDtos());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize("RequireAdminRole")]
|
||||||
|
[HttpPost("scan")]
|
||||||
|
public ActionResult Scan()
|
||||||
|
{
|
||||||
|
_taskScheduler.ScanSiteThemes();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize("RequireAdminRole")]
|
||||||
|
[HttpPost("update-default")]
|
||||||
|
public async Task<ActionResult> UpdateDefault(UpdateDefaultSiteThemeDto dto)
|
||||||
|
{
|
||||||
|
await _siteThemeService.UpdateDefault(dto.ThemeId);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns css content to the UI. UI is expected to escape the content
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("download-content")]
|
||||||
|
public async Task<ActionResult<string>> GetThemeContent(int themeId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Ok(await _siteThemeService.GetContent(themeId));
|
||||||
|
}
|
||||||
|
catch (KavitaException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,7 +78,8 @@ namespace API.Controllers
|
|||||||
existingPreferences.BookReaderDarkMode = preferencesDto.BookReaderDarkMode;
|
existingPreferences.BookReaderDarkMode = preferencesDto.BookReaderDarkMode;
|
||||||
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
|
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
|
||||||
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
|
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
|
||||||
existingPreferences.SiteDarkMode = preferencesDto.SiteDarkMode;
|
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
||||||
|
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||||
|
|
||||||
|
30
API/DTOs/Theme/SiteThemeDto.cs
Normal file
30
API/DTOs/Theme/SiteThemeDto.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using API.Entities.Enums.Theme;
|
||||||
|
using API.Services;
|
||||||
|
|
||||||
|
namespace API.DTOs.Theme;
|
||||||
|
|
||||||
|
public class SiteThemeDto
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the Theme
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// File path to the content. Stored under <see cref="DirectoryService.SiteThemeDirectory"/>.
|
||||||
|
/// Must be a .css file
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Only one theme can have this. Will auto-set this as default for new user accounts
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Where did the theme come from
|
||||||
|
/// </summary>
|
||||||
|
public ThemeProvider Provider { get; set; }
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
public DateTime LastModified { get; set; }
|
||||||
|
public string Selector => "bg-" + Name.ToLower();
|
||||||
|
}
|
6
API/DTOs/Theme/UpdateDefaultSiteThemeDto.cs
Normal file
6
API/DTOs/Theme/UpdateDefaultSiteThemeDto.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace API.DTOs.Theme;
|
||||||
|
|
||||||
|
public class UpdateDefaultSiteThemeDto
|
||||||
|
{
|
||||||
|
public int ThemeId { get; set; }
|
||||||
|
}
|
@ -5,8 +5,8 @@ namespace API.DTOs
|
|||||||
{
|
{
|
||||||
public string Username { get; init; }
|
public string Username { get; init; }
|
||||||
public string Email { get; init; }
|
public string Email { get; init; }
|
||||||
public string Token { get; init; }
|
public string Token { get; set; }
|
||||||
public string RefreshToken { get; init; }
|
public string RefreshToken { get; set; }
|
||||||
public string ApiKey { get; init; }
|
public string ApiKey { get; init; }
|
||||||
public UserPreferencesDto Preferences { get; set; }
|
public UserPreferencesDto Preferences { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using API.Entities.Enums;
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
namespace API.DTOs
|
namespace API.DTOs
|
||||||
{
|
{
|
||||||
@ -16,6 +17,6 @@ namespace API.DTOs
|
|||||||
public string BookReaderFontFamily { get; set; }
|
public string BookReaderFontFamily { get; set; }
|
||||||
public bool BookReaderTapToPaginate { get; set; }
|
public bool BookReaderTapToPaginate { get; set; }
|
||||||
public ReadingDirection BookReaderReadingDirection { get; set; }
|
public ReadingDirection BookReaderReadingDirection { get; set; }
|
||||||
public bool SiteDarkMode { get; set; }
|
public SiteTheme Theme { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ namespace API.Data
|
|||||||
public DbSet<Person> Person { get; set; }
|
public DbSet<Person> Person { get; set; }
|
||||||
public DbSet<Genre> Genre { get; set; }
|
public DbSet<Genre> Genre { get; set; }
|
||||||
public DbSet<Tag> Tag { get; set; }
|
public DbSet<Tag> Tag { get; set; }
|
||||||
|
public DbSet<SiteTheme> SiteTheme { get; set; }
|
||||||
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
|
1391
API/Data/Migrations/20220215163317_SiteTheme.Designer.cs
generated
Normal file
1391
API/Data/Migrations/20220215163317_SiteTheme.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
79
API/Data/Migrations/20220215163317_SiteTheme.cs
Normal file
79
API/Data/Migrations/20220215163317_SiteTheme.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class SiteTheme : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SiteDarkMode",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "ThemeId",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SiteTheme",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
NormalizedName = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
FileName = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
IsDefault = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Provider = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
LastModified = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SiteTheme", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AppUserPreferences_ThemeId",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
column: "ThemeId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_AppUserPreferences_SiteTheme_ThemeId",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
column: "ThemeId",
|
||||||
|
principalTable: "SiteTheme",
|
||||||
|
principalColumn: "Id");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_AppUserPreferences_SiteTheme_ThemeId",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SiteTheme");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_AppUserPreferences_ThemeId",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ThemeId",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "SiteDarkMode",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.0");
|
modelBuilder.HasAnnotation("ProductVersion", "6.0.1");
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
{
|
{
|
||||||
@ -198,7 +198,7 @@ namespace API.Data.Migrations
|
|||||||
b.Property<int>("ScalingOption")
|
b.Property<int>("ScalingOption")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<bool>("SiteDarkMode")
|
b.Property<int?>("ThemeId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
@ -206,6 +206,8 @@ namespace API.Data.Migrations
|
|||||||
b.HasIndex("AppUserId")
|
b.HasIndex("AppUserId")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("ThemeId");
|
||||||
|
|
||||||
b.ToTable("AppUserPreferences");
|
b.ToTable("AppUserPreferences");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -687,6 +689,38 @@ namespace API.Data.Migrations
|
|||||||
b.ToTable("ServerSetting");
|
b.ToTable("ServerSetting");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.SiteTheme", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDefault")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastModified")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Provider")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("SiteTheme");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.Tag", b =>
|
modelBuilder.Entity("API.Entities.Tag", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@ -967,7 +1001,13 @@ namespace API.Data.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("API.Entities.SiteTheme", "Theme")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ThemeId");
|
||||||
|
|
||||||
b.Navigation("AppUser");
|
b.Navigation("AppUser");
|
||||||
|
|
||||||
|
b.Navigation("Theme");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
modelBuilder.Entity("API.Entities.AppUserProgress", b =>
|
||||||
|
107
API/Data/Repositories/SiteThemeRepository.cs
Normal file
107
API/Data/Repositories/SiteThemeRepository.cs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs.Theme;
|
||||||
|
using API.Entities;
|
||||||
|
using AutoMapper;
|
||||||
|
using AutoMapper.QueryableExtensions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Data.Repositories;
|
||||||
|
|
||||||
|
public interface ISiteThemeRepository
|
||||||
|
{
|
||||||
|
void Add(SiteTheme theme);
|
||||||
|
void Remove(SiteTheme theme);
|
||||||
|
void Update(SiteTheme siteTheme);
|
||||||
|
Task<IEnumerable<SiteThemeDto>> GetThemeDtos();
|
||||||
|
Task<SiteThemeDto> GetThemeDto(int themeId);
|
||||||
|
Task<SiteThemeDto> GetThemeDtoByName(string themeName);
|
||||||
|
Task<SiteTheme> GetDefaultTheme();
|
||||||
|
Task<IEnumerable<SiteTheme>> GetThemes();
|
||||||
|
|
||||||
|
Task<SiteTheme> GetThemeById(int themeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SiteThemeRepository : ISiteThemeRepository
|
||||||
|
{
|
||||||
|
private readonly DataContext _context;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public SiteThemeRepository(DataContext context, IMapper mapper)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(SiteTheme theme)
|
||||||
|
{
|
||||||
|
_context.Add(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(SiteTheme theme)
|
||||||
|
{
|
||||||
|
_context.Remove(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(SiteTheme siteTheme)
|
||||||
|
{
|
||||||
|
_context.Entry(siteTheme).State = EntityState.Modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<SiteThemeDto>> GetThemeDtos()
|
||||||
|
{
|
||||||
|
return await _context.SiteTheme
|
||||||
|
.ProjectTo<SiteThemeDto>(_mapper.ConfigurationProvider)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SiteThemeDto> GetThemeDtoByName(string themeName)
|
||||||
|
{
|
||||||
|
return await _context.SiteTheme
|
||||||
|
.Where(t => t.Name.Equals(themeName))
|
||||||
|
.ProjectTo<SiteThemeDto>(_mapper.ConfigurationProvider)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns default theme, if the default theme is not available, returns the dark theme
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<SiteTheme> GetDefaultTheme()
|
||||||
|
{
|
||||||
|
var result = await _context.SiteTheme
|
||||||
|
.Where(t => t.IsDefault)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return await _context.SiteTheme
|
||||||
|
.Where(t => t.NormalizedName == "dark")
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<SiteTheme>> GetThemes()
|
||||||
|
{
|
||||||
|
return await _context.SiteTheme
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SiteTheme> GetThemeById(int themeId)
|
||||||
|
{
|
||||||
|
return await _context.SiteTheme
|
||||||
|
.Where(t => t.Id == themeId)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SiteThemeDto> GetThemeDto(int themeId)
|
||||||
|
{
|
||||||
|
return await _context.SiteTheme
|
||||||
|
.Where(t => t.Id == themeId)
|
||||||
|
.ProjectTo<SiteThemeDto>(_mapper.ConfigurationProvider)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
}
|
@ -55,6 +55,7 @@ public interface IUserRepository
|
|||||||
Task<AppUser> GetUserByEmailAsync(string email);
|
Task<AppUser> GetUserByEmailAsync(string email);
|
||||||
Task<IEnumerable<AppUser>> GetAllUsers();
|
Task<IEnumerable<AppUser>> GetAllUsers();
|
||||||
|
|
||||||
|
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserRepository : IUserRepository
|
public class UserRepository : IUserRepository
|
||||||
@ -227,6 +228,15 @@ public class UserRepository : IUserRepository
|
|||||||
return await _context.AppUser.ToListAsync();
|
return await _context.AppUser.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId)
|
||||||
|
{
|
||||||
|
return await _context.AppUserPreferences
|
||||||
|
.Include(p => p.Theme)
|
||||||
|
.Where(p => p.Theme.Id == themeId)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||||
{
|
{
|
||||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||||
@ -244,7 +254,8 @@ public class UserRepository : IUserRepository
|
|||||||
|
|
||||||
public async Task<AppUserRating> GetUserRatingAsync(int seriesId, int userId)
|
public async Task<AppUserRating> GetUserRatingAsync(int seriesId, int userId)
|
||||||
{
|
{
|
||||||
return await _context.AppUserRating.Where(r => r.SeriesId == seriesId && r.AppUserId == userId)
|
return await _context.AppUserRating
|
||||||
|
.Where(r => r.SeriesId == seriesId && r.AppUserId == userId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,6 +263,8 @@ public class UserRepository : IUserRepository
|
|||||||
{
|
{
|
||||||
return await _context.AppUserPreferences
|
return await _context.AppUserPreferences
|
||||||
.Include(p => p.AppUser)
|
.Include(p => p.AppUser)
|
||||||
|
.Include(p => p.Theme)
|
||||||
|
.AsSplitQuery()
|
||||||
.SingleOrDefaultAsync(p => p.AppUser.UserName == username);
|
.SingleOrDefaultAsync(p => p.AppUser.UserName == username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@ -6,6 +7,7 @@ using System.Threading.Tasks;
|
|||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Enums.Theme;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
@ -21,6 +23,34 @@ namespace API.Data
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static IList<ServerSetting> DefaultSettings;
|
public static IList<ServerSetting> DefaultSettings;
|
||||||
|
|
||||||
|
public static readonly IList<SiteTheme> DefaultThemes = new List<SiteTheme>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Dark",
|
||||||
|
NormalizedName = Parser.Parser.Normalize("Dark"),
|
||||||
|
Provider = ThemeProvider.System,
|
||||||
|
FileName = "dark.scss",
|
||||||
|
IsDefault = true,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Light",
|
||||||
|
NormalizedName = Parser.Parser.Normalize("Light"),
|
||||||
|
Provider = ThemeProvider.System,
|
||||||
|
FileName = "light.scss",
|
||||||
|
IsDefault = false,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "E-Ink",
|
||||||
|
NormalizedName = Parser.Parser.Normalize("E-Ink"),
|
||||||
|
Provider = ThemeProvider.System,
|
||||||
|
FileName = "eink.scss",
|
||||||
|
IsDefault = false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
|
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
|
||||||
{
|
{
|
||||||
var roles = typeof(PolicyConstants)
|
var roles = typeof(PolicyConstants)
|
||||||
@ -41,6 +71,22 @@ namespace API.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task SeedThemes(DataContext context)
|
||||||
|
{
|
||||||
|
await context.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
foreach (var theme in DefaultThemes)
|
||||||
|
{
|
||||||
|
var existing = context.SiteTheme.FirstOrDefault(s => s.Name.Equals(theme.Name));
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
await context.SiteTheme.AddAsync(theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task SeedSettings(DataContext context, IDirectoryService directoryService)
|
public static async Task SeedSettings(DataContext context, IDirectoryService directoryService)
|
||||||
{
|
{
|
||||||
await context.Database.EnsureCreatedAsync();
|
await context.Database.EnsureCreatedAsync();
|
||||||
|
@ -21,6 +21,7 @@ public interface IUnitOfWork
|
|||||||
IPersonRepository PersonRepository { get; }
|
IPersonRepository PersonRepository { get; }
|
||||||
IGenreRepository GenreRepository { get; }
|
IGenreRepository GenreRepository { get; }
|
||||||
ITagRepository TagRepository { get; }
|
ITagRepository TagRepository { get; }
|
||||||
|
ISiteThemeRepository SiteThemeRepository { get; }
|
||||||
bool Commit();
|
bool Commit();
|
||||||
Task<bool> CommitAsync();
|
Task<bool> CommitAsync();
|
||||||
bool HasChanges();
|
bool HasChanges();
|
||||||
@ -56,6 +57,7 @@ public class UnitOfWork : IUnitOfWork
|
|||||||
public IPersonRepository PersonRepository => new PersonRepository(_context, _mapper);
|
public IPersonRepository PersonRepository => new PersonRepository(_context, _mapper);
|
||||||
public IGenreRepository GenreRepository => new GenreRepository(_context, _mapper);
|
public IGenreRepository GenreRepository => new GenreRepository(_context, _mapper);
|
||||||
public ITagRepository TagRepository => new TagRepository(_context, _mapper);
|
public ITagRepository TagRepository => new TagRepository(_context, _mapper);
|
||||||
|
public ISiteThemeRepository SiteThemeRepository => new SiteThemeRepository(_context, _mapper);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Commits changes to the DB. Completes the open transaction.
|
/// Commits changes to the DB. Completes the open transaction.
|
||||||
|
@ -58,11 +58,11 @@ namespace API.Entities
|
|||||||
/// Book Reader Option: What direction should the next/prev page buttons go
|
/// Book Reader Option: What direction should the next/prev page buttons go
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight;
|
public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UI Site Global Setting: Whether the UI should render in Dark mode or not.
|
/// UI Site Global Setting: The UI theme the user should use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SiteDarkMode { get; set; } = true;
|
/// <remarks>Should default to Dark</remarks>
|
||||||
|
public SiteTheme Theme { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
17
API/Entities/Enums/Theme/ThemeProvider.cs
Normal file
17
API/Entities/Enums/Theme/ThemeProvider.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace API.Entities.Enums.Theme;
|
||||||
|
|
||||||
|
public enum ThemeProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Theme is provided by System
|
||||||
|
/// </summary>
|
||||||
|
[Description("System")]
|
||||||
|
System = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// Theme is provided by the User (ie it's custom)
|
||||||
|
/// </summary>
|
||||||
|
[Description("User")]
|
||||||
|
User = 2
|
||||||
|
}
|
37
API/Entities/SiteTheme.cs
Normal file
37
API/Entities/SiteTheme.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using API.Entities.Enums.Theme;
|
||||||
|
using API.Entities.Interfaces;
|
||||||
|
using API.Services;
|
||||||
|
|
||||||
|
namespace API.Entities;
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a set of css overrides the user can upload to Kavita and will load into webui
|
||||||
|
/// </summary>
|
||||||
|
public class SiteTheme : IEntityDate
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the Theme
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized name for lookups
|
||||||
|
/// </summary>
|
||||||
|
public string NormalizedName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// File path to the content. Stored under <see cref="DirectoryService.SiteThemeDirectory"/>.
|
||||||
|
/// Must be a .css file
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Only one theme can have this. Will auto-set this as default for new user accounts
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Where did the theme come from
|
||||||
|
/// </summary>
|
||||||
|
public ThemeProvider Provider { get; set; }
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
public DateTime LastModified { get; set; }
|
||||||
|
}
|
@ -39,6 +39,7 @@ namespace API.Extensions
|
|||||||
services.AddScoped<IAccountService, AccountService>();
|
services.AddScoped<IAccountService, AccountService>();
|
||||||
services.AddScoped<IEmailService, EmailService>();
|
services.AddScoped<IEmailService, EmailService>();
|
||||||
services.AddScoped<IBookmarkService, BookmarkService>();
|
services.AddScoped<IBookmarkService, BookmarkService>();
|
||||||
|
services.AddScoped<ISiteThemeService, SiteThemeService>();
|
||||||
|
|
||||||
services.AddScoped<IFileSystem, FileSystem>();
|
services.AddScoped<IFileSystem, FileSystem>();
|
||||||
services.AddScoped<IFileService, FileService>();
|
services.AddScoped<IFileService, FileService>();
|
||||||
|
@ -7,6 +7,7 @@ using API.DTOs.Reader;
|
|||||||
using API.DTOs.ReadingLists;
|
using API.DTOs.ReadingLists;
|
||||||
using API.DTOs.Search;
|
using API.DTOs.Search;
|
||||||
using API.DTOs.Settings;
|
using API.DTOs.Settings;
|
||||||
|
using API.DTOs.Theme;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
@ -119,10 +120,14 @@ namespace API.Helpers
|
|||||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor)));
|
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor)));
|
||||||
|
|
||||||
|
|
||||||
|
CreateMap<AppUser, UserDto>();
|
||||||
|
CreateMap<SiteTheme, SiteThemeDto>();
|
||||||
|
CreateMap<AppUserPreferences, UserPreferencesDto>()
|
||||||
|
.ForMember(dest => dest.Theme,
|
||||||
|
opt =>
|
||||||
|
opt.MapFrom(src => src.Theme));
|
||||||
|
|
||||||
|
|
||||||
CreateMap<AppUserPreferences, UserPreferencesDto>();
|
|
||||||
|
|
||||||
CreateMap<AppUserBookmark, BookmarkDto>();
|
CreateMap<AppUserBookmark, BookmarkDto>();
|
||||||
|
|
||||||
CreateMap<ReadingList, ReadingListDto>();
|
CreateMap<ReadingList, ReadingListDto>();
|
||||||
@ -146,6 +151,7 @@ namespace API.Helpers
|
|||||||
CreateMap<RegisterDto, AppUser>();
|
CreateMap<RegisterDto, AppUser>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CreateMap<IEnumerable<ServerSetting>, ServerSettingDto>()
|
CreateMap<IEnumerable<ServerSetting>, ServerSettingDto>()
|
||||||
.ConvertUsing<ServerSettingConverter>();
|
.ConvertUsing<ServerSettingConverter>();
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,7 @@ namespace API
|
|||||||
|
|
||||||
await Seed.SeedRoles(roleManager);
|
await Seed.SeedRoles(roleManager);
|
||||||
await Seed.SeedSettings(context, directoryService);
|
await Seed.SeedSettings(context, directoryService);
|
||||||
|
await Seed.SeedThemes(context);
|
||||||
await Seed.SeedUserApiKeys(context);
|
await Seed.SeedUserApiKeys(context);
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ namespace API.Services
|
|||||||
string LogDirectory { get; }
|
string LogDirectory { get; }
|
||||||
string TempDirectory { get; }
|
string TempDirectory { get; }
|
||||||
string ConfigDirectory { get; }
|
string ConfigDirectory { get; }
|
||||||
|
string SiteThemeDirectory { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Original BookmarkDirectory. Only used for resetting directory. Use <see cref="ServerSettingKey.BackupDirectory"/> for actual path.
|
/// Original BookmarkDirectory. Only used for resetting directory. Use <see cref="ServerSettingKey.BackupDirectory"/> for actual path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -64,6 +65,7 @@ namespace API.Services
|
|||||||
public string TempDirectory { get; }
|
public string TempDirectory { get; }
|
||||||
public string ConfigDirectory { get; }
|
public string ConfigDirectory { get; }
|
||||||
public string BookmarkDirectory { get; }
|
public string BookmarkDirectory { get; }
|
||||||
|
public string SiteThemeDirectory { get; }
|
||||||
private readonly ILogger<DirectoryService> _logger;
|
private readonly ILogger<DirectoryService> _logger;
|
||||||
|
|
||||||
private static readonly Regex ExcludeDirectories = new Regex(
|
private static readonly Regex ExcludeDirectories = new Regex(
|
||||||
@ -81,6 +83,7 @@ namespace API.Services
|
|||||||
TempDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "temp");
|
TempDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "temp");
|
||||||
ConfigDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config");
|
ConfigDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config");
|
||||||
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
|
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
|
||||||
|
SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -22,6 +22,7 @@ public interface ITaskScheduler
|
|||||||
void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false);
|
void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false);
|
||||||
void CancelStatsTasks();
|
void CancelStatsTasks();
|
||||||
Task RunStatCollection();
|
Task RunStatCollection();
|
||||||
|
void ScanSiteThemes();
|
||||||
}
|
}
|
||||||
public class TaskScheduler : ITaskScheduler
|
public class TaskScheduler : ITaskScheduler
|
||||||
{
|
{
|
||||||
@ -35,6 +36,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
|
|
||||||
private readonly IStatsService _statsService;
|
private readonly IStatsService _statsService;
|
||||||
private readonly IVersionUpdaterService _versionUpdaterService;
|
private readonly IVersionUpdaterService _versionUpdaterService;
|
||||||
|
private readonly ISiteThemeService _siteThemeService;
|
||||||
|
|
||||||
public static BackgroundJobServer Client => new BackgroundJobServer();
|
public static BackgroundJobServer Client => new BackgroundJobServer();
|
||||||
private static readonly Random Rnd = new Random();
|
private static readonly Random Rnd = new Random();
|
||||||
@ -42,7 +44,8 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
|
|
||||||
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService,
|
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService,
|
||||||
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService,
|
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService,
|
||||||
ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService)
|
ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService,
|
||||||
|
ISiteThemeService siteThemeService)
|
||||||
{
|
{
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -53,6 +56,7 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
_cleanupService = cleanupService;
|
_cleanupService = cleanupService;
|
||||||
_statsService = statsService;
|
_statsService = statsService;
|
||||||
_versionUpdaterService = versionUpdaterService;
|
_versionUpdaterService = versionUpdaterService;
|
||||||
|
_siteThemeService = siteThemeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ScheduleTasks()
|
public async Task ScheduleTasks()
|
||||||
@ -124,6 +128,12 @@ public class TaskScheduler : ITaskScheduler
|
|||||||
BackgroundJob.Enqueue(() => _statsService.Send());
|
BackgroundJob.Enqueue(() => _statsService.Send());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ScanSiteThemes()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starting Site Theme scan");
|
||||||
|
BackgroundJob.Enqueue(() => _siteThemeService.Scan());
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region UpdateTasks
|
#region UpdateTasks
|
||||||
|
163
API/Services/Tasks/SiteThemeService.cs
Normal file
163
API/Services/Tasks/SiteThemeService.cs
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums.Theme;
|
||||||
|
using API.SignalR;
|
||||||
|
using Kavita.Common;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace API.Services.Tasks;
|
||||||
|
|
||||||
|
public interface ISiteThemeService
|
||||||
|
{
|
||||||
|
Task<string> GetContent(int themeId);
|
||||||
|
Task Scan();
|
||||||
|
Task UpdateDefault(int themeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SiteThemeService : ISiteThemeService
|
||||||
|
{
|
||||||
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IHubContext<MessageHub> _messageHub;
|
||||||
|
|
||||||
|
public SiteThemeService(IDirectoryService directoryService, IUnitOfWork unitOfWork, IHubContext<MessageHub> messageHub)
|
||||||
|
{
|
||||||
|
_directoryService = directoryService;
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_messageHub = messageHub;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given a themeId, return the content inside that file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="themeId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="KavitaException"></exception>
|
||||||
|
public async Task<string> GetContent(int themeId)
|
||||||
|
{
|
||||||
|
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
|
||||||
|
if (theme == null) throw new KavitaException("Theme file missing or invalid");
|
||||||
|
var themeFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName);
|
||||||
|
if (string.IsNullOrEmpty(themeFile) || !_directoryService.FileSystem.File.Exists(themeFile))
|
||||||
|
throw new KavitaException("Theme file missing or invalid");
|
||||||
|
|
||||||
|
return await _directoryService.FileSystem.File.ReadAllTextAsync(themeFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scans the site theme directory for custom css files and updates what the system has on store
|
||||||
|
/// </summary>
|
||||||
|
public async Task Scan()
|
||||||
|
{
|
||||||
|
_directoryService.ExistOrCreate(_directoryService.SiteThemeDirectory);
|
||||||
|
var reservedNames = Seed.DefaultThemes.Select(t => t.NormalizedName).ToList();
|
||||||
|
var themeFiles = _directoryService.GetFilesWithExtension(Parser.Parser.NormalizePath(_directoryService.SiteThemeDirectory), @"\.css")
|
||||||
|
.Where(name => !reservedNames.Contains(Parser.Parser.Normalize(name))).ToList();
|
||||||
|
|
||||||
|
var allThemes = (await _unitOfWork.SiteThemeRepository.GetThemes()).ToList();
|
||||||
|
var totalThemesToIterate = themeFiles.Count;
|
||||||
|
var themeIteratedCount = 0;
|
||||||
|
|
||||||
|
// First remove any files from allThemes that are User Defined and not on disk
|
||||||
|
var userThemes = allThemes.Where(t => t.Provider == ThemeProvider.User).ToList();
|
||||||
|
foreach (var userTheme in userThemes)
|
||||||
|
{
|
||||||
|
var filepath = Parser.Parser.NormalizePath(
|
||||||
|
_directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, userTheme.FileName));
|
||||||
|
if (!_directoryService.FileSystem.File.Exists(filepath))
|
||||||
|
{
|
||||||
|
// I need to do the removal different. I need to update all userpreferences to use DefaultTheme
|
||||||
|
allThemes.Remove(userTheme);
|
||||||
|
await RemoveTheme(userTheme);
|
||||||
|
|
||||||
|
await _messageHub.Clients.All.SendAsync(SignalREvents.SiteThemeProgress,
|
||||||
|
MessageFactory.SiteThemeProgressEvent(1, totalThemesToIterate, userTheme.FileName, 0F));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new custom themes
|
||||||
|
var allThemeNames = allThemes.Select(t => t.NormalizedName).ToList();
|
||||||
|
foreach (var themeFile in themeFiles)
|
||||||
|
{
|
||||||
|
var themeName =
|
||||||
|
Parser.Parser.Normalize(_directoryService.FileSystem.Path.GetFileNameWithoutExtension(themeFile));
|
||||||
|
if (allThemeNames.Contains(themeName))
|
||||||
|
{
|
||||||
|
themeIteratedCount += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_unitOfWork.SiteThemeRepository.Add(new SiteTheme()
|
||||||
|
{
|
||||||
|
Name = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(themeFile),
|
||||||
|
NormalizedName = themeName,
|
||||||
|
FileName = _directoryService.FileSystem.Path.GetFileName(themeFile),
|
||||||
|
Provider = ThemeProvider.User,
|
||||||
|
IsDefault = false,
|
||||||
|
});
|
||||||
|
await _messageHub.Clients.All.SendAsync(SignalREvents.SiteThemeProgress,
|
||||||
|
MessageFactory.SiteThemeProgressEvent(themeIteratedCount, totalThemesToIterate, themeName, themeIteratedCount / (totalThemesToIterate * 1.0f)));
|
||||||
|
themeIteratedCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (_unitOfWork.HasChanges())
|
||||||
|
{
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _messageHub.Clients.All.SendAsync(SignalREvents.SiteThemeProgress,
|
||||||
|
MessageFactory.SiteThemeProgressEvent(totalThemesToIterate, totalThemesToIterate, "", 1F));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the theme and any references to it from Pref and sets them to the default at the time.
|
||||||
|
/// This commits to DB.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="theme"></param>
|
||||||
|
private async Task RemoveTheme(SiteTheme theme)
|
||||||
|
{
|
||||||
|
var prefs = await _unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(theme.Id);
|
||||||
|
var defaultTheme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
|
||||||
|
foreach (var pref in prefs)
|
||||||
|
{
|
||||||
|
pref.Theme = defaultTheme;
|
||||||
|
_unitOfWork.UserRepository.Update(pref);
|
||||||
|
}
|
||||||
|
_unitOfWork.SiteThemeRepository.Remove(theme);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the themeId to the default theme, all others are marked as non-default
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="themeId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="KavitaException">If theme does not exist</exception>
|
||||||
|
public async Task UpdateDefault(int themeId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
|
||||||
|
if (theme == null) throw new KavitaException("Theme file missing or invalid");
|
||||||
|
|
||||||
|
foreach (var siteTheme in await _unitOfWork.SiteThemeRepository.GetThemes())
|
||||||
|
{
|
||||||
|
siteTheme.IsDefault = (siteTheme.Id == themeId);
|
||||||
|
_unitOfWork.SiteThemeRepository.Update(siteTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_unitOfWork.HasChanges()) return;
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await _unitOfWork.RollbackAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using API.DTOs.Update;
|
using API.DTOs.Update;
|
||||||
|
|
||||||
namespace API.SignalR
|
namespace API.SignalR
|
||||||
@ -160,5 +161,20 @@ namespace API.SignalR
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SignalRMessage SiteThemeProgressEvent(int themeIteratedCount, int totalThemesToIterate, string themeName, float progress)
|
||||||
|
{
|
||||||
|
return new SignalRMessage()
|
||||||
|
{
|
||||||
|
Name = SignalREvents.SiteThemeProgress,
|
||||||
|
Body = new
|
||||||
|
{
|
||||||
|
TotalUpdates = totalThemesToIterate,
|
||||||
|
CurrentCount = themeIteratedCount,
|
||||||
|
ThemeName = themeName,
|
||||||
|
Progress = progress
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,5 +54,10 @@
|
|||||||
/// A cover was updated
|
/// A cover was updated
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string CoverUpdate = "CoverUpdate";
|
public const string CoverUpdate = "CoverUpdate";
|
||||||
|
/// <summary>
|
||||||
|
/// A custom site theme was removed or added
|
||||||
|
/// </summary>
|
||||||
|
public const string SiteThemeProgress = "SiteThemeProgress";
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,4 +4,5 @@
|
|||||||
3. Run Kavita executable.
|
3. Run Kavita executable.
|
||||||
4. Open localhost:5000 and setup your account and libraries in the UI.
|
4. Open localhost:5000 and setup your account and libraries in the UI.
|
||||||
|
|
||||||
If updating, copy everything but the config/ directory over. Restart Kavita.
|
How to Update
|
||||||
|
1. Copy everything but the config/ directory over. Restart Kavita.
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
"vendorChunk": true,
|
"vendorChunk": true,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"buildOptimizer": false,
|
"buildOptimizer": false,
|
||||||
"optimization": true,
|
"optimization": false,
|
||||||
"namedChunks": true
|
"namedChunks": true
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
|
30
UI/Web/package-lock.json
generated
30
UI/Web/package-lock.json
generated
@ -2405,18 +2405,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ng-bootstrap/ng-bootstrap": {
|
"@ng-bootstrap/ng-bootstrap": {
|
||||||
"version": "11.0.0",
|
"version": "12.0.0-beta.4",
|
||||||
"resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-11.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-12.0.0-beta.4.tgz",
|
||||||
"integrity": "sha512-qDnB0+jbpQ4wjXpM4NPRAtwmgTDUCjGavoeRDZHOvFfYvx/MBf1RTjZEqTJ1Yqq1pKP4BWpzxCgVTunfnpmsjA==",
|
"integrity": "sha512-iOXZT4FLouAGJDRw4ruogyR+lg648nywNWKUxW7l+mtMC9i4kdpfo4beQ/nqb4Uq2zMDs9zj4MbKVI391+kMnA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ngtools/webpack": {
|
"@ngtools/webpack": {
|
||||||
@ -2581,6 +2574,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
|
||||||
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g=="
|
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g=="
|
||||||
},
|
},
|
||||||
|
"@popperjs/core": {
|
||||||
|
"version": "2.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
|
||||||
|
"integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA=="
|
||||||
|
},
|
||||||
"@schematics/angular": {
|
"@schematics/angular": {
|
||||||
"version": "13.2.3",
|
"version": "13.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-13.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-13.2.3.tgz",
|
||||||
@ -3841,9 +3839,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"bootstrap": {
|
"bootstrap": {
|
||||||
"version": "4.6.1",
|
"version": "5.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
|
||||||
"integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og=="
|
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q=="
|
||||||
},
|
},
|
||||||
"bowser": {
|
"bowser": {
|
||||||
"version": "2.11.0",
|
"version": "2.11.0",
|
||||||
@ -11848,9 +11846,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"swiper": {
|
"swiper": {
|
||||||
"version": "8.0.3",
|
"version": "8.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.0.6.tgz",
|
||||||
"integrity": "sha512-mpw7v/Lkh48LQUxtJuFD+3Lls8LViNi3j1fbk45fNo9DXZxXK/e7NMixxS27OxvC5wx+5H3bet1O2pdjk7akBA==",
|
"integrity": "sha512-Ssyu1+FeNATF/G8e84QG+ZUNtUOAZ5vngdgxzczh0oWZPhGUVgkdv+BoePUuaCXLAFXnwVpNjgLIcGnxMdmWPA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"dom7": "^4.0.4",
|
"dom7": "^4.0.4",
|
||||||
"ssr-window": "^4.0.2"
|
"ssr-window": "^4.0.2"
|
||||||
|
@ -28,11 +28,12 @@
|
|||||||
"@angular/router": "~13.2.2",
|
"@angular/router": "~13.2.2",
|
||||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||||
"@microsoft/signalr": "^6.0.2",
|
"@microsoft/signalr": "^6.0.2",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^12.0.0-beta.4",
|
||||||
"@ngx-lite/nav-drawer": "^0.4.7",
|
"@ngx-lite/nav-drawer": "^0.4.7",
|
||||||
"@ngx-lite/util": "0.0.1",
|
"@ngx-lite/util": "0.0.1",
|
||||||
|
"@popperjs/core": "^2.11.2",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.5",
|
||||||
"bootstrap": "^4.6.1",
|
"bootstrap": "^5.1.2",
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"lazysizes": "^5.3.2",
|
"lazysizes": "^5.3.2",
|
||||||
@ -41,7 +42,7 @@
|
|||||||
"ngx-file-drop": "^13.0.0",
|
"ngx-file-drop": "^13.0.0",
|
||||||
"ngx-toastr": "^14.2.1",
|
"ngx-toastr": "^14.2.1",
|
||||||
"rxjs": "~7.5.4",
|
"rxjs": "~7.5.4",
|
||||||
"swiper": "^8.0.3",
|
"swiper": "^8.0.6",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"webpack-bundle-analyzer": "^4.5.0",
|
"webpack-bundle-analyzer": "^4.5.0",
|
||||||
"zone.js": "~0.11.4"
|
"zone.js": "~0.11.4"
|
||||||
|
@ -2,29 +2,29 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">
|
<h4 class="modal-title" id="modal-basic-title">
|
||||||
{{series.name}} Review</h4>
|
{{series.name}} Review</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form [formGroup]="reviewGroup">
|
<form [formGroup]="reviewGroup">
|
||||||
<div class="form-group">
|
<div class="row g-0">
|
||||||
<label for="rating">Rating</label>
|
<label for="rating" class="form-label">Rating</label>
|
||||||
<div>
|
<div>
|
||||||
<ngb-rating style="margin-top: 2px; font-size: 1.5rem;" formControlName="rating"></ngb-rating>
|
<ngb-rating style="margin-top: 2px; font-size: 1.5rem;" formControlName="rating"></ngb-rating>
|
||||||
<button class="btn btn-information ml-2" (click)="clearRating()"><i aria-hidden="true" class="fa fa-ban"></i><span class="phone-hidden"> Clear</span></button>
|
<button class="btn btn-icon ms-2" (click)="clearRating()"><i aria-hidden="true" class="fa fa-ban"></i><span class="phone-hidden"> Clear</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="row g-0">
|
||||||
<label for="review">Review</label>
|
<label for="review" class="form-label">Review</label>
|
||||||
<textarea id="review" class="form-control" formControlName="review" rows="3"></textarea>
|
<textarea id="review" class="form-control" formControlName="review" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="submit" class="btn btn-secondary" (click)="close()">Close</button>
|
<button class="btn btn-secondary" (click)="close()">Close</button>
|
||||||
<button type="submit" class="btn btn-primary" (click)="save()">Save</button>
|
<button type="submit" class="btn btn-primary" (click)="save()">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
export interface SiteThemeProgressEvent {
|
||||||
|
totalUpdates: number;
|
||||||
|
currentCount: number;
|
||||||
|
themeName: string;
|
||||||
|
progress: number;
|
||||||
|
eventTime: string;
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { PageSplitOption } from './page-split-option';
|
|||||||
import { READER_MODE } from './reader-mode';
|
import { READER_MODE } from './reader-mode';
|
||||||
import { ReadingDirection } from './reading-direction';
|
import { ReadingDirection } from './reading-direction';
|
||||||
import { ScalingOption } from './scaling-option';
|
import { ScalingOption } from './scaling-option';
|
||||||
|
import { SiteTheme } from './site-theme';
|
||||||
|
|
||||||
export interface Preferences {
|
export interface Preferences {
|
||||||
// Manga Reader
|
// Manga Reader
|
||||||
@ -22,7 +23,7 @@ export interface Preferences {
|
|||||||
bookReaderReadingDirection: ReadingDirection;
|
bookReaderReadingDirection: ReadingDirection;
|
||||||
|
|
||||||
// Global
|
// Global
|
||||||
siteDarkMode: boolean;
|
theme: SiteTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}];
|
export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}];
|
||||||
|
22
UI/Web/src/app/_models/preferences/site-theme.ts
Normal file
22
UI/Web/src/app/_models/preferences/site-theme.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Where does the theme come from
|
||||||
|
*/
|
||||||
|
export enum ThemeProvider {
|
||||||
|
System = 1,
|
||||||
|
User = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme for the whole instance
|
||||||
|
*/
|
||||||
|
export interface SiteTheme {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
filePath: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
provider: ThemeProvider;
|
||||||
|
/**
|
||||||
|
* The actual class the root is defined against. It is generated at the backend.
|
||||||
|
*/
|
||||||
|
selector: string;
|
||||||
|
}
|
@ -7,6 +7,7 @@ import { Preferences } from '../_models/preferences/preferences';
|
|||||||
import { User } from '../_models/user';
|
import { User } from '../_models/user';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MessageHubService } from './message-hub.service';
|
import { MessageHubService } from './message-hub.service';
|
||||||
|
import { ThemeService } from '../theme.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -33,7 +34,7 @@ export class AccountService implements OnDestroy {
|
|||||||
private readonly onDestroy = new Subject<void>();
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
|
||||||
constructor(private httpClient: HttpClient, private router: Router,
|
constructor(private httpClient: HttpClient, private router: Router,
|
||||||
private messageHub: MessageHubService) {}
|
private messageHub: MessageHubService, private themeService: ThemeService) {}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onDestroy.next();
|
this.onDestroy.next();
|
||||||
@ -61,6 +62,7 @@ export class AccountService implements OnDestroy {
|
|||||||
map((response: User) => {
|
map((response: User) => {
|
||||||
const user = response;
|
const user = response;
|
||||||
if (user) {
|
if (user) {
|
||||||
|
console.log('Login: ', user);
|
||||||
this.setCurrentUser(user);
|
this.setCurrentUser(user);
|
||||||
this.messageHub.createHubConnection(user, this.hasAdminRole(user));
|
this.messageHub.createHubConnection(user, this.hasAdminRole(user));
|
||||||
}
|
}
|
||||||
@ -77,6 +79,11 @@ export class AccountService implements OnDestroy {
|
|||||||
|
|
||||||
localStorage.setItem(this.userKey, JSON.stringify(user));
|
localStorage.setItem(this.userKey, JSON.stringify(user));
|
||||||
localStorage.setItem(this.lastLoginKey, user.username);
|
localStorage.setItem(this.lastLoginKey, user.username);
|
||||||
|
if (user.preferences) {
|
||||||
|
this.themeService.setTheme(user.preferences.theme.name);
|
||||||
|
} else {
|
||||||
|
this.themeService.setTheme(this.themeService.defaultTheme);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentUserSource.next(user);
|
this.currentUserSource.next(user);
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { Inject, Injectable, OnDestroy } from '@angular/core';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { ThemeService } from '../theme.service';
|
||||||
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
import { RecentlyAddedItem } from '../_models/recently-added-item';
|
||||||
import { AccountService } from './account.service';
|
import { AccountService } from './account.service';
|
||||||
import { NavService } from './nav.service';
|
import { NavService } from './nav.service';
|
||||||
@ -19,9 +21,9 @@ export class ImageService implements OnDestroy {
|
|||||||
|
|
||||||
private onDestroy: Subject<void> = new Subject();
|
private onDestroy: Subject<void> = new Subject();
|
||||||
|
|
||||||
constructor(private navSerivce: NavService, private accountService: AccountService) {
|
constructor(private accountService: AccountService, private themeService: ThemeService) {
|
||||||
this.navSerivce.darkMode$.subscribe(res => {
|
this.themeService.currentTheme$.pipe(takeUntil(this.onDestroy)).subscribe(theme => {
|
||||||
if (res) {
|
if (this.themeService.isDarkTheme()) {
|
||||||
this.placeholderImage = 'assets/images/image-placeholder.dark-min.png';
|
this.placeholderImage = 'assets/images/image-placeholder.dark-min.png';
|
||||||
this.errorImage = 'assets/images/error-placeholder2.dark-min.png';
|
this.errorImage = 'assets/images/error-placeholder2.dark-min.png';
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import { EventEmitter, Injectable } from '@angular/core';
|
import { EventEmitter, Injectable } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
|
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
|
||||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component';
|
|
||||||
import { RefreshMetadataEvent } from '../_models/events/refresh-metadata-event';
|
|
||||||
import { ProgressEvent } from '../_models/events/scan-library-progress-event';
|
import { ProgressEvent } from '../_models/events/scan-library-progress-event';
|
||||||
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
|
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
|
||||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||||
|
import { SiteThemeProgressEvent } from '../_models/events/site-theme-progress-event';
|
||||||
import { User } from '../_models/user';
|
import { User } from '../_models/user';
|
||||||
|
|
||||||
export enum EVENTS {
|
export enum EVENTS {
|
||||||
@ -25,6 +23,10 @@ export enum EVENTS {
|
|||||||
BackupDatabaseProgress = 'BackupDatabaseProgress',
|
BackupDatabaseProgress = 'BackupDatabaseProgress',
|
||||||
CleanupProgress = 'CleanupProgress',
|
CleanupProgress = 'CleanupProgress',
|
||||||
DownloadProgress = 'DownloadProgress',
|
DownloadProgress = 'DownloadProgress',
|
||||||
|
/**
|
||||||
|
* A custom user site theme is added or removed during a scan
|
||||||
|
*/
|
||||||
|
SiteThemeProgress = 'SiteThemeProgress',
|
||||||
/**
|
/**
|
||||||
* A cover is updated
|
* A cover is updated
|
||||||
*/
|
*/
|
||||||
@ -122,6 +124,13 @@ export class MessageHubService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.hubConnection.on(EVENTS.SiteThemeProgress, resp => {
|
||||||
|
this.messagesSource.next({
|
||||||
|
event: EVENTS.SiteThemeProgress,
|
||||||
|
payload: resp.body as SiteThemeProgressEvent
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.hubConnection.on(EVENTS.SeriesAddedToCollection, resp => {
|
this.hubConnection.on(EVENTS.SeriesAddedToCollection, resp => {
|
||||||
this.messagesSource.next({
|
this.messagesSource.next({
|
||||||
event: EVENTS.SeriesAddedToCollection,
|
event: EVENTS.SeriesAddedToCollection,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ReplaySubject } from 'rxjs';
|
import { ReplaySubject } from 'rxjs';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -9,14 +9,7 @@ export class NavService {
|
|||||||
private navbarVisibleSource = new ReplaySubject<boolean>(1);
|
private navbarVisibleSource = new ReplaySubject<boolean>(1);
|
||||||
navbarVisible$ = this.navbarVisibleSource.asObservable();
|
navbarVisible$ = this.navbarVisibleSource.asObservable();
|
||||||
|
|
||||||
private darkMode: boolean = true;
|
constructor() {
|
||||||
private darkModeSource = new ReplaySubject<boolean>(1);
|
|
||||||
darkMode$ = this.darkModeSource.asObservable();
|
|
||||||
|
|
||||||
private renderer: Renderer2;
|
|
||||||
|
|
||||||
constructor(rendererFactory: RendererFactory2) {
|
|
||||||
this.renderer = rendererFactory.createRenderer(null, null);
|
|
||||||
this.showNavBar();
|
this.showNavBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,26 +20,4 @@ export class NavService {
|
|||||||
hideNavBar() {
|
hideNavBar() {
|
||||||
this.navbarVisibleSource.next(false);
|
this.navbarVisibleSource.next(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDarkMode() {
|
|
||||||
this.darkMode = !this.darkMode;
|
|
||||||
this.updateColorScheme();
|
|
||||||
this.darkModeSource.next(this.darkMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDarkMode(mode: boolean) {
|
|
||||||
this.darkMode = mode;
|
|
||||||
this.updateColorScheme();
|
|
||||||
this.darkModeSource.next(this.darkMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateColorScheme() {
|
|
||||||
if (this.darkMode) {
|
|
||||||
this.renderer.setStyle(document.querySelector('html'), 'color-scheme', 'dark');
|
|
||||||
} else {
|
|
||||||
this.renderer.setStyle(document.querySelector('html'), 'color-scheme', 'light');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,25 @@
|
|||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">Choose a Directory</h4>
|
<h4 class="modal-title" id="modal-basic-title">Choose a Directory</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="filter">Filter</label>
|
<label for="filter" class="form-label">Filter</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="filter" autocomplete="off" class="form-control" [(ngModel)]="filterQuery" type="text" aria-describedby="reset-input">
|
<input id="filter" autocomplete="off" class="form-control" [(ngModel)]="filterQuery" type="text" aria-describedby="reset-input">
|
||||||
<div class="input-group-append">
|
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="filterQuery = '';">Clear</button>
|
||||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="filterQuery = '';">Clear</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav aria-label="directory breadcrumb">
|
<nav aria-label="directory breadcrumb">
|
||||||
<ol class="breadcrumb" *ngIf="routeStack.peek() !== undefined; else noBreadcrumb">
|
<ol class="breadcrumb" *ngIf="routeStack.peek() !== undefined; else noBreadcrumb">
|
||||||
<li class="breadcrumb-item {{route === routeStack.peek() ? 'active' : ''}}" *ngFor="let route of routeStack.items; let index = index">
|
<li class="breadcrumb-item {{route === routeStack.peek() ? 'active' : ''}}" *ngFor="let route of routeStack.items; let index = index">
|
||||||
<ng-container *ngIf="route === routeStack.peek(); else nonActive">
|
<ng-container *ngIf="route === routeStack.peek(); else nonActive">
|
||||||
{{route}}
|
{{route}}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #nonActive>
|
<ng-template #nonActive>
|
||||||
<a href="javascript:void(0);" (click)="navigateTo(index)">{{route}}</a>
|
<a href="javascript:void(0);" (click)="navigateTo(index)">{{route}}</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<ng-template #noBreadcrumb>
|
<ng-template #noBreadcrumb>
|
||||||
<div class="breadcrumb">Select a folder to view breadcrumb. Don't see your directory, try checking / first.</div>
|
<div class="breadcrumb">Select a folder to view breadcrumb. Don't see your directory, try checking / first.</div>
|
||||||
@ -33,17 +28,17 @@
|
|||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<div class="list-group-item list-group-item-action">
|
<div class="list-group-item list-group-item-action">
|
||||||
<button (click)="goBack()" class="btn btn-secondary" [disabled]="routeStack.peek() === undefined">
|
<button (click)="goBack()" class="btn btn-secondary" [disabled]="routeStack.peek() === undefined">
|
||||||
<i class="fa fa-arrow-left mr-2" aria-hidden="true"></i>
|
<i class="fa fa-arrow-left me-2" aria-hidden="true"></i>
|
||||||
Back
|
Back
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" class="btn btn-primary float-right" [disabled]="routeStack.peek() === undefined" (click)="shareFolder('', $event)">Share</button>
|
<button type="button" class="btn btn-primary float-end" [disabled]="routeStack.peek() === undefined" (click)="shareFolder('', $event)">Share</button>
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="list-group scrollable">
|
<ul class="list-group scrollable">
|
||||||
<button *ngFor="let folder of folders | filter: filterFolder" class="list-group-item list-group-item-action" (click)="selectNode(folder)">
|
<button *ngFor="let folder of folders | filter: filterFolder" class="list-group-item list-group-item-action" (click)="selectNode(folder)">
|
||||||
<span>{{getStem(folder)}}</span>
|
<span>{{getStem(folder)}}</span>
|
||||||
<button type="button" class="btn btn-primary float-right" (click)="shareFolder(folder, $event)">Share</button>
|
<button type="button" class="btn btn-primary float-end" (click)="shareFolder(folder, $event)">Share</button>
|
||||||
</button>
|
</button>
|
||||||
<div class="list-group-item text-center" *ngIf="folders.length === 0">
|
<div class="list-group-item text-center" *ngIf="folders.length === 0">
|
||||||
There are no folders here
|
There are no folders here
|
||||||
@ -51,6 +46,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<a class="btn btn-info" *ngIf="helpUrl.length > 0" href="{{helpUrl}}" target="_blank">Help</a>
|
<a class="btn btn-icon" *ngIf="helpUrl.length > 0" href="{{helpUrl}}" target="_blank">Help</a>
|
||||||
<button type="button" class="btn btn-secondary" (click)="close()">Cancel</button>
|
<button type="button" class="btn btn-secondary" (click)="close()">Cancel</button>
|
||||||
</div>
|
</div>
|
@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">Library Access</h4>
|
<h4 class="modal-title" id="modal-basic-title">Library Access</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
@ -2,35 +2,35 @@
|
|||||||
<form [formGroup]="libraryForm">
|
<form [formGroup]="libraryForm">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{this.library !== undefined ? 'Edit' : 'New'}} Library</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{this.library !== undefined ? 'Edit' : 'New'}} Library</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="alert alert-info" *ngIf="errorMessage !== ''">
|
<div class="alert alert-info" *ngIf="errorMessage !== ''">
|
||||||
<strong>Error: </strong> {{errorMessage}}
|
<strong>Error: </strong> {{errorMessage}}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="library-name">Name</label>
|
<label for="library-name" class="form-label">Name</label>
|
||||||
<input id="library-name" class="form-control" formControlName="name" type="text">
|
<input id="library-name" class="form-control" formControlName="name" type="text">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="library-type">Type</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="typeTooltip" role="button" tabindex="0"></i>
|
<label for="library-type" class="form-label">Type</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="typeTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #typeTooltip>Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Book work the same way as Manga but fall back to embedded data.</ng-template>
|
<ng-template #typeTooltip>Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Book work the same way as Manga but fall back to embedded data.</ng-template>
|
||||||
<span class="sr-only" id="library-type-help">Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Book work the same way as Manga but fall back to embedded data.</span>
|
<span class="visually-hidden" id="library-type-help">Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Book work the same way as Manga but fall back to embedded data.</span>
|
||||||
<select class="form-control" id="library-type" formControlName="type" [attr.disabled]="this.library" aria-describedby="library-type-help">
|
<select class="form-select" id="library-type" formControlName="type" [attr.disabled]="this.library" aria-describedby="library-type-help">
|
||||||
<option [value]="i" *ngFor="let opt of libraryTypes; let i = index">{{opt}}</option>
|
<option [value]="i" *ngFor="let opt of libraryTypes; let i = index">{{opt}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<h4>Folders <button type="button" class="btn float-right btn-sm" (click)="openDirectoryPicker()"><i class="fa fa-plus" aria-hidden="true"></i></button></h4>
|
<h4>Folders <button type="button" class="btn float-end btn-sm" (click)="openDirectoryPicker()"><i class="fa fa-plus" aria-hidden="true"></i></button></h4>
|
||||||
|
|
||||||
<ul class="list-group" style="width: 100%">
|
<ul class="list-group" style="width: 100%">
|
||||||
<li class="list-group-item" *ngFor="let folder of selectedFolders; let i = index">
|
<li class="list-group-item" *ngFor="let folder of selectedFolders; let i = index">
|
||||||
{{folder}}
|
{{folder}}
|
||||||
<button class="btn float-right btn-sm" (click)="removeFolder(folder)"><i class="fa fa-times-circle" aria-hidden="true"></i></button>
|
<button class="btn float-end btn-sm" (click)="removeFolder(folder)"><i class="fa fa-times-circle" aria-hidden="true"></i></button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
<form [formGroup]="resetPasswordForm">
|
<form [formGroup]="resetPasswordForm">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">Reset {{member.username | sentenceCase}}'s Password</h4>
|
<h4 class="modal-title" id="modal-basic-title">Reset {{member.username | sentenceCase}}'s Password</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="alert alert-info" *ngIf="errorMessage !== ''">
|
<div class="alert alert-info" *ngIf="errorMessage !== ''">
|
||||||
<strong>Error: </strong> {{errorMessage}}
|
<strong>Error: </strong> {{errorMessage}}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="password">New Password</label>
|
<label for="password" class="form-label">New Password</label>
|
||||||
<input id="password" class="form-control" minlength="4" formControlName="password" type="password">
|
<input id="password" class="form-control" minlength="4" formControlName="password" type="password">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,13 +3,13 @@
|
|||||||
<div class="card w-100 mb-2" style="width: 18rem;">
|
<div class="card w-100 mb-2" style="width: 18rem;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title">{{update.updateTitle}}
|
<h4 class="card-title">{{update.updateTitle}}
|
||||||
<span class="badge badge-secondary" *ngIf="update.updateVersion === installedVersion">Installed</span>
|
<span class="badge bg-secondary" *ngIf="update.updateVersion === installedVersion">Installed</span>
|
||||||
<span class="badge badge-secondary" *ngIf="update.updateVersion > installedVersion">Available</span>
|
<span class="badge bg-secondary" *ngIf="update.updateVersion > installedVersion">Available</span>
|
||||||
</h4>
|
</h4>
|
||||||
<h6 class="card-subtitle mb-2 text-muted">Published: {{update.publishDate | date: 'short'}}</h6>
|
<h6 class="card-subtitle mb-2 text-muted">Published: {{update.publishDate | date: 'short'}}</h6>
|
||||||
|
|
||||||
<pre class="card-text update-body" [innerHtml]="update.updateBody | safeHtml"></pre>
|
<pre class="card-text update-body" [innerHtml]="update.updateBody | safeHtml"></pre>
|
||||||
<a *ngIf="!update.isDocker" href="{{update.updateUrl}}" class="btn btn-{{indx === 0 ? 'primary' : 'secondary'}} float-right" target="_blank">Download</a>
|
<a *ngIf="!update.isDocker" href="{{update.updateUrl}}" class="btn btn-{{indx === 0 ? 'primary' : 'secondary'}} float-end" target="_blank">Download</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Admin Dashboard</h2>
|
<h2>Admin Dashboard</h2>
|
||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs nav-pills">
|
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">
|
||||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||||
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | sentenceCase }}</a>
|
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | sentenceCase }}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.container {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
@ -1,16 +1,16 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">Edit {{member.username | sentenceCase}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">Edit {{member.username | sentenceCase}}</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<form [formGroup]="userForm">
|
<form [formGroup]="userForm">
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col-md-6 col-sm-12 pr-2">
|
<div class="col-md-6 col-sm-12 pe-2">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="username">Username</label>
|
<label for="username" class="form-label">Username</label>
|
||||||
<input id="username" class="form-control" formControlName="username" type="text">
|
<input id="username" class="form-control" formControlName="username" type="text">
|
||||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="userForm.dirty || userForm.touched">
|
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="userForm.dirty || userForm.touched">
|
||||||
<div *ngIf="userForm.get('username')?.errors?.required">
|
<div *ngIf="userForm.get('username')?.errors?.required">
|
||||||
@ -20,8 +20,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-sm-12">
|
<div class="col-md-6 col-sm-12">
|
||||||
<div class="form-group" style="width:100%">
|
<div class="mb-3" style="width:100%">
|
||||||
<label for="email">Email</label>
|
<label for="email" class="form-label">Email</label>
|
||||||
<input class="form-control" type="email" id="email" formControlName="email" [disabled]="true">
|
<input class="form-control" type="email" id="email" formControlName="email" [disabled]="true">
|
||||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="userForm.dirty || userForm.touched">
|
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="userForm.dirty || userForm.touched">
|
||||||
<div *ngIf="userForm.get('email')?.errors?.required">
|
<div *ngIf="userForm.get('email')?.errors?.required">
|
||||||
@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true" [member]="member"></app-role-selector>
|
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true" [member]="member"></app-role-selector>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">Invite User</h4>
|
<h4 class="modal-title" id="modal-basic-title">Invite User</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -16,9 +16,9 @@
|
|||||||
|
|
||||||
|
|
||||||
<form [formGroup]="inviteForm">
|
<form [formGroup]="inviteForm">
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="form-group" style="width:100%">
|
<div class="mb-3" style="width:100%">
|
||||||
<label for="email">Email</label>
|
<label for="email" class="form-label">Email</label>
|
||||||
<input class="form-control" type="email" id="email" formControlName="email" required>
|
<input class="form-control" type="email" id="email" formControlName="email" required>
|
||||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="inviteForm.dirty || inviteForm.touched">
|
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="inviteForm.dirty || inviteForm.touched">
|
||||||
<div *ngIf="email?.errors?.required">
|
<div *ngIf="email?.errors?.required">
|
||||||
@ -33,7 +33,7 @@
|
|||||||
<a class="email-link" href="{{emailLink}}" target="_blank">{{emailLink}}</a>
|
<a class="email-link" href="{{emailLink}}" target="_blank">{{emailLink}}</a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true"></app-role-selector>
|
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true"></app-role-selector>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-8"><h3>Libraries</h3></div>
|
<div class="col-8"><h3>Libraries</h3></div>
|
||||||
<div class="col-4"><button class="btn btn-primary float-right" (click)="addLibrary()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Add Library</span></button></div>
|
<div class="col-4"><button class="btn btn-primary float-end" (click)="addLibrary()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Add Library</span></button></div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-group" *ngIf="!createLibraryToggle; else createLibrary">
|
<ul class="list-group" *ngIf="!createLibraryToggle; else createLibrary">
|
||||||
<li *ngFor="let library of libraries; let idx = index; trackby: trackbyLibrary" class="list-group-item">
|
<li *ngFor="let library of libraries; let idx = index; trackby: trackbyLibrary" class="list-group-item no-hover">
|
||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<span id="library-name--{{idx}}">{{library.name}}</span>
|
<span id="library-name--{{idx}}">{{library.name}}</span>
|
||||||
<div class="spinner-border text-primary" style="width: 1.5rem; height: 1.5rem;" role="status" *ngIf="scanInProgress.hasOwnProperty(library.id) && scanInProgress[library.id].progress" title="Scan in progress. Started at {{scanInProgress[library.id].timestamp | date: 'short'}}">
|
<div class="spinner-border text-primary" style="width: 1.5rem; height: 1.5rem;" role="status" *ngIf="scanInProgress.hasOwnProperty(library.id) && scanInProgress[library.id].progress" title="Scan in progress. Started at {{scanInProgress[library.id].timestamp | date: 'short'}}">
|
||||||
<span class="sr-only">Scan for {{library.name}} in progress</span>
|
<span class="visually-hidden">Scan for {{library.name}} in progress</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="float-right">
|
<div class="float-end">
|
||||||
<button class="btn btn-secondary mr-2 btn-sm" (click)="scanLibrary(library)" placement="top" ngbTooltip="Scan Library" attr.aria-label="Scan Library"><i class="fa fa-sync-alt" title="Scan"></i></button>
|
<button class="btn btn-secondary me-2 btn-sm" (click)="scanLibrary(library)" placement="top" ngbTooltip="Scan Library" attr.aria-label="Scan Library"><i class="fa fa-sync-alt" title="Scan"></i></button>
|
||||||
<button class="btn btn-danger mr-2 btn-sm" [disabled]="deletionInProgress" (click)="deleteLibrary(library)"><i class="fa fa-trash" placement="top" ngbTooltip="Delete Library" attr.aria-label="Delete {{library.name | sentenceCase}}"></i></button>
|
<button class="btn btn-danger me-2 btn-sm" [disabled]="deletionInProgress" (click)="deleteLibrary(library)"><i class="fa fa-trash" placement="top" ngbTooltip="Delete Library" attr.aria-label="Delete {{library.name | sentenceCase}}"></i></button>
|
||||||
<button class="btn btn-primary btn-sm" (click)="editLibrary(library)"><i class="fa fa-pen" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{library.name | sentenceCase}}"></i></button>
|
<button class="btn btn-primary btn-sm" (click)="editLibrary(library)"><i class="fa fa-pen" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{library.name | sentenceCase}}"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
|
@ -1,65 +1,63 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
||||||
<p class="text-warning pt-2">Port and Logging Level require a manual restart of Kavita to take effect.</p>
|
<p class="text-warning pt-2">Port and Logging Level require a manual restart of Kavita to take effect.</p>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="settings-cachedir">Cache Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheDirectoryTooltip" role="button" tabindex="0"></i>
|
<label for="settings-cachedir" class="form-label">Cache Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheDirectoryTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #cacheDirectoryTooltip>Where the server place temporary files when reading. This will be cleaned up on a regular basis.</ng-template>
|
<ng-template #cacheDirectoryTooltip>Where the server place temporary files when reading. This will be cleaned up on a regular basis.</ng-template>
|
||||||
<span class="sr-only" id="settings-cachedir-help">Where the server place temporary files when reading. This will be cleaned up on a regular basis.</span>
|
<span class="visually-hidden" id="settings-cachedir-help">Where the server place temporary files when reading. This will be cleaned up on a regular basis.</span>
|
||||||
<input readonly id="settings-cachedir" aria-describedby="settings-cachedir-help" class="form-control" formControlName="cacheDirectory" type="text">
|
<input readonly id="settings-cachedir" aria-describedby="settings-cachedir-help" class="form-control" formControlName="cacheDirectory" type="text">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="settings-bookmarksdir">Bookmarks Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="bookmarksDirectoryTooltip" role="button" tabindex="0"></i>
|
<label for="settings-bookmarksdir" class="form-label">Bookmarks Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="bookmarksDirectoryTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #bookmarksDirectoryTooltip>Location where bookmarks will be stored. Bookmarks are source files and can be large. Choose a location with adequate storage. Directory is managed, other files within directory will be deleted.</ng-template>
|
<ng-template #bookmarksDirectoryTooltip>Location where bookmarks will be stored. Bookmarks are source files and can be large. Choose a location with adequate storage. Directory is managed, other files within directory will be deleted.</ng-template>
|
||||||
<span class="sr-only" id="settings-bookmarksdir-help"><ng-container [ngTemplateOutlet]="bookmarksDirectoryTooltip"></ng-container></span>
|
<span class="visually-hidden" id="settings-bookmarksdir-help"><ng-container [ngTemplateOutlet]="bookmarksDirectoryTooltip"></ng-container></span>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input readonly id="settings-bookmarksdir" aria-describedby="settings-bookmarksdir-help" class="form-control" formControlName="bookmarksDirectory" type="text" aria-describedby="change-bookmarks-dir">
|
<input readonly id="settings-bookmarksdir" aria-describedby="settings-bookmarksdir-help" class="form-control" formControlName="bookmarksDirectory" type="text" aria-describedby="change-bookmarks-dir">
|
||||||
<div class="input-group-append">
|
<button id="change-bookmarks-dir" class="btn btn-primary" (click)="openDirectoryChooser(settingsForm.get('bookmarksDirectory')?.value, 'bookmarksDirectory')">
|
||||||
<button id="change-bookmarks-dir" class="btn btn-primary" (click)="openDirectoryChooser(settingsForm.get('bookmarksDirectory')?.value, 'bookmarksDirectory')">
|
Change
|
||||||
Change
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="form-group">
|
<!-- <div class="mb-3">
|
||||||
<label for="settings-baseurl">Base Url</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="baseUrlTooltip" role="button" tabindex="0"></i>
|
<label for="settings-baseurl">Base Url</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="baseUrlTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #baseUrlTooltip>Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita</ng-template>
|
<ng-template #baseUrlTooltip>Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita</ng-template>
|
||||||
<span class="sr-only" id="settings-baseurl-help">Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita</span>
|
<span class="visually-hidden" id="settings-baseurl-help">Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita</span>
|
||||||
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text">
|
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text">
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0 mb-2">
|
||||||
<div class="form-group col-md-6 col-sm-12 pr-2">
|
<div class="form-group col-md-6 col-sm-12 pe-2">
|
||||||
<label for="settings-port">Port</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
|
<label for="settings-port" class="form-label">Port</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #portTooltip>Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</ng-template>
|
<ng-template #portTooltip>Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</ng-template>
|
||||||
<span class="sr-only" id="settings-port-help">Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</span>
|
<span class="visually-hidden" id="settings-port-help">Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</span>
|
||||||
<input id="settings-port" aria-describedby="settings-port-help" class="form-control" formControlName="port" type="number" step="1" min="1" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
<input id="settings-port" aria-describedby="settings-port-help" class="form-control" formControlName="port" type="number" step="1" min="1" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group col-md-6 col-sm-12">
|
<div class="form-group col-md-6 col-sm-12">
|
||||||
<label for="logging-level-port">Logging Level</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="loggingLevelTooltip" role="button" tabindex="0"></i>
|
<label for="logging-level-port" class="form-label">Logging Level</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="loggingLevelTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #loggingLevelTooltip>Use debug to help identify issues. Debug can eat up a lot of disk space. Requires restart to take effect.</ng-template>
|
<ng-template #loggingLevelTooltip>Use debug to help identify issues. Debug can eat up a lot of disk space. Requires restart to take effect.</ng-template>
|
||||||
<span class="sr-only" id="logging-level-port-help">Port the server listens on. Requires restart to take effect.</span>
|
<span class="visually-hidden" id="logging-level-port-help">Port the server listens on. Requires restart to take effect.</span>
|
||||||
<select id="logging-level-port" aria-describedby="logging-level-port-help" class="form-control" aria-describedby="settings-tasks-scan-help" formControlName="loggingLevel">
|
<select id="logging-level-port" aria-describedby="logging-level-port-help" class="form-select" aria-describedby="settings-tasks-scan-help" formControlName="loggingLevel">
|
||||||
<option *ngFor="let level of logLevels" [value]="level">{{level | titlecase}}</option>
|
<option *ngFor="let level of logLevels" [value]="level">{{level | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="stat-collection" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
|
<label for="stat-collection" class="form-label" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
|
||||||
<p class="accent" id="collection-info">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features, bug fixes, and preformance tuning. Requires restart to take effect.</p>
|
<p class="accent" id="collection-info">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features, bug fixes, and preformance tuning. Requires restart to take effect.</p>
|
||||||
<div class="form-check">
|
<div class="form-check form-switch">
|
||||||
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input" formControlName="allowStatCollection">
|
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input" formControlName="allowStatCollection">
|
||||||
<label for="stat-collection" class="form-check-label">Send Data</label>
|
<label for="stat-collection" class="form-check-label">Send Data</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="opds" aria-describedby="opds-info">OPDS</label>
|
<label for="opds" aria-describedby="opds-info" class="form-label">OPDS</label>
|
||||||
<p class="accent" id="opds-info">OPDS support will allow all users to use OPDS to read and download content from the server. If OPDS is enabled, a user will not need download permissions to download media while using it.</p>
|
<p class="accent" id="opds-info">OPDS support will allow all users to use OPDS to read and download content from the server. If OPDS is enabled, a user will not need download permissions to download media while using it.</p>
|
||||||
<div class="form-check">
|
<div class="form-check form-switch">
|
||||||
<input id="opds" type="checkbox" aria-label="OPDS Support" class="form-check-input" formControlName="enableOpds">
|
<input id="opds" type="checkbox" aria-label="OPDS Support" class="form-check-input" formControlName="enableOpds">
|
||||||
<label for="opds" class="form-check-label">Enable OPDS</label>
|
<label for="opds" class="form-check-label">Enable OPDS</label>
|
||||||
</div>
|
</div>
|
||||||
@ -70,47 +68,45 @@
|
|||||||
email service. Set the url of the email service and use the Test button to ensure it works. At any time you can reset to ours. There is no way to disable emails althought confirmation links will always
|
email service. Set the url of the email service and use the Test button to ensure it works. At any time you can reset to ours. There is no way to disable emails althought confirmation links will always
|
||||||
be saved to logs.
|
be saved to logs.
|
||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="settings-emailservice">Email Service Url</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="emailServiceTooltip" role="button" tabindex="0"></i>
|
<label for="settings-emailservice" class="form-label">Email Service Url</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="emailServiceTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #emailServiceTooltip>Use fully qualified url of the email service. Do not include ending slash.</ng-template>
|
<ng-template #emailServiceTooltip>Use fully qualified url of the email service. Do not include ending slash.</ng-template>
|
||||||
<span class="sr-only" id="settings-emailservice-help"><ng-container [ngTemplateOutlet]="emailServiceTooltip"></ng-container></span>
|
<span class="visually-hidden" id="settings-emailservice-help"><ng-container [ngTemplateOutlet]="emailServiceTooltip"></ng-container></span>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="settings-emailservice" aria-describedby="settings-emailservice-help" class="form-control" formControlName="emailServiceUrl" type="text" aria-describedby="change-bookmarks-dir">
|
<input id="settings-emailservice" aria-describedby="settings-emailservice-help" class="form-control" formControlName="emailServiceUrl" type="text" aria-describedby="change-bookmarks-dir">
|
||||||
<div class="input-group-append">
|
<button class="btn btn-outline-secondary" (click)="resetEmailServiceUrl()">
|
||||||
<button class="btn btn-secondary" (click)="resetEmailServiceUrl()">
|
Reset
|
||||||
Reset
|
</button>
|
||||||
</button>
|
<button class="btn btn-outline-secondary" (click)="testEmailServiceUrl()">
|
||||||
<button class="btn btn-secondary" (click)="testEmailServiceUrl()">
|
Test
|
||||||
Test
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<h4>Reoccuring Tasks</h4>
|
<h4>Reoccuring Tasks</h4>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="settings-tasks-scan">Library Scan</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>
|
<label for="settings-tasks-scan" class="form-label">Library Scan</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #taskScanTooltip>How often Kavita will scan and refresh metatdata around manga files.</ng-template>
|
<ng-template #taskScanTooltip>How often Kavita will scan and refresh metatdata around manga files.</ng-template>
|
||||||
<span class="sr-only" id="settings-tasks-scan-help">How often Kavita will scan and refresh metatdata around manga files.</span>
|
<span class="visually-hidden" id="settings-tasks-scan-help">How often Kavita will scan and refresh metatdata around manga files.</span>
|
||||||
<select class="form-control" aria-describedby="settings-tasks-scan-help" formControlName="taskScan" id="settings-tasks-scan">
|
<select class="form-select" aria-describedby="settings-tasks-scan-help" formControlName="taskScan" id="settings-tasks-scan">
|
||||||
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
|
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="settings-tasks-backup">Library Database Backup</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
|
<label for="settings-tasks-backup" class="form-label">Library Database Backup</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #taskBackupTooltip>How often Kavita will backup the database.</ng-template>
|
<ng-template #taskBackupTooltip>How often Kavita will backup the database.</ng-template>
|
||||||
<span class="sr-only" id="settings-tasks-backup-help">How often Kavita will backup the database.</span>
|
<span class="visually-hidden" id="settings-tasks-backup-help">How often Kavita will backup the database.</span>
|
||||||
<select class="form-control" aria-describedby="settings-tasks-backup-help" formControlName="taskBackup" id="settings-tasks-backup">
|
<select class="form-select" aria-describedby="settings-tasks-backup-help" formControlName="taskBackup" id="settings-tasks-backup">
|
||||||
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
|
<option *ngFor="let freq of taskFrequencies" [value]="freq">{{freq | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="float-right">
|
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
|
||||||
<button type="button" class="btn btn-secondary mr-2" (click)="resetToDefaults()">Reset to Default</button>
|
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">Reset to Default</button>
|
||||||
<button type="button" class="btn btn-secondary mr-2" (click)="resetForm()">Reset</button>
|
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">Reset</button>
|
||||||
<button type="submit" class="btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
@ -1,11 +1,11 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|
||||||
<div class="float-right">
|
<div class="float-end">
|
||||||
<div class="d-inline-block" ngbDropdown #myDrop="ngbDropdown">
|
<div class="d-inline-block" ngbDropdown #myDrop="ngbDropdown">
|
||||||
<button class="btn btn-outline-primary mr-2" id="dropdownManual" ngbDropdownToggle>
|
<button class="btn btn-outline-primary me-2" id="dropdownManual" ngbDropdownToggle>
|
||||||
<ng-container *ngIf="backupDBInProgress || clearCacheInProgress || isCheckingForUpdate || downloadLogsInProgress">
|
<ng-container *ngIf="backupDBInProgress || clearCacheInProgress || isCheckingForUpdate || downloadLogsInProgress">
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
<span class="sr-only">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
Actions
|
Actions
|
||||||
</button>
|
</button>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<h3>About System</h3>
|
<h3>About System</h3>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div class="form-group" *ngIf="serverInfo">
|
<div class="mb-3" *ngIf="serverInfo">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Version</dt>
|
<dt>Version</dt>
|
||||||
<dd>{{serverInfo.kavitaVersion}}</dd>
|
<dd>{{serverInfo.kavitaVersion}}</dd>
|
||||||
|
@ -4,28 +4,28 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-8"><h3>Pending Invites</h3></div>
|
<div class="col-8"><h3>Pending Invites</h3></div>
|
||||||
<div class="col-4"><button class="btn btn-primary float-right" (click)="inviteUser()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Invite</span></button></div>
|
<div class="col-4"><button class="btn btn-primary float-end" (click)="inviteUser()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Invite</span></button></div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<li class="list-group-item" *ngFor="let invite of pendingInvites; let idx = index;">
|
<li class="list-group-item no-hover" *ngFor="let invite of pendingInvites; let idx = index;">
|
||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<span id="member-name--{{idx}}">{{invite.username | titlecase}} </span>
|
<span id="member-name--{{idx}}">{{invite.username | titlecase}} </span>
|
||||||
<div class="float-right">
|
<div class="float-end">
|
||||||
<button class="btn btn-danger mr-2" (click)="deleteUser(invite)">Cancel</button>
|
<button class="btn btn-danger me-2" (click)="deleteUser(invite)">Cancel</button>
|
||||||
<button class="btn btn-secondary mr-2" (click)="resendEmail(invite)">Resend</button>
|
<button class="btn btn-secondary me-2" (click)="resendEmail(invite)">Resend</button>
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div>Invited: {{invite.created | date: 'short'}}</div>
|
<div>Invited: {{invite.created | date: 'short'}}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li *ngIf="loadingMembers" class="list-group-item">
|
<li *ngIf="loadingMembers" class="list-group-item no-hover">
|
||||||
<div class="spinner-border text-secondary" role="status">
|
<div class="spinner-border text-secondary" role="status">
|
||||||
<span class="invisible">Loading...</span>
|
<span class="invisible">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item" *ngIf="pendingInvites.length === 0 && !loadingMembers">
|
<li class="list-group-item no-hover" *ngIf="pendingInvites.length === 0 && !loadingMembers">
|
||||||
There are no invited Users
|
There are no invited Users
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -35,18 +35,18 @@
|
|||||||
|
|
||||||
<h3 class="mt-3">Active Users</h3>
|
<h3 class="mt-3">Active Users</h3>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<li *ngFor="let member of members; let idx = index;" class="list-group-item">
|
<li *ngFor="let member of members; let idx = index;" class="list-group-item no-hover">
|
||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<i class="presence fa fa-circle" title="Active" aria-hidden="true" *ngIf="false && (messageHub.onlineUsers$ | async)?.includes(member.username)"></i>
|
<i class="presence fa fa-circle" title="Active" aria-hidden="true" *ngIf="false && (messageHub.onlineUsers$ | async)?.includes(member.username)"></i>
|
||||||
<span id="member-name--{{idx}}">{{member.username | titlecase}} </span>
|
<span id="member-name--{{idx}}">{{member.username | titlecase}} </span>
|
||||||
<span *ngIf="member.username === loggedInUsername">
|
<span *ngIf="member.username === loggedInUsername">
|
||||||
<i class="fas fa-star" aria-hidden="true"></i>
|
<i class="fas fa-star" aria-hidden="true"></i>
|
||||||
<span class="sr-only">(You)</span>
|
<span class="visually-hidden">(You)</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="float-right" *ngIf="canEditMember(member)">
|
<div class="float-end" *ngIf="canEditMember(member)">
|
||||||
<button class="btn btn-danger mr-2" (click)="deleteUser(member)" placement="top" ngbTooltip="Delete User" attr.aria-label="Delete User {{member.username | titlecase}}"><i class="fa fa-trash" aria-hidden="true"></i></button>
|
<button class="btn btn-danger me-2" (click)="deleteUser(member)" placement="top" ngbTooltip="Delete User" attr.aria-label="Delete User {{member.username | titlecase}}"><i class="fa fa-trash" aria-hidden="true"></i></button>
|
||||||
<button class="btn btn-secondary mr-2" (click)="updatePassword(member)" placement="top" ngbTooltip="Change Password" attr.aria-label="Change Password for {{member.username | titlecase}}"><i class="fa fa-key" aria-hidden="true"></i></button>
|
<button class="btn btn-secondary me-2" (click)="updatePassword(member)" placement="top" ngbTooltip="Change Password" attr.aria-label="Change Password for {{member.username | titlecase}}"><i class="fa fa-key" aria-hidden="true"></i></button>
|
||||||
<button class="btn btn-primary" (click)="openEditUser(member)" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{member.username | titlecase}}"><i class="fa fa-pen" aria-hidden="true"></i></button>
|
<button class="btn btn-primary" (click)="openEditUser(member)" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{member.username | titlecase}}"><i class="fa fa-pen" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
@ -57,10 +57,10 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!hasAdminRole(member)">Sharing: {{formatLibraries(member)}}</div>
|
<div *ngIf="!hasAdminRole(member)">Sharing: {{formatLibraries(member)}}</div>
|
||||||
<div>
|
<div class="row g-0">
|
||||||
Roles: <span *ngIf="getRoles(member).length === 0; else showRoles">None</span>
|
Roles: <span *ngIf="getRoles(member).length === 0; else showRoles">None</span>
|
||||||
<ng-template #showRoles>
|
<ng-template #showRoles>
|
||||||
<app-tag-badge *ngFor="let role of getRoles(member)">{{role}}</app-tag-badge>
|
<app-tag-badge *ngFor="let role of getRoles(member)" class="col-auto">{{role}}</app-tag-badge>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,7 @@ import { OnDeckComponent } from './on-deck/on-deck.component';
|
|||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
import { AllSeriesComponent } from './all-series/all-series.component';
|
import { AllSeriesComponent } from './all-series/all-series.component';
|
||||||
import { AdminGuard } from './_guards/admin.guard';
|
import { AdminGuard } from './_guards/admin.guard';
|
||||||
|
import { ThemeTestComponent } from './theme-test/theme-test.component';
|
||||||
|
|
||||||
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
|
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
|
||||||
|
|
||||||
@ -71,6 +72,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{path: 'login', component: UserLoginComponent}, // TODO: move this to registration module
|
{path: 'login', component: UserLoginComponent}, // TODO: move this to registration module
|
||||||
{path: 'no-connection', component: NotConnectedComponent},
|
{path: 'no-connection', component: NotConnectedComponent},
|
||||||
|
{path: 'theme', component: ThemeTestComponent},
|
||||||
{path: '**', component: UserLoginComponent, pathMatch: 'full'}
|
{path: '**', component: UserLoginComponent, pathMatch: 'full'}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<app-nav-header></app-nav-header>
|
<app-nav-header></app-nav-header>
|
||||||
<div [ngStyle]="(navService?.navbarVisible$ | async) ? {'padding-top': 'calc(56px + 5px)', 'height': '100%'} : {}">
|
<div [ngStyle]="(navService?.navbarVisible$ | async) ? {'padding-top': 'calc(57px)', 'height': '100%'} : {}">
|
||||||
<a id="content"></a>
|
<a id="content"></a>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, HostListener, Inject, OnInit } from '@angular/core';
|
||||||
import { NavigationStart, Router } from '@angular/router';
|
import { NavigationStart, Router } from '@angular/router';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { AccountService } from './_services/account.service';
|
import { AccountService } from './_services/account.service';
|
||||||
@ -7,6 +7,8 @@ import { MessageHubService } from './_services/message-hub.service';
|
|||||||
import { NavService } from './_services/nav.service';
|
import { NavService } from './_services/nav.service';
|
||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { ThemeService } from './theme.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -17,7 +19,8 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(private accountService: AccountService, public navService: NavService,
|
constructor(private accountService: AccountService, public navService: NavService,
|
||||||
private messageHub: MessageHubService, private libraryService: LibraryService,
|
private messageHub: MessageHubService, private libraryService: LibraryService,
|
||||||
private router: Router, private ngbModal: NgbModal, private ratingConfig: NgbRatingConfig) {
|
router: Router, private ngbModal: NgbModal, ratingConfig: NgbRatingConfig,
|
||||||
|
@Inject(DOCUMENT) private document: Document) {
|
||||||
|
|
||||||
// Setup default rating config
|
// Setup default rating config
|
||||||
ratingConfig.max = 5;
|
ratingConfig.max = 5;
|
||||||
@ -33,22 +36,34 @@ export class AppComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
@HostListener('resize')
|
||||||
this.setCurrentUser();
|
onResize() {
|
||||||
|
this.setDocHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HostListener('orientationchange')
|
||||||
|
onOrientationChange() {
|
||||||
|
this.setDocHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.setCurrentUser();
|
||||||
|
|
||||||
|
this.setDocHeight();
|
||||||
|
}
|
||||||
|
|
||||||
setCurrentUser() {
|
setCurrentUser() {
|
||||||
const user = this.accountService.getUserFromLocalStorage();
|
const user = this.accountService.getUserFromLocalStorage();
|
||||||
this.accountService.setCurrentUser(user);
|
this.accountService.setCurrentUser(user);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.navService.setDarkMode(user.preferences.siteDarkMode);
|
|
||||||
this.messageHub.createHubConnection(user, this.accountService.hasAdminRole(user));
|
this.messageHub.createHubConnection(user, this.accountService.hasAdminRole(user));
|
||||||
this.libraryService.getLibraryNames().pipe(take(1)).subscribe(() => {/* No Operation */});
|
this.libraryService.getLibraryNames().pipe(take(1)).subscribe(() => {/* No Operation */});
|
||||||
} else {
|
|
||||||
this.navService.setDarkMode(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
setDocHeight() {
|
||||||
|
// Sets a CSS variable for the actual device viewport height. Needed for mobile dev.
|
||||||
|
this.document.documentElement.style.setProperty('--vh', `${window.innerHeight/100}px`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -37,6 +37,7 @@ import { AllSeriesComponent } from './all-series/all-series.component';
|
|||||||
import { PublicationStatusPipe } from './publication-status.pipe';
|
import { PublicationStatusPipe } from './publication-status.pipe';
|
||||||
import { RegistrationModule } from './registration/registration.module';
|
import { RegistrationModule } from './registration/registration.module';
|
||||||
import { GroupedTypeaheadComponent } from './grouped-typeahead/grouped-typeahead.component';
|
import { GroupedTypeaheadComponent } from './grouped-typeahead/grouped-typeahead.component';
|
||||||
|
import { ThemeTestComponent } from './theme-test/theme-test.component';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -58,6 +59,7 @@ import { GroupedTypeaheadComponent } from './grouped-typeahead/grouped-typeahead
|
|||||||
SeriesMetadataDetailComponent,
|
SeriesMetadataDetailComponent,
|
||||||
AllSeriesComponent,
|
AllSeriesComponent,
|
||||||
GroupedTypeaheadComponent,
|
GroupedTypeaheadComponent,
|
||||||
|
ThemeTestComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<div class="container-flex {{darkMode ? 'dark-mode' : ''}} reader-container" tabindex="0" #reader>
|
<div class="container-flex {{darkMode ? 'dark-mode' : ''}} reader-container" tabindex="0" #reader>
|
||||||
<div class="fixed-top" #stickyTop>
|
<div class="fixed-top" #stickyTop>
|
||||||
<a class="sr-only sr-only-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
||||||
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
||||||
<app-drawer #commentDrawer="drawer" [isOpen]="drawerOpen" [style.--drawer-width]="'300px'" [options]="{topOffset: topOffset}" [style.--drawer-background-color]="drawerBackgroundColor" (drawerClosed)="closeDrawer()">
|
<app-drawer #commentDrawer="drawer" [isOpen]="drawerOpen" [style.--drawer-width]="'300px'" [options]="{topOffset: topOffset}" [style.--drawer-background-color]="drawerBackgroundColor" (drawerClosed)="closeDrawer()">
|
||||||
<div header>
|
<div header>
|
||||||
<h2 style="margin-top: 0.5rem">Book Settings
|
<h2 style="margin-top: 0.5rem">Book Settings
|
||||||
<button type="button" class="close" aria-label="Close" (click)="commentDrawer.close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="commentDrawer.close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -16,8 +16,8 @@
|
|||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
|
||||||
<form [formGroup]="settingsForm">
|
<form [formGroup]="settingsForm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="library-type">Font Family</label>
|
<label for="library-type" class="form-label">Font Family</label>
|
||||||
<select class="form-control" id="library-type" formControlName="bookReaderFontFamily">
|
<select class="form-control" id="library-type" formControlName="bookReaderFontFamily">
|
||||||
<option [value]="opt" *ngFor="let opt of fontFamilies; let i = index">{{opt | titlecase}}</option>
|
<option [value]="opt" *ngFor="let opt of fontFamilies; let i = index">{{opt | titlecase}}</option>
|
||||||
</select>
|
</select>
|
||||||
@ -25,42 +25,42 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label id="fontsize">Font Size</label>
|
<label id="fontsize" class="form-label">Font Size</label>
|
||||||
<button (click)="updateFontSize(-10)" class="btn btn-icon" title="Decrease" aria-labelledby="fontsize"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
<button (click)="updateFontSize(-10)" class="btn btn-icon" title="Decrease" aria-labelledby="fontsize"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
||||||
<span>{{pageStyles['font-size']}}</span>
|
<span>{{pageStyles['font-size']}}</span>
|
||||||
<button (click)="updateFontSize(10)" class="btn btn-icon" title="Increase" aria-labelledby="fontsize"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
<button (click)="updateFontSize(10)" class="btn btn-icon" title="Increase" aria-labelledby="fontsize"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label id="linespacing">Line Spacing</label>
|
<label id="linespacing" class="form-label">Line Spacing</label>
|
||||||
<button (click)="updateLineSpacing(-10)" class="btn btn-icon" title="Decrease" aria-labelledby="linespacing"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
<button (click)="updateLineSpacing(-10)" class="btn btn-icon" title="Decrease" aria-labelledby="linespacing"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
||||||
<span>{{pageStyles['line-height']}}</span>
|
<span>{{pageStyles['line-height']}}</span>
|
||||||
<button (click)="updateLineSpacing(10)" class="btn btn-icon" title="Increase" aria-labelledby="linespacing"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
<button (click)="updateLineSpacing(10)" class="btn btn-icon" title="Increase" aria-labelledby="linespacing"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label id="margin">Margin</label>
|
<label id="margin" class="form-label">Margin</label>
|
||||||
<button (click)="updateMargin(-5)" class="btn btn-icon" title="Remove Margin" aria-labelledby="margin"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
<button (click)="updateMargin(-5)" class="btn btn-icon" title="Remove Margin" aria-labelledby="margin"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
||||||
<span>{{pageStyles['margin-right']}}</span>
|
<span>{{pageStyles['margin-right']}}</span>
|
||||||
<button (click)="updateMargin(5)" class="btn btn-icon" title="Add Margin" aria-labelledby="margin"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
<button (click)="updateMargin(5)" class="btn btn-icon" title="Add Margin" aria-labelledby="margin"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label id="readingdirection">Reading Direction</label>
|
<label id="readingdirection" class="form-label">Reading Direction</label>
|
||||||
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}"><i class="fa {{readingDirection === 0 ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i><span class="phone-hidden"> {{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}</span></button>
|
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}"><i class="fa {{readingDirection === 0 ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i><span class="phone-hidden"> {{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label id="darkmode">Dark Mode</label>
|
<label id="darkmode" class="form-label">Dark Mode</label>
|
||||||
<button (click)="toggleDarkMode(false)" class="btn btn-icon" aria-labelledby="darkmode" title="Off"><i class="fa fa-sun" aria-hidden="true"></i></button>
|
<button (click)="toggleDarkMode(false)" class="btn btn-icon" aria-labelledby="darkmode" title="Off"><i class="fa fa-sun" aria-hidden="true"></i></button>
|
||||||
<button (click)="toggleDarkMode(true)" class="btn btn-icon" aria-labelledby="darkmode" title="On"><i class="fa fa-moon" aria-hidden="true"></i></button>
|
<button (click)="toggleDarkMode(true)" class="btn btn-icon" aria-labelledby="darkmode" title="On"><i class="fa fa-moon" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label id="tap-pagination">Tap Pagination <i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="tapPaginationTooltip" role="button" tabindex="0" aria-describedby="tap-pagination-help"></i></label>
|
<label id="tap-pagination" class="form-label">Tap Pagination <i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="tapPaginationTooltip" role="button" tabindex="0" aria-describedby="tap-pagination-help"></i></label>
|
||||||
<ng-template #tapPaginationTooltip>The ability to click the sides of the page to page left and right</ng-template>
|
<ng-template #tapPaginationTooltip>The ability to click the sides of the page to page left and right</ng-template>
|
||||||
<span class="sr-only" id="tap-pagination-help">The ability to click the sides of the page to page left and right</span>
|
<span class="visually-hidden" id="tap-pagination-help">The ability to click the sides of the page to page left and right</span>
|
||||||
<button (click)="toggleClickToPaginate()" class="btn btn-icon" aria-labelledby="tap-pagination"><i class="fa fa-arrows-alt-h {{clickToPaginate ? 'icon-primary-color' : ''}}" aria-hidden="true"></i><span *ngIf="darkMode"> {{clickToPaginate ? 'On' : 'Off'}}</span></button>
|
<button (click)="toggleClickToPaginate()" class="btn btn-icon" aria-labelledby="tap-pagination"><i class="fa fa-arrows-alt-h {{clickToPaginate ? 'icon-primary-color' : ''}}" aria-hidden="true"></i><span *ngIf="darkMode"> {{clickToPaginate ? 'On' : 'Off'}}</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label id="fullscreen">Fullscreen <i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="fullscreenTooltip" role="button" tabindex="0" aria-describedby="fullscreen-help"></i></label>
|
<label id="fullscreen" class="form-label">Fullscreen <i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="fullscreenTooltip" role="button" tabindex="0" aria-describedby="fullscreen-help"></i></label>
|
||||||
<ng-template #fullscreenTooltip>Put reader in fullscreen mode</ng-template>
|
<ng-template #fullscreenTooltip>Put reader in fullscreen mode</ng-template>
|
||||||
<span class="sr-only" id="fullscreen-help">
|
<span class="visually-hidden" id="fullscreen-help">
|
||||||
<ng-container [ngTemplateOutlet]="fullscreenTooltip"></ng-container>
|
<ng-container [ngTemplateOutlet]="fullscreenTooltip"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
<button (click)="toggleFullscreen()" class="btn btn-icon" aria-labelledby="fullscreen">
|
<button (click)="toggleFullscreen()" class="btn btn-icon" aria-labelledby="fullscreen">
|
||||||
@ -68,11 +68,11 @@
|
|||||||
<span *ngIf="darkMode"> {{isFullscreen ? 'Exit' : 'Enter'}}</span>
|
<span *ngIf="darkMode"> {{isFullscreen ? 'Exit' : 'Enter'}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters justify-content-between">
|
<div class="row g-0 justify-content-between">
|
||||||
<button (click)="resetSettings()" class="btn btn-primary col">Reset to Defaults</button>
|
<button (click)="resetSettings()" class="btn btn-primary col">Reset to Defaults</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||||
<div class="col-1 page-stub">{{pageNum}}</div>
|
<div class="col-1 page-stub">{{pageNum}}</div>
|
||||||
<div class="col-8" style="margin-top: 15px;padding-right:10px">
|
<div class="col-8" style="margin-top: 15px;padding-right:10px">
|
||||||
@ -125,7 +125,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #actionBar>
|
<ng-template #actionBar>
|
||||||
<div class="reading-bar row no-gutters justify-content-between">
|
<div class="reading-bar row g-0 justify-content-between">
|
||||||
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="prevPage()"
|
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="prevPage()"
|
||||||
[disabled]="IsPrevDisabled"
|
[disabled]="IsPrevDisabled"
|
||||||
title="{{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}} Page">
|
title="{{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}} Page">
|
||||||
@ -137,12 +137,12 @@
|
|||||||
<div class="book-title col-2 phone-hidden">
|
<div class="book-title col-2 phone-hidden">
|
||||||
<ng-container *ngIf="isLoading; else showTitle">
|
<ng-container *ngIf="isLoading; else showTitle">
|
||||||
<div class="spinner-border spinner-border-sm text-primary" style="border-radius: 50%;" role="status">
|
<div class="spinner-border spinner-border-sm text-primary" style="border-radius: 50%;" role="status">
|
||||||
<span class="sr-only">Loading book...</span>
|
<span class="visually-hidden">Loading book...</span>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #showTitle>
|
<ng-template #showTitle>
|
||||||
{{bookTitle}}
|
{{bookTitle}}
|
||||||
<span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" aria-label="Incognito mode is on. Toggle to turn off.">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="sr-only">Incognito Mode</span>)</span>
|
<span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" aria-label="Incognito mode is on. Toggle to turn off.">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">Incognito Mode</span>)</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i><span class="phone-hidden"> Close</span></button>
|
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i><span class="phone-hidden"> Close</span></button>
|
||||||
|
@ -24,6 +24,7 @@ import { ScrollService } from 'src/app/scroll.service';
|
|||||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||||
import { LibraryService } from 'src/app/_services/library.service';
|
import { LibraryService } from 'src/app/_services/library.service';
|
||||||
import { LibraryType } from 'src/app/_models/library';
|
import { LibraryType } from 'src/app/_models/library';
|
||||||
|
import { ThemeService } from 'src/app/theme.service';
|
||||||
|
|
||||||
|
|
||||||
interface PageStyle {
|
interface PageStyle {
|
||||||
@ -260,7 +261,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
private renderer: Renderer2, private navService: NavService, private toastr: ToastrService,
|
private renderer: Renderer2, private navService: NavService, private toastr: ToastrService,
|
||||||
private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService,
|
private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService,
|
||||||
private scrollService: ScrollService, private utilityService: UtilityService, private libraryService: LibraryService,
|
private scrollService: ScrollService, private utilityService: UtilityService, private libraryService: LibraryService,
|
||||||
@Inject(DOCUMENT) private document: Document) {
|
@Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {
|
||||||
this.navService.hideNavBar();
|
this.navService.hideNavBar();
|
||||||
|
|
||||||
this.darkModeStyleElem = this.renderer.createElement('style');
|
this.darkModeStyleElem = this.renderer.createElement('style');
|
||||||
@ -382,7 +383,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
const bodyNode = this.document.querySelector('body');
|
const bodyNode = this.document.querySelector('body');
|
||||||
if (bodyNode !== undefined && bodyNode !== null && this.originalBodyColor !== undefined) {
|
if (bodyNode !== undefined && bodyNode !== null && this.originalBodyColor !== undefined) {
|
||||||
bodyNode.style.background = this.originalBodyColor;
|
bodyNode.style.background = this.originalBodyColor;
|
||||||
if (this.user.preferences.siteDarkMode) {
|
if (this.themeService.isDarkTheme()) {
|
||||||
bodyNode.classList.add('bg-dark');
|
bodyNode.classList.add('bg-dark');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -968,7 +969,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
setOverrideStyles() {
|
setOverrideStyles() {
|
||||||
const bodyNode = this.document.querySelector('body');
|
const bodyNode = this.document.querySelector('body');
|
||||||
if (bodyNode !== undefined && bodyNode !== null) {
|
if (bodyNode !== undefined && bodyNode !== null) {
|
||||||
if (this.user.preferences.siteDarkMode) {
|
if (this.themeService.isDarkTheme()) {
|
||||||
bodyNode.classList.remove('bg-dark');
|
bodyNode.classList.remove('bg-dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{title}} Bookmarks</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{title}} Bookmarks</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -10,7 +10,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<ng-template #noBookmarks>No bookmarks yet</ng-template>
|
<ng-template #noBookmarks>No bookmarks yet</ng-template>
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div *ngFor="let bookmark of bookmarks; let idx = index">
|
<div *ngFor="let bookmark of bookmarks; let idx = index">
|
||||||
<app-bookmark [bookmark]="bookmark" (bookmarkRemoved)="removeBookmark(bookmark, idx)" class="col-auto"></app-bookmark>
|
<app-bookmark [bookmark]="bookmark" (bookmarkRemoved)="removeBookmark(bookmark, idx)" class="col-auto"></app-bookmark>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">Add to Collection</h4>
|
<h4 class="modal-title" id="modal-basic-title">Add to Collection</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<form style="width: 100%" [formGroup]="listForm">
|
<form style="width: 100%" [formGroup]="listForm">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group" *ngIf="lists.length >= 5">
|
<div class="mb-3" *ngIf="lists.length >= 5">
|
||||||
<label for="filter">Filter</label>
|
<label for="filter" class="form-label">Filter</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||||
<div class="input-group-append">
|
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
||||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
@ -23,7 +21,7 @@
|
|||||||
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No collections created yet</li>
|
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No collections created yet</li>
|
||||||
<li class="list-group-item" *ngIf="loading">
|
<li class="list-group-item" *ngIf="loading">
|
||||||
<div class="spinner-border text-secondary" role="status">
|
<div class="spinner-border text-secondary" role="status">
|
||||||
<span class="sr-only">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -32,7 +30,7 @@
|
|||||||
<div style="width: 100%;">
|
<div style="width: 100%;">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="col-9 col-lg-10">
|
<div class="col-9 col-lg-10">
|
||||||
<label class="sr-only" for="add-rlist">Collection</label>
|
<label class="visually-hidden" class="form-label" for="add-rlist">Collection</label>
|
||||||
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
|
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
<ng-template #comicHeader><h4 class="modal-title" id="modal-basic-title">
|
<ng-template #comicHeader><h4 class="modal-title" id="modal-basic-title">
|
||||||
{{parentName}} - {{data.number != 0 ? (isChapter ? 'Issue #' : 'Volume ') + data.number : 'Special'}} Details</h4>
|
{{parentName}} - {{data.number != 0 ? (isChapter ? 'Issue #' : 'Volume ') + data.number : 'Special'}} Details</h4>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body scrollable-modal" *ngIf="utilityService.isChapter(data)">
|
<div class="modal-body scrollable-modal" *ngIf="utilityService.isChapter(data)">
|
||||||
@ -18,15 +18,15 @@
|
|||||||
<h4 *ngIf="utilityService.isVolume(data)">Information</h4>
|
<h4 *ngIf="utilityService.isVolume(data)">Information</h4>
|
||||||
|
|
||||||
<ng-container *ngIf="utilityService.isVolume(data) || utilityService.isChapter(data)">
|
<ng-container *ngIf="utilityService.isVolume(data) || utilityService.isChapter(data)">
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
Id: {{data.id}}
|
Id: {{data.id}}
|
||||||
</div>
|
</div>
|
||||||
<div class="col" *ngIf="series !== undefined">
|
<div class="col" *ngIf="series !== undefined">
|
||||||
Format: <span class="badge badge-secondary">{{utilityService.mangaFormat(series.format) | sentenceCase}}</span>
|
Format: <span class="badge bg-secondary">{{utilityService.mangaFormat(series.format) | sentenceCase}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
||||||
Added: {{(data.created | date: 'short') || '-'}}
|
Added: {{(data.created | date: 'short') || '-'}}
|
||||||
</div>
|
</div>
|
||||||
@ -38,18 +38,18 @@
|
|||||||
|
|
||||||
<h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
<h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li class="media my-4" *ngFor="let chapter of chapters">
|
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}}">
|
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}}">
|
||||||
<app-image class="mr-2" width="74px" [imageUrl]="chapter.coverImage"></app-image>
|
<app-image class="me-2" width="74px" [imageUrl]="chapter.coverImage"></app-image>
|
||||||
</a>
|
</a>
|
||||||
<div class="media-body">
|
<div class="flex-grow-1">
|
||||||
<h5 class="mt-0 mb-1">
|
<h5 class="mt-0 mb-1">
|
||||||
<span *ngIf="chapter.number !== '0'; else specialHeader">
|
<span *ngIf="chapter.number !== '0'; else specialHeader">
|
||||||
<span >
|
<span >
|
||||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||||
</span>
|
</span>
|
||||||
<span class="badge badge-primary badge-pill">
|
<span class="badge bg-primary rounded-pill">
|
||||||
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||||
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
|
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
|
||||||
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
||||||
@ -58,9 +58,9 @@
|
|||||||
<ng-template #specialHeader>File(s)</ng-template>
|
<ng-template #specialHeader>File(s)</ng-template>
|
||||||
</h5>
|
</h5>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<li *ngFor="let file of chapter.files" class="list-group-item">
|
<li *ngFor="let file of chapter.files" class="list-group-item no-hover">
|
||||||
<span>{{file.filePath}}</span>
|
<span>{{file.filePath}}</span>
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
Pages: {{file.pages}}
|
Pages: {{file.pages}}
|
||||||
</div>
|
</div>
|
||||||
@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-info" [disabled]="!isAdmin" (click)="updateCover()">Update Cover</button>
|
<button type="button" class="btn btn-secondary" [disabled]="!isAdmin" (click)="updateCover()">Update Cover</button>
|
||||||
<button type="submit" class="btn btn-primary" (click)="close()">Close</button>
|
<button type="submit" class="btn btn-primary" (click)="close()">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">Edit {{tag?.title}} Collection</h4>
|
<h4 class="modal-title" id="modal-basic-title">Edit {{tag?.title}} Collection</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -16,8 +16,8 @@
|
|||||||
<a ngbNavLink>{{tabs[0]}}</a>
|
<a ngbNavLink>{{tabs[0]}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<form [formGroup]="collectionTagForm">
|
<form [formGroup]="collectionTagForm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="summary">Summary</label>
|
<label for="summary" class="form-label">Summary</label>
|
||||||
<textarea id="summary" class="form-control" formControlName="summary" rows="3"></textarea>
|
<textarea id="summary" class="form-control" formControlName="summary" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -67,6 +67,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" (click)="close()">Cancel</button>
|
<button type="button" class="btn btn-secondary" (click)="close()">Cancel</button>
|
||||||
<button type="button" class="btn btn-info" (click)="togglePromotion()">{{tag?.promoted ? 'Demote' : 'Promote'}}</button>
|
<button type="button" class="btn btn-secondary alt" (click)="togglePromotion()">{{tag?.promoted ? 'Demote' : 'Promote'}}</button>
|
||||||
<button type="button" class="btn btn-primary" (click)="save()">Save</button>
|
<button type="button" class="btn btn-primary" (click)="save()">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">
|
<h4 class="modal-title">
|
||||||
{{this.series.name}} Details</h4>
|
{{this.series.name}} Details</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
|
||||||
@ -13,52 +13,52 @@
|
|||||||
<a ngbNavLink>{{tabs[0]}}</a>
|
<a ngbNavLink>{{tabs[0]}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<form [formGroup]="editSeriesForm">
|
<form [formGroup]="editSeriesForm">
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="form-group" style="width: 100%">
|
<div class="mb-3" style="width: 100%">
|
||||||
<label for="name">Name</label>
|
<label for="name" class="form-label">Name</label>
|
||||||
<input id="name" class="form-control" formControlName="name" type="text">
|
<input id="name" class="form-control" formControlName="name" type="text">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="form-group" style="width: 100%">
|
<div class="mb-3" style="width: 100%">
|
||||||
<label for="sort-name">Sort Name</label>
|
<label for="sort-name" class="form-label">Sort Name</label>
|
||||||
<input id="sort-name" class="form-control" formControlName="sortName" type="text">
|
<input id="sort-name" class="form-control" formControlName="sortName" type="text">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="form-group" style="width: 100%">
|
<div class="mb-3" style="width: 100%">
|
||||||
<label for="localized-name">Localized Name</label>
|
<label for="localized-name" class="form-label">Localized Name</label>
|
||||||
<input id="localized-name" class="form-control" formControlName="localizedName" type="text">
|
<input id="localized-name" class="form-control" formControlName="localizedName" type="text">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters" *ngIf="metadata">
|
<div class="row g-0" *ngIf="metadata">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="author">Author</label>
|
<label for="author" class="form-label">Author</label>
|
||||||
<input id="author" class="form-control" placeholder="Not Implemented" readonly="true" formControlName="author" type="text">
|
<input id="author" class="form-control" placeholder="Not Implemented" readonly="true" formControlName="author" type="text">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="artist">Artist</label>
|
<label for="artist" class="form-label">Artist</label>
|
||||||
<input id="artist" class="form-control" placeholder="Not Implemented" readonly="true" formControlName="artist" type="text">
|
<input id="artist" class="form-control" placeholder="Not Implemented" readonly="true" formControlName="artist" type="text">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters" *ngIf="metadata">
|
<div class="row g-0" *ngIf="metadata">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="genres">Genres</label>
|
<label for="genres" class="form-label">Genres</label>
|
||||||
<input id="genres" class="form-control" placeholder="Not Implemented" readonly="true" formControlName="genres" type="text">
|
<input id="genres" class="form-control" placeholder="Not Implemented" readonly="true" formControlName="genres" type="text">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="collections">Collections</label>
|
<label for="collections" class="form-label">Collections</label>
|
||||||
<app-typeahead (selectedData)="updateCollections($event)" [settings]="settings">
|
<app-typeahead (selectedData)="updateCollections($event)" [settings]="settings">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.title}}
|
{{item.title}}
|
||||||
@ -71,9 +71,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="form-group" style="width: 100%">
|
<div class="mb-3" style="width: 100%">
|
||||||
<label for="summary">Summary</label>
|
<label for="summary" class="form-label">Summary</label>
|
||||||
<textarea id="summary" class="form-control" formControlName="summary" rows="4"></textarea>
|
<textarea id="summary" class="form-control" formControlName="summary" rows="4"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -94,7 +94,7 @@
|
|||||||
<a ngbNavLink>{{tabs[2]}}</a>
|
<a ngbNavLink>{{tabs[2]}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<h4>Information</h4>
|
<h4>Information</h4>
|
||||||
<div class="row no-gutters mb-2">
|
<div class="row g-0 mb-2">
|
||||||
<div class="col-md-6" *ngIf="libraryName">Library: {{libraryName | sentenceCase}}</div>
|
<div class="col-md-6" *ngIf="libraryName">Library: {{libraryName | sentenceCase}}</div>
|
||||||
<div class="col-md-6">Format: <app-tag-badge>{{utilityService.mangaFormat(series.format)}}</app-tag-badge></div>
|
<div class="col-md-6">Format: <app-tag-badge>{{utilityService.mangaFormat(series.format)}}</app-tag-badge></div>
|
||||||
</div>
|
</div>
|
||||||
@ -103,12 +103,12 @@
|
|||||||
<span class="invisible">Loading...</span>
|
<span class="invisible">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-unstyled" *ngIf="!isLoadingVolumes">
|
<ul class="list-unstyled" *ngIf="!isLoadingVolumes">
|
||||||
<li class="media my-4" *ngFor="let volume of seriesVolumes">
|
<li class="d-flex my-4" *ngFor="let volume of seriesVolumes">
|
||||||
<app-image class="mr-3" style="width: 74px;" width="74px" [imageUrl]="imageService.getVolumeCoverImage(volume.id)"></app-image>
|
<app-image class="me-3" style="width: 74px;" width="74px" [imageUrl]="imageService.getVolumeCoverImage(volume.id)"></app-image>
|
||||||
<div class="media-body">
|
<div class="flex-grow-1">
|
||||||
<h5 class="mt-0 mb-1">Volume {{volume.name}}</h5>
|
<h5 class="mt-0 mb-1">Volume {{volume.name}}</h5>
|
||||||
<div>
|
<div>
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
Added: {{volume.created | date: 'short'}}
|
Added: {{volume.created | date: 'short'}}
|
||||||
</div>
|
</div>
|
||||||
@ -116,7 +116,7 @@
|
|||||||
Last Modified: {{volume.lastModified | date: 'short'}}
|
Last Modified: {{volume.lastModified | date: 'short'}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<button type="button" class="btn btn-outline-primary" (click)="collapse.toggle()" [attr.aria-expanded]="!volumeCollapsed[volume.name]">
|
<button type="button" class="btn btn-outline-primary" (click)="collapse.toggle()" [attr.aria-expanded]="!volumeCollapsed[volume.name]">
|
||||||
View Files
|
View Files
|
||||||
@ -131,7 +131,7 @@
|
|||||||
<ul class="list-group mt-2">
|
<ul class="list-group mt-2">
|
||||||
<li *ngFor="let file of volume.volumeFiles.sort()" class="list-group-item">
|
<li *ngFor="let file of volume.volumeFiles.sort()" class="list-group-item">
|
||||||
<span>{{file.filePath}}</span>
|
<span>{{file.filePath}}</span>
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
Chapter: {{file.chapter}}
|
Chapter: {{file.chapter}}
|
||||||
</div>
|
</div>
|
||||||
@ -153,7 +153,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ml-4 flex-fill'}}"></div>
|
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
|
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
|
||||||
|
@ -6,12 +6,12 @@
|
|||||||
<span class="card-title" tabindex="0">
|
<span class="card-title" tabindex="0">
|
||||||
Page {{bookmark.page + 1}}
|
Page {{bookmark.page + 1}}
|
||||||
</span>
|
</span>
|
||||||
<span class="card-actions float-right" *ngIf="series != undefined">
|
<span class="card-actions float-end" *ngIf="series != undefined">
|
||||||
<button attr.aria-labelledby="series--{{series.name}}" class="btn btn-danger btn-sm" (click)="removeBookmark()"
|
<button attr.aria-labelledby="series--{{series.name}}" class="btn btn-danger btn-sm" (click)="removeBookmark()"
|
||||||
[disabled]="isClearing" placement="top" ngbTooltip="Remove Bookmark" attr.aria-label="Remove Bookmark">
|
[disabled]="isClearing" placement="top" ngbTooltip="Remove Bookmark" attr.aria-label="Remove Bookmark">
|
||||||
<ng-container *ngIf="isClearing; else notClearing">
|
<ng-container *ngIf="isClearing; else notClearing">
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
<span class="sr-only">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #notClearing>
|
<ng-template #notClearing>
|
||||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="d-flex justify-content-around align-items-center">
|
<div class="d-flex justify-content-around align-items-center">
|
||||||
<span class="highlight"><i class="fa fa-check" aria-hidden="true"></i> {{bulkSelectionService.totalSelections()}} selected</span>
|
<span class="highlight"><i class="fa fa-check" aria-hidden="true"></i> {{bulkSelectionService.totalSelections()}} selected</span>
|
||||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||||
<span id="bulk-actions-header" class="sr-only">Bulk Actions</span>
|
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||||
<button class="btn btn-icon" (click)="bulkSelectionService.deselectAll()"><i class="fa fa-times" aria-hidden="true"></i> Deselect All</button>
|
<button class="btn btn-icon" (click)="bulkSelectionService.deselectAll()"><i class="fa fa-times" aria-hidden="true"></i> Deselect All</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -1,15 +1,9 @@
|
|||||||
@use "../../../theme/colors";
|
|
||||||
|
|
||||||
.bulk-select {
|
.bulk-select {
|
||||||
background-color: colors.$dark-form-background-no-opacity;
|
background-color: var(--navbar-bg-color);
|
||||||
border-bottom: 2px solid colors.$primary-color;
|
border-bottom: 2px solid var(--primary-color);
|
||||||
color: white;
|
color: var(--navbar-text-color);
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon {
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
color: colors.$primary-color !important;
|
color: var(--primary-color) !important;
|
||||||
}
|
}
|
@ -1,18 +1,20 @@
|
|||||||
<div class="container-fluid" style="padding-top: 10px">
|
<div class="container-fluid" style="padding-top: 10px">
|
||||||
<div class="row no-gutters pb-2">
|
<div class="row g-0 pb-2">
|
||||||
<div class="col mr-auto">
|
<div class="col me-auto">
|
||||||
<h2 style="display: inline-block">
|
<h2 style="display: inline-block">
|
||||||
<span *ngIf="actions.length > 0" class="">
|
<span *ngIf="actions.length > 0" class="">
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>
|
||||||
</span>{{header}}
|
</span>{{header}}
|
||||||
<span class="badge badge-primary badge-pill" attr.aria-label="{{pagination.totalItems}} total items" *ngIf="pagination != undefined">{{pagination.totalItems}}</span>
|
<span class="badge bg-primary rounded-pill" attr.aria-label="{{pagination.totalItems}} total items" *ngIf="pagination != undefined">{{pagination.totalItems}}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button *ngIf="!filteringDisabled" class="btn btn-secondary btn-small" (click)="collapse.toggle()" [attr.aria-expanded]="!filteringCollapsed" placement="left" ngbTooltip="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting">
|
<div class="col-auto align-self-end">
|
||||||
<i class="fa fa-filter" aria-hidden="true"></i>
|
<button *ngIf="!filteringDisabled" class="btn btn-secondary btn-small" (click)="collapse.toggle()" [attr.aria-expanded]="!filteringCollapsed" placement="left" ngbTooltip="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting">
|
||||||
<span class="sr-only">Sort / Filter</span>
|
<i class="fa fa-filter" aria-hidden="true"></i>
|
||||||
</button>
|
<span class="visually-hidden">Sort / Filter</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="phone-hidden">
|
<div class="phone-hidden">
|
||||||
@ -25,8 +27,8 @@
|
|||||||
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [style.--drawer-background-color]="'#010409'" (drawerClosed)="filteringCollapsed = !filteringCollapsed">
|
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [style.--drawer-background-color]="'#010409'" (drawerClosed)="filteringCollapsed = !filteringCollapsed">
|
||||||
<div header>
|
<div header>
|
||||||
<h2 style="margin-top: 0.5rem">Book Settings
|
<h2 style="margin-top: 0.5rem">Book Settings
|
||||||
<button type="button" class="close" aria-label="Close" (click)="commentDrawer.close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="commentDrawer.close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -40,11 +42,11 @@
|
|||||||
<ng-template #filterSection>
|
<ng-template #filterSection>
|
||||||
<ng-template #globalFilterTooltip>This is library agnostic</ng-template>
|
<ng-template #globalFilterTooltip>This is library agnostic</ng-template>
|
||||||
<div class="filter-section mx-auto pb-3">
|
<div class="filter-section mx-auto pb-3">
|
||||||
<div class="row justify-content-center no-gutters">
|
<div class="row justify-content-center g-0">
|
||||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.formatDisabled">
|
<div class="col-md-2 me-3" *ngIf="!filterSettings.formatDisabled">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="format">Format</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="globalFilterTooltip" role="button" tabindex="0"></i>
|
<label for="format" class="form-label">Format</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="globalFilterTooltip" role="button" tabindex="0"></i>
|
||||||
<span class="sr-only" id="filter-global-format-help"><ng-container [ngTemplateOutlet]="globalFilterTooltip"></ng-container></span>
|
<span class="visually-hidden" id="filter-global-format-help"><ng-container [ngTemplateOutlet]="globalFilterTooltip"></ng-container></span>
|
||||||
<app-typeahead (selectedData)="updateFormatFilters($event)" [settings]="formatSettings" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updateFormatFilters($event)" [settings]="formatSettings" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.title}}
|
{{item.title}}
|
||||||
@ -56,9 +58,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3"*ngIf="!filterSettings.libraryDisabled">
|
<div class="col-md-2 me-3"*ngIf="!filterSettings.libraryDisabled">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="libraries">Libraries</label>
|
<label for="libraries" class="form-label">Libraries</label>
|
||||||
<app-typeahead (selectedData)="updateLibraryFilters($event)" [settings]="librarySettings" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updateLibraryFilters($event)" [settings]="librarySettings" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.name}}
|
{{item.name}}
|
||||||
@ -70,10 +72,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.collectionDisabled">
|
<div class="col-md-2 me-3" *ngIf="!filterSettings.collectionDisabled">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="collections">Collections</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="globalFilterTooltip" role="button" tabindex="0"></i>
|
<label for="collections" class="form-label">Collections</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="globalFilterTooltip" role="button" tabindex="0"></i>
|
||||||
<span class="sr-only" id="filter-global-collections-help"><ng-container [ngTemplateOutlet]="globalFilterTooltip"></ng-container></span>
|
<span class="visually-hidden" id="filter-global-collections-help"><ng-container [ngTemplateOutlet]="globalFilterTooltip"></ng-container></span>
|
||||||
<app-typeahead (selectedData)="updateCollectionFilters($event)" [settings]="collectionSettings" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updateCollectionFilters($event)" [settings]="collectionSettings" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.title}}
|
{{item.title}}
|
||||||
@ -85,9 +87,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.genresDisabled">
|
<div class="col-md-2 me-3" *ngIf="!filterSettings.genresDisabled">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="genres">Genres</label>
|
<label for="genres" class="form-label">Genres</label>
|
||||||
<app-typeahead (selectedData)="updateGenreFilters($event)" [settings]="genreSettings" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updateGenreFilters($event)" [settings]="genreSettings" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.title}}
|
{{item.title}}
|
||||||
@ -99,9 +101,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.tagsDisabled">
|
<div class="col-md-2 me-3" *ngIf="!filterSettings.tagsDisabled">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="tags">Tags</label>
|
<label for="tags" class="form-label">Tags</label>
|
||||||
<app-typeahead (selectedData)="updateTagFilters($event)" [settings]="tagsSettings" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updateTagFilters($event)" [settings]="tagsSettings" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.title}}
|
{{item.title}}
|
||||||
@ -113,11 +115,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-content-center no-gutters">
|
<div class="row justify-content-center g-0">
|
||||||
<!-- The People row -->
|
<!-- The People row -->
|
||||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.CoverArtist)">
|
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.CoverArtist)">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="cover-artist">Cover Artists</label>
|
<label for="cover-artist" class="form-label">Cover Artists</label>
|
||||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.name}}
|
{{item.name}}
|
||||||
@ -129,9 +131,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Writer)">
|
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Writer)">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="writers">Writers</label>
|
<label for="writers" class="form-label">Writers</label>
|
||||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.name}}
|
{{item.name}}
|
||||||
@ -143,9 +145,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Publisher)">
|
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Publisher)">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="publisher">Publisher</label>
|
<label for="publisher" class="form-label">Publisher</label>
|
||||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.name}}
|
{{item.name}}
|
||||||
@ -157,9 +159,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Penciller)">
|
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Penciller)">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="penciller">Penciller</label>
|
<label for="penciller" class="form-label">Penciller</label>
|
||||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.name}}
|
{{item.name}}
|
||||||
@ -171,9 +173,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Letterer)">
|
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Letterer)">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="letterer">Letterer</label>
|
<label for="letterer" class="form-label">Letterer</label>
|
||||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.name}}
|
{{item.name}}
|
||||||
@ -185,9 +187,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Inker)">
|
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Inker)">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="inker">Inker</label>
|
<label for="inker" class="form-label">Inker</label>
|
||||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.name}}
|
{{item.name}}
|
||||||
@ -199,9 +201,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Editor)">
|
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Editor)">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="editor">Editor</label>
|
<label for="editor" class="form-label">Editor</label>
|
||||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.name}}
|
{{item.name}}
|
||||||
@ -213,9 +215,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Colorist)">
|
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Colorist)">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="colorist">Colorist</label>
|
<label for="colorist" class="form-label">Colorist</label>
|
||||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.name}}
|
{{item.name}}
|
||||||
@ -227,9 +229,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Character)">
|
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Character)">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="character">Character</label>
|
<label for="character" class="form-label">Character</label>
|
||||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.name}}
|
{{item.name}}
|
||||||
@ -241,9 +243,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Translator)">
|
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Translator)">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="translators">Translators</label>
|
<label for="translators" class="form-label">Translators</label>
|
||||||
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.name}}
|
{{item.name}}
|
||||||
@ -255,9 +257,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-content-center no-gutters">
|
<div class="row justify-content-center g-0">
|
||||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.readProgressDisabled">
|
<div class="col-md-2 me-3" *ngIf="!filterSettings.readProgressDisabled">
|
||||||
<label>Read Progress</label>
|
<label class="form-label">Read Progress</label>
|
||||||
<form [formGroup]="readProgressGroup">
|
<form [formGroup]="readProgressGroup">
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" id="notread" formControlName="notRead">
|
<input class="form-check-input" type="checkbox" id="notread" formControlName="notRead">
|
||||||
@ -274,8 +276,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.ratingDisabled">
|
<div class="col-md-2 me-3" *ngIf="!filterSettings.ratingDisabled">
|
||||||
<label for="ratings">Rating</label>
|
<label for="ratings" class="form-label">Rating</label>
|
||||||
<form class="form-inline">
|
<form class="form-inline">
|
||||||
<ngb-rating class="rating-star" [(rate)]="filter.rating" (rateChange)="updateRating($event)" [resettable]="true">
|
<ngb-rating class="rating-star" [(rate)]="filter.rating" (rateChange)="updateRating($event)" [resettable]="true">
|
||||||
<ng-template let-fill="fill" let-index="index">
|
<ng-template let-fill="fill" let-index="index">
|
||||||
@ -285,8 +287,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.ageRatingDisabled">
|
<div class="col-md-2 me-3" *ngIf="!filterSettings.ageRatingDisabled">
|
||||||
<label for="age-rating">Age Rating</label>
|
<label for="age-rating" class="form-label">Age Rating</label>
|
||||||
<app-typeahead (selectedData)="updateAgeRating($event)" [settings]="ageRatingSettings" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updateAgeRating($event)" [settings]="ageRatingSettings" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.title}}
|
{{item.title}}
|
||||||
@ -297,8 +299,8 @@
|
|||||||
</app-typeahead>
|
</app-typeahead>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.languageDisabled">
|
<div class="col-md-2 me-3" *ngIf="!filterSettings.languageDisabled">
|
||||||
<label for="languages">Language</label>
|
<label for="languages" class="form-label">Language</label>
|
||||||
<app-typeahead (selectedData)="updateLanguageRating($event)" [settings]="languageSettings" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updateLanguageRating($event)" [settings]="languageSettings" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.title}}
|
{{item.title}}
|
||||||
@ -309,8 +311,8 @@
|
|||||||
</app-typeahead>
|
</app-typeahead>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.publicationStatusDisabled">
|
<div class="col-md-2 me-3" *ngIf="!filterSettings.publicationStatusDisabled">
|
||||||
<label for="publication-status">Publication Status</label>
|
<label for="publication-status" class="form-label">Publication Status</label>
|
||||||
<app-typeahead (selectedData)="updatePublicationStatus($event)" [settings]="publicationStatusSettings" [reset]="resetTypeaheads">
|
<app-typeahead (selectedData)="updatePublicationStatus($event)" [settings]="publicationStatusSettings" [reset]="resetTypeaheads">
|
||||||
<ng-template #badgeItem let-item let-position="idx">
|
<ng-template #badgeItem let-item let-position="idx">
|
||||||
{{item.title}}
|
{{item.title}}
|
||||||
@ -320,13 +322,13 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</app-typeahead>
|
</app-typeahead>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 mr-3"></div>
|
<div class="col-md-2 me-3"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-content-center no-gutters">
|
<div class="row justify-content-center g-0">
|
||||||
<div class="col-md-2 mr-3" *ngIf="!filterSettings.sortDisabled">
|
<div class="col-md-2 me-3" *ngIf="!filterSettings.sortDisabled">
|
||||||
<form [formGroup]="sortGroup">
|
<form [formGroup]="sortGroup">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="sort-options">Sort By</label>
|
<label for="sort-options" class="form-label">Sort By</label>
|
||||||
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
|
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
|
||||||
<i class="fa fa-arrow-up" title="Ascending" *ngIf="isAscendingSort; else descSort"></i>
|
<i class="fa fa-arrow-up" title="Ascending" *ngIf="isAscendingSort; else descSort"></i>
|
||||||
<ng-template #descSort>
|
<ng-template #descSort>
|
||||||
@ -341,14 +343,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 mr-3" *ngIf="filterSettings.sortDisabled"></div>
|
<div class="col-md-2 me-3" *ngIf="filterSettings.sortDisabled"></div>
|
||||||
<div class="col-md-2 mr-3"></div>
|
<div class="col-md-2 me-3"></div>
|
||||||
<div class="col-md-2 mr-3"></div>
|
<div class="col-md-2 me-3"></div>
|
||||||
<div class="col-md-2 mr-3 mt-4">
|
<div class="col-md-2 me-3">
|
||||||
<button class="btn btn-secondary btn-block" (click)="clear()">Clear</button>
|
<button class="btn btn-secondary col-12" (click)="clear()">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2 mr-3 mt-4">
|
<div class="col-md-2 me-3">
|
||||||
<button class="btn btn-primary btn-block" (click)="apply()">Apply</button>
|
<button class="btn btn-primary col-12" (click)="apply()">Apply</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -357,7 +359,7 @@
|
|||||||
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'top' }"></ng-container>
|
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'top' }"></ng-container>
|
||||||
|
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col-auto" *ngFor="let item of items; trackBy:trackByIdentity; index as i">
|
<div class="col-auto" *ngFor="let item of items; trackBy:trackByIdentity; index as i">
|
||||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
@ -388,7 +390,7 @@
|
|||||||
<label
|
<label
|
||||||
id="paginationInputLabel-{{id}}"
|
id="paginationInputLabel-{{id}}"
|
||||||
for="paginationInput-{{id}}"
|
for="paginationInput-{{id}}"
|
||||||
class="col-form-label mr-2 ml-1"
|
class="col-form-label me-2 ms-1 form-label"
|
||||||
>Page</label>
|
>Page</label>
|
||||||
<input #i
|
<input #i
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
@use '../../../theme/colors';
|
|
||||||
|
|
||||||
.star {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: colors.$rating-empty;
|
|
||||||
}
|
|
||||||
.filled {
|
|
||||||
color: colors.$rating-filled;
|
|
||||||
}
|
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<span class="download" *ngIf="download$ | async as download">
|
<span class="download" *ngIf="download$ | async as download">
|
||||||
<app-circular-loader [currentValue]="download.progress"></app-circular-loader>
|
<app-circular-loader [currentValue]="download.progress"></app-circular-loader>
|
||||||
<span class="sr-only" role="status">
|
<span class="visually-hidden" role="status">
|
||||||
{{download.progress}}% downloaded
|
{{download.progress}}% downloaded
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@ -27,8 +27,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="count" *ngIf="count > 1">
|
<div class="count" *ngIf="count > 1">
|
||||||
<span class="badge badge-primary">{{count}}</span>
|
<span class="badge bg-primary">{{count}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0">
|
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0">
|
||||||
@ -36,12 +37,12 @@
|
|||||||
<span class="card-title" placement="top" id="{{title}}_{{entity?.id}}" [ngbTooltip]="tooltipTitle" (click)="handleClick()" tabindex="0">
|
<span class="card-title" placement="top" id="{{title}}_{{entity?.id}}" [ngbTooltip]="tooltipTitle" (click)="handleClick()" tabindex="0">
|
||||||
<span *ngIf="isPromoted()">
|
<span *ngIf="isPromoted()">
|
||||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||||
<span class="sr-only">(promoted)</span>
|
<span class="visually-hidden">(promoted)</span>
|
||||||
</span>
|
</span>
|
||||||
<i class="fa {{utilityService.mangaFormatIcon(format)}}" aria-hidden="true" *ngIf="format != MangaFormat.UNKNOWN" title="{{utilityService.mangaFormat(format)}}"></i><span class="sr-only">{{utilityService.mangaFormat(format)}}</span>
|
<i class="fa {{utilityService.mangaFormatIcon(format)}}" aria-hidden="true" *ngIf="format != MangaFormat.UNKNOWN" title="{{utilityService.mangaFormat(format)}}"></i><span class="visually-hidden">{{utilityService.mangaFormat(format)}}</span>
|
||||||
{{title}}
|
{{title}}
|
||||||
</span>
|
</span>
|
||||||
<span class="card-actions float-right">
|
<span class="card-actions float-end">
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@use '../../../theme/colors';
|
|
||||||
|
|
||||||
$triangle-size: 30px;
|
$triangle-size: 30px;
|
||||||
$image-height: 230px;
|
$image-height: 230px;
|
||||||
@ -7,7 +7,7 @@ $image-width: 160px;
|
|||||||
.error-banner {
|
.error-banner {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
background-color: colors.$error-color;
|
background-color: var(--toast-error-bg-color);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: white;
|
color: white;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@ -25,6 +25,11 @@ $image-width: 160px;
|
|||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--card-bg-color);
|
||||||
|
color: var(--card-text-color);
|
||||||
|
border-color: var(--card-border-color);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
@ -39,7 +44,7 @@ $image-width: 160px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selected-highlight {
|
.selected-highlight {
|
||||||
outline: 2px solid colors.$primary-color;
|
outline: 2px solid var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -52,7 +57,7 @@ $image-width: 160px;
|
|||||||
height: 5px;
|
height: 5px;
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
color: colors.$primary-color;
|
color: var(--card-progress-bar-color);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,7 +78,7 @@ $image-width: 160px;
|
|||||||
height: 0;
|
height: 0;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 0 $triangle-size $triangle-size 0;
|
border-width: 0 $triangle-size $triangle-size 0;
|
||||||
border-color: transparent colors.$primary-color transparent transparent;
|
border-color: transparent var(--primary-color) transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -106,10 +111,12 @@ $image-width: 160px;
|
|||||||
|
|
||||||
.bulk-mode {
|
.bulk-mode {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
z-index: 110;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-item {
|
.overlay-item {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,3 +149,17 @@ $image-width: 160px;
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 230px;
|
||||||
|
z-index: 10;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-overlay:hover {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
@ -4,13 +4,13 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- Arc Information -->
|
<!-- Arc Information -->
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
Id: {{chapter.id}}
|
Id: {{chapter.id}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
Title: {{chapter.titleName || '-'}}
|
Title: {{chapter.titleName || '-'}}
|
||||||
</div>
|
</div>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col" *ngIf="chapter.hasOwnProperty('created')">
|
<div class="col" *ngIf="chapter.hasOwnProperty('created')">
|
||||||
Added: {{(chapter.created | date: 'short') || '-'}}
|
Added: {{(chapter.created | date: 'short') || '-'}}
|
||||||
</div>
|
</div>
|
||||||
@ -30,11 +30,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="list-unstyled" >
|
<ul class="list-unstyled" >
|
||||||
<li class="media my-4">
|
<li class="d-flex my-4">
|
||||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}}">
|
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}}">
|
||||||
<app-image class="mr-3" width="74px" [imageUrl]="chapter.coverImage"></app-image>
|
<app-image class="me-3" width="74px" [imageUrl]="chapter.coverImage"></app-image>
|
||||||
</a>
|
</a>
|
||||||
<div class="media-body">
|
<div class="flex-grow-1">
|
||||||
<h5 class="mt-0 mb-1">
|
<h5 class="mt-0 mb-1">
|
||||||
<span *ngIf="chapter.number !== '0'; else specialHeader">
|
<span *ngIf="chapter.number !== '0'; else specialHeader">
|
||||||
<!-- TODO: Add back in
|
<!-- TODO: Add back in
|
||||||
@ -42,7 +42,7 @@
|
|||||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||||
</span> -->
|
</span> -->
|
||||||
<span class="badge badge-primary badge-pill">
|
<span class="badge bg-primary rounded-pill">
|
||||||
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||||
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
|
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
|
||||||
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
||||||
@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<div class="row no-gutters mt-1" *ngIf="chapter.writers && chapter.writers.length > 0">
|
<div class="row g-0 mt-1" *ngIf="chapter.writers && chapter.writers.length > 0">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h5>Writers</h5>
|
<h5>Writers</h5>
|
||||||
</div>
|
</div>
|
||||||
@ -65,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters mt-1" *ngIf="chapter.coverArtist && chapter.coverArtist.length > 0">
|
<div class="row g-0 mt-1" *ngIf="chapter.coverArtist && chapter.coverArtist.length > 0">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h5>Artists</h5>
|
<h5>Artists</h5>
|
||||||
</div>
|
</div>
|
||||||
@ -74,7 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters mt-1" *ngIf="chapter.publisher && chapter.publisher.length > 0">
|
<div class="row g-0 mt-1" *ngIf="chapter.publisher && chapter.publisher.length > 0">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h5>Publishers</h5>
|
<h5>Publishers</h5>
|
||||||
</div>
|
</div>
|
||||||
@ -98,7 +98,7 @@
|
|||||||
Arc Information
|
Arc Information
|
||||||
|
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
Id: {{chapter.id}}
|
Id: {{chapter.id}}
|
||||||
</div>
|
</div>
|
||||||
@ -107,7 +107,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col" *ngIf="chapter.hasOwnProperty('created')">
|
<div class="col" *ngIf="chapter.hasOwnProperty('created')">
|
||||||
Added: {{(chapter.created | date: 'short') || '-'}}
|
Added: {{(chapter.created | date: 'short') || '-'}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,13 +3,13 @@
|
|||||||
<ngx-file-drop (onFileDrop)="dropped($event)"
|
<ngx-file-drop (onFileDrop)="dropped($event)"
|
||||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" accept=".png,.jpg,.jpeg" [directory]="false" dropZoneClassName="file-upload" contentClassName="file-upload-zone" [directory]="false">
|
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" accept=".png,.jpg,.jpeg" [directory]="false" dropZoneClassName="file-upload" contentClassName="file-upload-zone" [directory]="false">
|
||||||
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
||||||
<div class="row no-gutters mt-3 pb-3" *ngIf="mode === 'all'">
|
<div class="row g-0 mt-3 pb-3" *ngIf="mode === 'all'">
|
||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
<div class="row no-gutters mb-3">
|
<div class="row g-0 mb-3">
|
||||||
<i class="fa fa-file-upload mx-auto" style="font-size: 24px;" aria-hidden="true"></i>
|
<i class="fa fa-file-upload mx-auto" style="font-size: 24px;" aria-hidden="true"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
<a class="col" style="padding-right:0px" href="javascript:void(0)" (click)="mode = 'url'; setupEnterHandler()"><span class="phone-hidden">Enter a </span>Url</a>
|
<a class="col" style="padding-right:0px" href="javascript:void(0)" (click)="mode = 'url'; setupEnterHandler()"><span class="phone-hidden">Enter a </span>Url</a>
|
||||||
<span class="col" style="padding-right:0px">•</span>
|
<span class="col" style="padding-right:0px">•</span>
|
||||||
@ -23,17 +23,16 @@
|
|||||||
|
|
||||||
|
|
||||||
<ng-container *ngIf="mode === 'url'">
|
<ng-container *ngIf="mode === 'url'">
|
||||||
<div class="row no-gutters mt-3 pb-3 ml-md-2 mr-md-2">
|
<div class="row g-0 mt-3 pb-3 ms-md-2 me-md-2">
|
||||||
<div class="input-group col-md-10 mr-md-2" style="width: 100%">
|
<div class="input-group col-md-10 me-md-2" style="width: 100%">
|
||||||
|
<!-- TOOD: Bootstrap migration: This should be replaced with just the label-->
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<label class="input-group-text" for="load-image">Url</label>
|
<label class="input-group-text form-label" for="load-image">Url</label>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" autocomplete="off" class="form-control" formControlName="coverImageUrl" placeholder="https://" id="load-image" class="form-control">
|
<input type="text" autocomplete="off" class="form-control" formControlName="coverImageUrl" placeholder="https://" id="load-image" class="form-control">
|
||||||
<div class="input-group-append">
|
<button class="btn btn-outline-secondary" type="button" id="load-image-addon" (click)="loadImage(); mode='all';" [disabled]="form.get('coverImageUrl')?.value.length === 0">
|
||||||
<button class="btn btn-outline-secondary" type="button" id="load-image-addon" (click)="loadImage(); mode='all';" [disabled]="form.get('coverImageUrl')?.value.length === 0">
|
Load
|
||||||
Load
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="col btn btn-secondary" href="javascript:void(0)" (click)="mode = 'all'" aria-label="Back">
|
<button class="col btn btn-secondary" href="javascript:void(0)" (click)="mode = 'all'" aria-label="Back">
|
||||||
<i class="fas fa-share" aria-hidden="true" style="transform: rotateY(180deg)"></i>
|
<i class="fas fa-share" aria-hidden="true" style="transform: rotateY(180deg)"></i>
|
||||||
@ -51,7 +50,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="row no-gutters chooser" style="padding-top: 10px">
|
<div class="row g-0 chooser" style="padding-top: 10px">
|
||||||
<div class="image-card col-auto {{selectedIndex === idx ? 'selected' : ''}}" *ngFor="let url of imageUrls; let idx = index;" tabindex="0" attr.aria-label="Image {{idx + 1}}" (click)="selectImage(idx)">
|
<div class="image-card col-auto {{selectedIndex === idx ? 'selected' : ''}}" *ngFor="let url of imageUrls; let idx = index;" tabindex="0" attr.aria-label="Image {{idx + 1}}" (click)="selectImage(idx)">
|
||||||
<app-image class="card-img-top" height="230px" width="158px" [imageUrl]="url"></app-image>
|
<app-image class="card-img-top" height="230px" width="158px" [imageUrl]="url"></app-image>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
@use '../../../theme/colors';
|
|
||||||
$image-height: 230px;
|
$image-height: 230px;
|
||||||
$image-width: 160px;
|
$image-width: 160px;
|
||||||
|
|
||||||
@ -14,7 +13,7 @@ $image-width: 160px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
outline: 5px solid colors.$primary-color;
|
outline: 5px solid var(--primary-color);
|
||||||
outline-width: medium;
|
outline-width: medium;
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
}
|
}
|
||||||
@ -22,7 +21,7 @@ $image-width: 160px;
|
|||||||
ngx-file-drop ::ng-deep > div {
|
ngx-file-drop ::ng-deep > div {
|
||||||
// styling for the outer drop box
|
// styling for the outer drop box
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 2px solid colors.$primary-color;
|
border: 2px solid var(--primary-color);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<span>{{file.filePath}}</span>
|
<span>{{file.filePath}}</span>
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
Pages: {{file.pages}}
|
Pages: {{file.pages}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
$primary-color: #cc7b19;
|
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
@ -28,7 +27,6 @@ $primary-color: #cc7b19;
|
|||||||
.overlay {
|
.overlay {
|
||||||
height: 160px;
|
height: 160px;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
|
||||||
.overlay-item {
|
.overlay-item {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<div class="carousel-container" *ngIf="items.length > 0">
|
<div class="carousel-container" *ngIf="items.length > 0">
|
||||||
<div>
|
<div>
|
||||||
<h2 style="display: inline-block;"><a href="javascript:void(0)" (click)="sectionClicked($event)" class="section-title">{{title}}</a></h2>
|
<h2 style="display: inline-block;"><a href="javascript:void(0)" (click)="sectionClicked($event)" class="section-title">{{title}}</a></h2>
|
||||||
<div class="float-right">
|
<div class="float-end">
|
||||||
<button class="btn btn-icon" [disabled]="isBeginning" (click)="prevPage()"><i class="fa fa-angle-left" aria-hidden="true"></i><span class="sr-only">Previous Items</span></button>
|
<button class="btn btn-icon" [disabled]="isBeginning" (click)="prevPage()"><i class="fa fa-angle-left" aria-hidden="true"></i><span class="visually-hidden">Previous Items</span></button>
|
||||||
<button class="btn btn-icon" [disabled]="isEnd" (click)="nextPage()"><i class="fa fa-angle-right" aria-hidden="true"></i><span class="sr-only">Next Items</span></button>
|
<button class="btn btn-icon" [disabled]="isEnd" (click)="nextPage()"><i class="fa fa-angle-right" aria-hidden="true"></i><span class="visually-hidden">Next Items</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -8,41 +8,20 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.carousel-container {
|
||||||
font-size: 1.6rem;
|
|
||||||
font-weight: 400;
|
|
||||||
margin-left: 10px;
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
:active {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
:focus {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .bg-light {
|
|
||||||
.section-title {
|
.section-title {
|
||||||
color: black !important;
|
font-size: 1.6rem;
|
||||||
}
|
font-weight: 400;
|
||||||
|
margin-left: 10px;
|
||||||
}
|
color: var(--carousel-header-text-color);
|
||||||
// These are needed due to nested css within another component
|
text-decoration: var(--carousel-header-text-decoration);
|
||||||
::ng-deep .bg-dark {
|
|
||||||
.section-title {
|
&:hover, &:focus, &:active {
|
||||||
color: #efefef !important;
|
text-decoration: var(--carousel-hover-header-text-decoration);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .swiper-slide {
|
|
||||||
width: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .swiper-slide {
|
::ng-deep .swiper-slide {
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
|
import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
|
||||||
import { SwiperComponent } from 'swiper/angular';
|
import { SwiperComponent } from 'swiper/angular';
|
||||||
//import Swiper from 'swiper';
|
import { Swiper, SwiperEvents } from 'swiper/types';
|
||||||
//import { SwiperEvents, Swiper } from 'swiper/types';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-carousel-reel',
|
selector: 'app-carousel-reel',
|
||||||
@ -15,18 +14,17 @@ export class CarouselReelComponent implements OnInit {
|
|||||||
@Input() title = '';
|
@Input() title = '';
|
||||||
@Output() sectionClick = new EventEmitter<string>();
|
@Output() sectionClick = new EventEmitter<string>();
|
||||||
|
|
||||||
@ViewChild('swiper', { static: false }) swiper?: SwiperComponent;
|
swiper: Swiper | undefined;
|
||||||
|
|
||||||
|
|
||||||
//swiper!: Swiper;
|
|
||||||
trackByIdentity: (index: number, item: any) => string;
|
trackByIdentity: (index: number, item: any) => string;
|
||||||
|
|
||||||
get isEnd() {
|
get isEnd() {
|
||||||
return this.swiper?.swiperRef.isEnd;
|
return this.swiper?.isEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isBeginning() {
|
get isBeginning() {
|
||||||
return this.swiper?.swiperRef.isBeginning;
|
return this.swiper?.isBeginning;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -36,14 +34,16 @@ export class CarouselReelComponent implements OnInit {
|
|||||||
ngOnInit(): void {}
|
ngOnInit(): void {}
|
||||||
|
|
||||||
nextPage() {
|
nextPage() {
|
||||||
|
if (this.isEnd) return;
|
||||||
if (this.swiper) {
|
if (this.swiper) {
|
||||||
this.swiper.swiperRef.setProgress(this.swiper.swiperRef.progress + 0.25, 600);
|
this.swiper.setProgress(this.swiper.progress + 0.25, 600);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage() {
|
prevPage() {
|
||||||
|
if (this.isBeginning) return;
|
||||||
if (this.swiper) {
|
if (this.swiper) {
|
||||||
this.swiper.swiperRef.setProgress(this.swiper.swiperRef.progress - 0.25, 600);
|
this.swiper.setProgress(this.swiper.progress - 0.25, 600);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,14 +51,7 @@ export class CarouselReelComponent implements OnInit {
|
|||||||
this.sectionClick.emit(this.title);
|
this.sectionClick.emit(this.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
// onSwiper(eventParams: Parameters<SwiperEvents['init']>) {
|
onSwiper(eventParams: Parameters<SwiperEvents['init']>) {
|
||||||
// console.log('swiper: ', eventParams);
|
[this.swiper] = eventParams;
|
||||||
// [this.swiper] = eventParams;
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// onSwiper(params: Swiper) {
|
|
||||||
// // const [swiper] = params;
|
|
||||||
// // console.log(swiper);
|
|
||||||
// // return params;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,14 @@
|
|||||||
<app-image class="poster" maxWidth="481px" [imageUrl]="tagImage"></app-image>
|
<app-image class="poster" maxWidth="481px" [imageUrl]="tagImage"></app-image>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-10 col-xs-8 col-sm-6">
|
<div class="col-md-10 col-xs-8 col-sm-6">
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<h2>
|
<h2>
|
||||||
|
|
||||||
{{collectionTag.title}}
|
{{collectionTag.title}}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters mt-2 mb-2">
|
<div class="row g-0 mt-2 mb-2">
|
||||||
<div class="ml-2" *ngIf="isAdmin">
|
<div class="ms-2" *ngIf="isAdmin">
|
||||||
<button class="btn btn-secondary" (click)="openEditCollectionTagModal(collectionTag)" title="Edit Series information">
|
<button class="btn btn-secondary" (click)="openEditCollectionTagModal(collectionTag)" title="Edit Series information">
|
||||||
<span>
|
<span>
|
||||||
<i class="fa fa-pen" aria-hidden="true"></i>
|
<i class="fa fa-pen" aria-hidden="true"></i>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<app-read-more [text]="collectionTag.summary" [maxLength]="250"></app-read-more>
|
<app-read-more [text]="collectionTag.summary" [maxLength]="250"></app-read-more>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<form [formGroup]="typeaheadForm" class="grouped-typeahead">
|
<form [formGroup]="typeaheadForm" class="grouped-typeahead">
|
||||||
<div class="typeahead-input" [ngClass]="{'focused': hasFocus == true}" (click)="onInputFocus($event)">
|
<div class="typeahead-input" [ngClass]="{'focused': hasFocus == true}" (click)="onInputFocus($event)">
|
||||||
<div>
|
<div class="search">
|
||||||
<input #input [id]="id" type="text" autocomplete="off" formControlName="typeahead" [placeholder]="placeholder"
|
<input #input [id]="id" type="text" autocomplete="off" formControlName="typeahead" [placeholder]="placeholder"
|
||||||
aria-haspopup="listbox" aria-owns="dropdown" aria-expanded="hasFocus && (grouppedData.persons.length || grouppedData.collections.length || grouppedData.series.length || grouppedData.persons.length || grouppedData.tags.length || grouppedData.genres.length)"
|
aria-haspopup="listbox" aria-owns="dropdown" aria-expanded="hasFocus && (grouppedData.persons.length || grouppedData.collections.length || grouppedData.series.length || grouppedData.persons.length || grouppedData.tags.length || grouppedData.genres.length)"
|
||||||
aria-autocomplete="list" (focusout)="close($event)" (focus)="open($event)"
|
aria-autocomplete="list" (focusout)="close($event)" (focus)="open($event)"
|
||||||
>
|
>
|
||||||
<div class="spinner-border spinner-border-sm" role="status" *ngIf="isLoading">
|
<div class="spinner-border spinner-border-sm" role="status" *ngIf="isLoading">
|
||||||
<span class="sr-only">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="resetField()">
|
<button type="button" class="btn-close float-end" aria-label="Close" (click)="resetField()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
@use "../../theme/colors";
|
|
||||||
form {
|
form {
|
||||||
max-height: 38px;
|
max-height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
// input {
|
||||||
width: 15px;
|
// width: 15px;
|
||||||
opacity: 1;
|
// opacity: 1;
|
||||||
position: relative;
|
// position: relative;
|
||||||
left: 4px;
|
// left: 4px;
|
||||||
border: none;
|
// border: none;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.search-result img {
|
.search-result img {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.typeahead-input {
|
.typeahead-input {
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
@ -27,32 +31,35 @@ input {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
background-color: #fff;
|
background-color: var(--input-bg-color);
|
||||||
|
color: var(--body-text-color);
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
transition-property: all;
|
transition-property: all;
|
||||||
transition-duration: 0.3s;
|
transition-duration: 0.3s;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
.search {
|
||||||
.close {
|
display: flex;
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
top: 7px;
|
|
||||||
right: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width:650px) {
|
// .close {
|
||||||
.close {
|
// cursor: pointer;
|
||||||
top: 50%;
|
// position: absolute;
|
||||||
transform: translate(0, -60%);
|
// top: 7px;
|
||||||
}
|
// right: 10px;
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// @media only screen and (max-width:650px) {
|
||||||
|
// .close {
|
||||||
|
// top: 50%;
|
||||||
|
// transform: translate(0, -60%);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
outline: 0 !important;
|
outline: 0 !important;
|
||||||
border-radius: .28571429rem;
|
border-radius: .28571429rem;
|
||||||
display: inline-block !important;
|
|
||||||
padding: 0px !important;
|
padding: 0px !important;
|
||||||
min-height: 0px !important;
|
min-height: 0px !important;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
@ -60,32 +67,35 @@ input {
|
|||||||
text-indent: 0 !important;
|
text-indent: 0 !important;
|
||||||
line-height: inherit !important;
|
line-height: inherit !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
width: 300px;
|
width: 200px;
|
||||||
transition-property: all;
|
transition-property: all;
|
||||||
transition-duration: 0.3s;
|
transition-duration: 0.3s;
|
||||||
display: block;
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
position: relative;
|
||||||
|
left: 4px;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
width: calc(100vw - 400px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
padding-top: 6px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus-visible {
|
&.focused {
|
||||||
width: calc(100vw - 400px);
|
width: 100%;
|
||||||
|
border-color: var(--input-focused-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
input:empty {
|
|
||||||
padding-top: 6px !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeahead-input.focused {
|
|
||||||
width: 100%;
|
|
||||||
border-color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* small devices (phones, 650px and down) */
|
/* small devices (phones, 650px and down) */
|
||||||
@media only screen and (max-width:650px) {
|
@media only screen and (max-width:650px) {
|
||||||
.typeahead-input {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%
|
width: 100%
|
||||||
}
|
}
|
||||||
@ -95,21 +105,20 @@ input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .bg-dark .typeahead-input {
|
|
||||||
color: #efefef;
|
|
||||||
background-color: colors.$dark-bg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Causes bleedover
|
.section-header {
|
||||||
::ng-deep .bg-dark .dropdown .list-group-item.hover {
|
color: var(--body-text-color);
|
||||||
background-color: colors.$dark-hover-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--list-group-item-bg-color) !important;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: calc(100vh - 57px); //header offset
|
height: calc(100vh - 57px); //header offset
|
||||||
background: rgba(0,0,0,0.5);
|
background: var(--dropdown-overlay-color);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -155,39 +164,6 @@ ul ul {
|
|||||||
border-radius: 0px !important;
|
border-radius: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .bg-dark {
|
|
||||||
& .section-header {
|
|
||||||
|
|
||||||
background: colors.$dark-item-accent-bg;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .section-header:hover {
|
|
||||||
background-color: colors.$dark-item-accent-bg !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep .bg-light {
|
|
||||||
& .section-header {
|
|
||||||
|
|
||||||
background: colors.$white-item-accent-bg;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .section-header:hover, .list-group-item.section-header:hover {
|
|
||||||
background: colors.$white-item-accent-bg !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .list-group-item:hover {
|
|
||||||
background-color: colors.$primary-color !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner-border {
|
.spinner-border {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<button class="btn btn-icon mx-auto">
|
<button class="btn btn-icon mx-auto">
|
||||||
<i class="fa fa-angle-double-up animate" aria-hidden="true"></i>
|
<i class="fa fa-angle-double-up animate" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="sr-only">Scroll up to move to next chapter</span>
|
<span class="visually-hidden">Scroll up to move to next chapter</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngFor="let item of webtoonImages | async; let index = index;">
|
<ng-container *ngFor="let item of webtoonImages | async; let index = index;">
|
||||||
@ -40,7 +40,7 @@
|
|||||||
<button class="btn btn-icon mx-auto">
|
<button class="btn btn-icon mx-auto">
|
||||||
<i class="fa fa-angle-double-down animate" aria-hidden="true"></i>
|
<i class="fa fa-angle-double-down animate" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="sr-only">Scroll down to move to next chapter</span>
|
<span class="visually-hidden">Scroll down to move to next chapter</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 200px"></div>
|
<div style="height: 200px"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,17 +3,17 @@
|
|||||||
<div style="display: flex; margin-top: 5px;">
|
<div style="display: flex; margin-top: 5px;">
|
||||||
<button class="btn btn-icon" style="height: 100%" title="Back" (click)="closeReader()">
|
<button class="btn btn-icon" style="height: 100%" title="Back" (click)="closeReader()">
|
||||||
<i class="fa fa-arrow-left" aria-hidden="true"></i>
|
<i class="fa fa-arrow-left" aria-hidden="true"></i>
|
||||||
<span class="sr-only">Back</span>
|
<span class="visually-hidden">Back</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight: bold;">{{title}} <span class="clickable" *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" aria-label="Incognito mode is on. Toggle to turn off.">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="sr-only">Incognito Mode:</span>)</span></div>
|
<div style="font-weight: bold;">{{title}} <span class="clickable" *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" aria-label="Incognito mode is on. Toggle to turn off.">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">Incognito Mode:</span>)</span></div>
|
||||||
<div class="subtitle">
|
<div class="subtitle">
|
||||||
{{subtitle}}
|
{{subtitle}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-left: auto; padding-right: 3%;">
|
<div style="margin-left: auto; padding-right: 3%;">
|
||||||
<button class="btn btn-icon btn-small" role="checkbox" [attr.aria-checked]="pageBookmarked" title="{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()"><i class="{{pageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i><span class="sr-only">{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span></button>
|
<button class="btn btn-icon btn-small" role="checkbox" [attr.aria-checked]="pageBookmarked" title="{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()"><i class="{{pageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i><span class="visually-hidden">{{pageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -56,9 +56,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fixed-bottom overlay" *ngIf="menuOpen" [@slideFromBottom]="menuOpen">
|
<div class="fixed-bottom overlay" *ngIf="menuOpen" [@slideFromBottom]="menuOpen">
|
||||||
<div class="form-group" *ngIf="pageOptions != undefined && pageOptions.ceil != undefined">
|
<div class="mb-3" *ngIf="pageOptions != undefined && pageOptions.ceil != undefined">
|
||||||
<span class="sr-only" id="slider-info"></span>
|
<span class="visually-hidden" id="slider-info"></span>
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter();resetMenuCloseTimer();" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter();resetMenuCloseTimer();" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||||
<button class="btn btn-small btn-icon col-1" [disabled]="prevPageDisabled || pageNum === 0" (click)="goToPage(0);resetMenuCloseTimer();" title="First Page"><i class="fa fa-step-backward" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-1" [disabled]="prevPageDisabled || pageNum === 0" (click)="goToPage(0);resetMenuCloseTimer();" title="First Page"><i class="fa fa-step-backward" aria-hidden="true"></i></button>
|
||||||
<div class="col custom-slider" *ngIf="pageOptions.ceil > 0; else noSlider">
|
<div class="col custom-slider" *ngIf="pageOptions.ceil > 0; else noSlider">
|
||||||
@ -75,29 +75,29 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row pt-4 ml-2 mr-2">
|
<div class="row pt-4 ms-2 me-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<button class="btn btn-icon" (click)="setReadingDirection();resetMenuCloseTimer();" [disabled]="readerMode === READER_MODE.WEBTOON || readerMode === READER_MODE.MANGA_UD" aria-describedby="reading-direction" title="Reading Direction: {{readingDirection === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}">
|
<button class="btn btn-icon" (click)="setReadingDirection();resetMenuCloseTimer();" [disabled]="readerMode === READER_MODE.WEBTOON || readerMode === READER_MODE.MANGA_UD" aria-describedby="reading-direction" title="Reading Direction: {{readingDirection === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}">
|
||||||
<i class="fa fa-angle-double-{{readingDirection === ReadingDirection.LeftToRight ? 'right' : 'left'}}" aria-hidden="true"></i>
|
<i class="fa fa-angle-double-{{readingDirection === ReadingDirection.LeftToRight ? 'right' : 'left'}}" aria-hidden="true"></i>
|
||||||
<span id="reading-direction" class="sr-only">{{readingDirection === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}</span>
|
<span id="reading-direction" class="visually-hidden">{{readingDirection === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<button class="btn btn-icon" title="Reading Mode" (click)="toggleReaderMode();resetMenuCloseTimer();">
|
<button class="btn btn-icon" title="Reading Mode" (click)="toggleReaderMode();resetMenuCloseTimer();">
|
||||||
<i class="fa {{readerModeIcon}}" aria-hidden="true"></i>
|
<i class="fa {{readerModeIcon}}" aria-hidden="true"></i>
|
||||||
<span class="sr-only">Reading Mode</span>
|
<span class="visually-hidden">Reading Mode</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<button class="btn btn-icon" title="{{this.isFullscreen ? 'Collapse' : 'Fullscreen'}}" (click)="toggleFullscreen();resetMenuCloseTimer();">
|
<button class="btn btn-icon" title="{{this.isFullscreen ? 'Collapse' : 'Fullscreen'}}" (click)="toggleFullscreen();resetMenuCloseTimer();">
|
||||||
<i class="fa {{this.isFullscreen ? 'fa-compress-alt' : 'fa-expand-alt'}}" aria-hidden="true"></i>
|
<i class="fa {{this.isFullscreen ? 'fa-compress-alt' : 'fa-expand-alt'}}" aria-hidden="true"></i>
|
||||||
<span class="sr-only">{{this.isFullscreen ? 'Collapse' : 'Fullscreen'}}</span>
|
<span class="visually-hidden">{{this.isFullscreen ? 'Collapse' : 'Fullscreen'}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<button class="btn btn-icon" title="Settings" (click)="settingsOpen = !settingsOpen;resetMenuCloseTimer();">
|
<button class="btn btn-icon" title="Settings" (click)="settingsOpen = !settingsOpen;resetMenuCloseTimer();">
|
||||||
<i class="fa fa-sliders-h" aria-hidden="true"></i>
|
<i class="fa fa-sliders-h" aria-hidden="true"></i>
|
||||||
<span class="sr-only">Settings</span>
|
<span class="visually-hidden">Settings</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -105,13 +105,13 @@
|
|||||||
<form [formGroup]="generalSettingsForm">
|
<form [formGroup]="generalSettingsForm">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label for="page-splitting">Image Splitting</label>
|
<label for="page-splitting" class="form-label">Image Splitting</label>
|
||||||
<div class="split fa fa-image">
|
<div class="split fa fa-image">
|
||||||
<div class="{{splitIconClass}}"></div>
|
<div class="{{splitIconClass}}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
|
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
|
||||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text}}</option>
|
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text}}</option>
|
||||||
</select>
|
</select>
|
||||||
@ -121,7 +121,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label for="page-fitting">Image Scaling</label> <i class="fa {{getFittingIcon()}}" aria-hidden="true"></i>
|
<label for="page-fitting" class="form-label">Image Scaling</label> <i class="fa {{getFittingIcon()}}" aria-hidden="true"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<select class="form-control" id="page-fitting" formControlName="fittingOption">
|
<select class="form-control" id="page-fitting" formControlName="fittingOption">
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
@use '../../theme/colors';
|
|
||||||
|
|
||||||
$center-width: 50%;
|
$center-width: 50%;
|
||||||
$side-width: 25%;
|
$side-width: 25%;
|
||||||
|
|
||||||
@ -16,16 +14,16 @@ $pointer-offset: 5px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon {
|
// .btn-icon {
|
||||||
color: white;
|
// color: white;
|
||||||
}
|
// }
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader {
|
.reader {
|
||||||
background-color: black;
|
background-color: var(--manga-reader-bg-color);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@ -58,9 +56,9 @@ canvas {
|
|||||||
|
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
background-color: rgba(0,0,0,0.5);
|
background-color: var(--manga-reader-overlay-bg-color);
|
||||||
backdrop-filter: blur(10px); // BUG: This doesn't work on Firefox
|
backdrop-filter: var(--manga-reader-overlay-filter); // BUG: This doesn't work on Firefox
|
||||||
color: white;
|
color: var(--manga-reader-overlay-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fitting Options
|
// Fitting Options
|
||||||
@ -175,7 +173,7 @@ canvas {
|
|||||||
height: 2px;
|
height: 2px;
|
||||||
}
|
}
|
||||||
.custom-slider .ngx-slider .ngx-slider-selection {
|
.custom-slider .ngx-slider .ngx-slider-selection {
|
||||||
background: colors.$primary-color;
|
background: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-slider .ngx-slider .ngx-slider-pointer {
|
.custom-slider .ngx-slider .ngx-slider-pointer {
|
||||||
@ -183,7 +181,7 @@ canvas {
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
top: auto; /* to remove the default positioning */
|
top: auto; /* to remove the default positioning */
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: colors.$primary-color; // #333;
|
background-color: var(--primary-color); // #333;
|
||||||
border-top-left-radius: 3px;
|
border-top-left-radius: 3px;
|
||||||
border-top-right-radius: 3px;
|
border-top-right-radius: 3px;
|
||||||
}
|
}
|
||||||
@ -214,7 +212,7 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.custom-slider .ngx-slider .ngx-slider-tick.ngx-slider-selected {
|
.custom-slider .ngx-slider .ngx-slider-tick.ngx-slider-selected {
|
||||||
background: colors.$primary-color;
|
background: var(--primary-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,12 +233,12 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
background-color: rgba(65, 225, 100, 0.5) !important;
|
background-color: var(--manga-reader-next-highlight-bg-color) !important;
|
||||||
animation: fadein .5s both;
|
animation: fadein .5s both;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
.highlight-2 {
|
.highlight-2 {
|
||||||
background-color: rgba(65, 105, 225, 0.5) !important;
|
background-color: var(--manga-reader-prev-highlight-bg-color) !important;
|
||||||
animation: fadein .5s both;
|
animation: fadein .5s both;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
@ -255,6 +253,6 @@ canvas {
|
|||||||
border: 0px;
|
border: 0px;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
border: 5px solid colors.$primary-color;
|
border: 5px solid var(--primary-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<button type="button" class="btn btn-icon {{(progressEventsSource.getValue().length > 0 || updateAvailable) ? 'colored' : ''}}"
|
<button type="button" class="btn btn-icon {{(progressEventsSource.getValue().length > 0 || updateAvailable) ? 'colored' : ''}}"
|
||||||
[ngbPopover]="popContent" title="Activity" placement="bottom" [popoverClass]="'nav-events'">
|
[ngbPopover]="popContent" title="Activity" placement="bottom" [popoverClass]="'nav-events'">
|
||||||
<i aria-hidden="true" class="fa fa-wave-square"></i>
|
<i aria-hidden="true" class="fa fa-wave-square nav"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ng-template #popContent>
|
<ng-template #popContent>
|
||||||
@ -11,7 +11,7 @@
|
|||||||
<div class="spinner-border text-primary small-spinner"
|
<div class="spinner-border text-primary small-spinner"
|
||||||
role="status" title="Started at {{event.timestamp | date: 'short'}}"
|
role="status" title="Started at {{event.timestamp | date: 'short'}}"
|
||||||
attr.aria-valuetext="{{prettyPrintProgress(event.progress)}}%" [attr.aria-valuenow]="prettyPrintProgress(event.progress)">
|
attr.aria-valuetext="{{prettyPrintProgress(event.progress)}}%" [attr.aria-valuenow]="prettyPrintProgress(event.progress)">
|
||||||
<span class="sr-only">Scan for {{event.libraryName}} in progress</span>
|
<span class="visually-hidden">Scan for {{event.libraryName}} in progress</span>
|
||||||
</div>
|
</div>
|
||||||
{{prettyPrintProgress(event.progress)}}%
|
{{prettyPrintProgress(event.progress)}}%
|
||||||
{{prettyPrintEvent(event.eventType, event)}} {{event.libraryName}}
|
{{prettyPrintEvent(event.eventType, event)}} {{event.libraryName}}
|
||||||
|
@ -1,4 +1,28 @@
|
|||||||
@use "../../theme/colors";
|
// NOTE: I'm leaving this not fully customized because I'm planning to rewrite the whole design in v0.5.2/3
|
||||||
|
// These are customizations for events nav
|
||||||
|
.dark-menu {
|
||||||
|
background-color: var(--navbar-bg-color);
|
||||||
|
border-color: rgba(1, 4, 9, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-menu-item {
|
||||||
|
color: var(--body-text-color);
|
||||||
|
background-color: rgb(1, 4, 9);
|
||||||
|
border-color: rgba(1, 4, 9, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popovers need to be their own component
|
||||||
|
::ng-deep .bs-popover-bottom > .popover-arrow::after, .bs-popover-bottom > .popover-arrow::before {
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-events {
|
||||||
|
background-color: var(--navbar-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// .nav-events {
|
||||||
|
// background-color: white;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
.btn:focus, .btn:hover {
|
.btn:focus, .btn:hover {
|
||||||
@ -10,9 +34,7 @@
|
|||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-events {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-events .popover-body {
|
.nav-events .popover-body {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
@ -23,7 +45,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.colored {
|
.colored {
|
||||||
background-color: colors.$primary-color;
|
background-color: var(--primary-color);
|
||||||
border-radius: 60px;
|
border-radius: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +53,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
i.fa {
|
i.fa {
|
||||||
color: colors.$primary-color !important;
|
color: var(--primary-color) !important;
|
||||||
}
|
}
|
||||||
color: colors.$primary-color;
|
color: var(--primary-color);
|
||||||
}
|
}
|
@ -19,8 +19,9 @@ interface ProcessedEvent {
|
|||||||
|
|
||||||
type ProgressType = EVENTS.ScanLibraryProgress | EVENTS.RefreshMetadataProgress | EVENTS.BackupDatabaseProgress | EVENTS.CleanupProgress;
|
type ProgressType = EVENTS.ScanLibraryProgress | EVENTS.RefreshMetadataProgress | EVENTS.BackupDatabaseProgress | EVENTS.CleanupProgress;
|
||||||
|
|
||||||
const acceptedEvents = [EVENTS.ScanLibraryProgress, EVENTS.RefreshMetadataProgress, EVENTS.BackupDatabaseProgress, EVENTS.CleanupProgress, EVENTS.DownloadProgress];
|
const acceptedEvents = [EVENTS.ScanLibraryProgress, EVENTS.RefreshMetadataProgress, EVENTS.BackupDatabaseProgress, EVENTS.CleanupProgress, EVENTS.DownloadProgress, EVENTS.SiteThemeProgress];
|
||||||
|
|
||||||
|
// TODO: Rename this to events widget
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-nav-events-toggle',
|
selector: 'app-nav-events-toggle',
|
||||||
templateUrl: './nav-events-toggle.component.html',
|
templateUrl: './nav-events-toggle.component.html',
|
||||||
|
@ -1,112 +1,106 @@
|
|||||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top" *ngIf="navService?.navbarVisible$ | async">
|
<nav class="navbar navbar-expand-md navbar-dark fixed-top" *ngIf="navService?.navbarVisible$ | async">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="sr-only sr-only-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
||||||
<a class="navbar-brand dark-exempt" routerLink="/library" routerLinkActive="active"><img class="logo" src="../../assets/images/logo.png" alt="kavita icon" aria-hidden="true"/><span class="phone-hidden"> Kavita</span></a>
|
<a class="navbar-brand dark-exempt" routerLink="/library" routerLinkActive="active"><img class="logo" src="../../assets/images/logo.png" alt="kavita icon" aria-hidden="true"/><span class="d-none d-md-inline"> Kavita</span></a>
|
||||||
<ul class="navbar-nav col mr-auto">
|
<ul class="navbar-nav col me-auto">
|
||||||
|
|
||||||
<div class="nav-item" *ngIf="(accountService.currentUser$ | async) as user">
|
<div class="nav-item" *ngIf="(accountService.currentUser$ | async) as user">
|
||||||
<div>
|
<label for="nav-search" class="form-label visually-hidden">Search series</label>
|
||||||
<fieldset class="form-inline">
|
<div class="ng-autocomplete">
|
||||||
<div class="form-group" style="margin-bottom: 0px;">
|
<app-grouped-typeahead
|
||||||
<label for="nav-search" class="sr-only">Search series</label>
|
#search
|
||||||
<div class="ng-autocomplete">
|
id="nav-search"
|
||||||
<app-grouped-typeahead
|
[minQueryLength]="2"
|
||||||
#search
|
initialValue=""
|
||||||
id="nav-search"
|
placeholder="Search…"
|
||||||
[minQueryLength]="2"
|
[grouppedData]="searchResults"
|
||||||
initialValue=""
|
(inputChanged)="onChangeSearch($event)"
|
||||||
placeholder="Search…"
|
(clearField)="clearSearch()"
|
||||||
[grouppedData]="searchResults"
|
(focusChanged)="focusUpdate($event)"
|
||||||
(inputChanged)="onChangeSearch($event)"
|
>
|
||||||
(clearField)="clearSearch()"
|
|
||||||
(focusChanged)="focusUpdate($event)"
|
|
||||||
>
|
|
||||||
|
|
||||||
<ng-template #libraryTemplate let-item>
|
<ng-template #libraryTemplate let-item>
|
||||||
<div style="display: flex;padding: 5px;" (click)="clickLibraryResult(item)">
|
<div style="display: flex;padding: 5px;" (click)="clickLibraryResult(item)">
|
||||||
<div class="ml-1">
|
<div class="ms-1">
|
||||||
<span>{{item.name}}</span>
|
<span>{{item.name}}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #seriesTemplate let-item>
|
|
||||||
<div style="display: flex;padding: 5px;" (click)="clickSeriesSearchResult(item)">
|
|
||||||
<div style="width: 24px" class="mr-1">
|
|
||||||
<app-image class="mr-3 search-result" width="24px" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"></app-image>
|
|
||||||
</div>
|
|
||||||
<div class="ml-1">
|
|
||||||
<app-series-format [format]="item.format"></app-series-format>
|
|
||||||
<span *ngIf="item.name.toLowerCase().trim().indexOf(searchTerm) >= 0; else localizedName">{{item.name}}</span>
|
|
||||||
<ng-template #localizedName>
|
|
||||||
<span [innerHTML]="item.localizedName"></span>
|
|
||||||
</ng-template>
|
|
||||||
<span class="form-text" style="font-size: 0.8rem;">in {{item.libraryName}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #collectionTemplate let-item>
|
|
||||||
<div style="display: flex;padding: 5px;" (click)="clickCollectionSearchResult(item)">
|
|
||||||
<div style="width: 24px" class="mr-1">
|
|
||||||
<app-image class="mr-3 search-result" width="24px" [imageUrl]="imageService.getCollectionCoverImage(item.id)"></app-image>
|
|
||||||
</div>
|
|
||||||
<div class="ml-1">
|
|
||||||
<span>{{item.title}}</span>
|
|
||||||
<span *ngIf="item.promoted">
|
|
||||||
<i class="fa fa-angle-double-up" aria-hidden="true" title="Promoted"></i>
|
|
||||||
<span class="sr-only">(promoted)</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #readingListTemplate let-item>
|
|
||||||
<div style="display: flex;padding: 5px;" (click)="clickReadingListSearchResult(item)">
|
|
||||||
<div class="ml-1">
|
|
||||||
<span>{{item.title}}</span>
|
|
||||||
<span *ngIf="item.promoted">
|
|
||||||
<i class="fa fa-angle-double-up" aria-hidden="true" title="Promoted"></i>
|
|
||||||
<span class="sr-only">(promoted)</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #tagTemplate let-item>
|
|
||||||
<div style="display: flex;padding: 5px;" (click)="goTo('tags', item.id)">
|
|
||||||
<div class="ml-1">
|
|
||||||
<span>{{item.title}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #personTemplate let-item>
|
|
||||||
<div style="display: flex;padding: 5px;" class="clickable" (click)="goToPerson(item.role, item.id)">
|
|
||||||
<div class="ml-1">
|
|
||||||
|
|
||||||
<div [innerHTML]="item.name"></div>
|
|
||||||
<div>{{item.role | personRole}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #genreTemplate let-item>
|
|
||||||
<div style="display: flex;padding: 5px;" class="clickable" (click)="goTo('genres', item.id)">
|
|
||||||
<div class="ml-1">
|
|
||||||
<div [innerHTML]="item.title"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #noResultsTemplate let-notFound>
|
|
||||||
No results found
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
</app-grouped-typeahead>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #seriesTemplate let-item>
|
||||||
|
<div style="display: flex;padding: 5px;" (click)="clickSeriesSearchResult(item)">
|
||||||
|
<div style="width: 24px" class="me-1">
|
||||||
|
<app-image class="me-3 search-result" width="24px" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"></app-image>
|
||||||
|
</div>
|
||||||
|
<div class="ms-1">
|
||||||
|
<app-series-format [format]="item.format"></app-series-format>
|
||||||
|
<span *ngIf="item.name.toLowerCase().trim().indexOf(searchTerm) >= 0; else localizedName">{{item.name}}</span>
|
||||||
|
<ng-template #localizedName>
|
||||||
|
<span [innerHTML]="item.localizedName"></span>
|
||||||
|
</ng-template>
|
||||||
|
<div class="form-text" style="font-size: 0.8rem;">in {{item.libraryName}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #collectionTemplate let-item>
|
||||||
|
<div style="display: flex;padding: 5px;" (click)="clickCollectionSearchResult(item)">
|
||||||
|
<div style="width: 24px" class="me-1">
|
||||||
|
<app-image class="me-3 search-result" width="24px" [imageUrl]="imageService.getCollectionCoverImage(item.id)"></app-image>
|
||||||
|
</div>
|
||||||
|
<div class="ms-1">
|
||||||
|
<span>{{item.title}}</span>
|
||||||
|
<span *ngIf="item.promoted">
|
||||||
|
<i class="fa fa-angle-double-up" aria-hidden="true" title="Promoted"></i>
|
||||||
|
<span class="visually-hidden">(promoted)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #readingListTemplate let-item>
|
||||||
|
<div style="display: flex;padding: 5px;" (click)="clickReadingListSearchResult(item)">
|
||||||
|
<div class="ms-1">
|
||||||
|
<span>{{item.title}}</span>
|
||||||
|
<span *ngIf="item.promoted">
|
||||||
|
<i class="fa fa-angle-double-up" aria-hidden="true" title="Promoted"></i>
|
||||||
|
<span class="visually-hidden">(promoted)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #tagTemplate let-item>
|
||||||
|
<div style="display: flex;padding: 5px;" (click)="goTo('tags', item.id)">
|
||||||
|
<div class="ms-1">
|
||||||
|
<span>{{item.title}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #personTemplate let-item>
|
||||||
|
<div style="display: flex;padding: 5px;" class="clickable" (click)="goToPerson(item.role, item.id)">
|
||||||
|
<div class="ms-1">
|
||||||
|
|
||||||
|
<div [innerHTML]="item.name"></div>
|
||||||
|
<div>{{item.role | personRole}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #genreTemplate let-item>
|
||||||
|
<div style="display: flex;padding: 5px;" class="clickable" (click)="goTo('genres', item.id)">
|
||||||
|
<div class="ms-1">
|
||||||
|
<div [innerHTML]="item.title"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #noResultsTemplate let-notFound>
|
||||||
|
No results found
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
</app-grouped-typeahead>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
@ -114,8 +108,8 @@
|
|||||||
<ng-container *ngIf="!searchFocused">
|
<ng-container *ngIf="!searchFocused">
|
||||||
<div class="back-to-top">
|
<div class="back-to-top">
|
||||||
<button class="btn btn-icon scroll-to-top" (click)="scrollToTop()" *ngIf="backToTopNeeded">
|
<button class="btn btn-icon scroll-to-top" (click)="scrollToTop()" *ngIf="backToTopNeeded">
|
||||||
<i class="fa fa-angle-double-up" style="color: white" aria-hidden="true"></i>
|
<i class="fa fa-angle-double-up nav" aria-hidden="true"></i>
|
||||||
<span class="sr-only">Scroll to Top</span>
|
<span class="visually-hidden">Scroll to Top</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -123,10 +117,10 @@
|
|||||||
<div class="nav-item">
|
<div class="nav-item">
|
||||||
<app-nav-events-toggle [user]="user"></app-nav-events-toggle>
|
<app-nav-events-toggle [user]="user"></app-nav-events-toggle>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item pr-2 not-xs-only">
|
<div class="nav-item pe-2 not-xs-only">
|
||||||
<a routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')" class="dark-exempt btn btn-icon">
|
<a routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')" class="dark-exempt btn btn-icon" title="Server Settings">
|
||||||
<i class="fa fa-cogs" aria-hidden="true" style="color: white"></i>
|
<i class="fa fa-cogs nav" aria-hidden="true"></i>
|
||||||
<span class="sr-only">Server Settings</span>
|
<span class="visually-hidden">Server Settings</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,24 +1,21 @@
|
|||||||
@import '~bootstrap/scss/mixins/_breakpoints.scss';
|
|
||||||
|
|
||||||
$primary-color: white;
|
|
||||||
$bg-color: rgb(22, 27, 34);
|
|
||||||
|
|
||||||
.btn:focus, .btn:hover {
|
.btn:focus, .btn:hover {
|
||||||
box-shadow: 0 0 0 0.1rem rgba(255, 255, 255, 1);
|
box-shadow: 0 0 0 0.1rem var(--navbar-btn-hover-outline-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
background-color: $bg-color;
|
background-color: var(--navbar-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* small devices (phones, 650px and down) */
|
/* small devices (phones, 650px and down) */
|
||||||
@media only screen and (max-width:650px) { //370
|
@media only screen and (max-width:650px) {
|
||||||
.navbar-nav {
|
.navbar-nav {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// On Really small screens, hide the server settings wheel and show it in nav
|
// On Really small screens, hide the server settings wheel and show it in nav
|
||||||
|
// TODO: Look into doing this with bootstrap 5 (and moving to _utilities.scss)
|
||||||
.xs-only {
|
.xs-only {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -39,7 +36,7 @@ $bg-color: rgb(22, 27, 34);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
font-family: "Spartan", sans-serif;
|
font-family: var(--brand-font-family);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
@ -54,7 +51,7 @@ $bg-color: rgb(22, 27, 34);
|
|||||||
|
|
||||||
.focus-visible:focus {
|
.focus-visible:focus {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
color: white;
|
color: var(--nav-header-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ng-autocomplete {
|
.ng-autocomplete {
|
||||||
@ -62,7 +59,7 @@ $bg-color: rgb(22, 27, 34);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.primary-text {
|
.primary-text {
|
||||||
color: $primary-color;
|
color: var(--nav-header-text-color);
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,27 +72,12 @@ $bg-color: rgb(22, 27, 34);
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 576px) {
|
@media (min-width: var(--grid-breakpoints-sm)) {
|
||||||
.form-inline .form-group {
|
.form-inline .form-group {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)) {
|
|
||||||
.ng-autocomplete {
|
|
||||||
width: 100%; // 232px
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-to-top:hover {
|
.scroll-to-top:hover {
|
||||||
animation: MoveUpDown 1s linear infinite;
|
animation: MoveUpDown 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes MoveUpDown {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
}
|
|
@ -50,15 +50,15 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
|||||||
private scrollService: ScrollService) { }
|
private scrollService: ScrollService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.navService.darkMode$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
|
// this.navService.darkMode$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
|
||||||
if (res) {
|
// if (res) {
|
||||||
this.document.body.classList.remove('bg-light');
|
// this.document.body.classList.remove('bg-light');
|
||||||
this.document.body.classList.add('bg-dark');
|
// this.document.body.classList.add('bg-dark');
|
||||||
} else {
|
// } else {
|
||||||
this.document.body.classList.remove('bg-dark');
|
// this.document.body.classList.remove('bg-dark');
|
||||||
this.document.body.classList.add('bg-light');
|
// this.document.body.classList.add('bg-light');
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener("window:scroll", [])
|
@HostListener("window:scroll", [])
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">Add to Reading List</h4>
|
<h4 class="modal-title" id="modal-basic-title">Add to Reading List</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<form style="width: 100%" [formGroup]="listForm">
|
<form style="width: 100%" [formGroup]="listForm">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group" *ngIf="lists.length >= 5">
|
<div class="mb-3" *ngIf="lists.length >= 5">
|
||||||
<label for="filter">Filter</label>
|
<label for="filter" class="form-label">Filter</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||||
<div class="input-group-append">
|
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
||||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
@ -23,7 +21,7 @@
|
|||||||
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No lists created yet</li>
|
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No lists created yet</li>
|
||||||
<li class="list-group-item" *ngIf="loading">
|
<li class="list-group-item" *ngIf="loading">
|
||||||
<div class="spinner-border text-secondary" role="status">
|
<div class="spinner-border text-secondary" role="status">
|
||||||
<span class="sr-only">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -32,7 +30,7 @@
|
|||||||
<div style="width: 100%;">
|
<div style="width: 100%;">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="col-9 col-lg-10">
|
<div class="col-9 col-lg-10">
|
||||||
<label class="sr-only" for="add-rlist">Reading List</label>
|
<label class="form-label visually-hidden" for="add-rlist">Reading List</label>
|
||||||
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
|
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
|
|
||||||
@use '../../../../theme/colors';
|
|
||||||
.clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clickable:hover, .clickable:focus {
|
.clickable:hover, .clickable:focus {
|
||||||
background-color: colors.$primary-color;
|
background-color: var(--primary-color);
|
||||||
}
|
}
|
@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">Edit {{readingList.title}} Reading List</h4>
|
<h4 class="modal-title" id="modal-basic-title">Edit {{readingList.title}} Reading List</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -11,20 +11,20 @@
|
|||||||
Promotion means that the list can be seen server-wide, not just for admin users. All series that are within this list will still have user-access restrictions placed on them.
|
Promotion means that the list can be seen server-wide, not just for admin users. All series that are within this list will still have user-access restrictions placed on them.
|
||||||
</p>
|
</p>
|
||||||
<form [formGroup]="reviewGroup">
|
<form [formGroup]="reviewGroup">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="title">Name</label>
|
<label for="title" class="form-label">Name</label>
|
||||||
<input id="title" class="form-control" formControlName="title" type="text">
|
<input id="title" class="form-control" formControlName="title" type="text">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="summary">Summary</label>
|
<label for="summary" class="form-label">Summary</label>
|
||||||
<textarea id="summary" class="form-control" formControlName="summary" rows="3"></textarea>
|
<textarea id="summary" class="form-control" formControlName="summary" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
|
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
|
||||||
<button type="button" class="btn btn-info" (click)="togglePromotion()">{{readingList.promoted ? 'Demote' : 'Promote'}}</button>
|
<button type="button" class="btn btn-secondary alt" (click)="togglePromotion()">{{readingList.promoted ? 'Demote' : 'Promote'}}</button>
|
||||||
<button type="submit" class="btn btn-primary" [disabled]="reviewGroup.get('title')?.value.trim().length === 0" (click)="save()">Save</button>
|
<button type="submit" class="btn btn-primary" [disabled]="reviewGroup.get('title')?.value.trim().length === 0" (click)="save()">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
<div cdkDropList class="{{items.length > 0 ? 'example-list list-group-flush' : ''}}" (cdkDropListDropped)="drop($event)">
|
<div cdkDropList class="{{items.length > 0 ? 'example-list list-group-flush' : ''}}" (cdkDropListDropped)="drop($event)">
|
||||||
<div class="example-box" *ngFor="let item of items; index as i" cdkDrag [cdkDragData]="item" cdkDragBoundary=".example-list">
|
<div class="example-box" *ngFor="let item of items; index as i" cdkDrag [cdkDragData]="item" cdkDragBoundary=".example-list">
|
||||||
<div class="mr-3 align-middle">
|
<div class="me-3 align-middle">
|
||||||
<i class="fa fa-grip-vertical drag-handle" aria-hidden="true" cdkDragHandle></i>
|
<i class="fa fa-grip-vertical drag-handle" aria-hidden="true" cdkDragHandle></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container style="display: inline-block" [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
<ng-container style="display: inline-block" [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||||
|
|
||||||
<div class="align-middle" style="padding-top: 40px">
|
<div class="align-middle" style="padding-top: 40px">
|
||||||
<label for="reorder-{{i}}" class="sr-only">Reorder</label>
|
<label for="reorder-{{i}}" class="form-label visually-hidden">Reorder</label>
|
||||||
<input *ngIf="accessibilityMode" id="reorder-{{i}}" type="number" min="0" [max]="items.length - 1" [value]="i" style="width: 40px" (focusout)="updateIndex(i, item)" (keydown.enter)="updateIndex(i, item)" aria-describedby="instructions">
|
<input *ngIf="accessibilityMode" id="reorder-{{i}}" type="number" min="0" [max]="items.length - 1" [value]="i" style="width: 40px" (focusout)="updateIndex(i, item)" (keydown.enter)="updateIndex(i, item)" aria-describedby="instructions">
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-icon pull-right" (click)="removeItem(item, i)">
|
<button class="btn btn-icon pull-right" (click)="removeItem(item, i)">
|
||||||
<i class="fa fa-times" aria-hidden="true"></i>
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
<span class="sr-only" attr.aria-labelledby="item.id--{{i}}">Remove item</span>
|
<span class="visually-hidden" attr.aria-labelledby="item.id--{{i}}">Remove item</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="sr-only" id="instructions">
|
<p class="visually-hidden" id="instructions">
|
||||||
|
|
||||||
</p>
|
</p>
|
@ -1,19 +1,19 @@
|
|||||||
<div class="container-fluid mt-2" *ngIf="readingList">
|
<div class="container-fluid mt-2" *ngIf="readingList">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<!-- Title row-->
|
<!-- Title row-->
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
|
|
||||||
<h2 style="display: inline-block">
|
<h2 style="display: inline-block">
|
||||||
<span *ngIf="actions.length > 0">
|
<span *ngIf="actions.length > 0">
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title"></app-card-actionables>
|
||||||
</span>
|
</span>
|
||||||
{{readingList.title}} <span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
{{readingList.title}} <span *ngIf="readingList?.promoted">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||||
<span class="badge badge-primary badge-pill" attr.aria-label="{{items.length}} total items">{{items.length}}</span>
|
<span class="badge bg-primary rounded-pill" attr.aria-label="{{items.length}} total items">{{items.length}}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<!-- Action row-->
|
<!-- Action row-->
|
||||||
<div class="row no-gutters">
|
<div class="row g-0">
|
||||||
<div class="mr-2">
|
<div class="col-auto me-2">
|
||||||
<button class="btn btn-primary" title="Read" (click)="read()">
|
<button class="btn btn-primary" title="Read" (click)="read()">
|
||||||
<span>
|
<span>
|
||||||
<i class="fa fa-book-open" aria-hidden="true"></i>
|
<i class="fa fa-book-open" aria-hidden="true"></i>
|
||||||
@ -21,7 +21,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="col-auto">
|
||||||
<button class="btn btn-secondary" (click)="removeRead()" [disabled]="readingList?.promoted && !this.isAdmin">
|
<button class="btn btn-secondary" (click)="removeRead()" [disabled]="readingList?.promoted && !this.isAdmin">
|
||||||
<span>
|
<span>
|
||||||
<i class="fa fa-check"></i>
|
<i class="fa fa-check"></i>
|
||||||
@ -29,7 +29,7 @@
|
|||||||
<span class="read-btn--text"> Remove Read</span>
|
<span class="read-btn--text"> Remove Read</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-2 mt-2" *ngIf="!(readingList?.promoted && !this.isAdmin)">
|
<div class="col-auto ms-2 mt-2" *ngIf="!(readingList?.promoted && !this.isAdmin)">
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" id="accessibilit-mode" [value]="accessibilityMode" (change)="accessibilityMode = !accessibilityMode">
|
<input class="form-check-input" type="checkbox" id="accessibilit-mode" [value]="accessibilityMode" (change)="accessibilityMode = !accessibilityMode">
|
||||||
<label class="form-check-label" for="accessibilit-mode">Order Numbers</label>
|
<label class="form-check-label" for="accessibilit-mode">Order Numbers</label>
|
||||||
@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Summary row-->
|
<!-- Summary row-->
|
||||||
<div class="row no-gutters mt-2">
|
<div class="row g-0 mt-2">
|
||||||
<app-read-more [text]="readingList.summary" [maxLength]="250"></app-read-more>
|
<app-read-more [text]="readingList.summary" [maxLength]="250"></app-read-more>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -48,18 +48,18 @@
|
|||||||
|
|
||||||
<app-dragable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" (itemRemove)="itemRemoved($event)" [accessibilityMode]="accessibilityMode">
|
<app-dragable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" (itemRemove)="itemRemoved($event)" [accessibilityMode]="accessibilityMode">
|
||||||
<ng-template #draggableItem let-item let-position="idx">
|
<ng-template #draggableItem let-item let-position="idx">
|
||||||
<div class="media" style="width: 100%;">
|
<div class="d-flex" style="width: 100%;">
|
||||||
<app-image width="74px" class="img-top mr-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
|
<app-image width="74px" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
|
||||||
<div class="media-body">
|
<div class="flex-grow-1">
|
||||||
<h5 class="mt-0 mb-1" id="item.id--{{position}}">{{formatTitle(item)}}
|
<h5 class="mt-0 mb-1" id="item.id--{{position}}">{{formatTitle(item)}}
|
||||||
<span class="badge badge-primary badge-pill">
|
<span class="badge bg-primary rounded-pill">
|
||||||
<span *ngIf="item.pagesRead > 0 && item.pagesRead < item.pagesTotal">{{item.pagesRead}} / {{item.pagesTotal}}</span>
|
<span *ngIf="item.pagesRead > 0 && item.pagesRead < item.pagesTotal">{{item.pagesRead}} / {{item.pagesTotal}}</span>
|
||||||
<span *ngIf="item.pagesRead === 0">UNREAD</span>
|
<span *ngIf="item.pagesRead === 0">UNREAD</span>
|
||||||
<span *ngIf="item.pagesRead === item.pagesTotal">READ</span>
|
<span *ngIf="item.pagesRead === item.pagesTotal">READ</span>
|
||||||
</span>
|
</span>
|
||||||
</h5>
|
</h5>
|
||||||
<i class="fa {{utilityService.mangaFormatIcon(item.seriesFormat)}}" aria-hidden="true" *ngIf="item.seriesFormat != MangaFormat.UNKNOWN" title="{{utilityService.mangaFormat(item.seriesFormat)}}"></i>
|
<i class="fa {{utilityService.mangaFormatIcon(item.seriesFormat)}}" aria-hidden="true" *ngIf="item.seriesFormat != MangaFormat.UNKNOWN" title="{{utilityService.mangaFormat(item.seriesFormat)}}"></i>
|
||||||
<span class="sr-only">{{utilityService.mangaFormat(item.seriesFormat)}}</span>
|
<span class="visually-hidden">{{utilityService.mangaFormat(item.seriesFormat)}}</span>
|
||||||
|
|
||||||
<a href="/library/{{item.libraryId}}/series/{{item.seriesId}}">{{item.seriesName}}</a>
|
<a href="/library/{{item.libraryId}}/series/{{item.seriesId}}">{{item.seriesName}}</a>
|
||||||
<span *ngIf="item.promoted">
|
<span *ngIf="item.promoted">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">Account Migration</h4>
|
<h4 class="modal-title" id="modal-basic-title">Account Migration</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -12,8 +12,8 @@
|
|||||||
<p class="text-danger" *ngIf="error.length > 0">{{error}}</p>
|
<p class="text-danger" *ngIf="error.length > 0">{{error}}</p>
|
||||||
|
|
||||||
<form [formGroup]="registerForm">
|
<form [formGroup]="registerForm">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="username">Username</label>
|
<label for="username" class="form-label">Username</label>
|
||||||
<input id="username" class="form-control" formControlName="username" type="text">
|
<input id="username" class="form-control" formControlName="username" type="text">
|
||||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||||
<div *ngIf="registerForm.get('username')?.errors?.required">
|
<div *ngIf="registerForm.get('username')?.errors?.required">
|
||||||
@ -22,8 +22,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="width:100%">
|
<div class="mb-3" style="width:100%">
|
||||||
<label for="email">Email</label>
|
<label for="email" class="form-label">Email</label>
|
||||||
<input class="form-control" type="email" id="email" formControlName="email" required>
|
<input class="form-control" type="email" id="email" formControlName="email" required>
|
||||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||||
<div *ngIf="registerForm.get('email')?.errors?.required">
|
<div *ngIf="registerForm.get('email')?.errors?.required">
|
||||||
@ -35,8 +35,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="password">Password</label>
|
<label for="password" class="form-label">Password</label>
|
||||||
<input id="password" class="form-control" maxlength="32" minlength="6" formControlName="password" type="password" aria-describedby="password-help">
|
<input id="password" class="form-control" maxlength="32" minlength="6" formControlName="password" type="password" aria-describedby="password-help">
|
||||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||||
<div *ngIf="registerForm.get('password')?.errors?.required">
|
<div *ngIf="registerForm.get('password')?.errors?.required">
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<form [formGroup]="registerForm" (ngSubmit)="submit()">
|
<form [formGroup]="registerForm" (ngSubmit)="submit()">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="username">Username</label>
|
<label for="username" class="form-label">Username</label>
|
||||||
<input id="username" class="form-control" formControlName="username" type="text">
|
<input id="username" class="form-control" formControlName="username" type="text">
|
||||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||||
<div *ngIf="registerForm.get('username')?.errors?.required">
|
<div *ngIf="registerForm.get('username')?.errors?.required">
|
||||||
@ -20,8 +20,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="width:100%">
|
<div class="mb-3" style="width:100%">
|
||||||
<label for="email">Email</label>
|
<label for="email" class="form-label">Email</label>
|
||||||
<input class="form-control" type="email" id="email" formControlName="email" required>
|
<input class="form-control" type="email" id="email" formControlName="email" required>
|
||||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||||
<div *ngIf="registerForm.get('email')?.errors?.required">
|
<div *ngIf="registerForm.get('email')?.errors?.required">
|
||||||
@ -33,12 +33,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="password">Password</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="passwordTooltip" role="button" tabindex="0"></i>
|
<label for="password" class="form-label">Password</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="passwordTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #passwordTooltip>
|
<ng-template #passwordTooltip>
|
||||||
Password must be between 6 and 32 characters in length
|
Password must be between 6 and 32 characters in length
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<span class="sr-only" id="password-help"><ng-container [ngTemplateOutlet]="passwordTooltip"></ng-container></span>
|
<span class="visually-hidden" id="password-help"><ng-container [ngTemplateOutlet]="passwordTooltip"></ng-container></span>
|
||||||
<input id="password" class="form-control" maxlength="32" minlength="6" formControlName="password" type="password" aria-describedby="password-help">
|
<input id="password" class="form-control" maxlength="32" minlength="6" formControlName="password" type="password" aria-describedby="password-help">
|
||||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||||
<div *ngIf="registerForm.get('password')?.errors?.required">
|
<div *ngIf="registerForm.get('password')?.errors?.required">
|
||||||
@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="float-right">
|
<div class="float-end">
|
||||||
<button class="btn btn-secondary alt" type="submit">Register</button>
|
<button class="btn btn-secondary alt" type="submit">Register</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
|
|||||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
import { ThemeService } from 'src/app/theme.service';
|
||||||
import { AccountService } from 'src/app/_services/account.service';
|
import { AccountService } from 'src/app/_services/account.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -10,8 +11,6 @@ import { AccountService } from 'src/app/_services/account.service';
|
|||||||
styleUrls: ['./confirm-email.component.scss']
|
styleUrls: ['./confirm-email.component.scss']
|
||||||
})
|
})
|
||||||
export class ConfirmEmailComponent implements OnInit {
|
export class ConfirmEmailComponent implements OnInit {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Email token used for validating
|
* Email token used for validating
|
||||||
*/
|
*/
|
||||||
@ -29,8 +28,9 @@ export class ConfirmEmailComponent implements OnInit {
|
|||||||
errors: Array<string> = [];
|
errors: Array<string> = [];
|
||||||
|
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, private toastr: ToastrService) {
|
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||||
|
private toastr: ToastrService, private themeService: ThemeService) {
|
||||||
|
this.themeService.setTheme(this.themeService.defaultTheme);
|
||||||
const token = this.route.snapshot.queryParamMap.get('token');
|
const token = this.route.snapshot.queryParamMap.get('token');
|
||||||
const email = this.route.snapshot.queryParamMap.get('email');
|
const email = this.route.snapshot.queryParamMap.get('email');
|
||||||
if (token == undefined || token === '' || token === null) {
|
if (token == undefined || token === '' || token === null) {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user